synabun 1.3.0 → 2.0.0-beta

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 (1153) hide show
  1. package/CHANGELOG.md +197 -662
  2. package/LICENSE-COMMERCIAL.md +33 -0
  3. package/NOTICE +17 -0
  4. package/README.md +175 -853
  5. package/THIRD-PARTY-LICENSES.md +131 -0
  6. package/benchmarks/browser-live-results.json +79 -0
  7. package/benchmarks/browser-v2-results.json +258 -0
  8. package/benchmarks/fixtures/browser/README.md +57 -0
  9. package/benchmarks/fixtures/browser/page-state.json +132 -0
  10. package/benchmarks/fixtures/browser/social.json +92 -0
  11. package/benchmarks/fixtures/browser/target.json +319 -0
  12. package/benchmarks/fixtures/desktop/README.md +58 -0
  13. package/benchmarks/fixtures/desktop/abstention.json +59 -0
  14. package/benchmarks/fixtures/desktop/adversarial.json +22 -0
  15. package/benchmarks/fixtures/desktop/forbidden.json +93 -0
  16. package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
  17. package/benchmarks/fixtures/desktop/target.json +136 -0
  18. package/bin/synabun.js +30 -0
  19. package/docs/browser-v2.md +109 -0
  20. package/docs/hook-integrity.md +89 -0
  21. package/docs/judgments.md +182 -0
  22. package/docs/style-guide.md +246 -0
  23. package/docs/whatsapp.md +325 -0
  24. package/hooks/claude-code/post-plan.mjs +613 -391
  25. package/hooks/claude-code/post-remember.mjs +246 -122
  26. package/hooks/claude-code/pre-compact.mjs +171 -110
  27. package/hooks/claude-code/pre-task.mjs +133 -0
  28. package/hooks/claude-code/pre-websearch.mjs +15 -4
  29. package/hooks/claude-code/prompt-origin.mjs +94 -0
  30. package/hooks/claude-code/prompt-submit.mjs +1244 -861
  31. package/hooks/claude-code/session-start.mjs +114 -156
  32. package/hooks/claude-code/shared.mjs +1228 -368
  33. package/hooks/claude-code/state.mjs +167 -0
  34. package/hooks/claude-code/stop.mjs +515 -140
  35. package/hooks/claude-code/subagent-start.mjs +154 -0
  36. package/hooks/claude-code/subagent-stop.mjs +102 -0
  37. package/hooks/claude-code/task-gate.mjs +150 -0
  38. package/lib/claude-hooks.js +84 -0
  39. package/lib/claude-usage-audit.js +141 -0
  40. package/lib/client-config-repair.js +144 -0
  41. package/lib/data-home-migration.js +498 -0
  42. package/lib/first-launch-protection.js +240 -0
  43. package/lib/paths.d.ts +21 -0
  44. package/lib/paths.js +103 -0
  45. package/mcp-server/dist/config.d.ts +22 -0
  46. package/mcp-server/dist/config.d.ts.map +1 -0
  47. package/mcp-server/dist/config.js +109 -0
  48. package/mcp-server/dist/config.js.map +1 -0
  49. package/mcp-server/dist/http.d.ts +30 -0
  50. package/mcp-server/dist/http.d.ts.map +1 -0
  51. package/mcp-server/dist/http.js +276 -0
  52. package/mcp-server/dist/http.js.map +1 -0
  53. package/mcp-server/dist/index.d.ts +48 -0
  54. package/mcp-server/dist/index.d.ts.map +1 -0
  55. package/mcp-server/dist/index.js +538 -0
  56. package/mcp-server/dist/index.js.map +1 -0
  57. package/mcp-server/dist/preload-http.d.ts +3 -0
  58. package/mcp-server/dist/preload-http.d.ts.map +1 -0
  59. package/mcp-server/dist/preload-http.js +28 -0
  60. package/mcp-server/dist/preload-http.js.map +1 -0
  61. package/mcp-server/dist/preload.d.ts +3 -0
  62. package/mcp-server/dist/preload.d.ts.map +1 -0
  63. package/mcp-server/dist/preload.js +32 -0
  64. package/mcp-server/dist/preload.js.map +1 -0
  65. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
  66. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
  67. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
  68. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
  69. package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
  70. package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
  71. package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
  72. package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
  73. package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
  74. package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
  75. package/mcp-server/dist/services/assistant-judgments.js +128 -0
  76. package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
  77. package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
  78. package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
  79. package/mcp-server/dist/services/browser-assist-gate.js +344 -0
  80. package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
  81. package/mcp-server/dist/services/browser-assist.d.ts +104 -0
  82. package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
  83. package/mcp-server/dist/services/browser-assist.js +282 -0
  84. package/mcp-server/dist/services/browser-assist.js.map +1 -0
  85. package/mcp-server/dist/services/browser-bench.d.ts +772 -0
  86. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
  87. package/mcp-server/dist/services/browser-bench.js +424 -0
  88. package/mcp-server/dist/services/browser-bench.js.map +1 -0
  89. package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
  90. package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
  91. package/mcp-server/dist/services/browser-judgments.js +363 -0
  92. package/mcp-server/dist/services/browser-judgments.js.map +1 -0
  93. package/mcp-server/dist/services/browser-risk.d.ts +166 -0
  94. package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
  95. package/mcp-server/dist/services/browser-risk.js +458 -0
  96. package/mcp-server/dist/services/browser-risk.js.map +1 -0
  97. package/mcp-server/dist/services/categories.d.ts +61 -0
  98. package/mcp-server/dist/services/categories.d.ts.map +1 -0
  99. package/mcp-server/dist/services/categories.js +377 -0
  100. package/mcp-server/dist/services/categories.js.map +1 -0
  101. package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
  102. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
  103. package/mcp-server/dist/services/codex-config-heal.js +346 -0
  104. package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
  105. package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
  106. package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
  107. package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
  108. package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
  109. package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
  110. package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
  111. package/mcp-server/dist/services/desktop-assist.js +266 -0
  112. package/mcp-server/dist/services/desktop-assist.js.map +1 -0
  113. package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
  114. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
  115. package/mcp-server/dist/services/desktop-bench.js +311 -0
  116. package/mcp-server/dist/services/desktop-bench.js.map +1 -0
  117. package/mcp-server/dist/services/desktop-client.d.ts +23 -0
  118. package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
  119. package/mcp-server/dist/services/desktop-client.js +79 -0
  120. package/mcp-server/dist/services/desktop-client.js.map +1 -0
  121. package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
  122. package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
  123. package/mcp-server/dist/services/desktop-judgments.js +177 -0
  124. package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
  125. package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
  126. package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
  127. package/mcp-server/dist/services/desktop-risk.js +605 -0
  128. package/mcp-server/dist/services/desktop-risk.js.map +1 -0
  129. package/mcp-server/dist/services/discord.d.ts +112 -0
  130. package/mcp-server/dist/services/discord.d.ts.map +1 -0
  131. package/mcp-server/dist/services/discord.js +477 -0
  132. package/mcp-server/dist/services/discord.js.map +1 -0
  133. package/mcp-server/dist/services/edit-stale.d.ts +81 -0
  134. package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
  135. package/mcp-server/dist/services/edit-stale.js +358 -0
  136. package/mcp-server/dist/services/edit-stale.js.map +1 -0
  137. package/mcp-server/dist/services/embedding-service.d.ts +16 -0
  138. package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
  139. package/mcp-server/dist/services/embedding-service.js +126 -0
  140. package/mcp-server/dist/services/embedding-service.js.map +1 -0
  141. package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
  142. package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
  143. package/mcp-server/dist/services/embedding-worker.js +52 -0
  144. package/mcp-server/dist/services/embedding-worker.js.map +1 -0
  145. package/mcp-server/dist/services/fb-regions.d.ts +71 -0
  146. package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
  147. package/mcp-server/dist/services/fb-regions.js +211 -0
  148. package/mcp-server/dist/services/fb-regions.js.map +1 -0
  149. package/mcp-server/dist/services/file-checksums.d.ts +22 -0
  150. package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
  151. package/mcp-server/dist/services/file-checksums.js +55 -0
  152. package/mcp-server/dist/services/file-checksums.js.map +1 -0
  153. package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
  154. package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
  155. package/mcp-server/dist/services/file-excerpt.js +65 -0
  156. package/mcp-server/dist/services/file-excerpt.js.map +1 -0
  157. package/mcp-server/dist/services/identity.d.ts +124 -0
  158. package/mcp-server/dist/services/identity.d.ts.map +1 -0
  159. package/mcp-server/dist/services/identity.js +241 -0
  160. package/mcp-server/dist/services/identity.js.map +1 -0
  161. package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
  162. package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
  163. package/mcp-server/dist/services/local-embeddings.js +3 -0
  164. package/mcp-server/dist/services/local-embeddings.js.map +1 -0
  165. package/mcp-server/dist/services/memory-client.d.ts +4 -0
  166. package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
  167. package/mcp-server/dist/services/memory-client.js +27 -0
  168. package/mcp-server/dist/services/memory-client.js.map +1 -0
  169. package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
  170. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
  171. package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
  172. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
  173. package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
  174. package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
  175. package/mcp-server/dist/services/memory-judgments.js +674 -0
  176. package/mcp-server/dist/services/memory-judgments.js.map +1 -0
  177. package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
  178. package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
  179. package/mcp-server/dist/services/memory-maintenance.js +403 -0
  180. package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
  181. package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
  182. package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
  183. package/mcp-server/dist/services/memory-retrieval.js +412 -0
  184. package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
  185. package/mcp-server/dist/services/memory-schema.d.ts +16 -0
  186. package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
  187. package/mcp-server/dist/services/memory-schema.js +282 -0
  188. package/mcp-server/dist/services/memory-schema.js.map +1 -0
  189. package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
  190. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
  191. package/mcp-server/dist/services/memory-sessions.js +110 -0
  192. package/mcp-server/dist/services/memory-sessions.js.map +1 -0
  193. package/mcp-server/dist/services/memory-triage.d.ts +60 -0
  194. package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
  195. package/mcp-server/dist/services/memory-triage.js +189 -0
  196. package/mcp-server/dist/services/memory-triage.js.map +1 -0
  197. package/mcp-server/dist/services/memory-verification.d.ts +24 -0
  198. package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
  199. package/mcp-server/dist/services/memory-verification.js +92 -0
  200. package/mcp-server/dist/services/memory-verification.js.map +1 -0
  201. package/mcp-server/dist/services/memory-writer.d.ts +26 -0
  202. package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
  203. package/mcp-server/dist/services/memory-writer.js +126 -0
  204. package/mcp-server/dist/services/memory-writer.js.map +1 -0
  205. package/mcp-server/dist/services/neural-interface.d.ts +439 -0
  206. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
  207. package/mcp-server/dist/services/neural-interface.js +1135 -0
  208. package/mcp-server/dist/services/neural-interface.js.map +1 -0
  209. package/mcp-server/dist/services/profiles.d.ts +120 -0
  210. package/mcp-server/dist/services/profiles.d.ts.map +1 -0
  211. package/mcp-server/dist/services/profiles.js +387 -0
  212. package/mcp-server/dist/services/profiles.js.map +1 -0
  213. package/mcp-server/dist/services/secret-gate.d.ts +29 -0
  214. package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
  215. package/mcp-server/dist/services/secret-gate.js +43 -0
  216. package/mcp-server/dist/services/secret-gate.js.map +1 -0
  217. package/mcp-server/dist/services/server-instructions.d.ts +10 -0
  218. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
  219. package/mcp-server/dist/services/server-instructions.js +101 -0
  220. package/mcp-server/dist/services/server-instructions.js.map +1 -0
  221. package/mcp-server/dist/services/sqlite.d.ts +235 -0
  222. package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
  223. package/mcp-server/dist/services/sqlite.js +1185 -0
  224. package/mcp-server/dist/services/sqlite.js.map +1 -0
  225. package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
  226. package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
  227. package/mcp-server/dist/services/typesafe-bench.js +218 -0
  228. package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
  229. package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
  230. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
  231. package/mcp-server/dist/services/typesafe-config.js +746 -0
  232. package/mcp-server/dist/services/typesafe-config.js.map +1 -0
  233. package/mcp-server/dist/services/typesafe.d.ts +213 -0
  234. package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
  235. package/mcp-server/dist/services/typesafe.js +330 -0
  236. package/mcp-server/dist/services/typesafe.js.map +1 -0
  237. package/mcp-server/dist/services/vector-cache.d.ts +68 -0
  238. package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
  239. package/mcp-server/dist/services/vector-cache.js +271 -0
  240. package/mcp-server/dist/services/vector-cache.js.map +1 -0
  241. package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
  242. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
  243. package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
  244. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
  245. package/mcp-server/dist/tools/agents.d.ts +212 -0
  246. package/mcp-server/dist/tools/agents.d.ts.map +1 -0
  247. package/mcp-server/dist/tools/agents.js +407 -0
  248. package/mcp-server/dist/tools/agents.js.map +1 -0
  249. package/mcp-server/dist/tools/bluesky.d.ts +39 -0
  250. package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
  251. package/mcp-server/dist/tools/bluesky.js +1144 -0
  252. package/mcp-server/dist/tools/bluesky.js.map +1 -0
  253. package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
  254. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
  255. package/mcp-server/dist/tools/browser-advanced.js +106 -0
  256. package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
  257. package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
  258. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
  259. package/mcp-server/dist/tools/browser-batch.js +187 -0
  260. package/mcp-server/dist/tools/browser-batch.js.map +1 -0
  261. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
  262. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
  263. package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
  264. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
  265. package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
  266. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
  267. package/mcp-server/dist/tools/browser-interact.js +300 -0
  268. package/mcp-server/dist/tools/browser-interact.js.map +1 -0
  269. package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
  270. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
  271. package/mcp-server/dist/tools/browser-navigate.js +142 -0
  272. package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
  273. package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
  274. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
  275. package/mcp-server/dist/tools/browser-observe.js +2009 -0
  276. package/mcp-server/dist/tools/browser-observe.js.map +1 -0
  277. package/mcp-server/dist/tools/browser.d.ts +34 -0
  278. package/mcp-server/dist/tools/browser.d.ts.map +1 -0
  279. package/mcp-server/dist/tools/browser.js +112 -0
  280. package/mcp-server/dist/tools/browser.js.map +1 -0
  281. package/mcp-server/dist/tools/card-tools.d.ts +82 -0
  282. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
  283. package/mcp-server/dist/tools/card-tools.js +137 -0
  284. package/mcp-server/dist/tools/card-tools.js.map +1 -0
  285. package/mcp-server/dist/tools/card.d.ts +8 -0
  286. package/mcp-server/dist/tools/card.d.ts.map +1 -0
  287. package/mcp-server/dist/tools/card.js +16 -0
  288. package/mcp-server/dist/tools/card.js.map +1 -0
  289. package/mcp-server/dist/tools/category.d.ts +32 -0
  290. package/mcp-server/dist/tools/category.d.ts.map +1 -0
  291. package/mcp-server/dist/tools/category.js +290 -0
  292. package/mcp-server/dist/tools/category.js.map +1 -0
  293. package/mcp-server/dist/tools/choice.d.ts +160 -0
  294. package/mcp-server/dist/tools/choice.d.ts.map +1 -0
  295. package/mcp-server/dist/tools/choice.js +80 -0
  296. package/mcp-server/dist/tools/choice.js.map +1 -0
  297. package/mcp-server/dist/tools/computer.d.ts +71 -0
  298. package/mcp-server/dist/tools/computer.d.ts.map +1 -0
  299. package/mcp-server/dist/tools/computer.js +174 -0
  300. package/mcp-server/dist/tools/computer.js.map +1 -0
  301. package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
  302. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
  303. package/mcp-server/dist/tools/discord-channel.js +236 -0
  304. package/mcp-server/dist/tools/discord-channel.js.map +1 -0
  305. package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
  306. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
  307. package/mcp-server/dist/tools/discord-guild.js +138 -0
  308. package/mcp-server/dist/tools/discord-guild.js.map +1 -0
  309. package/mcp-server/dist/tools/discord-member.d.ts +26 -0
  310. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
  311. package/mcp-server/dist/tools/discord-member.js +122 -0
  312. package/mcp-server/dist/tools/discord-member.js.map +1 -0
  313. package/mcp-server/dist/tools/discord-message.d.ts +36 -0
  314. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
  315. package/mcp-server/dist/tools/discord-message.js +184 -0
  316. package/mcp-server/dist/tools/discord-message.js.map +1 -0
  317. package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
  318. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
  319. package/mcp-server/dist/tools/discord-onboarding.js +179 -0
  320. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
  321. package/mcp-server/dist/tools/discord-role.d.ts +34 -0
  322. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
  323. package/mcp-server/dist/tools/discord-role.js +178 -0
  324. package/mcp-server/dist/tools/discord-role.js.map +1 -0
  325. package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
  326. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
  327. package/mcp-server/dist/tools/discord-thread.js +88 -0
  328. package/mcp-server/dist/tools/discord-thread.js.map +1 -0
  329. package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
  330. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
  331. package/mcp-server/dist/tools/discord-webhook.js +154 -0
  332. package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
  333. package/mcp-server/dist/tools/discord.d.ts +7 -0
  334. package/mcp-server/dist/tools/discord.d.ts.map +1 -0
  335. package/mcp-server/dist/tools/discord.js +25 -0
  336. package/mcp-server/dist/tools/discord.js.map +1 -0
  337. package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
  338. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
  339. package/mcp-server/dist/tools/fb-groups.js +369 -0
  340. package/mcp-server/dist/tools/fb-groups.js.map +1 -0
  341. package/mcp-server/dist/tools/forget.d.ts +14 -0
  342. package/mcp-server/dist/tools/forget.d.ts.map +1 -0
  343. package/mcp-server/dist/tools/forget.js +26 -0
  344. package/mcp-server/dist/tools/forget.js.map +1 -0
  345. package/mcp-server/dist/tools/git-tools.d.ts +24 -0
  346. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
  347. package/mcp-server/dist/tools/git-tools.js +107 -0
  348. package/mcp-server/dist/tools/git-tools.js.map +1 -0
  349. package/mcp-server/dist/tools/git.d.ts +7 -0
  350. package/mcp-server/dist/tools/git.d.ts.map +1 -0
  351. package/mcp-server/dist/tools/git.js +11 -0
  352. package/mcp-server/dist/tools/git.js.map +1 -0
  353. package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
  354. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
  355. package/mcp-server/dist/tools/gsc/experience.js +351 -0
  356. package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
  357. package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
  358. package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
  359. package/mcp-server/dist/tools/gsc/helpers.js +258 -0
  360. package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
  361. package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
  362. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
  363. package/mcp-server/dist/tools/gsc/indexing.js +357 -0
  364. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
  365. package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
  366. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
  367. package/mcp-server/dist/tools/gsc/inspect.js +321 -0
  368. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
  369. package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
  370. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
  371. package/mcp-server/dist/tools/gsc/navigate.js +197 -0
  372. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
  373. package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
  374. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
  375. package/mcp-server/dist/tools/gsc/performance.js +274 -0
  376. package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
  377. package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
  378. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
  379. package/mcp-server/dist/tools/gsc/settings.js +425 -0
  380. package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
  381. package/mcp-server/dist/tools/gsc.d.ts +7 -0
  382. package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
  383. package/mcp-server/dist/tools/gsc.js +51 -0
  384. package/mcp-server/dist/tools/gsc.js.map +1 -0
  385. package/mcp-server/dist/tools/image-tools.d.ts +24 -0
  386. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
  387. package/mcp-server/dist/tools/image-tools.js +80 -0
  388. package/mcp-server/dist/tools/image-tools.js.map +1 -0
  389. package/mcp-server/dist/tools/image.d.ts +10 -0
  390. package/mcp-server/dist/tools/image.d.ts.map +1 -0
  391. package/mcp-server/dist/tools/image.js +14 -0
  392. package/mcp-server/dist/tools/image.js.map +1 -0
  393. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
  394. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
  395. package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
  396. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
  397. package/mcp-server/dist/tools/leonardo.d.ts +13 -0
  398. package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
  399. package/mcp-server/dist/tools/leonardo.js +21 -0
  400. package/mcp-server/dist/tools/leonardo.js.map +1 -0
  401. package/mcp-server/dist/tools/loop.d.ts +30 -0
  402. package/mcp-server/dist/tools/loop.d.ts.map +1 -0
  403. package/mcp-server/dist/tools/loop.js +401 -0
  404. package/mcp-server/dist/tools/loop.js.map +1 -0
  405. package/mcp-server/dist/tools/memories.d.ts +56 -0
  406. package/mcp-server/dist/tools/memories.d.ts.map +1 -0
  407. package/mcp-server/dist/tools/memories.js +171 -0
  408. package/mcp-server/dist/tools/memories.js.map +1 -0
  409. package/mcp-server/dist/tools/morelogin.d.ts +24 -0
  410. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
  411. package/mcp-server/dist/tools/morelogin.js +79 -0
  412. package/mcp-server/dist/tools/morelogin.js.map +1 -0
  413. package/mcp-server/dist/tools/profile.d.ts +17 -0
  414. package/mcp-server/dist/tools/profile.d.ts.map +1 -0
  415. package/mcp-server/dist/tools/profile.js +114 -0
  416. package/mcp-server/dist/tools/profile.js.map +1 -0
  417. package/mcp-server/dist/tools/recall.d.ts +71 -0
  418. package/mcp-server/dist/tools/recall.d.ts.map +1 -0
  419. package/mcp-server/dist/tools/recall.js +147 -0
  420. package/mcp-server/dist/tools/recall.js.map +1 -0
  421. package/mcp-server/dist/tools/reflect.d.ts +56 -0
  422. package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
  423. package/mcp-server/dist/tools/reflect.js +157 -0
  424. package/mcp-server/dist/tools/reflect.js.map +1 -0
  425. package/mcp-server/dist/tools/remember.d.ts +62 -0
  426. package/mcp-server/dist/tools/remember.d.ts.map +1 -0
  427. package/mcp-server/dist/tools/remember.js +201 -0
  428. package/mcp-server/dist/tools/remember.js.map +1 -0
  429. package/mcp-server/dist/tools/response.d.ts +23 -0
  430. package/mcp-server/dist/tools/response.d.ts.map +1 -0
  431. package/mcp-server/dist/tools/response.js +24 -0
  432. package/mcp-server/dist/tools/response.js.map +1 -0
  433. package/mcp-server/dist/tools/restore.d.ts +14 -0
  434. package/mcp-server/dist/tools/restore.d.ts.map +1 -0
  435. package/mcp-server/dist/tools/restore.js +23 -0
  436. package/mcp-server/dist/tools/restore.js.map +1 -0
  437. package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
  438. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
  439. package/mcp-server/dist/tools/style-guide-tools.js +231 -0
  440. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
  441. package/mcp-server/dist/tools/style-guide.d.ts +8 -0
  442. package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
  443. package/mcp-server/dist/tools/style-guide.js +12 -0
  444. package/mcp-server/dist/tools/style-guide.js.map +1 -0
  445. package/mcp-server/dist/tools/sync.d.ts +20 -0
  446. package/mcp-server/dist/tools/sync.d.ts.map +1 -0
  447. package/mcp-server/dist/tools/sync.js +156 -0
  448. package/mcp-server/dist/tools/sync.js.map +1 -0
  449. package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
  450. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
  451. package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
  452. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
  453. package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
  454. package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
  455. package/mcp-server/dist/tools/tictactoe.js +11 -0
  456. package/mcp-server/dist/tools/tictactoe.js.map +1 -0
  457. package/mcp-server/dist/tools/utils.d.ts +19 -0
  458. package/mcp-server/dist/tools/utils.d.ts.map +1 -0
  459. package/mcp-server/dist/tools/utils.js +54 -0
  460. package/mcp-server/dist/tools/utils.js.map +1 -0
  461. package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
  462. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
  463. package/mcp-server/dist/tools/whiteboard-format.js +337 -0
  464. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
  465. package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
  466. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
  467. package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
  468. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
  469. package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
  470. package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
  471. package/mcp-server/dist/tools/whiteboard.js +16 -0
  472. package/mcp-server/dist/tools/whiteboard.js.map +1 -0
  473. package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
  474. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
  475. package/mcp-server/dist/tools/youtube/analytics.js +83 -0
  476. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
  477. package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
  478. package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
  479. package/mcp-server/dist/tools/youtube/dedup.js +56 -0
  480. package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
  481. package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
  482. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
  483. package/mcp-server/dist/tools/youtube/download.js +30 -0
  484. package/mcp-server/dist/tools/youtube/download.js.map +1 -0
  485. package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
  486. package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
  487. package/mcp-server/dist/tools/youtube/helpers.js +143 -0
  488. package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
  489. package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
  490. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
  491. package/mcp-server/dist/tools/youtube/media.js +97 -0
  492. package/mcp-server/dist/tools/youtube/media.js.map +1 -0
  493. package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
  494. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
  495. package/mcp-server/dist/tools/youtube/metadata.js +106 -0
  496. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
  497. package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
  498. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
  499. package/mcp-server/dist/tools/youtube/navigate.js +71 -0
  500. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
  501. package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
  502. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
  503. package/mcp-server/dist/tools/youtube/playlist.js +128 -0
  504. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
  505. package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
  506. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
  507. package/mcp-server/dist/tools/youtube/schedule.js +147 -0
  508. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
  509. package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
  510. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
  511. package/mcp-server/dist/tools/youtube/source.js +85 -0
  512. package/mcp-server/dist/tools/youtube/source.js.map +1 -0
  513. package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
  514. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
  515. package/mcp-server/dist/tools/youtube/upload.js +255 -0
  516. package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
  517. package/mcp-server/dist/tools/youtube.d.ts +12 -0
  518. package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
  519. package/mcp-server/dist/tools/youtube.js +38 -0
  520. package/mcp-server/dist/tools/youtube.js.map +1 -0
  521. package/mcp-server/dist/tui.d.ts +3 -0
  522. package/mcp-server/dist/tui.d.ts.map +1 -0
  523. package/mcp-server/dist/tui.js +806 -0
  524. package/mcp-server/dist/tui.js.map +1 -0
  525. package/mcp-server/dist/types.d.ts +77 -0
  526. package/mcp-server/dist/types.d.ts.map +1 -0
  527. package/mcp-server/dist/types.js +2 -0
  528. package/mcp-server/dist/types.js.map +1 -0
  529. package/mcp-server/package-lock.json +4798 -0
  530. package/mcp-server/package.json +55 -38
  531. package/mcp-server/run.mjs +12 -2
  532. package/mcp-server/src/config.ts +48 -3
  533. package/mcp-server/src/http.ts +296 -93
  534. package/mcp-server/src/index.ts +546 -203
  535. package/mcp-server/src/preload-http.ts +19 -18
  536. package/mcp-server/src/preload.ts +19 -18
  537. package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
  538. package/mcp-server/src/services/assistant-judgments.ts +157 -0
  539. package/mcp-server/src/services/browser-assist-gate.ts +383 -0
  540. package/mcp-server/src/services/browser-assist.ts +267 -0
  541. package/mcp-server/src/services/browser-bench.ts +477 -0
  542. package/mcp-server/src/services/browser-judgments.ts +414 -0
  543. package/mcp-server/src/services/browser-risk.ts +484 -0
  544. package/mcp-server/src/services/categories.ts +36 -19
  545. package/mcp-server/src/services/codex-config-heal.ts +403 -0
  546. package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
  547. package/mcp-server/src/services/desktop-assist.ts +308 -0
  548. package/mcp-server/src/services/desktop-bench.ts +372 -0
  549. package/mcp-server/src/services/desktop-client.ts +76 -0
  550. package/mcp-server/src/services/desktop-judgments.ts +218 -0
  551. package/mcp-server/src/services/desktop-risk.ts +743 -0
  552. package/mcp-server/src/services/edit-stale.ts +299 -0
  553. package/mcp-server/src/services/embedding-service.ts +108 -0
  554. package/mcp-server/src/services/embedding-worker.ts +46 -0
  555. package/mcp-server/src/services/fb-regions.ts +221 -0
  556. package/mcp-server/src/services/file-checksums.ts +26 -9
  557. package/mcp-server/src/services/file-excerpt.ts +59 -0
  558. package/mcp-server/src/services/identity.ts +291 -0
  559. package/mcp-server/src/services/local-embeddings.ts +2 -77
  560. package/mcp-server/src/services/memory-client.ts +23 -0
  561. package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
  562. package/mcp-server/src/services/memory-judgments.ts +901 -0
  563. package/mcp-server/src/services/memory-maintenance.ts +360 -0
  564. package/mcp-server/src/services/memory-retrieval.ts +349 -0
  565. package/mcp-server/src/services/memory-schema.ts +280 -0
  566. package/mcp-server/src/services/memory-sessions.ts +87 -0
  567. package/mcp-server/src/services/memory-triage.ts +182 -0
  568. package/mcp-server/src/services/memory-verification.ts +68 -0
  569. package/mcp-server/src/services/memory-writer.ts +125 -0
  570. package/mcp-server/src/services/neural-interface.ts +1217 -73
  571. package/mcp-server/src/services/profiles.ts +428 -0
  572. package/mcp-server/src/services/secret-gate.ts +52 -0
  573. package/mcp-server/src/services/server-instructions.ts +122 -0
  574. package/mcp-server/src/services/sqlite.ts +1456 -823
  575. package/mcp-server/src/services/typesafe-bench.ts +247 -0
  576. package/mcp-server/src/services/typesafe-config.ts +790 -0
  577. package/mcp-server/src/services/typesafe.ts +390 -0
  578. package/mcp-server/src/services/vector-cache.ts +255 -0
  579. package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
  580. package/mcp-server/src/tools/agents.ts +539 -0
  581. package/mcp-server/src/tools/bluesky.ts +1186 -0
  582. package/mcp-server/src/tools/browser-advanced.ts +15 -6
  583. package/mcp-server/src/tools/browser-batch.ts +178 -0
  584. package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
  585. package/mcp-server/src/tools/browser-interact.ts +362 -255
  586. package/mcp-server/src/tools/browser-navigate.ts +164 -77
  587. package/mcp-server/src/tools/browser-observe.ts +2222 -1330
  588. package/mcp-server/src/tools/browser.ts +181 -107
  589. package/mcp-server/src/tools/card.ts +7 -5
  590. package/mcp-server/src/tools/choice.ts +103 -0
  591. package/mcp-server/src/tools/computer.ts +190 -0
  592. package/mcp-server/src/tools/discord.ts +10 -23
  593. package/mcp-server/src/tools/fb-groups.ts +387 -0
  594. package/mcp-server/src/tools/forget.ts +1 -1
  595. package/mcp-server/src/tools/git.ts +3 -1
  596. package/mcp-server/src/tools/gsc/experience.ts +440 -0
  597. package/mcp-server/src/tools/gsc/helpers.ts +288 -0
  598. package/mcp-server/src/tools/gsc/indexing.ts +495 -0
  599. package/mcp-server/src/tools/gsc/inspect.ts +391 -0
  600. package/mcp-server/src/tools/gsc/navigate.ts +236 -0
  601. package/mcp-server/src/tools/gsc/performance.ts +352 -0
  602. package/mcp-server/src/tools/gsc/settings.ts +564 -0
  603. package/mcp-server/src/tools/gsc.ts +95 -0
  604. package/mcp-server/src/tools/image.ts +3 -1
  605. package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
  606. package/mcp-server/src/tools/leonardo.ts +7 -5
  607. package/mcp-server/src/tools/loop.ts +118 -25
  608. package/mcp-server/src/tools/memories.ts +65 -5
  609. package/mcp-server/src/tools/morelogin.ts +81 -0
  610. package/mcp-server/src/tools/profile.ts +122 -0
  611. package/mcp-server/src/tools/recall.ts +105 -261
  612. package/mcp-server/src/tools/reflect.ts +171 -145
  613. package/mcp-server/src/tools/remember.ts +223 -115
  614. package/mcp-server/src/tools/style-guide-tools.ts +250 -0
  615. package/mcp-server/src/tools/style-guide.ts +16 -0
  616. package/mcp-server/src/tools/sync.ts +66 -9
  617. package/mcp-server/src/tools/tictactoe.ts +3 -1
  618. package/mcp-server/src/tools/utils.ts +22 -0
  619. package/mcp-server/src/tools/whiteboard-format.ts +394 -0
  620. package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
  621. package/mcp-server/src/tools/whiteboard.ts +7 -5
  622. package/mcp-server/src/tools/youtube/analytics.ts +106 -0
  623. package/mcp-server/src/tools/youtube/dedup.ts +57 -0
  624. package/mcp-server/src/tools/youtube/download.ts +41 -0
  625. package/mcp-server/src/tools/youtube/helpers.ts +183 -0
  626. package/mcp-server/src/tools/youtube/media.ts +109 -0
  627. package/mcp-server/src/tools/youtube/metadata.ts +112 -0
  628. package/mcp-server/src/tools/youtube/navigate.ts +81 -0
  629. package/mcp-server/src/tools/youtube/playlist.ts +134 -0
  630. package/mcp-server/src/tools/youtube/schedule.ts +158 -0
  631. package/mcp-server/src/tools/youtube/source.ts +101 -0
  632. package/mcp-server/src/tools/youtube/upload.ts +296 -0
  633. package/mcp-server/src/tools/youtube.ts +47 -0
  634. package/mcp-server/src/tui.ts +8 -9
  635. package/neural-interface/games/TicTacToe/1.svg +1 -0
  636. package/neural-interface/games/TicTacToe/2.svg +1 -0
  637. package/neural-interface/games/TicTacToe/3.svg +1 -0
  638. package/neural-interface/games/TicTacToe/4.svg +1 -0
  639. package/neural-interface/games/TicTacToe/5.svg +1 -0
  640. package/neural-interface/games/TicTacToe/6.svg +1 -0
  641. package/neural-interface/games/TicTacToe/7.svg +1 -0
  642. package/neural-interface/games/TicTacToe/8.svg +1 -0
  643. package/neural-interface/games/TicTacToe/9.svg +1 -0
  644. package/neural-interface/games/TicTacToe/Board.svg +1 -0
  645. package/neural-interface/games/TicTacToe/Circle.svg +1 -0
  646. package/neural-interface/games/TicTacToe/Cross.svg +1 -0
  647. package/neural-interface/i18n/en.json +6983 -0
  648. package/neural-interface/i18n/pt-BR.json +6987 -0
  649. package/neural-interface/lib/assistant-api.js +1005 -0
  650. package/neural-interface/lib/assistant-attachments.js +121 -0
  651. package/neural-interface/lib/assistant-brains/claude.js +207 -0
  652. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
  653. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
  654. package/neural-interface/lib/assistant-brains/codex.js +213 -0
  655. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
  656. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
  657. package/neural-interface/lib/assistant-brains/opencode.js +320 -0
  658. package/neural-interface/lib/assistant-budget.js +318 -0
  659. package/neural-interface/lib/assistant-catalog.js +731 -0
  660. package/neural-interface/lib/assistant-clarify.js +664 -0
  661. package/neural-interface/lib/assistant-config.js +232 -0
  662. package/neural-interface/lib/assistant-dispatch.js +2744 -0
  663. package/neural-interface/lib/assistant-envelope.js +579 -0
  664. package/neural-interface/lib/assistant-evidence.js +236 -0
  665. package/neural-interface/lib/assistant-hidden-models.js +272 -0
  666. package/neural-interface/lib/assistant-jev.js +117 -0
  667. package/neural-interface/lib/assistant-judgments.js +111 -0
  668. package/neural-interface/lib/assistant-media.js +335 -0
  669. package/neural-interface/lib/assistant-memory.js +398 -0
  670. package/neural-interface/lib/assistant-persona.js +444 -0
  671. package/neural-interface/lib/assistant-plan-permissions.js +448 -0
  672. package/neural-interface/lib/assistant-playbooks/design.md +87 -0
  673. package/neural-interface/lib/assistant-playbooks.js +72 -0
  674. package/neural-interface/lib/assistant-pricing.js +159 -0
  675. package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
  676. package/neural-interface/lib/assistant-route-gate.js +212 -0
  677. package/neural-interface/lib/assistant-router.js +1324 -0
  678. package/neural-interface/lib/assistant-runtime.js +3337 -0
  679. package/neural-interface/lib/assistant-task-prompt.js +472 -0
  680. package/neural-interface/lib/assistant-usage.js +1688 -0
  681. package/neural-interface/lib/augmented-path.js +50 -0
  682. package/neural-interface/lib/automation-storage-migration.js +54 -0
  683. package/neural-interface/lib/backup-service.js +851 -0
  684. package/neural-interface/lib/backup-zip-reader.js +262 -0
  685. package/neural-interface/lib/browser-assist-api.js +41 -0
  686. package/neural-interface/lib/browser-console.js +176 -0
  687. package/neural-interface/lib/browser-execution.js +241 -0
  688. package/neural-interface/lib/browser-extraction.js +191 -0
  689. package/neural-interface/lib/browser-screenshot.js +399 -0
  690. package/neural-interface/lib/browser-semantic-context.js +639 -0
  691. package/neural-interface/lib/browser-snapshots.js +192 -0
  692. package/neural-interface/lib/browser-tab-recovery.js +119 -0
  693. package/neural-interface/lib/browser-targets.js +70 -0
  694. package/neural-interface/lib/browser-tool-policy.js +926 -0
  695. package/neural-interface/lib/claude-accounts.js +230 -0
  696. package/neural-interface/lib/claude-agent-bridge.js +3296 -0
  697. package/neural-interface/lib/claude-bypass-policy.js +99 -0
  698. package/neural-interface/lib/claude-history.js +429 -0
  699. package/neural-interface/lib/claude-model-catalog.js +483 -0
  700. package/neural-interface/lib/claude-panel-requests.js +165 -0
  701. package/neural-interface/lib/claude-panel-session.js +272 -0
  702. package/neural-interface/lib/claude-permission-rules.js +153 -0
  703. package/neural-interface/lib/claude-session-cleanup.js +71 -0
  704. package/neural-interface/lib/claude-session-list.js +82 -0
  705. package/neural-interface/lib/claude-session-ops.js +140 -0
  706. package/neural-interface/lib/claude-settings-view.js +51 -0
  707. package/neural-interface/lib/claude-skin-connect.js +79 -0
  708. package/neural-interface/lib/claude-stall-policy.js +7 -0
  709. package/neural-interface/lib/claude-temporary.js +435 -0
  710. package/neural-interface/lib/cli-update-planner.js +312 -0
  711. package/neural-interface/lib/codex-capabilities.d.ts +41 -0
  712. package/neural-interface/lib/codex-capabilities.js +148 -0
  713. package/neural-interface/lib/codex-collaboration-mode.js +17 -0
  714. package/neural-interface/lib/codex-compat-rpc.js +241 -0
  715. package/neural-interface/lib/codex-mcp-approvals.js +56 -0
  716. package/neural-interface/lib/codex-mcp-apps.js +47 -0
  717. package/neural-interface/lib/codex-model-context.js +202 -0
  718. package/neural-interface/lib/codex-plan-store.js +71 -0
  719. package/neural-interface/lib/codex-request-journal.js +242 -0
  720. package/neural-interface/lib/codex-runtime-path.js +213 -0
  721. package/neural-interface/lib/codex-session-catalog.js +231 -0
  722. package/neural-interface/lib/codex-session-settings.js +46 -0
  723. package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
  724. package/neural-interface/lib/codex-thread-history.js +165 -0
  725. package/neural-interface/lib/codex-user-verification.js +34 -0
  726. package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
  727. package/neural-interface/lib/confined-fs.js +256 -0
  728. package/neural-interface/lib/db.js +922 -0
  729. package/neural-interface/lib/design-md.js +9 -0
  730. package/neural-interface/lib/desktop/api.js +92 -0
  731. package/neural-interface/lib/desktop/audit.js +62 -0
  732. package/neural-interface/lib/desktop/build.js +423 -0
  733. package/neural-interface/lib/desktop/config.js +606 -0
  734. package/neural-interface/lib/desktop/coords.js +76 -0
  735. package/neural-interface/lib/desktop/fake-helper.js +1192 -0
  736. package/neural-interface/lib/desktop/frames.js +77 -0
  737. package/neural-interface/lib/desktop/grants.js +84 -0
  738. package/neural-interface/lib/desktop/guards.js +170 -0
  739. package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
  740. package/neural-interface/lib/desktop/lease.js +51 -0
  741. package/neural-interface/lib/desktop/manager.js +545 -0
  742. package/neural-interface/lib/desktop/press-contexts.js +103 -0
  743. package/neural-interface/lib/desktop/protocol.js +495 -0
  744. package/neural-interface/lib/desktop/service.js +1202 -0
  745. package/neural-interface/lib/effort-levels.js +148 -0
  746. package/neural-interface/lib/event-loop-monitor.js +67 -0
  747. package/neural-interface/lib/exec-loop-command.js +209 -0
  748. package/neural-interface/lib/folder-picker.ps1 +77 -0
  749. package/neural-interface/lib/http-guards.js +287 -0
  750. package/neural-interface/lib/jsonl-lines.js +94 -0
  751. package/neural-interface/lib/loop-logger.js +127 -0
  752. package/neural-interface/lib/mcp-client-config.js +133 -0
  753. package/neural-interface/lib/mcp-installer.js +487 -0
  754. package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
  755. package/neural-interface/lib/mcp-profile-servers.js +120 -0
  756. package/neural-interface/lib/memo-cache.js +84 -0
  757. package/neural-interface/lib/memory-api.js +758 -0
  758. package/neural-interface/lib/memory-map-api.js +1150 -0
  759. package/neural-interface/lib/memory-map-layout.js +872 -0
  760. package/neural-interface/lib/memory-map-worker.js +165 -0
  761. package/neural-interface/lib/memory-session-capture.js +81 -0
  762. package/neural-interface/lib/memory-timers.js +62 -0
  763. package/neural-interface/lib/morelogin.js +301 -0
  764. package/neural-interface/lib/native-binary-runtime.js +650 -0
  765. package/neural-interface/lib/native-loop-goal.js +33 -0
  766. package/neural-interface/lib/native-loop-providers.js +1016 -0
  767. package/neural-interface/lib/native-loop-runtime.js +747 -0
  768. package/neural-interface/lib/opencode-hidden-models.js +113 -0
  769. package/neural-interface/lib/opencode-history.js +253 -0
  770. package/neural-interface/lib/opencode-runtime-config.js +58 -0
  771. package/neural-interface/lib/opencode-v2-client.js +486 -0
  772. package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
  773. package/neural-interface/lib/path-confine.js +213 -0
  774. package/neural-interface/lib/plugin-installer.js +264 -0
  775. package/neural-interface/lib/project-storage.js +766 -0
  776. package/neural-interface/lib/pty-host/core.js +603 -0
  777. package/neural-interface/lib/pty-host/flow.js +43 -0
  778. package/neural-interface/lib/pty-host/host.js +47 -0
  779. package/neural-interface/lib/pty-host/manager.js +360 -0
  780. package/neural-interface/lib/pty-host/protocol.js +81 -0
  781. package/neural-interface/lib/pty-host/snapshot.js +133 -0
  782. package/neural-interface/lib/remote-policy.js +582 -0
  783. package/neural-interface/lib/rulesets/api.js +175 -0
  784. package/neural-interface/lib/rulesets/fs-safe.js +245 -0
  785. package/neural-interface/lib/rulesets/installer.js +1453 -0
  786. package/neural-interface/lib/rulesets/json-edit.js +186 -0
  787. package/neural-interface/lib/rulesets/lock.js +170 -0
  788. package/neural-interface/lib/rulesets/managed-text.js +314 -0
  789. package/neural-interface/lib/rulesets/normalise.js +24 -0
  790. package/neural-interface/lib/rulesets/render.js +84 -0
  791. package/neural-interface/lib/schedule-launch-intent.js +40 -0
  792. package/neural-interface/lib/session-cache-builder.js +524 -0
  793. package/neural-interface/lib/session-chunker.js +344 -0
  794. package/neural-interface/lib/session-indexer.js +327 -0
  795. package/neural-interface/lib/session-title-generator.js +227 -0
  796. package/neural-interface/lib/skill-prompt-bridge.js +11 -0
  797. package/neural-interface/lib/style-guide/api.js +375 -0
  798. package/neural-interface/lib/style-guide/color.js +352 -0
  799. package/neural-interface/lib/style-guide/export-css.js +99 -0
  800. package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
  801. package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
  802. package/neural-interface/lib/style-guide/fonts.json +227 -0
  803. package/neural-interface/lib/style-guide/import.js +1268 -0
  804. package/neural-interface/lib/style-guide/pointers.js +103 -0
  805. package/neural-interface/lib/style-guide/presets.js +232 -0
  806. package/neural-interface/lib/style-guide/render-design-md.js +319 -0
  807. package/neural-interface/lib/style-guide/schema.js +848 -0
  808. package/neural-interface/lib/style-guide/security.js +43 -0
  809. package/neural-interface/lib/style-guide/store.js +491 -0
  810. package/neural-interface/lib/style-guide/summary.js +106 -0
  811. package/neural-interface/lib/style-guide/tokens.js +170 -0
  812. package/neural-interface/lib/synabun-update-plan.js +305 -0
  813. package/neural-interface/lib/synabun-version.js +179 -0
  814. package/neural-interface/lib/system-restore.js +99 -0
  815. package/neural-interface/lib/terminal-env.js +86 -0
  816. package/neural-interface/lib/ui-blob-store.js +106 -0
  817. package/neural-interface/lib/vector-cache.js +2 -0
  818. package/neural-interface/lib/virtual-socket.js +60 -0
  819. package/neural-interface/lib/whatsapp/api.js +146 -0
  820. package/neural-interface/lib/whatsapp/auth-store.js +389 -0
  821. package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
  822. package/neural-interface/lib/whatsapp/brain.js +128 -0
  823. package/neural-interface/lib/whatsapp/bridge.js +1499 -0
  824. package/neural-interface/lib/whatsapp/cards.js +519 -0
  825. package/neural-interface/lib/whatsapp/commands.js +92 -0
  826. package/neural-interface/lib/whatsapp/config.js +291 -0
  827. package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
  828. package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
  829. package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
  830. package/neural-interface/lib/whatsapp/connector/package.json +8 -0
  831. package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
  832. package/neural-interface/lib/whatsapp/format.js +552 -0
  833. package/neural-interface/lib/whatsapp/host-core.js +1225 -0
  834. package/neural-interface/lib/whatsapp/host.js +119 -0
  835. package/neural-interface/lib/whatsapp/identity.js +288 -0
  836. package/neural-interface/lib/whatsapp/inbound.js +149 -0
  837. package/neural-interface/lib/whatsapp/installer.js +562 -0
  838. package/neural-interface/lib/whatsapp/manager.js +638 -0
  839. package/neural-interface/lib/whatsapp/paths.js +81 -0
  840. package/neural-interface/lib/whatsapp/protocol.js +327 -0
  841. package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
  842. package/neural-interface/lib/whatsapp/redact.js +130 -0
  843. package/neural-interface/lib/whatsapp/service.js +1305 -0
  844. package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
  845. package/neural-interface/package-lock.json +4682 -0
  846. package/neural-interface/package.json +58 -27
  847. package/neural-interface/public/apple-touch-icon.png +0 -0
  848. package/neural-interface/public/category-logos/synabun.png +0 -0
  849. package/neural-interface/public/claude.svg +1 -0
  850. package/neural-interface/public/favicon-16x16.png +0 -0
  851. package/neural-interface/public/favicon-32x32.png +0 -0
  852. package/neural-interface/public/icon-192.png +0 -0
  853. package/neural-interface/public/icon-512.png +0 -0
  854. package/neural-interface/public/index.html +32 -182
  855. package/neural-interface/public/index2d.html +2 -2
  856. package/neural-interface/public/logo-synabun.png +0 -0
  857. package/neural-interface/public/logoHD.png +0 -0
  858. package/neural-interface/public/mcp.svg +4 -0
  859. package/neural-interface/public/offline.html +335 -299
  860. package/neural-interface/public/onboarding.html +692 -155
  861. package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
  862. package/neural-interface/public/shared/agent-runtime-options.js +596 -0
  863. package/neural-interface/public/shared/api.js +339 -28
  864. package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
  865. package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
  866. package/neural-interface/public/shared/assistant/asst-api.js +270 -0
  867. package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
  868. package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
  869. package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
  870. package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
  871. package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
  872. package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
  873. package/neural-interface/public/shared/assistant/asst-mascot.js +311 -0
  874. package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
  875. package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
  876. package/neural-interface/public/shared/assistant/asst-rack-styles.js +589 -0
  877. package/neural-interface/public/shared/assistant/asst-render.js +3296 -0
  878. package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
  879. package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
  880. package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
  881. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
  882. package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
  883. package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
  884. package/neural-interface/public/shared/assistant/asst-state.js +727 -0
  885. package/neural-interface/public/shared/assistant/asst-styles.js +1684 -0
  886. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
  887. package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
  888. package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
  889. package/neural-interface/public/shared/automation-context.js +75 -0
  890. package/neural-interface/public/shared/blob-store.js +99 -0
  891. package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
  892. package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
  893. package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
  894. package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
  895. package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
  896. package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
  897. package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
  898. package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
  899. package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
  900. package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
  901. package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
  902. package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
  903. package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
  904. package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
  905. package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
  906. package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
  907. package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
  908. package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
  909. package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
  910. package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
  911. package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
  912. package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
  913. package/neural-interface/public/shared/cli-status.js +169 -0
  914. package/neural-interface/public/shared/constants.js +22 -5
  915. package/neural-interface/public/shared/cp/cp-agents.js +186 -0
  916. package/neural-interface/public/shared/cp/cp-bash.js +252 -0
  917. package/neural-interface/public/shared/cp/cp-compose.js +159 -0
  918. package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
  919. package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
  920. package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
  921. package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
  922. package/neural-interface/public/shared/cp/cp-diff.js +255 -0
  923. package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
  924. package/neural-interface/public/shared/cp/cp-events.js +846 -0
  925. package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
  926. package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
  927. package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
  928. package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
  929. package/neural-interface/public/shared/cp/cp-restore.js +224 -0
  930. package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
  931. package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
  932. package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
  933. package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
  934. package/neural-interface/public/shared/cp/cp-styles.js +349 -0
  935. package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
  936. package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
  937. package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
  938. package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
  939. package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
  940. package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
  941. package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
  942. package/neural-interface/public/shared/graph-activity.js +68 -0
  943. package/neural-interface/public/shared/html-shell.js +107 -12
  944. package/neural-interface/public/shared/i18n.js +132 -34
  945. package/neural-interface/public/shared/node-positions-store.js +44 -0
  946. package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
  947. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
  948. package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
  949. package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
  950. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
  951. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
  952. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
  953. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
  954. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
  955. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
  956. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
  957. package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
  958. package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
  959. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
  960. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
  961. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
  962. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
  963. package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
  964. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
  965. package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
  966. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
  967. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
  968. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
  969. package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
  970. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
  971. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
  972. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
  973. package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
  974. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
  975. package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
  976. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
  977. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
  978. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
  979. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
  980. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
  981. package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
  982. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
  983. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
  984. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
  985. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
  986. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
  987. package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
  988. package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
  989. package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
  990. package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
  991. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
  992. package/neural-interface/public/shared/provider-icons.js +30 -0
  993. package/neural-interface/public/shared/session-title.js +100 -0
  994. package/neural-interface/public/shared/settings/settings-ia.js +389 -0
  995. package/neural-interface/public/shared/settings/settings-kit.js +234 -0
  996. package/neural-interface/public/shared/settings/settings-search.js +463 -0
  997. package/neural-interface/public/shared/settings/settings-shell.js +570 -0
  998. package/neural-interface/public/shared/state.js +5 -0
  999. package/neural-interface/public/shared/storage.js +50 -7
  1000. package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
  1001. package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
  1002. package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
  1003. package/neural-interface/public/shared/styleguide/sg-colors.js +148 -0
  1004. package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
  1005. package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
  1006. package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
  1007. package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
  1008. package/neural-interface/public/shared/styleguide/sg-import-export.js +47 -0
  1009. package/neural-interface/public/shared/styleguide/sg-kit.js +281 -0
  1010. package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
  1011. package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
  1012. package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
  1013. package/neural-interface/public/shared/styleguide/sg-panel.js +229 -0
  1014. package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
  1015. package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
  1016. package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
  1017. package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
  1018. package/neural-interface/public/shared/styleguide/sg-typography.js +124 -0
  1019. package/neural-interface/public/shared/styles.css +7883 -1155
  1020. package/neural-interface/public/shared/synabun-mascot.js +1625 -0
  1021. package/neural-interface/public/shared/synabun-ticker.js +376 -0
  1022. package/neural-interface/public/shared/term-renderer.js +343 -0
  1023. package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
  1024. package/neural-interface/public/shared/ui-assistant.js +77 -0
  1025. package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
  1026. package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
  1027. package/neural-interface/public/shared/ui-codex-panel.js +12 -0
  1028. package/neural-interface/public/shared/ui-cost-widget.js +55 -34
  1029. package/neural-interface/public/shared/ui-detail.js +77 -14
  1030. package/neural-interface/public/shared/ui-explorer.js +360 -181
  1031. package/neural-interface/public/shared/ui-file-explorer.js +485 -107
  1032. package/neural-interface/public/shared/ui-help.js +2 -2
  1033. package/neural-interface/public/shared/ui-image-gallery.js +45 -21
  1034. package/neural-interface/public/shared/ui-judgments.js +1346 -0
  1035. package/neural-interface/public/shared/ui-keybinds.js +3 -2
  1036. package/neural-interface/public/shared/ui-link.js +105 -51
  1037. package/neural-interface/public/shared/ui-loading.js +4 -4
  1038. package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
  1039. package/neural-interface/public/shared/ui-menubar.js +89 -4
  1040. package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
  1041. package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
  1042. package/neural-interface/public/shared/ui-native-window-id.js +7 -0
  1043. package/neural-interface/public/shared/ui-navbar.js +357 -222
  1044. package/neural-interface/public/shared/ui-notifications.js +58 -12
  1045. package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
  1046. package/neural-interface/public/shared/ui-panels.js +15 -7
  1047. package/neural-interface/public/shared/ui-resume.js +823 -653
  1048. package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
  1049. package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
  1050. package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
  1051. package/neural-interface/public/shared/ui-sessions.js +789 -34
  1052. package/neural-interface/public/shared/ui-settings.js +5793 -2190
  1053. package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
  1054. package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
  1055. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
  1056. package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
  1057. package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
  1058. package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
  1059. package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
  1060. package/neural-interface/public/shared/ui-skills.js +18 -32
  1061. package/neural-interface/public/shared/ui-styleguide.js +35 -0
  1062. package/neural-interface/public/shared/ui-sync.js +87 -0
  1063. package/neural-interface/public/shared/ui-terminal.js +7877 -6385
  1064. package/neural-interface/public/shared/ui-tool-updates.js +292 -0
  1065. package/neural-interface/public/shared/ui-tooltip.js +156 -15
  1066. package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
  1067. package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
  1068. package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
  1069. package/neural-interface/public/shared/ui-tutorial.css +102 -0
  1070. package/neural-interface/public/shared/ui-tutorial.js +557 -1076
  1071. package/neural-interface/public/shared/ui-update.js +420 -68
  1072. package/neural-interface/public/shared/ui-whatsapp.js +941 -0
  1073. package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
  1074. package/neural-interface/public/shared/ui-workspaces.js +2 -1
  1075. package/neural-interface/public/shared/utils.js +58 -0
  1076. package/neural-interface/public/shared/vterm-buffer.js +15 -5
  1077. package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
  1078. package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
  1079. package/neural-interface/public/sidepanel-session.html +18 -0
  1080. package/neural-interface/public/sw.js +8 -2
  1081. package/neural-interface/public/synabun.png +0 -0
  1082. package/neural-interface/public/variant/2d/graph.js +1707 -1635
  1083. package/neural-interface/public/variant/2d/lasso.js +2 -1
  1084. package/neural-interface/public/variant/2d/layout.js +595 -594
  1085. package/neural-interface/public/variant/2d/main.js +556 -521
  1086. package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
  1087. package/neural-interface/public/variant/3d/main.js +516 -500
  1088. package/neural-interface/public/variant/3d/map-controls.js +144 -0
  1089. package/neural-interface/public/variant/3d/map-data.js +251 -0
  1090. package/neural-interface/public/variant/3d/map-labels.js +241 -0
  1091. package/neural-interface/public/variant/3d/map-pick.js +90 -0
  1092. package/neural-interface/public/variant/3d/map-renderer.js +521 -0
  1093. package/neural-interface/public/variant/3d/map-shaders.js +158 -0
  1094. package/neural-interface/public/variant/3d/map-view.js +618 -0
  1095. package/neural-interface/public/vendor/three/manifest.json +3 -0
  1096. package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
  1097. package/neural-interface/public/vendor/xterm/manifest.json +3 -0
  1098. package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
  1099. package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
  1100. package/neural-interface/scripts/build-vendor.mjs +64 -0
  1101. package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
  1102. package/neural-interface/scripts/rebuild-pty.js +37 -23
  1103. package/neural-interface/scripts/vendor-three-entry.js +24 -0
  1104. package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
  1105. package/neural-interface/server.js +33948 -15000
  1106. package/neural-interface/skins/default/skin.css +90 -0
  1107. package/neural-interface/skins/default/skin.json +8 -0
  1108. package/neural-interface/templates/rulesets/README.md +9 -0
  1109. package/neural-interface/templates/rulesets/claude.md +3 -0
  1110. package/neural-interface/templates/rulesets/codex.md +2 -0
  1111. package/neural-interface/templates/rulesets/coexistence.md +6 -0
  1112. package/neural-interface/templates/rulesets/core.md +26 -0
  1113. package/neural-interface/templates/rulesets/cursor.md +2 -0
  1114. package/neural-interface/templates/rulesets/gemini.md +2 -0
  1115. package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
  1116. package/neural-interface/templates/rulesets/manifest.json +12 -0
  1117. package/neural-interface/templates/rulesets/opencode.md +2 -0
  1118. package/package.json +119 -53
  1119. package/postinstall.js +125 -26
  1120. package/preuninstall.js +187 -0
  1121. package/scripts/audit-claude-hook-usage.mjs +12 -0
  1122. package/scripts/audit-memory-duplicates.mjs +44 -0
  1123. package/scripts/measure-claude-hook-overhead.mjs +77 -0
  1124. package/scripts/recover-user-config.mjs +254 -0
  1125. package/scripts/restore-data-home-from-backup.mjs +239 -0
  1126. package/setup.js +804 -260
  1127. package/skills/leonardo/SKILL.md +19 -10
  1128. package/skills/leonardo/modules/image-prompter.md +18 -12
  1129. package/skills/leonardo/modules/video-prompter.md +19 -13
  1130. package/skills/synabun/SKILL.md +209 -185
  1131. package/skills/synabun/icon.png +0 -0
  1132. package/skills/synabun/modules/audit.md +380 -374
  1133. package/skills/synabun/modules/changelog.md +15 -6
  1134. package/skills/synabun/modules/idea.md +148 -144
  1135. package/skills/synabun/modules/memorize.md +13 -4
  1136. package/skills/synabun/modules/schedule.md +198 -0
  1137. package/skills/synabun/synabunicon.png +0 -0
  1138. package/updater.cmd +31 -0
  1139. package/updater.mjs +382 -0
  1140. package/updater.sh +29 -0
  1141. package/hooks/claude-code/test-user-learning.mjs +0 -531
  1142. package/neural-interface/public/claude-chat.css +0 -878
  1143. package/neural-interface/public/claude-chat.html +0 -92
  1144. package/neural-interface/public/claude-chat.js +0 -1168
  1145. package/neural-interface/public/index-legacy.html +0 -14677
  1146. package/neural-interface/public/index2d-legacy.html +0 -8193
  1147. package/neural-interface/public/shared/html-term-renderer.js +0 -1022
  1148. package/neural-interface/public/variant/3d/background.js +0 -884
  1149. package/neural-interface/public/variant/3d/camera.js +0 -661
  1150. package/neural-interface/public/variant/3d/gfx.js +0 -116
  1151. package/neural-interface/public/variant/3d/graph.js +0 -2399
  1152. package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
  1153. package/neural-interface/templates/CLAUDE-template.md +0 -211
@@ -11,12 +11,26 @@ import { state, emit, on } from './state.js';
11
11
  import { KEYS } from './constants.js';
12
12
  import { storage } from './storage.js';
13
13
  import { isGuest, hasPermission } from './ui-sync.js';
14
+ import { openSettingsModal } from './ui-settings.js';
15
+ import { fetchRulesStatus } from './api.js';
14
16
 
15
17
  // ── Helpers ──
16
18
 
17
19
  const $ = (id) => document.getElementById(id);
18
20
  const _genId = () => 'wb-' + Date.now() + '-' + Math.random().toString(36).slice(2, 5);
19
21
 
22
+ // Stable per-tab id (survives reloads via sessionStorage). Sent with every
23
+ // viewport/geometry/screenshot message so the server can tell windows apart
24
+ // and pick the focus-active one for MCP coordinate math + screenshots.
25
+ const _clientId = (() => {
26
+ const fresh = () => 'wbc-' + Math.random().toString(36).slice(2, 10);
27
+ try {
28
+ let id = sessionStorage.getItem('wb-client-id');
29
+ if (!id) { id = fresh(); sessionStorage.setItem('wb-client-id', id); }
30
+ return id;
31
+ } catch { return fresh(); }
32
+ })();
33
+
20
34
  // ── Internal state ──
21
35
 
22
36
  let _elements = []; // Array<{id, type, x, y, width, height, ...}>
@@ -79,19 +93,31 @@ const SECTION_TYPES = {
79
93
 
80
94
  // Persist debounce + cross-client sync
81
95
  let _persistTimer = null;
82
- let _isRemoteUpdate = false; // Suppress WS echo when receiving remote state
83
96
  const PERSIST_DELAY = 600;
84
97
  const MAX_UNDO = 50;
85
98
  const SNAP_DIST = 30; // px for arrow anchor snapping
86
99
  const MAX_IMAGE_DIM = 1920;
87
100
 
88
- /** Cross-browser clipboard write with fallback for non-secure contexts. */
101
+ // Rendered-size measurement for auto-sized text/list cards (see _runMeasurePass)
102
+ let _measureScheduled = false;
103
+ let _measurePending = false; // a pass was skipped mid-gesture; run it at mouseup
104
+ const _geometryDirty = new Map(); // id → { id, width, height } awaiting send to the server
105
+ let _geometryTimer = null;
106
+ const GEOMETRY_SEND_DELAY = 250;
107
+
108
+ /**
109
+ * Cross-browser clipboard write with fallback for non-secure contexts.
110
+ * Returns a Promise that resolves true on success, false on failure — callers
111
+ * need this to show accurate feedback (a silent failure is how the "Copy image
112
+ * path" button used to claim success while the OS clipboard was empty).
113
+ */
89
114
  function _clipboardWrite(text) {
90
115
  if (navigator.clipboard?.writeText) {
91
- navigator.clipboard.writeText(text).catch(() => _clipboardFallback(text));
92
- } else {
93
- _clipboardFallback(text);
116
+ return navigator.clipboard.writeText(text)
117
+ .then(() => true)
118
+ .catch(() => _clipboardFallback(text));
94
119
  }
120
+ return Promise.resolve(_clipboardFallback(text));
95
121
  }
96
122
  function _clipboardFallback(text) {
97
123
  const ta = document.createElement('textarea');
@@ -99,8 +125,10 @@ function _clipboardFallback(text) {
99
125
  ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0';
100
126
  document.body.appendChild(ta);
101
127
  ta.select();
102
- try { document.execCommand('copy'); } catch {}
128
+ let ok = false;
129
+ try { ok = document.execCommand('copy'); } catch {}
103
130
  ta.remove();
131
+ return ok;
104
132
  }
105
133
 
106
134
  /** Snap a value to grid if grid snapping is enabled. */
@@ -114,13 +142,93 @@ const IMAGE_QUALITY = 0.8;
114
142
  // DOM refs (set in init)
115
143
  let _root, _toolbar, _canvas, _arrowsSvg, _elementsDiv, _arrowPreview, _arrowHint;
116
144
 
145
+ // The rules reminder beside the logo: shown only while a connected tool has no
146
+ // SynaBun rules, an edited copy or damaged markers (GET /api/setup/rules).
147
+ const RULES_REMINDER_STATES = ['not-installed', 'modified', 'conflict'];
148
+ let _rulesReminderSections = []; // Settings > Setup sections of the tools that need attention
149
+
117
150
 
118
151
  // ═══════════════════════════════════════════
119
152
  // COORDINATE TRANSFORMS
120
153
  // ═══════════════════════════════════════════
121
154
 
155
+ // Cached _root rect. Reading getBoundingClientRect() inside mousemove (60Hz)
156
+ // forces a layout flush. We capture it once at drag/pen/marquee start and
157
+ // reuse it for the rest of the gesture, then invalidate on mouseup/resize/scroll.
158
+ let _rootRectCached = null;
159
+
160
+ function _getRootRect() {
161
+ if (_rootRectCached) return _rootRectCached;
162
+ if (!_root) return { left: 0, top: 0, width: 0, height: 0, right: 0, bottom: 0 };
163
+ _rootRectCached = _root.getBoundingClientRect();
164
+ return _rootRectCached;
165
+ }
166
+
167
+ function _invalidateRootRect() {
168
+ _rootRectCached = null;
169
+ }
170
+
171
+ /**
172
+ * Abort any in-flight pointer gesture. Called whenever the board state is
173
+ * replaced under the user (state:full from another client, workspace restore,
174
+ * clear) and when a mouseup can no longer reach us (window blur / hidden tab),
175
+ * so a stale _drag/_marquee/_penPoints/_shapeCreating flag can never swallow
176
+ * every subsequent mousemove. `pointerOnly` keeps a multi-click arrow in
177
+ * progress (its waypoints survive an alt-tab; Escape still cancels it).
178
+ */
179
+ function _cancelGesture({ pointerOnly = false } = {}) {
180
+ let needsRender = false;
181
+ if (_drag) {
182
+ // Positions already mutated by mousemove stay where they are — make sure
183
+ // they reach the server even though no mouseup will finalize them.
184
+ _drag = null;
185
+ persistDebounced();
186
+ }
187
+ if (_marquee) {
188
+ _marquee.rect?.remove();
189
+ _marquee = null;
190
+ }
191
+ if (_shapeCreating) {
192
+ _elements = _elements.filter(el => el.id !== _shapeCreating.id);
193
+ _shapeCreating = null;
194
+ needsRender = true;
195
+ }
196
+ if (_sectionCreating) {
197
+ _elements = _elements.filter(el => el.id !== _sectionCreating.id);
198
+ _sectionCreating = null;
199
+ needsRender = true;
200
+ }
201
+ if (_penLiveEl) _penLiveEl.remove();
202
+ _penLiveEl = null;
203
+ _penPoints = null;
204
+ if (!pointerOnly) {
205
+ _arrowCreating = null;
206
+ clearArrowPreview();
207
+ hideArrowHint();
208
+ }
209
+ document.body.classList.remove('ui-interacting');
210
+ _invalidateRootRect();
211
+ _invalidateElementMap();
212
+ if (needsRender) renderAll();
213
+ }
214
+
215
+ /**
216
+ * Prepare for a whole-board replacement (restore / clear / remote state:full):
217
+ * optionally commit + exit text/list editing, abort gestures, drop selection UI.
218
+ */
219
+ function _beginStateReplace({ exitEdits = false } = {}) {
220
+ if (exitEdits) {
221
+ exitEditMode();
222
+ exitListEditMode();
223
+ }
224
+ _cancelGesture();
225
+ hideContextMenu();
226
+ _selectedId = null;
227
+ _selectedIds.clear();
228
+ }
229
+
122
230
  function clientToCanvas(clientX, clientY) {
123
- const rect = _root.getBoundingClientRect();
231
+ const rect = _getRootRect();
124
232
  return {
125
233
  x: clientX - rect.left,
126
234
  y: clientY - rect.top,
@@ -160,10 +268,22 @@ function undo() {
160
268
  } else if (entry.action === 'move' || entry.action === 'resize' || entry.action === 'edit') {
161
269
  const idx = _elements.findIndex(el => el.id === entry.before.id);
162
270
  if (idx >= 0) _elements[idx] = { ...entry.before };
271
+ } else if (entry.action === 'batch') {
272
+ // Reverse an external batch: restore removed, restore updated befores, drop added
273
+ for (const el of entry.before.removed) _elements.push({ ...el });
274
+ for (const snap of entry.before.updated) {
275
+ const idx = _elements.findIndex(el => el.id === snap.id);
276
+ if (idx >= 0) _elements[idx] = { ...snap, points: snap.points ? snap.points.map(p => [...p]) : undefined };
277
+ }
278
+ if (entry.before.added.length) {
279
+ const ids = new Set(entry.before.added);
280
+ _elements = _elements.filter(el => !ids.has(el.id));
281
+ }
163
282
  }
164
283
 
165
284
  _selectedId = null;
166
285
  _selectedIds.clear();
286
+ _invalidateElementMap();
167
287
  renderAll();
168
288
  persistDebounced();
169
289
  updateUndoButtons();
@@ -193,10 +313,22 @@ function redo() {
193
313
  const idx = _elements.findIndex(el => el.id === entry.after.id);
194
314
  if (idx >= 0) _elements[idx] = { ...entry.after };
195
315
  else _elements.push({ ...entry.after });
316
+ } else if (entry.action === 'batch') {
317
+ const removedIds = new Set(entry.before.removed.map(el => el.id));
318
+ if (removedIds.size) _elements = _elements.filter(el => !removedIds.has(el.id));
319
+ for (const snap of entry.after.updated) {
320
+ const idx = _elements.findIndex(el => el.id === snap.id);
321
+ const copy = { ...snap, points: snap.points ? snap.points.map(p => [...p]) : undefined };
322
+ if (idx >= 0) _elements[idx] = copy; else _elements.push(copy);
323
+ }
324
+ for (const el of entry.after.added) {
325
+ if (!_elements.some(e => e.id === el.id)) _elements.push({ ...el });
326
+ }
196
327
  }
197
328
 
198
329
  _selectedId = null;
199
330
  _selectedIds.clear();
331
+ _invalidateElementMap();
200
332
  renderAll();
201
333
  persistDebounced();
202
334
  updateUndoButtons();
@@ -209,6 +341,64 @@ function updateUndoButtons() {
209
341
  if (redoBtn) redoBtn.classList.toggle('disabled', !_redoStack.length);
210
342
  }
211
343
 
344
+ function isRulesetAlertDismissed() {
345
+ return storage.getItem(KEYS.WHITEBOARD_RULESET_ALERT_DISMISSED) === '1';
346
+ }
347
+
348
+ function syncRulesetAlertVisibility() {
349
+ const alert = $('wb-ruleset-alert');
350
+ if (!alert) return;
351
+ const logo = document.querySelector('#static-bg .static-bg-logo');
352
+ const logoHidden = logo?.style.display === 'none';
353
+ alert.classList.toggle('hidden', isRulesetAlertDismissed() || logoHidden || _rulesReminderSections.length === 0);
354
+ }
355
+
356
+ // Which connected tools need their rules looked at. A tool the user opted out
357
+ // of (managed === false) is not nagged about; an unknown status (no route, a
358
+ // failed request, a guest) shows nothing.
359
+ function applyRulesStatusToAlert(status) {
360
+ const hosts = status?.ok && status.hosts ? status.hosts : {};
361
+ _rulesReminderSections = Object.entries(hosts)
362
+ .filter(([, info]) => info?.mcp && RULES_REMINDER_STATES.includes(info.state) && !(info.state === 'not-installed' && info.managed === false))
363
+ .map(([host]) => `setup-${host}`);
364
+ syncRulesetAlertVisibility();
365
+ }
366
+
367
+ async function refreshRulesetAlert() {
368
+ if (isRulesetAlertDismissed()) return;
369
+ applyRulesStatusToAlert(await fetchRulesStatus());
370
+ }
371
+
372
+ function initRulesetAlert() {
373
+ const alert = $('wb-ruleset-alert');
374
+ if (!alert) return;
375
+
376
+ const setupBtn = $('wb-ruleset-settings');
377
+ const dismissBtn = $('wb-ruleset-dismiss');
378
+
379
+ setupBtn?.addEventListener('click', (e) => {
380
+ e.stopPropagation();
381
+ openSettingsModal({
382
+ tab: 'ai-connections',
383
+ expand: ['setup-rules', ..._rulesReminderSections],
384
+ highlight: _rulesReminderSections.length ? _rulesReminderSections : ['setup-rules'],
385
+ scrollTo: 'setup-rules',
386
+ });
387
+ });
388
+
389
+ dismissBtn?.addEventListener('click', (e) => {
390
+ e.stopPropagation();
391
+ storage.setItem(KEYS.WHITEBOARD_RULESET_ALERT_DISMISSED, '1');
392
+ syncRulesetAlertVisibility();
393
+ });
394
+
395
+ // Settings > Setup passes the fresh status on after every rules action.
396
+ on('rules:changed', applyRulesStatusToAlert);
397
+
398
+ syncRulesetAlertVisibility();
399
+ refreshRulesetAlert();
400
+ }
401
+
212
402
 
213
403
  // ═══════════════════════════════════════════
214
404
  // PERSISTENCE
@@ -218,7 +408,7 @@ function updateUndoButtons() {
218
408
  function flushListEdits() {
219
409
  const dom = _root?.querySelector('.wb-list.editing');
220
410
  if (!dom) return;
221
- const ul = dom.querySelector('ul');
411
+ const ul = dom.querySelector('ul, ol');
222
412
  const elId = dom.dataset.wbId;
223
413
  const el = _elements.find(e => e.id === elId);
224
414
  if (el && ul) {
@@ -244,22 +434,35 @@ function persistDebounced() {
244
434
  flushTextEdits();
245
435
  flushListEdits();
246
436
  const snapshot = { elements: _elements, nextZIndex: _nextZIndex };
247
- storage.setItem(KEYS.WHITEBOARD, JSON.stringify(snapshot));
248
- // Send to server for cross-client sync (skip if remote update or guest without whiteboard perm)
249
- if (_ws && _ws.readyState === 1 && !_isRemoteUpdate && !(isGuest() && !hasPermission('whiteboard'))) {
437
+ // Guest edits without whiteboard permission never persist. (Remote-originated
438
+ // changes never reach here: the external command handlers don't persist.)
439
+ if (isGuest() && !hasPermission('whiteboard')) return;
440
+ if (_ws && _ws.readyState === 1) {
441
+ // Live cross-client relay + server persistence (saveWhiteboard) in one.
442
+ // The whiteboard no longer rides the ui-state PATCH cycle.
250
443
  _ws.send(JSON.stringify({ type: 'state:sync', snapshot }));
444
+ } else {
445
+ // Socket down — persist via REST so a reload doesn't lose the edit.
446
+ fetch('/api/whiteboard', {
447
+ method: 'PUT',
448
+ headers: { 'Content-Type': 'application/json' },
449
+ body: JSON.stringify(snapshot),
450
+ }).catch(() => {});
251
451
  }
252
452
  }, PERSIST_DELAY);
253
453
  }
254
454
 
255
- function loadPersisted() {
455
+ async function loadPersisted() {
456
+ // Board now lives in its own server store (data/whiteboard.json), not in the
457
+ // client storage / ui-state PATCH cycle. Fetch it on init.
256
458
  try {
257
- const raw = storage.getItem(KEYS.WHITEBOARD);
258
- if (!raw) return;
259
- const data = JSON.parse(raw);
459
+ const res = await fetch('/api/whiteboard');
460
+ if (!res.ok) return;
461
+ const data = await res.json();
260
462
  if (Array.isArray(data.elements)) _elements = data.elements;
261
463
  if (data.nextZIndex) _nextZIndex = data.nextZIndex;
262
- } catch { /* ignore corrupt data */ }
464
+ _invalidateElementMap();
465
+ } catch { /* server unreachable — start empty */ }
263
466
  }
264
467
 
265
468
 
@@ -290,14 +493,44 @@ function shortenPoints(pts, amount) {
290
493
  return result;
291
494
  }
292
495
 
496
+ /** Box size for anchor/centre math. Prefers the stored size — for text/list
497
+ * cards that is the browser-measured (or server-estimated) footprint — and
498
+ * only falls back to the legacy 100x60 phantom when nothing is stored. */
499
+ function _elBox(el) {
500
+ return {
501
+ w: el.width > 0 ? el.width : 100,
502
+ h: el.height > 0 ? el.height : 60,
503
+ };
504
+ }
505
+
506
+ /** Anchor-adjusted copy of an arrow's points — the exact geometry the DOM
507
+ * renders. Shared by renderArrows, the drag fast path and the screenshot so
508
+ * the three can never disagree. Resolution is progressive on the same copy
509
+ * (a 2-point arrow's end is aimed at the resolved start); the server-side
510
+ * port in mcp-server/src/services/whiteboard-geometry.ts mirrors this. */
511
+ function resolveArrowPoints(el) {
512
+ const pts = (el.points || []).map(p => [...p]);
513
+ if (pts.length < 2) return pts;
514
+ if (el.startAnchor) {
515
+ const pt = getAnchorPoint(el.startAnchor, pts[1][0], pts[1][1]);
516
+ if (pt) pts[0] = [pt.x, pt.y];
517
+ }
518
+ if (el.endAnchor) {
519
+ const pt = getAnchorPoint(el.endAnchor, pts[pts.length - 2][0], pts[pts.length - 2][1]);
520
+ if (pt) pts[pts.length - 1] = [pt.x, pt.y];
521
+ }
522
+ return pts;
523
+ }
524
+
293
525
  /** Get arrow endpoint from an anchored element. Returns edge intersection point. */
294
526
  function getAnchorPoint(anchorId, fromX, fromY) {
295
- const el = _elements.find(e => e.id === anchorId);
527
+ const el = _findEl(anchorId);
296
528
  if (!el || el.type === 'arrow') return null;
297
- const cx = el.x + (el.width || 100) / 2;
298
- const cy = el.y + (el.height || 60) / 2;
299
- const hw = (el.width || 100) / 2;
300
- const hh = (el.height || 60) / 2;
529
+ const { w, h } = _elBox(el);
530
+ const cx = el.x + w / 2;
531
+ const cy = el.y + h / 2;
532
+ const hw = w / 2;
533
+ const hh = h / 2;
301
534
 
302
535
  const dx = fromX - cx;
303
536
  const dy = fromY - cy;
@@ -320,8 +553,9 @@ function findNearestAnchor(cx, cy, excludeId) {
320
553
  let bestDist = SNAP_DIST;
321
554
  for (const el of _elements) {
322
555
  if (el.type === 'arrow' || el.id === excludeId) continue;
323
- const ecx = el.x + (el.width || 100) / 2;
324
- const ecy = el.y + (el.height || 60) / 2;
556
+ const box = _elBox(el);
557
+ const ecx = el.x + box.w / 2;
558
+ const ecy = el.y + box.h / 2;
325
559
  const d = Math.sqrt((cx - ecx) ** 2 + (cy - ecy) ** 2);
326
560
  if (d < bestDist) {
327
561
  bestDist = d;
@@ -331,6 +565,19 @@ function findNearestAnchor(cx, cy, excludeId) {
331
565
  return best;
332
566
  }
333
567
 
568
+ /** Ids of arrows anchored (start or end) to any element id in `movedIds`.
569
+ * Precomputed at drag start so the per-frame fast path only re-routes the
570
+ * handful of arrows actually pinned to the element(s) being moved. */
571
+ function _anchoredArrowIds(movedIds) {
572
+ const set = movedIds instanceof Set ? movedIds : new Set(movedIds);
573
+ const out = [];
574
+ for (const a of _elements) {
575
+ if (a.type !== 'arrow') continue;
576
+ if (set.has(a.startAnchor) || set.has(a.endAnchor)) out.push(a.id);
577
+ }
578
+ return out;
579
+ }
580
+
334
581
 
335
582
  // ═══════════════════════════════════════════
336
583
  // IMAGE COMPRESSION
@@ -366,13 +613,60 @@ function compressImage(blob) {
366
613
  // RENDERING
367
614
  // ═══════════════════════════════════════════
368
615
 
616
+ // Element ID → element lookup cache. Rebuilt lazily; many mousemove paths
617
+ // previously did O(n) Array.prototype.find on every event.
618
+ //
619
+ // The cache is SELF-VALIDATING. An earlier version keyed it on array length
620
+ // only, so a same-count delete→add (or a whole-array replacement from a
621
+ // workspace restore / another client's state:full) left it pointing at ghost
622
+ // objects or missing new ids — drags silently no-op'd until the count changed
623
+ // again (the "board is locked until I draw a pen stroke" bug). Every hit is now
624
+ // checked against the live array by index and the map is rebuilt on any
625
+ // mismatch, miss, identity change or length change.
626
+ let _elementMap = null; // Map<id, { el, idx }>
627
+ let _elementMapSrc = null; // the _elements array the map was built from
628
+ let _elementMapLen = -1;
629
+
630
+ function _rebuildElementMap() {
631
+ _elementMap = new Map();
632
+ for (let i = 0; i < _elements.length; i++) {
633
+ _elementMap.set(_elements[i].id, { el: _elements[i], idx: i });
634
+ }
635
+ _elementMapSrc = _elements;
636
+ _elementMapLen = _elements.length;
637
+ }
638
+
639
+ function _findEl(id) {
640
+ if (!id) return null;
641
+ if (!_elementMap || _elementMapSrc !== _elements || _elementMapLen !== _elements.length) {
642
+ _rebuildElementMap();
643
+ }
644
+ const hit = _elementMap.get(id);
645
+ if (hit && _elements[hit.idx] === hit.el) return hit.el;
646
+ // Stale entry (object replaced in place / same-length splice+push) or a
647
+ // brand-new id: rebuild once and retry.
648
+ _rebuildElementMap();
649
+ const again = _elementMap.get(id);
650
+ return again ? again.el : null;
651
+ }
652
+
653
+ function _invalidateElementMap() {
654
+ _elementMap = null;
655
+ _elementMapSrc = null;
656
+ _elementMapLen = -1;
657
+ }
658
+
659
+ // Synchronous render. Earlier perf pass tried rAF-batching this — that caused
660
+ // visible drag lag because the move/resize/rotate mousemove paths each call
661
+ // renderAll() once and rely on the DOM update completing before the next
662
+ // pointer event. Keep the call synchronous; the real per-frame cost lives in
663
+ // _findEl/_getRootRect (already optimized), not in scheduling.
369
664
  function renderAll() {
370
665
  renderElements();
371
666
  renderArrows();
372
- // Reposition context menu if element(s) selected
373
667
  if (_ctxMenu && _selectedIds.size > 0) {
374
668
  if (_selectedIds.size === 1) {
375
- const el = _elements.find(e => e.id === _selectedId);
669
+ const el = _findEl(_selectedId);
376
670
  if (el) positionContextMenu(el);
377
671
  } else {
378
672
  positionContextMenuMulti();
@@ -380,6 +674,8 @@ function renderAll() {
380
674
  }
381
675
  }
382
676
 
677
+ function renderAllImmediate() { renderAll(); }
678
+
383
679
  function renderElements() {
384
680
  if (!_elementsDiv) return;
385
681
  const existing = new Map();
@@ -403,6 +699,79 @@ function renderElements() {
403
699
  for (const [id, dom] of existing) {
404
700
  if (!activeIds.has(id)) dom.remove();
405
701
  }
702
+
703
+ _scheduleMeasure();
704
+ }
705
+
706
+ // ── Rendered-size measurement (text/list auto-size) ──────────────────
707
+ // Text and list cards size themselves from content (CSS width:max-content), so
708
+ // their stored width/height used to be 0/undefined forever. That left the MCP
709
+ // tools, the server's viewport clamp and arrow anchoring blind to their real
710
+ // footprint. After every render we read the border-box size once (coalesced
711
+ // into a single rAF pass, never during a gesture) and write it back into the
712
+ // element, then ship it to the server as a `geometry` message. This pass never
713
+ // pushes undo entries, never persists the board and never re-renders — no loops.
714
+
715
+ function _scheduleMeasure() {
716
+ if (_measureScheduled) return;
717
+ _measureScheduled = true;
718
+ requestAnimationFrame(_runMeasurePass);
719
+ }
720
+
721
+ function _gestureActive() {
722
+ return !!(_drag || _marquee || _shapeCreating || _sectionCreating || _penPoints);
723
+ }
724
+
725
+ function _runMeasurePass() {
726
+ _measureScheduled = false;
727
+ if (!_elementsDiv) return;
728
+ if (_gestureActive()) { _measurePending = true; return; }
729
+
730
+ const domById = new Map();
731
+ for (const child of _elementsDiv.children) domById.set(child.dataset.wbId, child);
732
+
733
+ const changedIds = [];
734
+ for (const el of _elements) {
735
+ if (el.type !== 'text' && el.type !== 'list') continue;
736
+ const dom = domById.get(el.id);
737
+ if (!dom) continue;
738
+ if (dom.classList.contains('editing') || dom.contains(document.activeElement)) continue;
739
+ const w = dom.offsetWidth, h = dom.offsetHeight; // border-box; immune to transform:rotate
740
+ if (!w || !h) continue;
741
+ if (el.measured && Math.abs((el.width || 0) - w) < 1 && Math.abs((el.height || 0) - h) < 1) continue;
742
+ el.width = w;
743
+ el.height = h;
744
+ el.measured = true;
745
+ delete el.estimated;
746
+ _geometryDirty.set(el.id, { id: el.id, width: w, height: h });
747
+ changedIds.push(el.id);
748
+ }
749
+ if (!changedIds.length) return;
750
+
751
+ // Arrows pinned to a re-measured card need re-routing (the edge intersection
752
+ // depends on the box) — touch only those paths, never the whole SVG.
753
+ for (const aid of _anchoredArrowIds(changedIds)) {
754
+ const a = _findEl(aid);
755
+ if (a) _renderOneArrowPaths(a);
756
+ }
757
+ if (_ctxMenu && _selectedIds.size === 1) {
758
+ const sel = _findEl(_selectedId);
759
+ if (sel) positionContextMenu(sel);
760
+ }
761
+ _queueGeometrySend();
762
+ }
763
+
764
+ function _queueGeometrySend() {
765
+ clearTimeout(_geometryTimer);
766
+ _geometryTimer = setTimeout(() => {
767
+ _geometryTimer = null;
768
+ if (!_geometryDirty.size) return;
769
+ if (isGuest() && !hasPermission('whiteboard')) { _geometryDirty.clear(); return; }
770
+ if (!_ws || _ws.readyState !== 1) return; // stays dirty — flushed on the next open/pass
771
+ const items = [..._geometryDirty.values()];
772
+ _geometryDirty.clear();
773
+ _ws.send(JSON.stringify({ type: 'geometry', clientId: _clientId, items }));
774
+ }, GEOMETRY_SEND_DELAY);
406
775
  }
407
776
 
408
777
  function createElementDOM(el) {
@@ -410,11 +779,12 @@ function createElementDOM(el) {
410
779
  div.dataset.wbId = el.id;
411
780
 
412
781
  if (el.type === 'text') {
782
+ // No resize handle: text cards auto-size from content (width:max-content).
413
783
  div.className = 'wb-text';
414
- div.innerHTML = `<div class="wb-text-content" contenteditable="false"></div><div class="wb-resize-handle"></div>`;
784
+ div.innerHTML = `<div class="wb-text-content" contenteditable="false"></div>`;
415
785
  } else if (el.type === 'list') {
416
786
  div.className = 'wb-list';
417
- div.innerHTML = `<ul></ul><div class="wb-resize-handle"></div>`;
787
+ div.innerHTML = el.ordered ? '<ol></ol>' : '<ul></ul>';
418
788
  } else if (el.type === 'shape') {
419
789
  div.className = 'wb-shape';
420
790
  const ns = 'http://www.w3.org/2000/svg';
@@ -426,7 +796,8 @@ function createElementDOM(el) {
426
796
  div.appendChild(handle);
427
797
  } else if (el.type === 'image') {
428
798
  div.className = 'wb-image';
429
- div.innerHTML = `<img src="" alt=""><span class="wb-copy-path" data-tooltip="Copy image path"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></span><div class="wb-resize-handle"></div>`;
799
+ div.setAttribute('draggable', 'false');
800
+ div.innerHTML = `<img src="" alt="" draggable="false"><span class="wb-copy-path" data-tooltip="Copy image path"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></span><div class="wb-resize-handle"></div>`;
430
801
  // Wire click-to-copy-path
431
802
  const copyBtn = div.querySelector('.wb-copy-path');
432
803
  copyBtn.addEventListener('click', (e) => {
@@ -444,13 +815,21 @@ function createElementDOM(el) {
444
815
  const msg = JSON.parse(evt.data);
445
816
  if (msg.type === 'image_saved' && msg.path) {
446
817
  _ws.removeEventListener('message', handler);
447
- _clipboardWrite(msg.path);
448
- copyBtn.dataset.tooltip = 'Copied!';
449
- copyBtn.classList.add('wb-copied');
450
- setTimeout(() => {
451
- copyBtn.dataset.tooltip = 'Copy image path';
452
- copyBtn.classList.remove('wb-copied');
453
- }, 1500);
818
+ Promise.resolve(_clipboardWrite(msg.path)).then((ok) => {
819
+ if (ok) {
820
+ copyBtn.dataset.tooltip = 'Copied!';
821
+ copyBtn.classList.add('wb-copied');
822
+ } else {
823
+ // Clipboard write silently failed (usually: page not focused,
824
+ // non-secure context, or browser permission). Surface it so
825
+ // the user doesn't paste an empty clipboard into the CLI.
826
+ copyBtn.dataset.tooltip = 'Copy blocked — focus page & retry';
827
+ }
828
+ setTimeout(() => {
829
+ copyBtn.dataset.tooltip = 'Copy image path';
830
+ copyBtn.classList.remove('wb-copied');
831
+ }, 1800);
832
+ });
454
833
  }
455
834
  } catch {}
456
835
  };
@@ -483,19 +862,34 @@ function updateElementDOM(dom, el) {
483
862
  if (el.type === 'text') {
484
863
  const content = dom.querySelector('.wb-text-content');
485
864
  if (content && !content.matches(':focus')) {
486
- content.innerText = el.content || '';
865
+ // Skip the write when unchanged — this runs on every drag frame and
866
+ // setting innerText forces a layout.
867
+ const key = el.content || '';
868
+ if (dom._wbTextKey !== key) { content.innerText = key; dom._wbTextKey = key; }
487
869
  }
488
870
  if (el.fontSize) dom.style.fontSize = el.fontSize + 'px';
489
871
  dom.style.fontWeight = el.bold ? '700' : '400';
490
872
  dom.style.fontStyle = el.italic ? 'italic' : 'normal';
491
873
  if (el.color) dom.style.color = el.color;
492
874
  } else if (el.type === 'list') {
493
- const ul = dom.querySelector('ul');
494
- if (ul && !dom.classList.contains('editing')) {
875
+ let list = dom.querySelector('ul, ol');
876
+ if (list && !dom.classList.contains('editing')) {
495
877
  const items = el.items && el.items.length ? el.items : [''];
496
- ul.innerHTML = items.map(item =>
497
- `<li>${item.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</li>`
498
- ).join('');
878
+ const wantTag = el.ordered ? 'OL' : 'UL';
879
+ if (list.tagName !== wantTag) {
880
+ // ordered flag changed — swap the container (never while editing)
881
+ const swapped = document.createElement(wantTag.toLowerCase());
882
+ list.replaceWith(swapped);
883
+ list = swapped;
884
+ dom._wbListKey = null;
885
+ }
886
+ const key = wantTag + '|' + JSON.stringify(items);
887
+ if (dom._wbListKey !== key) {
888
+ list.innerHTML = items.map(item =>
889
+ `<li>${item.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</li>`
890
+ ).join('');
891
+ dom._wbListKey = key;
892
+ }
499
893
  }
500
894
  if (el.fontSize) dom.style.fontSize = el.fontSize + 'px';
501
895
  if (el.color) dom.style.color = el.color;
@@ -537,6 +931,47 @@ function updateElementDOM(dom, el) {
537
931
  }
538
932
  }
539
933
 
934
+ // ── Arrowhead markers ──
935
+ // The static <defs> in html-shell.js only carry a white and a selected (blue)
936
+ // head. Arrows honour el.color on the shaft, so the head must follow: one
937
+ // <marker> per distinct colour, created on demand inside the persistent <defs>
938
+ // (renderArrows re-appends that node, so generated markers survive rebuilds).
939
+ const _markerIds = new Map(); // color → marker id
940
+
941
+ function _arrowMarkerId(color) {
942
+ if (!color) return 'wb-arrowhead';
943
+ let id = _markerIds.get(color);
944
+ if (!id) {
945
+ id = 'wb-ah-' + color.replace(/[^a-zA-Z0-9]/g, '_');
946
+ _markerIds.set(color, id);
947
+ }
948
+ return id;
949
+ }
950
+
951
+ function _ensureArrowMarker(color) {
952
+ const id = _arrowMarkerId(color);
953
+ if (!color || !_arrowsSvg) return id;
954
+ const defs = _arrowsSvg.querySelector('defs');
955
+ if (!defs || defs.querySelector('#' + id)) return id;
956
+ const ns = 'http://www.w3.org/2000/svg';
957
+ const marker = document.createElementNS(ns, 'marker');
958
+ marker.setAttribute('id', id);
959
+ marker.setAttribute('markerWidth', '20');
960
+ marker.setAttribute('markerHeight', '20');
961
+ marker.setAttribute('refX', '2');
962
+ marker.setAttribute('refY', '10');
963
+ marker.setAttribute('orient', 'auto');
964
+ marker.setAttribute('markerUnits', 'userSpaceOnUse');
965
+ marker.setAttribute('overflow', 'visible');
966
+ const head = document.createElementNS(ns, 'path');
967
+ head.setAttribute('d', 'M 2 2 L 18 10 L 2 18 Z');
968
+ head.setAttribute('fill', color);
969
+ head.setAttribute('stroke', 'none');
970
+ marker.appendChild(head);
971
+ defs.appendChild(marker);
972
+ return id;
973
+ }
974
+
540
975
  function renderArrows() {
541
976
  if (!_arrowsSvg) return;
542
977
  const defs = _arrowsSvg.querySelector('defs');
@@ -546,19 +981,9 @@ function renderArrows() {
546
981
  for (const el of _elements) {
547
982
  // ── Arrows ──
548
983
  if (el.type === 'arrow') {
549
- const pts = (el.points || []).map(p => [...p]);
984
+ const pts = resolveArrowPoints(el);
550
985
  if (pts.length < 2) continue;
551
986
 
552
- // Apply anchor offsets to first/last points
553
- if (el.startAnchor) {
554
- const pt = getAnchorPoint(el.startAnchor, pts[1][0], pts[1][1]);
555
- if (pt) { pts[0] = [pt.x, pt.y]; }
556
- }
557
- if (el.endAnchor) {
558
- const pt = getAnchorPoint(el.endAnchor, pts[pts.length - 2][0], pts[pts.length - 2][1]);
559
- if (pt) { pts[pts.length - 1] = [pt.x, pt.y]; }
560
- }
561
-
562
987
  const isSel = _selectedIds.has(el.id);
563
988
  const d = computeArrowPath(pts);
564
989
 
@@ -575,11 +1000,11 @@ function renderArrows() {
575
1000
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
576
1001
  path.setAttribute('d', d);
577
1002
  path.setAttribute('fill', 'none');
578
- path.setAttribute('stroke', isSel ? 'var(--accent-blue, #60a5fa)' : 'rgba(255,255,255,0.8)');
1003
+ path.setAttribute('stroke', isSel ? 'var(--accent-blue, #60a5fa)' : (el.color || 'rgba(255,255,255,0.8)'));
579
1004
  path.setAttribute('stroke-width', isSel ? '3' : '2');
580
1005
  path.setAttribute('stroke-linecap', 'butt');
581
1006
  path.setAttribute('stroke-linejoin', 'round');
582
- path.setAttribute('marker-end', isSel ? 'url(#wb-arrowhead-sel)' : 'url(#wb-arrowhead)');
1007
+ path.setAttribute('marker-end', isSel ? 'url(#wb-arrowhead-sel)' : `url(#${_ensureArrowMarker(el.color)})`);
583
1008
  path.dataset.wbId = el.id;
584
1009
  path.style.pointerEvents = 'none';
585
1010
  if (isSel) path.classList.add('selected');
@@ -616,13 +1041,92 @@ function renderArrows() {
616
1041
  }
617
1042
  }
618
1043
 
1044
+ // ── Drag fast path ──────────────────────────────────────────
1045
+ // During a single/multi-element drag we update ONLY the affected DOM node(s)
1046
+ // and ONLY the arrows anchored to them, instead of the full renderAll()
1047
+ // (which restyles every element AND tears down/rebuilds the entire arrows SVG
1048
+ // every mousemove). All synchronous — the 2026-05-11 rAF-batching attempt was
1049
+ // reverted because deferring breaks pointer responsiveness; the win here is
1050
+ // eliminating per-frame work, not scheduling it. Each helper returns false if
1051
+ // a target node is missing so the caller can fall back to renderAll() once.
1052
+
1053
+ /** Re-route one arrow/pen's existing <path> nodes in place (hit + visible),
1054
+ * recomputing `d` exactly as renderArrows() does. No innerHTML, no createElementNS. */
1055
+ function _renderOneArrowPaths(el) {
1056
+ if (!_arrowsSvg) return false;
1057
+ const nodes = _arrowsSvg.querySelectorAll(`path[data-wb-id="${el.id}"]`);
1058
+ if (!nodes.length) return false;
1059
+ let d;
1060
+ if (el.type === 'arrow') {
1061
+ const pts = resolveArrowPoints(el);
1062
+ if (pts.length < 2) return true;
1063
+ d = computeArrowPath(pts);
1064
+ } else { // pen
1065
+ d = el.pathD || pointsToSmoothPath(el.points || []);
1066
+ }
1067
+ for (const n of nodes) n.setAttribute('d', d);
1068
+ return true;
1069
+ }
1070
+
1071
+ /** Update a single element's DOM node via the existing updateElementDOM (which
1072
+ * handles position, size, rotation, shape inner-SVG redraw, section recolor). */
1073
+ function updateElementPosition(id) {
1074
+ if (!_elementsDiv) return false;
1075
+ const el = _findEl(id);
1076
+ if (!el) return false;
1077
+ const dom = _elementsDiv.querySelector(`[data-wb-id="${id}"]`);
1078
+ if (!dom) return false;
1079
+ updateElementDOM(dom, el);
1080
+ return true;
1081
+ }
1082
+
1083
+ /** Per-frame drag update. `ids` = moved element ids (Set|array);
1084
+ * `anchoredArrowIds` = arrows pinned to moved DOM elements (precomputed at
1085
+ * drag start). Returns false → caller does a one-shot renderAll() fallback. */
1086
+ function _fastDragUpdate(ids, anchoredArrowIds) {
1087
+ const idSet = ids instanceof Set ? ids : new Set(ids);
1088
+ let ok = true;
1089
+ for (const id of idSet) {
1090
+ const el = _findEl(id);
1091
+ if (!el) { ok = false; continue; }
1092
+ if (el.type === 'arrow' || el.type === 'pen') {
1093
+ if (!_renderOneArrowPaths(el)) ok = false;
1094
+ } else {
1095
+ if (!updateElementPosition(id)) ok = false;
1096
+ }
1097
+ }
1098
+ if (anchoredArrowIds) {
1099
+ for (const aid of anchoredArrowIds) {
1100
+ if (idSet.has(aid)) continue; // already updated above
1101
+ const a = _findEl(aid);
1102
+ if (a && !_renderOneArrowPaths(a)) ok = false;
1103
+ }
1104
+ }
1105
+ // Keep selection chrome tracking the element. positionContextMenu* now read
1106
+ // the cached gesture rect (_getRootRect), so this adds no per-frame layout.
1107
+ if (_ctxMenu && _selectedIds.size > 0) {
1108
+ if (_selectedIds.size === 1) {
1109
+ const sel = _findEl(_selectedId);
1110
+ if (sel) positionContextMenu(sel);
1111
+ } else {
1112
+ positionContextMenuMulti();
1113
+ }
1114
+ }
1115
+ return ok;
1116
+ }
1117
+
619
1118
 
620
1119
  // ═══════════════════════════════════════════
621
1120
  // ELEMENT CRUD
622
1121
  // ═══════════════════════════════════════════
623
1122
 
624
- function addElement(el, { skipPersist = false } = {}) {
625
- el.zIndex = _nextZIndex++;
1123
+ function addElement(el, { skipPersist = false, keepZIndex = false } = {}) {
1124
+ if (keepZIndex && Number.isFinite(el.zIndex)) {
1125
+ // Server-assigned z-order (MCP adds): honour it and keep our counter ahead.
1126
+ _nextZIndex = Math.max(_nextZIndex, el.zIndex + 1);
1127
+ } else {
1128
+ el.zIndex = _nextZIndex++;
1129
+ }
626
1130
  _elements.push(el);
627
1131
  pushUndo('add', null, { ...el });
628
1132
  renderAll();
@@ -637,8 +1141,9 @@ function deleteElement(id) {
637
1141
  _elements.splice(idx, 1);
638
1142
 
639
1143
  // Detach arrows anchored to deleted element
640
- const cx = el.x + (el.width || 100) / 2;
641
- const cy = el.y + (el.height || 60) / 2;
1144
+ const box = _elBox(el);
1145
+ const cx = el.x + box.w / 2;
1146
+ const cy = el.y + box.h / 2;
642
1147
  for (const a of _elements) {
643
1148
  if (a.type !== 'arrow' || !a.points) continue;
644
1149
  if (a.startAnchor === id) {
@@ -666,8 +1171,9 @@ function deleteMultipleElements(ids) {
666
1171
  const el = { ..._elements[idx] };
667
1172
  _elements.splice(idx, 1);
668
1173
  // Detach arrows anchored to this element
669
- const cx = el.x + (el.width || 100) / 2;
670
- const cy = el.y + (el.height || 60) / 2;
1174
+ const box = _elBox(el);
1175
+ const cx = el.x + box.w / 2;
1176
+ const cy = el.y + box.h / 2;
671
1177
  for (const a of _elements) {
672
1178
  if (a.type !== 'arrow' || !a.points) continue;
673
1179
  if (a.startAnchor === id) { a.points[0] = [cx, cy]; a.startAnchor = null; }
@@ -917,6 +1423,7 @@ function showContextMenu(id) {
917
1423
  startX: ev.clientX, startY: ev.clientY,
918
1424
  origX: el2.x, origY: el2.y,
919
1425
  before: { ...el2 },
1426
+ anchoredArrows: _anchoredArrowIds([id]),
920
1427
  };
921
1428
  });
922
1429
 
@@ -942,7 +1449,7 @@ function getElementCenter(el) {
942
1449
  }
943
1450
 
944
1451
  function positionContextMenu(el) {
945
- const rootRect = _root?.getBoundingClientRect();
1452
+ const rootRect = _getRootRect(); // cached for the gesture — no per-frame layout flush
946
1453
  if (!rootRect) return;
947
1454
 
948
1455
  let cx, topY, rightX, bottomY, leftX;
@@ -1060,7 +1567,7 @@ function showMultiContextMenu() {
1060
1567
 
1061
1568
  function positionContextMenuMulti() {
1062
1569
  if (!_ctxMenu || _selectedIds.size < 2) return;
1063
- const rootRect = _root?.getBoundingClientRect();
1570
+ const rootRect = _getRootRect(); // cached for the gesture — no per-frame layout flush
1064
1571
  if (!rootRect) return;
1065
1572
 
1066
1573
  const bounds = getMultiSelectBounds();
@@ -1541,6 +2048,7 @@ function enterEditMode(dom, elId) {
1541
2048
  el.content = content.innerText.trimEnd() || '';
1542
2049
  pushUndo('edit', before, { ...el });
1543
2050
  persistDebounced();
2051
+ _scheduleMeasure(); // content changed without a render — re-measure the card
1544
2052
  }
1545
2053
  };
1546
2054
  content.addEventListener('blur', onBlur);
@@ -1552,7 +2060,7 @@ function exitEditMode() {
1552
2060
  }
1553
2061
 
1554
2062
  function enterListEditMode(dom, elId) {
1555
- const ul = dom.querySelector('ul');
2063
+ const ul = dom.querySelector('ul, ol');
1556
2064
  if (!ul) return;
1557
2065
  dom.classList.add('editing');
1558
2066
 
@@ -1575,7 +2083,7 @@ function enterListEditMode(dom, elId) {
1575
2083
  function exitListEditMode() {
1576
2084
  const dom = _root?.querySelector('.wb-list.editing');
1577
2085
  if (!dom) return;
1578
- const ul = dom.querySelector('ul');
2086
+ const ul = dom.querySelector('ul, ol');
1579
2087
  const elId = dom.dataset.wbId;
1580
2088
 
1581
2089
  dom.classList.remove('editing');
@@ -1590,6 +2098,7 @@ function exitListEditMode() {
1590
2098
  el.items = rawItems.filter((t, i, arr) => t !== '' || arr.length === 1);
1591
2099
  if (!el.items.length) el.items = [''];
1592
2100
  pushUndo('edit', before, { ...el, items: [...el.items] });
2101
+ dom._wbListKey = null; // editing mutated the <li>s directly — force a clean rewrite
1593
2102
  renderElements();
1594
2103
  persistDebounced();
1595
2104
  }
@@ -1617,6 +2126,7 @@ function _startMultiDrag(e) {
1617
2126
  type: 'move-multi',
1618
2127
  startX: e.clientX, startY: e.clientY,
1619
2128
  snapshots,
2129
+ anchoredArrows: _anchoredArrowIds(_selectedIds),
1620
2130
  };
1621
2131
  }
1622
2132
 
@@ -1680,13 +2190,16 @@ function onMouseDown(e) {
1680
2190
  // Resize handle? (only when single-selected)
1681
2191
  if (e.target.closest('.wb-resize-handle') && targetId) {
1682
2192
  const el = _elements.find(e2 => e2.id === targetId);
1683
- if (el) {
2193
+ // Text/list cards auto-size — they have no handle; guard anyway so a
2194
+ // stray hit falls through to a plain move.
2195
+ if (el && el.type !== 'text' && el.type !== 'list') {
1684
2196
  selectElement(targetId);
1685
2197
  _drag = {
1686
2198
  type: 'resize', id: targetId,
1687
2199
  startX: e.clientX, startY: e.clientY,
1688
2200
  origW: el.width || 200, origH: el.height || 80,
1689
2201
  before: { ...el },
2202
+ anchoredArrows: _anchoredArrowIds([targetId]),
1690
2203
  };
1691
2204
  e.preventDefault();
1692
2205
  return;
@@ -1713,6 +2226,7 @@ function onMouseDown(e) {
1713
2226
  startX: e.clientX, startY: e.clientY,
1714
2227
  origX: el.x, origY: el.y,
1715
2228
  before: { ...el },
2229
+ anchoredArrows: _anchoredArrowIds([targetId]),
1716
2230
  };
1717
2231
  }
1718
2232
  e.preventDefault();
@@ -1908,6 +2422,13 @@ function onMouseDown(e) {
1908
2422
  function onMouseMove(e) {
1909
2423
  if (!_root) return;
1910
2424
 
2425
+ // While actively dragging/drawing, suppress ambient transitions + glow/pulse
2426
+ // animations (consumed by the body.ui-interacting CSS kill-switch at
2427
+ // styles.css:7231). classList.add is idempotent — cheap to call each move.
2428
+ if (_drag || _marquee || _shapeCreating || _sectionCreating || _penPoints) {
2429
+ document.body.classList.add('ui-interacting');
2430
+ }
2431
+
1911
2432
  // Marquee selection drag
1912
2433
  if (_marquee) {
1913
2434
  const pt = clientToCanvas(e.clientX, e.clientY);
@@ -1919,6 +2440,8 @@ function onMouseMove(e) {
1919
2440
  if (!_marquee.rect && (w > 4 || h > 4)) {
1920
2441
  _marquee.rect = document.createElement('div');
1921
2442
  _marquee.rect.className = 'wb-marquee';
2443
+ // Keep the marquee overlay above promoted images/elements with runtime zIndex values.
2444
+ _marquee.rect.style.zIndex = String(_nextZIndex + 100);
1922
2445
  _root.appendChild(_marquee.rect);
1923
2446
  }
1924
2447
  if (_marquee.rect) {
@@ -1937,7 +2460,7 @@ function onMouseMove(e) {
1937
2460
  const dx = state.gridSnap ? (_snap(_drag.snapshots[0]?.origX + rawDx) - _drag.snapshots[0]?.origX) : rawDx;
1938
2461
  const dy = state.gridSnap ? (_snap(_drag.snapshots[0]?.origY + rawDy) - _drag.snapshots[0]?.origY) : rawDy;
1939
2462
  for (const snap of _drag.snapshots) {
1940
- const el = _elements.find(el2 => el2.id === snap.id);
2463
+ const el = _findEl(snap.id);
1941
2464
  if (!el) continue;
1942
2465
  if (el.type === 'arrow') {
1943
2466
  el.points = snap.origPoints.map(([px, py]) => [px + dx, py + dy]);
@@ -1953,14 +2476,14 @@ function onMouseMove(e) {
1953
2476
  el.y = snap.origY + dy;
1954
2477
  }
1955
2478
  }
1956
- renderAll();
2479
+ if (!_fastDragUpdate(_drag.snapshots.map(s => s.id), _drag.anchoredArrows)) renderAll();
1957
2480
  return;
1958
2481
  }
1959
2482
 
1960
2483
  // Shape drag-to-create
1961
2484
  if (_shapeCreating) {
1962
2485
  const pt = clientToCanvas(e.clientX, e.clientY);
1963
- const el = _elements.find(el2 => el2.id === _shapeCreating.id);
2486
+ const el = _findEl(_shapeCreating.id);
1964
2487
  if (el) {
1965
2488
  const sx = _snap(Math.min(_shapeCreating.originX, pt.x));
1966
2489
  const sy = _snap(Math.min(_shapeCreating.originY, pt.y));
@@ -1976,7 +2499,7 @@ function onMouseMove(e) {
1976
2499
  // Section drag-to-create
1977
2500
  if (_sectionCreating) {
1978
2501
  const pt = clientToCanvas(e.clientX, e.clientY);
1979
- const el = _elements.find(el2 => el2.id === _sectionCreating.id);
2502
+ const el = _findEl(_sectionCreating.id);
1980
2503
  if (el) {
1981
2504
  const sx = _snap(Math.min(_sectionCreating.originX, pt.x));
1982
2505
  const sy = _snap(Math.min(_sectionCreating.originY, pt.y));
@@ -1991,7 +2514,7 @@ function onMouseMove(e) {
1991
2514
 
1992
2515
  // Pen drawing
1993
2516
  if (_penPoints) {
1994
- const rect = _root.getBoundingClientRect();
2517
+ const rect = _getRootRect();
1995
2518
  penMove(e.clientX - rect.left, e.clientY - rect.top);
1996
2519
  return;
1997
2520
  }
@@ -2000,11 +2523,11 @@ function onMouseMove(e) {
2000
2523
  if (_drag?.type === 'move') {
2001
2524
  const dx = e.clientX - _drag.startX;
2002
2525
  const dy = e.clientY - _drag.startY;
2003
- const el = _elements.find(el2 => el2.id === _drag.id);
2526
+ const el = _findEl(_drag.id);
2004
2527
  if (el) {
2005
2528
  el.x = _snap(_drag.origX + dx);
2006
2529
  el.y = _snap(_drag.origY + dy);
2007
- renderAll();
2530
+ if (!_fastDragUpdate([_drag.id], _drag.anchoredArrows)) renderAll();
2008
2531
  }
2009
2532
  return;
2010
2533
  }
@@ -2013,12 +2536,12 @@ function onMouseMove(e) {
2013
2536
  if (_drag?.type === 'move-arrow') {
2014
2537
  const dx = e.clientX - _drag.startX;
2015
2538
  const dy = e.clientY - _drag.startY;
2016
- const el = _elements.find(el2 => el2.id === _drag.id);
2539
+ const el = _findEl(_drag.id);
2017
2540
  if (el && el.points) {
2018
2541
  el.points = _drag.origPoints.map(([px, py]) => [px + dx, py + dy]);
2019
2542
  el.startAnchor = null;
2020
2543
  el.endAnchor = null;
2021
- renderAll();
2544
+ if (!_fastDragUpdate([_drag.id], null)) renderAll();
2022
2545
  }
2023
2546
  return;
2024
2547
  }
@@ -2027,13 +2550,13 @@ function onMouseMove(e) {
2027
2550
  if (_drag?.type === 'move-pen') {
2028
2551
  const dx = e.clientX - _drag.startX;
2029
2552
  const dy = e.clientY - _drag.startY;
2030
- const el = _elements.find(el2 => el2.id === _drag.id);
2553
+ const el = _findEl(_drag.id);
2031
2554
  if (el) {
2032
2555
  el.x = _drag.origX + dx;
2033
2556
  el.y = _drag.origY + dy;
2034
2557
  el.points = _drag.origPoints.map(([px, py]) => [px + dx, py + dy]);
2035
2558
  el.pathD = pointsToSmoothPath(el.points);
2036
- renderAll();
2559
+ if (!_fastDragUpdate([_drag.id], null)) renderAll();
2037
2560
  }
2038
2561
  return;
2039
2562
  }
@@ -2044,7 +2567,7 @@ function onMouseMove(e) {
2044
2567
  const angle = Math.atan2(pt2.y - _drag.centerY, pt2.x - _drag.centerX);
2045
2568
  const deltaRad = angle - _drag.startAngle;
2046
2569
  const deltaDeg = deltaRad * (180 / Math.PI);
2047
- const el = _elements.find(el2 => el2.id === _drag.id);
2570
+ const el = _findEl(_drag.id);
2048
2571
  if (el) {
2049
2572
  // For arrows: rotate all points around centroid
2050
2573
  if (el.type === 'arrow' && _drag.origPoints) {
@@ -2087,7 +2610,7 @@ function onMouseMove(e) {
2087
2610
  }
2088
2611
  el.rotation = Math.round(newRot * 10) / 10;
2089
2612
  }
2090
- renderAll();
2613
+ if (!_fastDragUpdate([_drag.id], _drag.anchoredArrows)) renderAll();
2091
2614
  }
2092
2615
  return;
2093
2616
  }
@@ -2096,11 +2619,11 @@ function onMouseMove(e) {
2096
2619
  if (_drag?.type === 'resize') {
2097
2620
  const dx = e.clientX - _drag.startX;
2098
2621
  const dy = e.clientY - _drag.startY;
2099
- const el = _elements.find(el2 => el2.id === _drag.id);
2622
+ const el = _findEl(_drag.id);
2100
2623
  if (el) {
2101
2624
  el.width = _snap(Math.max(60, _drag.origW + dx));
2102
2625
  el.height = _snap(Math.max(30, _drag.origH + dy));
2103
- renderAll();
2626
+ if (!_fastDragUpdate([_drag.id], _drag.anchoredArrows)) renderAll();
2104
2627
  }
2105
2628
  return;
2106
2629
  }
@@ -2114,6 +2637,12 @@ function onMouseMove(e) {
2114
2637
  }
2115
2638
 
2116
2639
  function onMouseUp(e) {
2640
+ // Any interaction is ending — lift the transition/animation kill-switch and
2641
+ // drop the gesture-cached root rect (both run first so they fire on every
2642
+ // early-return finalize path below).
2643
+ document.body.classList.remove('ui-interacting');
2644
+ _invalidateRootRect();
2645
+
2117
2646
  // Finalize marquee selection
2118
2647
  if (_marquee) {
2119
2648
  if (_marquee.rect) {
@@ -2251,6 +2780,8 @@ function onMouseUp(e) {
2251
2780
  _drag = null;
2252
2781
  persistDebounced();
2253
2782
  }
2783
+ // A measurement pass skipped during the gesture runs now.
2784
+ if (_measurePending) { _measurePending = false; _scheduleMeasure(); }
2254
2785
  }
2255
2786
 
2256
2787
  function onDblClick(e) {
@@ -2345,12 +2876,27 @@ function isWhiteboardActive() {
2345
2876
  return bg && bg.classList.contains('visible');
2346
2877
  }
2347
2878
 
2879
+ // A key pressed inside a side panel is the panel's: registerSidepanel marks every panel root
2880
+ // (Claude, Codex, OpenCode, Assistant) with data-sidepanel-provider.
2881
+ export function isSidepanelKeyTarget(el) {
2882
+ return !!el?.closest?.('[data-sidepanel-provider]');
2883
+ }
2884
+
2348
2885
  function onKeyDown(e) {
2349
2886
  if (!isWhiteboardActive()) return;
2350
2887
 
2351
- // Don't intercept keys when focus is in inputs outside the whiteboard (e.g. code editor)
2352
- const tag = document.activeElement?.tagName;
2353
- if ((tag === 'INPUT' || tag === 'TEXTAREA') && !document.activeElement.closest('#static-bg')) return;
2888
+ // Don't intercept keys when focus is in inputs/contenteditables outside the whiteboard
2889
+ // (e.g. code editor, floating CLI rename) — typing must produce characters, not trigger keybinds.
2890
+ const ae = document.activeElement;
2891
+ const tag = ae?.tagName;
2892
+ const isTextField = tag === 'INPUT' || tag === 'TEXTAREA' || ae?.isContentEditable;
2893
+ if (isTextField && !ae.closest('#static-bg')) return;
2894
+ // Nor while the keyboard is inside the Settings panel: its rows, buttons and dialogs take their own keys
2895
+ // (Escape empties its search, then closes it), and a board shortcut must not fire from a setting.
2896
+ if (ae?.closest?.('#settings-panel')) return;
2897
+ // Nor while it is inside a side panel: Escape there stops a turn, and Delete, Backspace or
2898
+ // Ctrl+A/Z/C on one of its controls is not a board edit.
2899
+ if (isSidepanelKeyTarget(e.target) || isSidepanelKeyTarget(ae)) return;
2354
2900
 
2355
2901
  const isEditingText = !!document.activeElement?.closest('.wb-text.editing');
2356
2902
  const editingListLi = document.activeElement?.closest('.wb-list.editing li') || null;
@@ -2417,11 +2963,13 @@ function onKeyDown(e) {
2417
2963
  if (_shapeCreating) {
2418
2964
  _elements = _elements.filter(el => el.id !== _shapeCreating.id);
2419
2965
  _shapeCreating = null;
2966
+ _invalidateElementMap();
2420
2967
  renderAll();
2421
2968
  }
2422
2969
  if (_sectionCreating) {
2423
2970
  _elements = _elements.filter(el => el.id !== _sectionCreating.id);
2424
2971
  _sectionCreating = null;
2972
+ _invalidateElementMap();
2425
2973
  renderAll();
2426
2974
  }
2427
2975
  if (_arrowCreating) { _arrowCreating = null; clearArrowPreview(); }
@@ -2737,6 +3285,7 @@ function onToolbarClick(e) {
2737
3285
  if (breathe) breathe.style.display = hidden ? 'none' : '';
2738
3286
  btn.classList.toggle('active', hidden);
2739
3287
  sessionStorage.setItem('wb-logo-hidden', hidden ? '1' : '');
3288
+ syncRulesetAlertVisibility();
2740
3289
  return;
2741
3290
  }
2742
3291
  }
@@ -2771,6 +3320,7 @@ export function restoreWhiteboardSnapshot(snap) {
2771
3320
  clearWhiteboard();
2772
3321
  return;
2773
3322
  }
3323
+ _beginStateReplace({ exitEdits: true });
2774
3324
  _elements = (snap.elements || []).map(el => {
2775
3325
  // Migrate legacy arrow format (startX/endX → points)
2776
3326
  if (el.type === 'arrow' && !el.points && el.startX != null) {
@@ -2788,11 +3338,13 @@ export function restoreWhiteboardSnapshot(snap) {
2788
3338
  _arrowCreating = null;
2789
3339
  _sectionCreating = null;
2790
3340
  _clipboard = null;
3341
+ _invalidateElementMap();
2791
3342
  renderAll();
2792
3343
  persistDebounced();
2793
3344
  }
2794
3345
 
2795
3346
  export function clearWhiteboard() {
3347
+ _beginStateReplace({ exitEdits: true });
2796
3348
  _elements = [];
2797
3349
  _nextZIndex = 1;
2798
3350
  _undoStack = [];
@@ -2802,6 +3354,7 @@ export function clearWhiteboard() {
2802
3354
  _arrowCreating = null;
2803
3355
  _sectionCreating = null;
2804
3356
  _clipboard = null;
3357
+ _invalidateElementMap();
2805
3358
  renderAll();
2806
3359
  persistDebounced();
2807
3360
  }
@@ -2862,10 +3415,11 @@ export function initWhiteboard() {
2862
3415
 
2863
3416
  _root.appendChild(_arrowHint);
2864
3417
 
2865
- // Load persisted state
2866
- loadPersisted();
3418
+ // Render immediately (likely empty) for an instant paint, then again once the
3419
+ // server returns the saved board (loadPersisted is async — fetches /api/whiteboard).
2867
3420
  renderAll();
2868
3421
  updateUndoButtons();
3422
+ loadPersisted().then(() => { renderAll(); updateUndoButtons(); });
2869
3423
 
2870
3424
  // Mouse events
2871
3425
  _root.addEventListener('mousedown', onMouseDown);
@@ -2873,13 +3427,32 @@ export function initWhiteboard() {
2873
3427
  window.addEventListener('mouseup', onMouseUp);
2874
3428
  _root.addEventListener('dblclick', onDblClick);
2875
3429
 
3430
+ // Never let the browser start a native HTML5 drag inside the board (an <img>
3431
+ // drag would swallow our mouseup and strand _drag). Nodes that opt in with
3432
+ // draggable="true" (e.g. a future drag-to-terminal handle) are exempt.
3433
+ _root.addEventListener('dragstart', (e) => {
3434
+ if (e.target?.closest?.('[draggable="true"]')) return;
3435
+ e.preventDefault();
3436
+ });
3437
+
2876
3438
  // Keyboard — capture phase so we fire before global keybinds
2877
3439
  document.addEventListener('keydown', onKeyDown, true);
2878
3440
 
2879
3441
  // Track Ctrl key for tool lock
2880
3442
  document.addEventListener('keydown', onCtrlTrack);
2881
3443
  document.addEventListener('keyup', onCtrlTrack);
2882
- window.addEventListener('blur', () => { _toolLocked = false; updateLockIndicator(); });
3444
+ // A window blur or hidden tab means the matching mouseup may never arrive —
3445
+ // drop the tool lock AND any in-flight pointer gesture so the board never
3446
+ // ends up with a stale _drag/_marquee/_penPoints swallowing every mousemove.
3447
+ window.addEventListener('blur', () => {
3448
+ _toolLocked = false;
3449
+ updateLockIndicator();
3450
+ _cancelGesture({ pointerOnly: true });
3451
+ });
3452
+ document.addEventListener('visibilitychange', () => {
3453
+ if (document.hidden) _cancelGesture({ pointerOnly: true });
3454
+ _sendViewport(); // `visible` flag feeds the server's primary-client pick
3455
+ });
2883
3456
 
2884
3457
  // Paste — listen on document (not _root) because _root can't receive focus
2885
3458
  // and paste events only fire on the focused element's ancestor chain.
@@ -2898,6 +3471,9 @@ export function initWhiteboard() {
2898
3471
  // Section picker
2899
3472
  initSectionPicker();
2900
3473
 
3474
+ // Logo-adjacent ruleset reminder
3475
+ initRulesetAlert();
3476
+
2901
3477
  // Prevent context menu on whiteboard
2902
3478
  _root.addEventListener('contextmenu', e => e.preventDefault());
2903
3479
 
@@ -2907,6 +3483,7 @@ export function initWhiteboard() {
2907
3483
 
2908
3484
  // Auto-focus whiteboard when entering focus mode so Ctrl+V paste works immediately
2909
3485
  on('focus:enter', () => {
3486
+ refreshRulesetAlert();
2910
3487
  setTimeout(() => _root.focus({ preventScroll: true }), 150);
2911
3488
  // Restore logo hidden state from session
2912
3489
  if (sessionStorage.getItem('wb-logo-hidden') === '1') {
@@ -2914,14 +3491,40 @@ export function initWhiteboard() {
2914
3491
  const breathe = document.querySelector('#static-bg .focus-breathe');
2915
3492
  if (logo) logo.style.display = 'none';
2916
3493
  if (breathe) breathe.style.display = 'none';
3494
+ syncRulesetAlertVisibility();
2917
3495
  const btn = document.getElementById('wb-toggle-logo');
2918
3496
  if (btn) btn.classList.add('active');
3497
+ } else {
3498
+ syncRulesetAlertVisibility();
2919
3499
  }
2920
3500
  });
2921
3501
 
2922
3502
  // Connect to server for external commands (Claude MCP)
2923
3503
  _connectWhiteboardWS();
2924
3504
 
3505
+ // Viewport truth: re-report the usable area whenever anything that bounds it
3506
+ // changes — root size, the CSS variables the navbar/terminal/right panel
3507
+ // publish on <html>, focus mode, panel visibility, tab visibility.
3508
+ if (typeof ResizeObserver !== 'undefined') {
3509
+ new ResizeObserver(() => _sendViewport()).observe(_root);
3510
+ }
3511
+ if (typeof MutationObserver !== 'undefined') {
3512
+ new MutationObserver(() => _sendViewport())
3513
+ .observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
3514
+ }
3515
+ // focus:enter/exit fire BEFORE ui-settings toggles #static-bg.visible (100 ms
3516
+ // stagger), so report after the class has landed.
3517
+ on('focus:enter', () => setTimeout(() => _sendViewport({ immediate: true }), 250));
3518
+ on('focus:exit', () => setTimeout(() => _sendViewport({ immediate: true }), 250));
3519
+ on('claude-panel:visibility', () => _sendViewport());
3520
+
3521
+ // First measurement of text/list cards once the real font is in (metrics
3522
+ // taken with a fallback font would be wrong by a few px per glyph).
3523
+ if (document.fonts?.ready) {
3524
+ document.fonts.ready.then(_scheduleMeasure).catch(() => {});
3525
+ document.fonts.addEventListener?.('loadingdone', _scheduleMeasure);
3526
+ }
3527
+
2925
3528
  // ── Guest permission: disable drawing tools when whiteboard perm is off ──
2926
3529
  function updateWbPermVisual() {
2927
3530
  if (!_toolbar || !_canvas) return;
@@ -2968,15 +3571,12 @@ function _connectWhiteboardWS() {
2968
3571
  _ws.onopen = () => {
2969
3572
  console.log('[whiteboard-ws] Connected');
2970
3573
  if (_wsReconnectTimer) { clearInterval(_wsReconnectTimer); _wsReconnectTimer = null; }
2971
- // Report usable viewport dimensions (excluding navbar, toolbar, terminal) for MCP coordinate mapping
2972
- if (_root) {
2973
- const rect = _root.getBoundingClientRect();
2974
- const titleBar = document.getElementById('title-bar');
2975
- const toolbar = document.getElementById('wb-toolbar');
2976
- const navH = titleBar ? titleBar.getBoundingClientRect().height : 0;
2977
- const tbW = toolbar ? toolbar.getBoundingClientRect().right - rect.left + 12 : 60;
2978
- _ws.send(JSON.stringify({ type: 'viewport', width: Math.round(rect.width - tbW), height: Math.round(rect.height - navH), yOffset: Math.round(navH), xOffset: Math.round(tbW) }));
2979
- }
3574
+ // Report the usable viewport (excluding navbar, tool rail, right panel,
3575
+ // terminal) for MCP coordinate mapping, then flush any measured sizes that
3576
+ // were collected while the socket was down.
3577
+ _lastViewportJson = '';
3578
+ _sendViewport({ immediate: true });
3579
+ _queueGeometrySend();
2980
3580
  };
2981
3581
 
2982
3582
  _ws.onmessage = (event) => {
@@ -2997,21 +3597,63 @@ function _connectWhiteboardWS() {
2997
3597
  _ws.onerror = () => {}; // onclose fires after
2998
3598
  }
2999
3599
 
3000
- // Debounced viewport resize reporting
3001
- let _viewportResizeTimer = null;
3002
- window.addEventListener('resize', () => {
3003
- clearTimeout(_viewportResizeTimer);
3004
- _viewportResizeTimer = setTimeout(() => {
3005
- if (_ws && _ws.readyState === 1 && _root) {
3006
- const rect = _root.getBoundingClientRect();
3007
- const titleBar = document.getElementById('title-bar');
3008
- const toolbar = document.getElementById('wb-toolbar');
3009
- const navH = titleBar ? titleBar.getBoundingClientRect().height : 0;
3010
- const tbW = toolbar ? toolbar.getBoundingClientRect().right - rect.left + 12 : 60;
3011
- _ws.send(JSON.stringify({ type: 'viewport', width: Math.round(rect.width - tbW), height: Math.round(rect.height - navH), yOffset: Math.round(navH), xOffset: Math.round(tbW) }));
3012
- }
3013
- }, 300);
3014
- });
3600
+ // ── Viewport reporting ──
3601
+
3602
+ function _cssPx(name) {
3603
+ const v = parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name));
3604
+ return Number.isFinite(v) ? v : 0;
3605
+ }
3606
+
3607
+ /** Usable whiteboard viewport in CSS px relative to #wb-root's top-left: the
3608
+ * root minus the left tool rail, the navbar and the docked right-side agent
3609
+ * panel (its reserved width is published as --right-panel-width by
3610
+ * ui-sidepanel-layout.js; 0 when hidden or floating). #wb-root already ends
3611
+ * above a docked terminal (--terminal-height). Sent to the server so MCP
3612
+ * coordinate math and screenshots describe what the user can actually see. */
3613
+ function _computeViewport() {
3614
+ const rect = _root.getBoundingClientRect();
3615
+ const titleBar = document.getElementById('title-bar');
3616
+ const toolbar = document.getElementById('wb-toolbar');
3617
+ const navH = titleBar ? titleBar.getBoundingClientRect().height : _cssPx('--navbar-height');
3618
+ const tbW = toolbar ? Math.max(0, toolbar.getBoundingClientRect().right - rect.left + 12) : 60;
3619
+ const rightW = Math.max(0, _cssPx('--right-panel-width') || _cssPx('--claude-panel-width'));
3620
+ return {
3621
+ type: 'viewport',
3622
+ clientId: _clientId,
3623
+ focusActive: isWhiteboardActive(),
3624
+ visible: document.visibilityState === 'visible',
3625
+ width: Math.max(0, Math.round(rect.width - tbW - rightW)),
3626
+ height: Math.max(0, Math.round(rect.height - navH)),
3627
+ xOffset: Math.round(tbW),
3628
+ yOffset: Math.round(navH),
3629
+ rootWidth: Math.round(rect.width),
3630
+ rootHeight: Math.round(rect.height),
3631
+ rightPanelWidth: Math.round(rightW),
3632
+ dpr: window.devicePixelRatio || 1,
3633
+ };
3634
+ }
3635
+
3636
+ let _viewportTimer = null;
3637
+ let _lastViewportJson = '';
3638
+
3639
+ /** Debounced (150 ms), deduplicated viewport report. Every trigger also drops
3640
+ * the gesture-cached root rect since the layout just changed. */
3641
+ function _sendViewport({ immediate = false } = {}) {
3642
+ _invalidateRootRect();
3643
+ clearTimeout(_viewportTimer);
3644
+ const send = () => {
3645
+ _viewportTimer = null;
3646
+ if (!_ws || _ws.readyState !== 1 || !_root) return;
3647
+ const json = JSON.stringify(_computeViewport());
3648
+ if (json === _lastViewportJson) return;
3649
+ _lastViewportJson = json;
3650
+ _ws.send(json);
3651
+ };
3652
+ if (immediate) send();
3653
+ else _viewportTimer = setTimeout(send, 150);
3654
+ }
3655
+
3656
+ window.addEventListener('resize', () => _sendViewport());
3015
3657
 
3016
3658
  function _handleExternalCommand(msg) {
3017
3659
  switch (msg.type) {
@@ -3020,7 +3662,17 @@ function _handleExternalCommand(msg) {
3020
3662
 
3021
3663
  case 'add':
3022
3664
  for (const el of (msg.elements || [])) {
3023
- addElement(el, { skipPersist: true });
3665
+ const idx = _elements.findIndex(e => e.id === el.id);
3666
+ if (idx >= 0) {
3667
+ // The server re-sent an id we already hold — replace in place
3668
+ // (no duplicate DOM node, no extra undo entry).
3669
+ _elements[idx] = el;
3670
+ if (Number.isFinite(el.zIndex)) _nextZIndex = Math.max(_nextZIndex, el.zIndex + 1);
3671
+ _invalidateElementMap();
3672
+ renderAll();
3673
+ continue;
3674
+ }
3675
+ addElement(el, { skipPersist: true, keepZIndex: true });
3024
3676
  }
3025
3677
  break;
3026
3678
 
@@ -3049,30 +3701,79 @@ function _handleExternalCommand(msg) {
3049
3701
  break;
3050
3702
  }
3051
3703
 
3052
- case 'clear':
3704
+ case 'clear': {
3705
+ // External (MCP) clear is UNDOABLE: push a delete-multi entry (undo()
3706
+ // pushes the copies back) instead of wiping the user's history.
3707
+ _beginStateReplace({ exitEdits: true });
3708
+ if (_elements.length) {
3709
+ pushUndo('delete-multi', _elements.map(el => ({ ...el, points: el.points ? el.points.map(p => [...p]) : undefined })), null);
3710
+ } else {
3711
+ _nextZIndex = 1;
3712
+ }
3053
3713
  _elements = [];
3054
- _nextZIndex = 1;
3055
- _undoStack = [];
3056
- _redoStack = [];
3057
- _selectedId = null;
3058
- _selectedIds.clear();
3059
- _arrowCreating = null;
3060
- _sectionCreating = null;
3714
+ _invalidateElementMap();
3061
3715
  renderAll();
3716
+ updateUndoButtons();
3062
3717
  break;
3718
+ }
3063
3719
 
3064
3720
  case 'state:full':
3065
- // Full state sync from another client — replace local state, save to localStorage only (no WS echo)
3721
+ // Full state sync relayed from another client. The server already
3722
+ // persisted it (saveWhiteboard); just apply locally — no re-persist.
3723
+ // Do NOT exit an in-progress text edit here (the user may be typing);
3724
+ // gestures are cancelled so a drag can't continue on ghost objects.
3725
+ _beginStateReplace({ exitEdits: false });
3066
3726
  _elements = msg.snapshot?.elements || [];
3067
3727
  _nextZIndex = msg.snapshot?.nextZIndex || 1;
3068
- _selectedId = null;
3069
- _selectedIds.clear();
3728
+ _invalidateElementMap();
3729
+ renderAll();
3730
+ break;
3731
+
3732
+ case 'batch': {
3733
+ // Several server-side ops (batch update/remove, delete cascades) applied
3734
+ // atomically: one undo entry, one render, no persist (server already has it).
3735
+ const ops = Array.isArray(msg.ops) ? msg.ops : [];
3736
+ if (!ops.length) break;
3737
+ const copyEl = (el) => ({ ...el, points: el.points ? el.points.map(p => [...p]) : undefined });
3738
+ const befores = [], afters = [], removed = [], addedIds = [], addedCopies = [];
3739
+ for (const op of ops) {
3740
+ if (op.op === 'update') {
3741
+ const el = _elements.find(e => e.id === op.id);
3742
+ if (!el) continue;
3743
+ befores.push(copyEl(el));
3744
+ for (const [key, value] of Object.entries(op.updates || {})) {
3745
+ if (key === 'id' || key === 'type') continue;
3746
+ el[key] = value;
3747
+ }
3748
+ afters.push(copyEl(el));
3749
+ } else if (op.op === 'remove') {
3750
+ const idx = _elements.findIndex(e => e.id === op.id);
3751
+ if (idx < 0) continue;
3752
+ removed.push(copyEl(_elements[idx]));
3753
+ _elements.splice(idx, 1);
3754
+ _selectedIds.delete(op.id);
3755
+ if (_selectedId === op.id) _selectedId = null;
3756
+ } else if (op.op === 'add' && op.element?.id) {
3757
+ if (_elements.some(e => e.id === op.element.id)) continue;
3758
+ const el = op.element;
3759
+ if (Number.isFinite(el.zIndex)) _nextZIndex = Math.max(_nextZIndex, el.zIndex + 1);
3760
+ else el.zIndex = _nextZIndex++;
3761
+ _elements.push(el);
3762
+ addedIds.push(el.id);
3763
+ addedCopies.push(copyEl(el));
3764
+ }
3765
+ }
3766
+ if (befores.length || removed.length || addedIds.length) {
3767
+ pushUndo('batch', { updated: befores, removed, added: addedIds }, { updated: afters, added: addedCopies });
3768
+ }
3769
+ if (_selectedIds.size === 0) hideContextMenu();
3770
+ _invalidateElementMap();
3070
3771
  renderAll();
3071
- storage.setItem(KEYS.WHITEBOARD, JSON.stringify({ elements: _elements, nextZIndex: _nextZIndex }));
3072
3772
  break;
3773
+ }
3073
3774
 
3074
3775
  case 'screenshot:request':
3075
- _captureScreenshot(msg.requestId);
3776
+ _captureScreenshot(msg.requestId, { crop: msg.crop });
3076
3777
  break;
3077
3778
 
3078
3779
  case 'screenshot:auto': {
@@ -3112,138 +3813,267 @@ function _loadImage(src) {
3112
3813
  });
3113
3814
  }
3114
3815
 
3115
- function _drawArrowhead(ctx, points) {
3816
+ function _roundRectPath(ctx, x, y, w, h, r) {
3817
+ const rr = Math.max(0, Math.min(r, w / 2, h / 2));
3818
+ ctx.moveTo(x + rr, y);
3819
+ ctx.lineTo(x + w - rr, y);
3820
+ ctx.arcTo(x + w, y, x + w, y + rr, rr);
3821
+ ctx.lineTo(x + w, y + h - rr);
3822
+ ctx.arcTo(x + w, y + h, x + w - rr, y + h, rr);
3823
+ ctx.lineTo(x + rr, y + h);
3824
+ ctx.arcTo(x, y + h, x, y + h - rr, rr);
3825
+ ctx.lineTo(x, y + rr);
3826
+ ctx.arcTo(x, y, x + rr, y, rr);
3827
+ ctx.closePath();
3828
+ }
3829
+
3830
+ /** `color` with an alpha byte applied (hex2 like '0d'). Handles #rgb/#rrggbb
3831
+ * and rgb()/rgba(); anything else is returned unchanged. */
3832
+ function _withAlpha(color, hex2) {
3833
+ const a = parseInt(hex2, 16) / 255;
3834
+ let m = /^#([0-9a-f]{6})$/i.exec(color);
3835
+ if (m) return color + hex2;
3836
+ m = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color);
3837
+ if (m) return `#${m[1]}${m[1]}${m[2]}${m[2]}${m[3]}${m[3]}${hex2}`;
3838
+ m = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(color);
3839
+ if (m) return `rgba(${m[1]},${m[2]},${m[3]},${a.toFixed(3)})`;
3840
+ return color;
3841
+ }
3842
+
3843
+ /** Arrowhead identical to the SVG marker (path M 2 2 L 18 10 L 2 18, refX 2,
3844
+ * refY 10): a 16px triangle extending past the last point along the end tangent. */
3845
+ function _drawArrowhead(ctx, points, fill) {
3116
3846
  if (points.length < 2) return;
3117
3847
  const last = points[points.length - 1];
3118
3848
  const prev = points[points.length - 2];
3119
3849
  const angle = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
3120
- const size = 20;
3121
- ctx.fillStyle = ctx.strokeStyle;
3850
+ ctx.save();
3851
+ ctx.translate(last[0], last[1]);
3852
+ ctx.rotate(angle);
3853
+ ctx.fillStyle = fill;
3122
3854
  ctx.beginPath();
3123
- ctx.moveTo(last[0], last[1]);
3124
- ctx.lineTo(last[0] - size * Math.cos(angle - Math.PI / 6), last[1] - size * Math.sin(angle - Math.PI / 6));
3125
- ctx.lineTo(last[0] - size * Math.cos(angle + Math.PI / 6), last[1] - size * Math.sin(angle + Math.PI / 6));
3855
+ ctx.moveTo(0, -8);
3856
+ ctx.lineTo(16, 0);
3857
+ ctx.lineTo(0, 8);
3126
3858
  ctx.closePath();
3127
3859
  ctx.fill();
3860
+ ctx.restore();
3861
+ }
3862
+
3863
+ /** Baseline offset from the top of a CSS line box of `lineH` px for the
3864
+ * current ctx.font (half-leading + ascent), with metric fallbacks. */
3865
+ function _lineBaseline(ctx, fs, lineH) {
3866
+ const m = ctx.measureText('Mg');
3867
+ const ascent = m.fontBoundingBoxAscent > 0 ? m.fontBoundingBoxAscent : fs * 1.02;
3868
+ const descent = m.fontBoundingBoxDescent > 0 ? m.fontBoundingBoxDescent : fs * 0.3;
3869
+ return (lineH - (ascent + descent)) / 2 + ascent;
3128
3870
  }
3129
3871
 
3130
- async function _captureScreenshot(requestId) {
3872
+ /**
3873
+ * Re-render the board to an offscreen canvas for the MCP whiteboard_screenshot
3874
+ * tool. This is a RECONSTRUCTION, so every stroke reuses the generators and
3875
+ * constants the DOM uses: JetBrains Mono with the CSS paddings, the seeded
3876
+ * hand-drawn shape paths, anchor-resolved Catmull-Rom arrows with the
3877
+ * marker-shaped head, section/image chrome. Output is scaled up to 2x (capped
3878
+ * so the long edge stays ≤ 2000 px) and the response carries the coordinate
3879
+ * mapping (image px ÷ scale = canvas px) plus the usable viewport.
3880
+ */
3881
+ async function _captureScreenshot(requestId, { crop } = {}) {
3131
3882
  try {
3132
- const canvas = document.createElement('canvas');
3883
+ flushTextEdits();
3884
+ flushListEdits();
3885
+ try {
3886
+ if (document.fonts?.ready) await document.fonts.ready;
3887
+ if (document.fonts?.load) {
3888
+ await Promise.allSettled([
3889
+ document.fonts.load('400 22px "JetBrains Mono"'),
3890
+ document.fonts.load('700 22px "JetBrains Mono"'),
3891
+ document.fonts.load('italic 400 22px "JetBrains Mono"'),
3892
+ ]);
3893
+ }
3894
+ } catch { /* draw with whatever is loaded */ }
3895
+
3133
3896
  const rootRect = _root.getBoundingClientRect();
3134
- canvas.width = rootRect.width;
3135
- canvas.height = rootRect.height;
3897
+ const vp = _computeViewport();
3898
+ const usable = { x: vp.xOffset, y: vp.yOffset, width: vp.width, height: vp.height };
3899
+ const box = crop === 'usable'
3900
+ ? usable
3901
+ : { x: 0, y: 0, width: Math.round(rootRect.width), height: Math.round(rootRect.height) };
3902
+ const outW = Math.max(1, Math.round(box.width));
3903
+ const outH = Math.max(1, Math.round(box.height));
3904
+ let scale = Math.min(2, window.devicePixelRatio || 1);
3905
+ const longEdge = Math.max(outW, outH);
3906
+ if (longEdge * scale > 2000) scale = 2000 / longEdge;
3907
+
3908
+ const canvas = document.createElement('canvas');
3909
+ canvas.width = Math.max(1, Math.round(outW * scale));
3910
+ canvas.height = Math.max(1, Math.round(outH * scale));
3136
3911
  const ctx = canvas.getContext('2d');
3912
+ ctx.scale(scale, scale);
3913
+ ctx.translate(-box.x, -box.y);
3914
+ ctx.imageSmoothingQuality = 'high';
3915
+
3916
+ // Background — same as #static-bg
3917
+ ctx.fillStyle = '#070709';
3918
+ ctx.fillRect(box.x, box.y, outW, outH);
3137
3919
 
3138
- // Background
3139
- ctx.fillStyle = '#0a0a0f';
3140
- ctx.fillRect(0, 0, canvas.width, canvas.height);
3920
+ const mono = '"JetBrains Mono", monospace';
3921
+ const bodyFont = getComputedStyle(document.body).fontFamily || 'sans-serif';
3922
+ const tttActive = _root.classList.contains('ttt-active');
3923
+ const domById = new Map();
3924
+ if (_elementsDiv) for (const child of _elementsDiv.children) domById.set(child.dataset.wbId, child);
3141
3925
 
3142
3926
  // Sort by zIndex for correct layering
3143
3927
  const sorted = [..._elements].sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
3144
3928
 
3145
3929
  for (const el of sorted) {
3146
3930
  ctx.save();
3147
-
3148
- if (el.rotation) {
3149
- const cx = el.x + (el.width || 0) / 2;
3150
- const cy = el.y + (el.height || 0) / 2;
3151
- ctx.translate(cx, cy);
3152
- ctx.rotate(el.rotation * Math.PI / 180);
3153
- ctx.translate(-cx, -cy);
3154
- }
3155
-
3156
- if (el.type === 'text') {
3157
- const weight = el.bold ? '700' : '400';
3158
- const style = el.italic ? 'italic ' : '';
3159
- ctx.font = `${style}${weight} ${el.fontSize || 22}px Caveat, cursive`;
3160
- ctx.fillStyle = el.color || 'rgba(255,255,255,0.85)';
3161
- ctx.textBaseline = 'top';
3162
- const lines = (el.content || '').split('\n');
3163
- let lineY = el.y + 10;
3164
- for (const line of lines) {
3165
- ctx.fillText(line, el.x + 14, lineY);
3166
- lineY += (el.fontSize || 22) * 1.35;
3931
+ try {
3932
+ // Border-box size: measured for text/list (DOM fallback if unmeasured),
3933
+ // explicit or default for the rest.
3934
+ let bw = el.width || 0, bh = el.height || 0;
3935
+ if ((el.type === 'text' || el.type === 'list') && (!bw || !bh)) {
3936
+ const dom = domById.get(el.id);
3937
+ if (dom) { bw = dom.offsetWidth; bh = dom.offsetHeight; }
3938
+ } else if (el.type === 'shape') {
3939
+ bw = el.width || 160; bh = el.height || 100;
3940
+ } else if (el.type === 'section') {
3941
+ const def = SECTION_TYPES[el.sectionType] || SECTION_TYPES.content;
3942
+ bw = el.width || def.w; bh = el.height || def.h;
3167
3943
  }
3168
- } else if (el.type === 'list') {
3169
- const fs = el.fontSize || 18;
3170
- ctx.font = `400 ${fs}px JetBrains Mono, monospace`;
3171
- ctx.fillStyle = el.color || 'rgba(255,255,255,0.85)';
3172
- ctx.textBaseline = 'top';
3173
- const items = el.items && el.items.length ? el.items : [''];
3174
- let lineY = el.y + 10;
3175
- for (const item of items) {
3176
- ctx.fillText('• ' + item, el.x + 14, lineY);
3177
- lineY += fs * 1.5;
3944
+
3945
+ // CSS transform: rotate() about the rendered box centre (DOM elements
3946
+ // only — arrows/pen bake rotation into their points)
3947
+ if (el.rotation && el.type !== 'arrow' && el.type !== 'pen') {
3948
+ const cx = el.x + bw / 2;
3949
+ const cy = el.y + bh / 2;
3950
+ ctx.translate(cx, cy);
3951
+ ctx.rotate(el.rotation * Math.PI / 180);
3952
+ ctx.translate(-cx, -cy);
3178
3953
  }
3179
- } else if (el.type === 'shape') {
3180
- ctx.strokeStyle = el.color || 'rgba(255,255,255,0.75)';
3181
- ctx.lineWidth = 3;
3182
- ctx.lineCap = 'round';
3183
- ctx.lineJoin = 'round';
3184
- const w = el.width || 160, h = el.height || 100;
3185
- if (el.shape === 'circle' || el.shape === 'drawn-circle') {
3186
- ctx.beginPath();
3187
- ctx.ellipse(el.x + w / 2, el.y + h / 2, w / 2, h / 2, 0, 0, Math.PI * 2);
3188
- ctx.stroke();
3189
- } else if (el.shape === 'pill') {
3190
- const r = Math.min(w, h) / 2;
3954
+
3955
+ if (el.type === 'text') {
3956
+ // .wb-text: 1px border + 10px/14px padding, line-height 1.35
3957
+ const fs = el.fontSize || 22;
3958
+ ctx.font = `${el.italic ? 'italic ' : ''}${el.bold ? '700' : '400'} ${fs}px ${mono}`;
3959
+ ctx.fillStyle = el.color || 'rgba(255,255,255,0.85)';
3960
+ ctx.textBaseline = 'alphabetic';
3961
+ ctx.textAlign = 'left';
3962
+ const lineH = fs * 1.35;
3963
+ const baseOff = _lineBaseline(ctx, fs, lineH);
3964
+ let lineTop = el.y + 11;
3965
+ for (const line of (el.content || '').split('\n')) {
3966
+ if (line) ctx.fillText(line, el.x + 15, lineTop + baseOff);
3967
+ lineTop += lineH;
3968
+ }
3969
+ } else if (el.type === 'list') {
3970
+ // .wb-list: same box, line-height 1.5, <ul>/<ol> padding-left 1.4em
3971
+ const fs = el.fontSize || 18;
3972
+ ctx.font = `400 ${fs}px ${mono}`;
3973
+ ctx.fillStyle = el.color || 'rgba(255,255,255,0.85)';
3974
+ ctx.textBaseline = 'alphabetic';
3975
+ const lineH = fs * 1.5;
3976
+ const baseOff = _lineBaseline(ctx, fs, lineH);
3977
+ const items = el.items && el.items.length ? el.items : [''];
3978
+ const contentLeft = el.x + 15 + 1.4 * fs;
3979
+ let lineTop = el.y + 11;
3980
+ items.forEach((item, i) => {
3981
+ const baseline = lineTop + baseOff;
3982
+ ctx.textAlign = 'right';
3983
+ ctx.fillText(el.ordered ? `${i + 1}.` : '•', contentLeft - 0.6 * fs, baseline);
3984
+ ctx.textAlign = 'left';
3985
+ if (item) ctx.fillText(item, contentLeft, baseline);
3986
+ lineTop += lineH;
3987
+ });
3988
+ } else if (el.type === 'shape') {
3989
+ // Same seeded hand-drawn path the DOM's inner SVG uses (stroke 4, 5px inset)
3990
+ const shape = el.shape || 'rect';
3991
+ const d = shape === 'drawn-circle'
3992
+ ? generateDrawnCirclePath(bw, bh, el.id)
3993
+ : generateHandDrawnShape(shape, bw, bh, el.id);
3994
+ ctx.translate(el.x, el.y);
3995
+ ctx.strokeStyle = el.color || 'rgba(255,255,255,0.8)';
3996
+ ctx.lineWidth = 4;
3997
+ ctx.lineCap = 'round';
3998
+ ctx.lineJoin = 'round';
3999
+ ctx.stroke(new Path2D(d));
4000
+ } else if (el.type === 'image' && el.dataUrl) {
4001
+ const img = await _loadImage(el.dataUrl).catch(() => null);
4002
+ if (img && bw > 0 && bh > 0) {
4003
+ // .wb-image: 2px border, radius 16 (img radius 14, object-fit: cover);
4004
+ // during tic-tac-toe the chrome is stripped and cell markers are 20% opaque
4005
+ const inset = tttActive ? 0 : 2;
4006
+ const radius = tttActive ? 0 : 14;
4007
+ const dx = el.x + inset, dy = el.y + inset;
4008
+ const dw = Math.max(0, bw - inset * 2), dh = Math.max(0, bh - inset * 2);
4009
+ const nw = img.naturalWidth || img.width || dw, nh = img.naturalHeight || img.height || dh;
4010
+ const s = Math.max(dw / nw, dh / nh);
4011
+ const sw = dw / s, sh = dh / s;
4012
+ const sx = (nw - sw) / 2, sy = (nh - sh) / 2;
4013
+ ctx.save();
4014
+ if (tttActive && String(el.id).startsWith('ttt-cell-')) ctx.globalAlpha = 0.2;
4015
+ ctx.beginPath();
4016
+ _roundRectPath(ctx, dx, dy, dw, dh, radius);
4017
+ ctx.clip();
4018
+ ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
4019
+ ctx.restore();
4020
+ if (!tttActive) {
4021
+ ctx.strokeStyle = 'rgba(255,255,255,0.08)';
4022
+ ctx.lineWidth = 2;
4023
+ ctx.beginPath();
4024
+ _roundRectPath(ctx, el.x + 1, el.y + 1, bw - 2, bh - 2, 16);
4025
+ ctx.stroke();
4026
+ }
4027
+ }
4028
+ } else if (el.type === 'arrow' && el.points && el.points.length >= 2) {
4029
+ // Anchor-resolved, Catmull-Rom smoothed — identical to renderArrows()
4030
+ const pts = resolveArrowPoints(el);
4031
+ ctx.strokeStyle = el.color || 'rgba(255,255,255,0.8)';
4032
+ ctx.lineWidth = 2;
4033
+ ctx.lineCap = 'butt';
4034
+ ctx.lineJoin = 'round';
4035
+ ctx.stroke(new Path2D(computeArrowPath(pts)));
4036
+ _drawArrowhead(ctx, pts, el.color || 'rgba(255,255,255,0.9)');
4037
+ } else if (el.type === 'pen' && el.points && el.points.length >= 2) {
4038
+ ctx.strokeStyle = el.color || 'rgba(255,255,255,0.8)';
4039
+ ctx.lineWidth = el.strokeWidth || 3;
4040
+ ctx.lineCap = 'round';
4041
+ ctx.lineJoin = 'round';
4042
+ ctx.stroke(new Path2D(el.pathD || pointsToSmoothPath(el.points)));
4043
+ } else if (el.type === 'section') {
4044
+ // .wb-section: 1px dashed border in the section colour, radius 4,
4045
+ // background colour@0d, centred 28px icon @15%, 11px mono label @70%
4046
+ const def = SECTION_TYPES[el.sectionType] || SECTION_TYPES.content;
4047
+ const color = el.color || def.color;
3191
4048
  ctx.beginPath();
3192
- ctx.roundRect(el.x, el.y, w, h, r);
4049
+ _roundRectPath(ctx, el.x, el.y, bw, bh, 4);
4050
+ ctx.fillStyle = _withAlpha(color, '0d');
4051
+ ctx.fill();
4052
+ ctx.setLineDash([3, 3]);
4053
+ ctx.lineWidth = 1;
4054
+ ctx.strokeStyle = color;
3193
4055
  ctx.stroke();
3194
- } else {
3195
- ctx.strokeRect(el.x, el.y, w, h);
4056
+ ctx.setLineDash([]);
4057
+ ctx.save();
4058
+ ctx.globalAlpha = 0.15;
4059
+ ctx.fillStyle = color;
4060
+ ctx.font = `28px ${bodyFont}`;
4061
+ ctx.textAlign = 'center';
4062
+ ctx.textBaseline = 'middle';
4063
+ ctx.fillText(def.icon, el.x + bw / 2, el.y + bh / 2);
4064
+ ctx.restore();
4065
+ ctx.save();
4066
+ ctx.globalAlpha = 0.7;
4067
+ ctx.fillStyle = color;
4068
+ ctx.font = `11px ${mono}`;
4069
+ ctx.textAlign = 'left';
4070
+ ctx.textBaseline = 'top';
4071
+ ctx.fillText(el.label || def.label, el.x + 8, el.y + 4);
4072
+ ctx.restore();
3196
4073
  }
3197
- } else if (el.type === 'image' && el.dataUrl) {
3198
- try {
3199
- const img = await _loadImage(el.dataUrl);
3200
- ctx.drawImage(img, el.x, el.y, el.width, el.height);
3201
- } catch { /* skip broken images */ }
3202
- } else if (el.type === 'arrow' && el.points && el.points.length >= 2) {
3203
- ctx.strokeStyle = el.color || 'rgba(255,255,255,0.7)';
3204
- ctx.lineWidth = 3;
3205
- ctx.lineCap = 'round';
3206
- ctx.lineJoin = 'round';
3207
- ctx.beginPath();
3208
- ctx.moveTo(el.points[0][0], el.points[0][1]);
3209
- for (let i = 1; i < el.points.length; i++) {
3210
- ctx.lineTo(el.points[i][0], el.points[i][1]);
3211
- }
3212
- ctx.stroke();
3213
- _drawArrowhead(ctx, el.points);
3214
- } else if (el.type === 'pen' && el.points && el.points.length >= 2) {
3215
- ctx.strokeStyle = el.color || 'rgba(255,255,255,0.7)';
3216
- ctx.lineWidth = el.strokeWidth || 3;
3217
- ctx.lineCap = 'round';
3218
- ctx.lineJoin = 'round';
3219
- const pathD = el.pathD || pointsToSmoothPath(el.points);
3220
- const p2d = new Path2D(pathD);
3221
- ctx.stroke(p2d);
3222
- } else if (el.type === 'section') {
3223
- const def = SECTION_TYPES[el.sectionType] || SECTION_TYPES.content;
3224
- const color = el.color || def.color;
3225
- const w = el.width || def.w;
3226
- const h = el.height || def.h;
3227
- ctx.strokeStyle = color;
3228
- ctx.lineWidth = 1;
3229
- ctx.setLineDash([6, 4]);
3230
- ctx.strokeRect(el.x, el.y, w, h);
3231
- ctx.setLineDash([]);
3232
- ctx.fillStyle = color + '1a';
3233
- ctx.fillRect(el.x, el.y, w, h);
3234
- ctx.font = '28px sans-serif';
3235
- ctx.fillStyle = color + '33';
3236
- ctx.textAlign = 'center';
3237
- ctx.textBaseline = 'middle';
3238
- ctx.fillText(def.icon, el.x + w / 2, el.y + h / 2);
3239
- ctx.font = '12px JetBrains Mono, monospace';
3240
- ctx.fillStyle = color;
3241
- ctx.textAlign = 'left';
3242
- ctx.textBaseline = 'top';
3243
- ctx.fillText(el.label || def.label, el.x + 8, el.y + 6);
3244
- ctx.textAlign = 'start';
4074
+ } catch (elErr) {
4075
+ console.warn('[whiteboard] screenshot: skipped element', el.id, elErr);
3245
4076
  }
3246
-
3247
4077
  ctx.restore();
3248
4078
  }
3249
4079
 
@@ -3251,12 +4081,25 @@ async function _captureScreenshot(requestId) {
3251
4081
  const base64 = dataUrl.split(',')[1];
3252
4082
 
3253
4083
  if (_ws && _ws.readyState === 1) {
3254
- _ws.send(JSON.stringify({ type: 'screenshot:response', requestId, data: base64 }));
4084
+ _ws.send(JSON.stringify({
4085
+ type: 'screenshot:response',
4086
+ requestId,
4087
+ data: base64,
4088
+ clientId: _clientId,
4089
+ focusActive: isWhiteboardActive(),
4090
+ width: outW,
4091
+ height: outH,
4092
+ scale,
4093
+ xOffset: vp.xOffset,
4094
+ yOffset: vp.yOffset,
4095
+ crop: crop === 'usable' ? 'usable' : 'full',
4096
+ usable,
4097
+ }));
3255
4098
  }
3256
4099
  } catch (err) {
3257
4100
  console.error('[whiteboard] Screenshot capture failed:', err);
3258
4101
  if (_ws && _ws.readyState === 1) {
3259
- _ws.send(JSON.stringify({ type: 'screenshot:error', requestId, error: err.message }));
4102
+ _ws.send(JSON.stringify({ type: 'screenshot:error', requestId, error: err.message, clientId: _clientId }));
3260
4103
  }
3261
4104
  }
3262
4105
  }