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
@@ -1,1635 +1,1707 @@
1
- // ═══════════════════════════════════════════
2
- // SynaBun Neural Interface — 2D Canvas Renderer
3
- // Pure canvas rendering with orbital layout,
4
- // viewport pan/zoom, hit detection, drag system
5
- // ═══════════════════════════════════════════
6
-
7
- import { gfx } from './gfx.js';
8
- import { computeLayout, resetLayout, flattenRegions, CARD_W, CARD_H } from './layout.js';
9
- import { drawBackground, initBackground } from './background.js';
10
- import { state, emit, on } from '../../shared/state.js';
11
- import { catColor, hexAlpha } from '../../shared/colors.js';
12
- import { KEYS } from '../../shared/constants.js';
13
- import { storage } from '../../shared/storage.js';
14
- import { truncate, normalizeNodes } from '../../shared/utils.js';
15
-
16
- // ═══════════════════════════════════════════
17
- // PRIVATE STATE
18
- // ═══════════════════════════════════════════
19
-
20
- let _canvas = null;
21
- let _ctx = null;
22
- let _container = null;
23
- let _animFrameId = null;
24
- let _callbacks = {};
25
-
26
- // Layout data
27
- let _regions = []; // CategoryRegion[] from computeLayout
28
- let _cards = []; // CardPosition[] from computeLayout
29
- let _cardById = new Map(); // id → CardPosition
30
-
31
- // Category logo preload cache
32
- const _categoryLogos = new Map();
33
-
34
- // Fallback logo for OpenClaw (hardcoded like 3D variant)
35
- const _openclawLogo = new Image();
36
- _openclawLogo.src = '/openclaw-logo-text.png';
37
- let _openclawLogoReady = false;
38
- _openclawLogo.onload = () => { _openclawLogoReady = true; };
39
-
40
- // Graph removal scheduling
41
- let _graphRemovalTimer = null;
42
-
43
- // ═══════════════════════════════════════════
44
- // PERFORMANCE CACHES
45
- // ═══════════════════════════════════════════
46
-
47
- // Neighbor adjacency map: id → Set<id> — built once in applyGraphData
48
- let _neighborMap = new Map();
49
-
50
- // Resolved links cache — built once in applyGraphData, avoids per-frame ID lookups
51
- // Each entry: { sourceCard, targetCard, strength, crossCat }
52
- let _resolvedLinks = [];
53
- let _resolvedLinksStrong = []; // strength >= 0.4 (shown at LOD 1)
54
-
55
- // Card text cache: nodeId → { content, lines }
56
- const _textCache = new Map();
57
-
58
- // DPR for high-DPI rendering (capped to limit fill rate on high-DPI displays)
59
- const MAX_DPR = 1.5;
60
- let _dpr = 1;
61
-
62
- // ═══════════════════════════════════════════
63
- // VIEWPORT (pan/zoom state)
64
- // ═══════════════════════════════════════════
65
-
66
- const _viewport = {
67
- offsetX: 0,
68
- offsetY: 0,
69
- scale: 0.4,
70
- minScale: 0.02,
71
- maxScale: 3.0,
72
- };
73
-
74
- // Animation state
75
- let _animating = false;
76
- let _animStart = 0;
77
- let _animDuration = 0;
78
- let _animFrom = {};
79
- let _animTo = {};
80
-
81
- // Smooth zoom state — lerps toward target for fluid feel
82
- const _smoothZoom = {
83
- targetScale: 0.4,
84
- anchorX: 0, // cursor position in buffer coords (zoom pivot)
85
- anchorY: 0,
86
- active: false,
87
- };
88
-
89
- // ═══════════════════════════════════════════
90
- // INTERACTION STATE
91
- // ═══════════════════════════════════════════
92
-
93
- const _pointer = {
94
- isPanning: false,
95
- isDragging: false,
96
- dragTarget: null, // { type: 'card'|'region', item: CardPosition|Region }
97
- dragSet: [], // items moving together (rigid-body)
98
- dragStartWorld: null, // world coords at drag start
99
- lastScreenX: 0,
100
- lastScreenY: 0,
101
- downScreenX: 0,
102
- downScreenY: 0,
103
- lastMoveTime: 0, // for velocity tracking
104
- downTime: 0,
105
- hasMoved: false,
106
- };
107
-
108
- // Position save timer
109
- let _posSaveTimer = null;
110
-
111
- // Focus mode — true when visualization is paused (zero-resource mode)
112
- let _focusPaused = false;
113
-
114
- // Pan inertia — coasting after mouse release
115
- const _panInertia = {
116
- vx: 0,
117
- vy: 0,
118
- active: false,
119
- };
120
-
121
- // ═══════════════════════════════════════════
122
- // COORDINATE TRANSFORMS
123
- // ═══════════════════════════════════════════
124
-
125
- export function screenToWorld(sx, sy) {
126
- return {
127
- x: (sx * _dpr - _viewport.offsetX) / _viewport.scale,
128
- y: (sy * _dpr - _viewport.offsetY) / _viewport.scale,
129
- };
130
- }
131
-
132
- export function worldToScreen(wx, wy) {
133
- return {
134
- x: (wx * _viewport.scale + _viewport.offsetX) / _dpr,
135
- y: (wy * _viewport.scale + _viewport.offsetY) / _dpr,
136
- };
137
- }
138
-
139
- /**
140
- * Get the visible world-space bounding box (with margin for partially visible items).
141
- * @param {number} margin Extra world-space pixels around the viewport edges
142
- * @returns {{ left: number, right: number, top: number, bottom: number }}
143
- */
144
- function _getVisibleBounds(margin = 0) {
145
- const invScale = 1 / _viewport.scale;
146
- const left = -_viewport.offsetX * invScale - margin;
147
- const top = -_viewport.offsetY * invScale - margin;
148
- const right = (_canvas.width - _viewport.offsetX) * invScale + margin;
149
- const bottom = (_canvas.height - _viewport.offsetY) * invScale + margin;
150
- return { left, right, top, bottom };
151
- }
152
-
153
- // ═══════════════════════════════════════════
154
- // PUBLIC API: Viewport accessors
155
- // ═══════════════════════════════════════════
156
-
157
- export function getViewport() { return { ..._viewport, dpr: _dpr }; }
158
- export function getAllCards() { return _cards; }
159
- export function getRegions() { return _regions; }
160
-
161
-
162
- // ═══════════════════════════════════════════
163
- // INIT
164
- // ═══════════════════════════════════════════
165
-
166
- /**
167
- * Initialize the 2D canvas renderer.
168
- * @param {HTMLElement} container The #graph-container element
169
- * @param {Object} callbacks { onRenderFramePost }
170
- * @returns {Object} Public API shim (centerAt, zoom, etc.)
171
- */
172
- export function initGraph(container, callbacks = {}) {
173
- _container = container;
174
- _callbacks = callbacks;
175
-
176
- // Create or find the main canvas
177
- _canvas = document.getElementById('canvas-main');
178
- if (!_canvas) {
179
- _canvas = document.createElement('canvas');
180
- _canvas.id = 'canvas-main';
181
- _canvas.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1;';
182
- container.appendChild(_canvas);
183
- }
184
- _ctx = _canvas.getContext('2d');
185
-
186
- // Size canvas (DPR-aware, capped for perf)
187
- _dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
188
- _resizeCanvas();
189
-
190
- // Center viewport
191
- _viewport.offsetX = _canvas.width / 2;
192
- _viewport.offsetY = _canvas.height / 2;
193
- _smoothZoom.targetScale = _viewport.scale;
194
-
195
- // Attach event listeners
196
- _canvas.addEventListener('pointerdown', _onPointerDown);
197
- _canvas.addEventListener('pointermove', _onPointerMove);
198
- _canvas.addEventListener('pointerup', _onPointerUp);
199
- _canvas.addEventListener('pointerleave', _onPointerLeave);
200
- _canvas.addEventListener('wheel', _onWheel, { passive: false });
201
- _canvas.addEventListener('dblclick', _onDblClick);
202
- _canvas.addEventListener('contextmenu', _onContextMenu);
203
- window.addEventListener('resize', _resizeCanvas);
204
-
205
- // Start render loop
206
- _animFrameId = requestAnimationFrame(_renderLoop);
207
-
208
- // Init background (logo preload)
209
- initBackground();
210
-
211
- return _getPublicAPI();
212
- }
213
-
214
- /**
215
- * Get the public API object (force-graph compatible shim).
216
- * @returns {Object|null}
217
- */
218
- export function getGraph() {
219
- if (!_canvas) return null;
220
- return _getPublicAPI();
221
- }
222
-
223
-
224
- // ═══════════════════════════════════════════
225
- // PUBLIC API: Data & Layout
226
- // ═══════════════════════════════════════════
227
-
228
- /**
229
- * Apply graph data: filters by activeCategories, runs layout, updates render state.
230
- * Call after data load, category change, or search change.
231
- */
232
- export function applyGraphData() {
233
- if (!_canvas) return;
234
-
235
- // Filter nodes by active categories
236
- const visibleNodes = state.allNodes.filter(n => {
237
- const cat = n.payload?.category;
238
- return cat && state.activeCategories.has(cat);
239
- });
240
-
241
- // Compute orbital layout
242
- const result = computeLayout(visibleNodes, state.categoryMetadata, state.activeCategories);
243
- _regions = result.regions;
244
- _cards = result.cards;
245
-
246
- // Build card lookup
247
- _cardById.clear();
248
- for (const card of _cards) {
249
- _cardById.set(card.node.id, card);
250
- }
251
-
252
- // Build neighbor adjacency map + resolved links (O(links) once, not per frame)
253
- _neighborMap.clear();
254
- _resolvedLinks = [];
255
- _resolvedLinksStrong = [];
256
- for (const link of state.allLinks) {
257
- const s = typeof link.source === 'object' ? link.source.id : link.source;
258
- const t = typeof link.target === 'object' ? link.target.id : link.target;
259
- if (!_neighborMap.has(s)) _neighborMap.set(s, new Set());
260
- if (!_neighborMap.has(t)) _neighborMap.set(t, new Set());
261
- _neighborMap.get(s).add(t);
262
- _neighborMap.get(t).add(s);
263
-
264
- const sc = _cardById.get(s);
265
- const tc = _cardById.get(t);
266
- if (!sc || !tc) continue;
267
- const crossCat = (sc.node.payload?.category !== tc.node.payload?.category);
268
- const entry = { sourceCard: sc, targetCard: tc, strength: link.strength || 0.5, crossCat };
269
- _resolvedLinks.push(entry);
270
- if (entry.strength >= 0.4) _resolvedLinksStrong.push(entry);
271
- }
272
-
273
- // Clear text cache (content may have changed)
274
- _textCache.clear();
275
-
276
- // Preload category logos
277
- const cats = [];
278
- for (const [name, meta] of Object.entries(state.categoryMetadata)) {
279
- if (meta.logo) cats.push({ name, logo: meta.logo });
280
- }
281
- preloadCategoryLogos(cats);
282
-
283
- emit('stats-changed');
284
- }
285
-
286
- /**
287
- * Preload category logo images from metadata.
288
- * @param {Array<{name:string, logo:string}>} categories
289
- */
290
- export function preloadCategoryLogos(categories) {
291
- const names = new Set(categories.map(c => c.name));
292
- for (const [k] of _categoryLogos) { if (!names.has(k)) _categoryLogos.delete(k); }
293
- for (const cat of categories) {
294
- if (!cat.logo) continue;
295
- const existing = _categoryLogos.get(cat.name);
296
- if (existing && existing.src === cat.logo) continue;
297
- const img = new Image();
298
- const entry = { img, ready: false, src: cat.logo };
299
- img.onload = () => { entry.ready = true; };
300
- img.onerror = () => { _categoryLogos.delete(cat.name); };
301
- img.src = cat.logo;
302
- _categoryLogos.set(cat.name, entry);
303
- }
304
- }
305
-
306
- /**
307
- * Re-fetch data from API and re-apply layout.
308
- */
309
- export async function refreshGraph() {
310
- const res = await fetch('/api/memories');
311
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
312
- const data = await res.json();
313
- state.allNodes = normalizeNodes(data.nodes);
314
- state.allLinks = data.links;
315
- applyGraphData();
316
- emit('data-loaded', data);
317
- }
318
-
319
- /**
320
- * Re-compute layout from scratch (replaces reheatSimulation).
321
- */
322
- export function reheatSimulation() {
323
- resetLayout();
324
- applyGraphData();
325
- }
326
-
327
- /**
328
- * Schedule a deferred graph data reapply (after deletion animation).
329
- */
330
- export function scheduleGraphRemoval(delay = 500) {
331
- cancelScheduledRemoval();
332
- _graphRemovalTimer = setTimeout(() => {
333
- applyGraphData();
334
- _graphRemovalTimer = null;
335
- }, delay);
336
- }
337
-
338
- export function cancelScheduledRemoval() {
339
- if (_graphRemovalTimer) {
340
- clearTimeout(_graphRemovalTimer);
341
- _graphRemovalTimer = null;
342
- }
343
- }
344
-
345
-
346
- /** Stop the render loop (focus mode). */
347
- export function stopAnimation() {
348
- if (_animFrameId) {
349
- cancelAnimationFrame(_animFrameId);
350
- _animFrameId = null;
351
- }
352
- }
353
-
354
- /** Restart the render loop after stopping. */
355
- export function startAnimation() {
356
- if (!_animFrameId && _canvas) {
357
- _animFrameId = requestAnimationFrame(_renderLoop);
358
- }
359
- }
360
-
361
- /** Pause all interaction (focus mode — zero resource consumption). */
362
- export function pauseInteraction() {
363
- _focusPaused = true;
364
- _panInertia.active = false;
365
- // Reset any in-flight drag/pan so state doesn't stick across focus toggles
366
- _pointer.isPanning = false;
367
- _pointer.isDragging = false;
368
- // Ensure canvas doesn't block pointer events to layers below (whiteboard).
369
- // This also covers the boot race where viz:toggle fires before _bootComplete.
370
- if (_canvas) _canvas.classList.add('viz-hidden-2d');
371
- }
372
-
373
- /** Resume interaction after focus mode exit. */
374
- export function resumeInteraction() {
375
- _focusPaused = false;
376
- if (_canvas) _canvas.classList.remove('viz-hidden-2d');
377
- }
378
-
379
- // ═══════════════════════════════════════════
380
- // RENDER LOOP
381
- // ═══════════════════════════════════════════
382
-
383
- function _renderLoop(time) {
384
- if (!_canvas || !_ctx) return;
385
- const w = _canvas.width;
386
- const h = _canvas.height;
387
- const ctx = _ctx;
388
-
389
- // Process viewport animation
390
- if (_animating) {
391
- const elapsed = time - _animStart;
392
- const t = Math.min(elapsed / _animDuration, 1);
393
- const ease = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2; // easeInOutQuad
394
-
395
- _viewport.offsetX = _animFrom.offsetX + (_animTo.offsetX - _animFrom.offsetX) * ease;
396
- _viewport.offsetY = _animFrom.offsetY + (_animTo.offsetY - _animFrom.offsetY) * ease;
397
- _viewport.scale = _animFrom.scale + (_animTo.scale - _animFrom.scale) * ease;
398
-
399
- if (t >= 1) {
400
- _animating = false;
401
- _smoothZoom.targetScale = _viewport.scale;
402
- _smoothZoom.active = false;
403
- }
404
- }
405
-
406
- // Process smooth zoom (lerp toward target scale with cursor as pivot)
407
- if (_smoothZoom.active) {
408
- const LERP = 0.18;
409
- const diff = _smoothZoom.targetScale - _viewport.scale;
410
- if (Math.abs(diff) < 0.0001) {
411
- _viewport.scale = _smoothZoom.targetScale;
412
- _smoothZoom.active = false;
413
- } else {
414
- const newScale = _viewport.scale + diff * LERP;
415
- const mx = _smoothZoom.anchorX;
416
- const my = _smoothZoom.anchorY;
417
- _viewport.offsetX = mx - (mx - _viewport.offsetX) * (newScale / _viewport.scale);
418
- _viewport.offsetY = my - (my - _viewport.offsetY) * (newScale / _viewport.scale);
419
- _viewport.scale = newScale;
420
- }
421
- }
422
-
423
- // Process pan inertia (coasting after mouse release)
424
- if (_panInertia.active) {
425
- const FRICTION = 0.92;
426
- _viewport.offsetX += _panInertia.vx;
427
- _viewport.offsetY += _panInertia.vy;
428
- _panInertia.vx *= FRICTION;
429
- _panInertia.vy *= FRICTION;
430
- if (Math.abs(_panInertia.vx) < 0.1 && Math.abs(_panInertia.vy) < 0.1) {
431
- _panInertia.active = false;
432
- }
433
- }
434
-
435
- // ── Compute visible world bounds (with margin for partially visible items) ──
436
- const cardMargin = Math.max(CARD_W, CARD_H);
437
- const vb = _getVisibleBounds(cardMargin);
438
-
439
- // ── LOD thresholds (card screen-space size in CSS pixels) ──
440
- const cardScreenW = CARD_W * _viewport.scale / _dpr;
441
- // LOD 0: full card (cardScreenW >= 25px)
442
- // LOD 1: simple colored rect (12-25px)
443
- // LOD 2: tiny dot (< 12px)
444
- const lod = cardScreenW >= 25 ? 0 : cardScreenW >= 12 ? 1 : 2;
445
-
446
- // ── 1. Background (screen-space) ──
447
- drawBackground(ctx, time, w, h);
448
-
449
- // ── 2. Apply pan/zoom transform ──
450
- ctx.save();
451
- ctx.translate(_viewport.offsetX, _viewport.offsetY);
452
- ctx.scale(_viewport.scale, _viewport.scale);
453
-
454
- // ── 3. Category region glows (culled, skip at LOD 2) ──
455
- if (gfx.regionGlowOpacity > 0 && lod < 2) {
456
- for (const region of _regions) {
457
- if (_isRegionVisible(region, vb)) _drawRegionGlow(ctx, region);
458
- for (const child of (region.children || [])) {
459
- if (_isRegionVisible(child, vb)) _drawRegionGlow(ctx, child);
460
- }
461
- }
462
- }
463
-
464
- // ── 4. Connection lines (batched + culled, LOD-aware, skip at LOD 2) ──
465
- if (state.linkMode !== 'off' && _resolvedLinks.length > 0 && lod < 2) {
466
- _drawLinks(ctx, vb, lod);
467
- }
468
-
469
- // ── 5. Memory cards (LOD-aware + culled) ──
470
- if (lod === 2) {
471
- // LOD 2: tiny dots — batch into single path per color for speed
472
- _drawCardDots(ctx, vb);
473
- } else if (lod === 1) {
474
- // LOD 1: simple colored rectangles, no text/shadows
475
- _drawCardSimple(ctx, vb);
476
- } else {
477
- // LOD 0: full detail
478
- for (const card of _cards) {
479
- if (card.x + CARD_W / 2 < vb.left || card.x - CARD_W / 2 > vb.right ||
480
- card.y + CARD_H / 2 < vb.top || card.y - CARD_H / 2 > vb.bottom) continue;
481
- _drawCard(ctx, card);
482
- }
483
- }
484
-
485
- // ── 6. Category labels (LOD-aware + culled) ──
486
- // Child labels hidden at LOD >= 1 (zoomed out too far to be useful)
487
- // Child labels fade near the LOD boundary for smooth transition
488
- const childLabelOpacity = lod >= 1 ? 0 : Math.min(1, (cardScreenW - 25) / 15); // fade 25→40px
489
- for (const region of _regions) {
490
- if (_isRegionVisible(region, vb)) _drawCategoryLabel(ctx, region, true, 1);
491
- if (childLabelOpacity > 0) {
492
- for (const child of (region.children || [])) {
493
- if (_isRegionVisible(child, vb)) _drawCategoryLabel(ctx, child, false, childLabelOpacity);
494
- }
495
- }
496
- }
497
-
498
- ctx.restore();
499
-
500
- // ── 7. Post-render callback (minimap, etc.) ──
501
- if (_callbacks.onRenderFramePost) {
502
- _callbacks.onRenderFramePost();
503
- }
504
- emit('render-frame-post');
505
-
506
- _animFrameId = requestAnimationFrame(_renderLoop);
507
- }
508
-
509
- /** Check if a region's bounding circle is within the visible bounds */
510
- function _isRegionVisible(region, vb) {
511
- const r = (region.radius || 200) + 100; // extra margin for glow/labels
512
- return region.cx + r > vb.left && region.cx - r < vb.right &&
513
- region.cy + r > vb.top && region.cy - r < vb.bottom;
514
- }
515
-
516
-
517
- // ═══════════════════════════════════════════
518
- // DRAWING: Region Glow
519
- // ═══════════════════════════════════════════
520
-
521
- function _drawRegionGlow(ctx, region) {
522
- if (!region.radius || region.radius <= 0) return;
523
- const r = region.radius + 40;
524
- const grad = ctx.createRadialGradient(region.cx, region.cy, 0, region.cx, region.cy, r);
525
- grad.addColorStop(0, hexAlpha(region.color, gfx.regionGlowOpacity));
526
- grad.addColorStop(1, hexAlpha(region.color, 0));
527
- ctx.fillStyle = grad;
528
- ctx.beginPath();
529
- ctx.arc(region.cx, region.cy, r, 0, Math.PI * 2);
530
- ctx.fill();
531
- }
532
-
533
-
534
- // ═══════════════════════════════════════════
535
- // DRAWING: Category Labels
536
- // ═══════════════════════════════════════════
537
-
538
- function _drawCategoryLabel(ctx, region, isParent, labelOpacity = 1) {
539
- // Respect label visibility toggle
540
- if (!state.labelsVisible) return;
541
-
542
- const mul = state.labelSizeMultiplier || 1;
543
- const logoEntry = _categoryLogos.get(region.name);
544
- // Fall back to hardcoded OpenClaw logo if no uploaded logo
545
- const hasLogo = (logoEntry && logoEntry.ready) || (region.name === 'openclaw' && _openclawLogoReady);
546
- const logoImg = (logoEntry && logoEntry.ready) ? logoEntry.img : _openclawLogo;
547
-
548
- // ── Zoom compensation: ensure labels stay readable at any zoom ──
549
- // Capped at 2.5x to prevent labels from growing larger than the gaps between categories
550
- const MAX_ZOOM_BOOST = isParent ? 2.5 : 2.0;
551
- const MIN_SCREEN_PX = isParent ? 18 : 13;
552
- const baseFontSize = isParent ? 38 : 22;
553
- const rawFontSize = baseFontSize * mul;
554
- const screenPx = rawFontSize * _viewport.scale / _dpr;
555
- const zoomBoost = screenPx < MIN_SCREEN_PX
556
- ? Math.min(MIN_SCREEN_PX / screenPx, MAX_ZOOM_BOOST)
557
- : 1;
558
-
559
- // Apply label opacity (for child label fade-out at zoom boundaries)
560
- const needsOpacity = labelOpacity < 1;
561
- if (needsOpacity) {
562
- ctx.save();
563
- ctx.globalAlpha = labelOpacity;
564
- }
565
-
566
- if (isParent) {
567
- // ── PARENT: logo-first if available, clean text otherwise ──
568
- const fontSize = Math.round(38 * mul * zoomBoost);
569
- const nameText = region.name.toUpperCase();
570
-
571
- // Subtle glow ring
572
- ctx.save();
573
- const glowR = 70 * mul * zoomBoost;
574
- const grad = ctx.createRadialGradient(region.cx, region.cy, glowR * 0.3, region.cx, region.cy, glowR);
575
- grad.addColorStop(0, hexAlpha(region.color, 0.12));
576
- grad.addColorStop(1, hexAlpha(region.color, 0));
577
- ctx.fillStyle = grad;
578
- ctx.beginPath();
579
- ctx.arc(region.cx, region.cy, glowR, 0, Math.PI * 2);
580
- ctx.fill();
581
- ctx.restore();
582
-
583
- let cursorY = region.cy; // tracks vertical stacking position
584
-
585
- if (hasLogo) {
586
- // Logo with its own zoom compensation (independent of text zoomBoost)
587
- // Logos are the primary identifier so they need a higher cap and their own min screen size
588
- const BASE_LOGO_H = 64 * mul;
589
- const MIN_LOGO_SCREEN = 32; // minimum screen pixels for parent logo
590
- const logoScreenH = BASE_LOGO_H * _viewport.scale / _dpr;
591
- const logoBoost = logoScreenH < MIN_LOGO_SCREEN
592
- ? Math.min(MIN_LOGO_SCREEN / logoScreenH, 5.0)
593
- : 1;
594
- const logoH = Math.round(BASE_LOGO_H * logoBoost);
595
- const aspect = (logoImg.naturalWidth || 1) / (logoImg.naturalHeight || 1);
596
- const logoW = Math.round(logoH * aspect);
597
- const logoY = region.cy - logoH / 2;
598
- ctx.save();
599
- ctx.globalAlpha = 0.92;
600
- ctx.drawImage(logoImg,
601
- region.cx - logoW / 2, logoY,
602
- logoW, logoH
603
- );
604
- ctx.restore();
605
- cursorY = logoY + logoH + 6;
606
- } else {
607
- // No logo — glass pill behind text
608
- ctx.font = `bold ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
609
- ctx.textAlign = 'center';
610
- ctx.textBaseline = 'middle';
611
-
612
- // Measure text to size the glass pill
613
- const textW = ctx.measureText(nameText).width;
614
- const padX = fontSize * 0.6;
615
- const padY = fontSize * 0.4;
616
-
617
- // Memory count text (compute now so pill can encompass both)
618
- const count = region.memories ? region.memories.length : 0;
619
- const childCount = (region.children || []).reduce((sum, c) => sum + (c.memories ? c.memories.length : 0), 0);
620
- const total = count + childCount;
621
- const subSize = Math.round(fontSize * 0.45);
622
- const countText = total > 0 ? `${total} ${total === 1 ? 'memory' : 'memories'}` : '';
623
- const countH = total > 0 ? subSize + padY * 0.5 : 0;
624
-
625
- const pillW = textW + padX * 2;
626
- const pillH = fontSize + padY * 2 + countH;
627
- const pillX = region.cx - pillW / 2;
628
- const pillY = cursorY - fontSize / 2 - padY;
629
- const pillR = Math.min(14, pillH / 2);
630
-
631
- // Glass pill — diffuse color-tinted glassmorphic
632
- ctx.save();
633
- const col = region.color;
634
-
635
- // Colored diffuse glow behind pill
636
- ctx.shadowColor = hexAlpha(col, 0.3);
637
- ctx.shadowBlur = 44;
638
- _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
639
- ctx.fillStyle = 'rgba(4, 5, 8, 0.92)';
640
- ctx.fill();
641
- ctx.shadowColor = 'transparent';
642
- ctx.shadowBlur = 0;
643
-
644
- // Color-tinted inner wash
645
- _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
646
- ctx.fillStyle = hexAlpha(col, 0.1);
647
- ctx.fill();
648
-
649
- // Top highlight edge
650
- const hlGrad = ctx.createLinearGradient(pillX, pillY, pillX, pillY + pillH * 0.35);
651
- hlGrad.addColorStop(0, 'rgba(255, 255, 255, 0.06)');
652
- hlGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
653
- _roundRect(ctx, pillX, pillY, pillW, pillH * 0.35, pillR);
654
- ctx.fillStyle = hlGrad;
655
- ctx.fill();
656
-
657
- // Color-tinted border
658
- _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
659
- ctx.strokeStyle = hexAlpha(col, 0.2);
660
- ctx.lineWidth = 1;
661
- ctx.stroke();
662
-
663
- ctx.restore();
664
-
665
- // Draw name text
666
- ctx.font = `bold ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
667
- ctx.textAlign = 'center';
668
- ctx.textBaseline = 'middle';
669
- ctx.fillStyle = region.color;
670
- ctx.fillText(nameText, region.cx, cursorY);
671
- cursorY += fontSize * 0.5 + 4;
672
-
673
- // Memory count inside the pill
674
- if (total > 0) {
675
- ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
676
- ctx.fillStyle = hexAlpha(region.color, 0.5);
677
- ctx.fillText(countText, region.cx, cursorY + subSize * 0.7);
678
- }
679
- }
680
-
681
- // Memory count below (only for logo path — text path handles it inside the pill)
682
- if (hasLogo) {
683
- const count = region.memories ? region.memories.length : 0;
684
- const childCount = (region.children || []).reduce((sum, c) => sum + (c.memories ? c.memories.length : 0), 0);
685
- const total = count + childCount;
686
- if (total > 0) {
687
- const fontSize2 = Math.round(38 * mul * zoomBoost);
688
- const subSize = Math.round(fontSize2 * 0.45);
689
- ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
690
- ctx.fillStyle = hexAlpha(region.color, 0.5);
691
- ctx.fillText(`${total} ${total === 1 ? 'memory' : 'memories'}`, region.cx, cursorY + subSize * 0.7);
692
- }
693
- }
694
-
695
- } else {
696
- // ── CHILD: smaller, simpler ──
697
- const fontSize = Math.round(22 * mul * zoomBoost);
698
- const nameText = region.name.toUpperCase();
699
-
700
- let cursorY = region.cy;
701
-
702
- if (hasLogo) {
703
- // Logo with its own zoom compensation (independent of text zoomBoost)
704
- const BASE_LOGO_H = 32 * mul;
705
- const MIN_LOGO_SCREEN = 20; // minimum screen pixels for child logo
706
- const logoScreenH = BASE_LOGO_H * _viewport.scale / _dpr;
707
- const logoBoost = logoScreenH < MIN_LOGO_SCREEN
708
- ? Math.min(MIN_LOGO_SCREEN / logoScreenH, 4.0)
709
- : 1;
710
- const logoH = Math.round(BASE_LOGO_H * logoBoost);
711
- const aspect = (logoImg.naturalWidth || 1) / (logoImg.naturalHeight || 1);
712
- const logoW = Math.round(logoH * aspect);
713
- const logoY = region.cy - logoH / 2;
714
- ctx.save();
715
- ctx.globalAlpha = 0.75;
716
- ctx.drawImage(logoImg,
717
- region.cx - logoW / 2, logoY,
718
- logoW, logoH
719
- );
720
- ctx.restore();
721
- cursorY = logoY + logoH + 4;
722
- } else {
723
- // No logo — glass pill behind text
724
- ctx.font = `600 ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
725
- ctx.textAlign = 'center';
726
- ctx.textBaseline = 'middle';
727
-
728
- const textW = ctx.measureText(nameText).width;
729
- const padX = fontSize * 0.5;
730
- const padY = fontSize * 0.35;
731
-
732
- // Count text
733
- const count = region.memories ? region.memories.length : 0;
734
- const subSize = Math.round(fontSize * 0.5);
735
- const countH = count > 0 ? subSize + padY * 0.4 : 0;
736
-
737
- const pillW = textW + padX * 2;
738
- const pillH = fontSize + padY * 2 + countH;
739
- const pillX = region.cx - pillW / 2;
740
- const pillY = cursorY - fontSize / 2 - padY;
741
- const pillR = Math.min(10, pillH / 2);
742
-
743
- // Glass pill — diffuse color-tinted glassmorphic
744
- ctx.save();
745
- const col = region.color;
746
-
747
- // Colored diffuse glow
748
- ctx.shadowColor = hexAlpha(col, 0.25);
749
- ctx.shadowBlur = 30;
750
- _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
751
- ctx.fillStyle = 'rgba(4, 5, 8, 0.88)';
752
- ctx.fill();
753
- ctx.shadowColor = 'transparent';
754
- ctx.shadowBlur = 0;
755
-
756
- // Color-tinted inner wash
757
- _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
758
- ctx.fillStyle = hexAlpha(col, 0.08);
759
- ctx.fill();
760
-
761
- // Color-tinted border
762
- _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
763
- ctx.strokeStyle = hexAlpha(col, 0.18);
764
- ctx.lineWidth = 1;
765
- ctx.stroke();
766
- ctx.restore();
767
-
768
- // Name text
769
- ctx.font = `600 ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
770
- ctx.textAlign = 'center';
771
- ctx.textBaseline = 'middle';
772
- ctx.fillStyle = hexAlpha(region.color, 0.8);
773
- ctx.fillText(nameText, region.cx, cursorY);
774
- cursorY += fontSize * 0.5 + 4;
775
-
776
- // Count inside pill
777
- if (count > 0) {
778
- ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
779
- ctx.fillStyle = hexAlpha(region.color, 0.4);
780
- ctx.fillText(`${count}`, region.cx, cursorY + subSize * 0.6);
781
- }
782
- }
783
-
784
- // Memory count (only for logo path)
785
- if (hasLogo) {
786
- const count = region.memories ? region.memories.length : 0;
787
- if (count > 0) {
788
- const subSize = Math.round(fontSize * 0.5);
789
- ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
790
- ctx.fillStyle = hexAlpha(region.color, 0.4);
791
- ctx.fillText(`${count}`, region.cx, cursorY + subSize * 0.6);
792
- }
793
- }
794
- }
795
-
796
- if (needsOpacity) ctx.restore();
797
- }
798
-
799
-
800
- // Pre-computed card background fill style (used by LOD 0, 1, and 2 drawing functions)
801
- const _cardBgFill = `rgba(15, 18, 25, ${gfx.cardOpacity})`;
802
-
803
-
804
- // ═══════════════════════════════════════════
805
- // DRAWING: LOD 2 — Dots (zoomed far out)
806
- // ═══════════════════════════════════════════
807
-
808
- /**
809
- * Draw all visible cards as tiny colored dots, batched by color.
810
- * At LOD 2, cards are ~2px circles — no text, no shadows, no borders.
811
- * Groups by category color for minimal state changes (one fillStyle + fill per color).
812
- */
813
- function _drawCardDots(ctx, vb) {
814
- const hasSearch = state.searchResults !== null;
815
- // Group visible cards by color
816
- const byColor = new Map();
817
- for (const card of _cards) {
818
- if (card.x < vb.left || card.x > vb.right ||
819
- card.y < vb.top || card.y > vb.bottom) continue;
820
- // Skip non-matching cards during search
821
- if (hasSearch && !state.searchResults.has(card.node.id)) continue;
822
- const color = catColor(card.node.payload?.category || '');
823
- let list = byColor.get(color);
824
- if (!list) { list = []; byColor.set(color, list); }
825
- list.push(card);
826
- }
827
-
828
- const TWO_PI = Math.PI * 2;
829
- for (const [color, cards] of byColor) {
830
- ctx.fillStyle = color;
831
- ctx.beginPath();
832
- for (const card of cards) {
833
- const dotR = hasSearch && state.searchResults.has(card.node.id) ? 5 : 3;
834
- ctx.moveTo(card.x + dotR, card.y);
835
- ctx.arc(card.x, card.y, dotR, 0, TWO_PI);
836
- }
837
- ctx.fill();
838
- }
839
- }
840
-
841
-
842
- // ═══════════════════════════════════════════
843
- // DRAWING: LOD 1 — Simple Rects (medium zoom)
844
- // ═══════════════════════════════════════════
845
-
846
- /**
847
- * Draw visible cards as simple colored rectangles — no text, no shadows, no font rendering.
848
- * Much faster than full cards but still shows shape and color coding.
849
- */
850
- function _drawCardSimple(ctx, vb) {
851
- const halfW = CARD_W / 2;
852
- const halfH = CARD_H / 2;
853
- const selectedId = state.selectedNodeId;
854
- const hasSearch = state.searchResults !== null;
855
-
856
- for (const card of _cards) {
857
- if (card.x + halfW < vb.left || card.x - halfW > vb.right ||
858
- card.y + halfH < vb.top || card.y - halfH > vb.bottom) continue;
859
- // Skip non-matching cards during search
860
- if (hasSearch && !state.searchResults.has(card.node.id)) continue;
861
-
862
- const isMatch = hasSearch && state.searchResults.has(card.node.id);
863
- const scale = isMatch ? 1.3 : 1;
864
- const w = CARD_W * scale;
865
- const h = CARD_H * scale;
866
- const x = card.x - w / 2;
867
- const y = card.y - h / 2;
868
- const color = catColor(card.node.payload?.category || '');
869
-
870
- // Card background
871
- ctx.fillStyle = _cardBgFill;
872
- ctx.fillRect(x, y, w, h);
873
-
874
- // Left color bar (thicker at this LOD for visibility)
875
- ctx.fillStyle = color;
876
- ctx.fillRect(x, y, 4, h);
877
-
878
- // Selection or search highlight
879
- if (card.node.id === selectedId || isMatch) {
880
- ctx.strokeStyle = color;
881
- ctx.lineWidth = isMatch ? 1.5 : 2;
882
- ctx.strokeRect(x, y, w, h);
883
- }
884
- }
885
- }
886
-
887
-
888
- // ═══════════════════════════════════════════
889
- // DRAWING: LOD 0 — Full Detail Cards
890
- // ═══════════════════════════════════════════
891
-
892
- function _drawCard(ctx, card) {
893
- const node = card.node;
894
- const cat = node.payload?.category || '';
895
- const color = catColor(cat);
896
- const importance = node.payload?.importance || 5;
897
- const content = node.payload?.content || '';
898
- const isSelected = node.id === state.selectedNodeId;
899
- const isHovered = node.id === state.hoveredNodeId;
900
- const isMultiSelected = state.multiSelected.has(node.id);
901
-
902
- // Opacity for search/focus dimming — use pre-built neighbor map (O(1) lookup)
903
- let opacity = 1;
904
- const isSearchMatch = state.searchResults && state.searchResults.has(node.id);
905
- const isSearchMiss = state.searchResults && !isSearchMatch;
906
- if (isSearchMiss) return; // Vanish non-matching memories during search
907
- if (isSearchMatch) opacity = 1; // Full brightness for matches
908
- if (state.focusedNodeId && state.focusedNodeId !== node.id) {
909
- const neighbors = _neighborMap.get(state.focusedNodeId);
910
- if (neighbors && neighbors.has(node.id)) opacity = 0.7;
911
- else opacity = 0.15;
912
- }
913
- if (opacity < 0.05) return;
914
-
915
- // Search matches get scaled up
916
- const searchScale = isSearchMatch ? 1.3 : 1;
917
- const scaledW = CARD_W * searchScale;
918
- const scaledH = CARD_H * searchScale;
919
- const x = card.x - scaledW / 2;
920
- const y = card.y - scaledH / 2;
921
- const needsShadow = isHovered || isSelected || isMultiSelected || isSearchMatch;
922
-
923
- // Only save/restore when we actually change global state
924
- if (opacity < 1 || needsShadow) {
925
- ctx.save();
926
- ctx.globalAlpha = opacity;
927
- }
928
-
929
- // ── Hover glow (shadow) ──
930
- if (isSearchMatch && !isHovered && !isSelected) {
931
- ctx.shadowColor = hexAlpha(color, 0.7);
932
- ctx.shadowBlur = 14;
933
- } else if (needsShadow) {
934
- ctx.shadowColor = isSelected ? color : hexAlpha(color, 0.6);
935
- ctx.shadowBlur = isSelected ? 16 : 10;
936
- }
937
-
938
- // ── Card background ──
939
- _roundRect(ctx, x, y, scaledW, scaledH, 8);
940
- ctx.fillStyle = _cardBgFill;
941
- ctx.fill();
942
-
943
- // Reset shadow after fill (only if set)
944
- if (needsShadow) {
945
- ctx.shadowColor = 'transparent';
946
- ctx.shadowBlur = 0;
947
- }
948
-
949
- // ── Selection/hover border ──
950
- if (isSelected || isMultiSelected) {
951
- _roundRect(ctx, x, y, scaledW, scaledH, 8);
952
- ctx.strokeStyle = color;
953
- ctx.lineWidth = 1.5;
954
- ctx.stroke();
955
- } else if (isHovered) {
956
- _roundRect(ctx, x, y, scaledW, scaledH, 8);
957
- ctx.strokeStyle = hexAlpha(color, 0.4);
958
- ctx.lineWidth = 1;
959
- ctx.stroke();
960
- } else if (isSearchMatch) {
961
- _roundRect(ctx, x, y, scaledW, scaledH, 8);
962
- ctx.strokeStyle = hexAlpha(color, 0.5);
963
- ctx.lineWidth = 1;
964
- ctx.stroke();
965
- }
966
-
967
- // ── Left color bar (simplified — no clip needed for 3px bar) ──
968
- ctx.fillStyle = color;
969
- ctx.fillRect(x, y + 8, 3, scaledH - 16);
970
-
971
- // ── Category label ──
972
- ctx.font = "bold 8px 'Inter', sans-serif";
973
- ctx.fillStyle = color;
974
- ctx.textAlign = 'left';
975
- ctx.textBaseline = 'top';
976
- ctx.fillText(cat.toUpperCase(), x + 8, y + 6);
977
-
978
- // ── Content preview (cached text wrapping) ──
979
- ctx.font = "10px 'Inter', sans-serif";
980
- ctx.fillStyle = 'rgba(200, 206, 220, 0.85)';
981
- const lines = _getWrappedText(ctx, node.id, content, scaledW - 16, 3);
982
- for (let i = 0; i < lines.length; i++) {
983
- ctx.fillText(lines[i], x + 8, y + 20 + i * 13);
984
- }
985
-
986
- // ── Importance stars ──
987
- if (importance > 0) {
988
- ctx.font = "8px 'Inter', sans-serif";
989
- ctx.fillStyle = 'rgba(255, 200, 50, 0.6)';
990
- ctx.textAlign = 'right';
991
- ctx.textBaseline = 'bottom';
992
- const stars = importance <= 5 ? '\u2606'.repeat(importance) : '\u2605'.repeat(Math.min(importance, 10));
993
- ctx.fillText(stars, x + scaledW - 6, y + scaledH - 4);
994
- }
995
-
996
- // ── Pin indicator ──
997
- if (card.pinned) {
998
- ctx.font = "9px sans-serif";
999
- ctx.fillStyle = 'rgba(255,255,255,0.3)';
1000
- ctx.textAlign = 'right';
1001
- ctx.textBaseline = 'top';
1002
- ctx.fillText('\uD83D\uDCCC', x + scaledW - 4, y + 4);
1003
- }
1004
-
1005
- if (opacity < 1 || needsShadow) {
1006
- ctx.restore();
1007
- }
1008
- }
1009
-
1010
- /**
1011
- * Get cached wrapped text lines for a card.
1012
- * Only re-wraps when content changes.
1013
- */
1014
- function _getWrappedText(ctx, nodeId, content, maxWidth, maxLines) {
1015
- const cached = _textCache.get(nodeId);
1016
- if (cached && cached.content === content) return cached.lines;
1017
- const lines = _wrapText(ctx, content, maxWidth, maxLines);
1018
- _textCache.set(nodeId, { content, lines });
1019
- return lines;
1020
- }
1021
-
1022
-
1023
- // ═══════════════════════════════════════════
1024
- // DRAWING: Links
1025
- // ═══════════════════════════════════════════
1026
-
1027
- function _drawLinks(ctx, vb, lod) {
1028
- // LOD 1: only draw strong links (strength >= 0.4), reduced opacity
1029
- const links = lod >= 1 ? _resolvedLinksStrong : _resolvedLinks;
1030
- const opacity = lod >= 1 ? gfx.linkOpacity * 0.5 : gfx.linkOpacity;
1031
- ctx.strokeStyle = `rgba(100, 140, 200, ${opacity})`;
1032
- ctx.lineWidth = 0.5;
1033
-
1034
- ctx.beginPath();
1035
- const isIntra = state.linkMode === 'intra';
1036
- const hasSearch = state.searchResults !== null;
1037
-
1038
- for (let i = 0, len = links.length; i < len; i++) {
1039
- const link = links[i];
1040
- const sc = link.sourceCard;
1041
- const tc = link.targetCard;
1042
-
1043
- // Viewport cull: skip if both endpoints are off-screen
1044
- const sx = sc.x, sy = sc.y, tx = tc.x, ty = tc.y;
1045
- if ((sx < vb.left && tx < vb.left) || (sx > vb.right && tx > vb.right) ||
1046
- (sy < vb.top && ty < vb.top) || (sy > vb.bottom && ty > vb.bottom)) continue;
1047
-
1048
- // Filter by link mode
1049
- if (isIntra && link.crossCat) continue;
1050
-
1051
- // Hide links where both endpoints are not search matches
1052
- if (hasSearch && !state.searchResults.has(sc.node.id) && !state.searchResults.has(tc.node.id)) continue;
1053
-
1054
- ctx.moveTo(sx, sy);
1055
- ctx.lineTo(tx, ty);
1056
- }
1057
- ctx.stroke();
1058
- }
1059
-
1060
-
1061
- // ═══════════════════════════════════════════
1062
- // DRAWING HELPERS
1063
- // ═══════════════════════════════════════════
1064
-
1065
- function _roundRect(ctx, x, y, w, h, r) {
1066
- ctx.beginPath();
1067
- ctx.moveTo(x + r, y);
1068
- ctx.lineTo(x + w - r, y);
1069
- ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1070
- ctx.lineTo(x + w, y + h - r);
1071
- ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1072
- ctx.lineTo(x + r, y + h);
1073
- ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1074
- ctx.lineTo(x, y + r);
1075
- ctx.quadraticCurveTo(x, y, x + r, y);
1076
- ctx.closePath();
1077
- }
1078
-
1079
- function _wrapText(ctx, text, maxWidth, maxLines) {
1080
- const words = (text || '').replace(/\n/g, ' ').split(' ');
1081
- const lines = [];
1082
- let line = '';
1083
- for (const word of words) {
1084
- const test = line + (line ? ' ' : '') + word;
1085
- if (ctx.measureText(test).width > maxWidth && line) {
1086
- lines.push(line);
1087
- if (lines.length >= maxLines) {
1088
- lines[lines.length - 1] = lines[lines.length - 1].replace(/...$/, '') + '\u2026';
1089
- return lines;
1090
- }
1091
- line = word;
1092
- } else {
1093
- line = test;
1094
- }
1095
- }
1096
- if (line) {
1097
- if (lines.length >= maxLines) {
1098
- lines[lines.length - 1] += '\u2026';
1099
- } else {
1100
- lines.push(line);
1101
- }
1102
- }
1103
- return lines;
1104
- }
1105
-
1106
-
1107
- // ═══════════════════════════════════════════
1108
- // HIT DETECTION
1109
- // ═══════════════════════════════════════════
1110
-
1111
- function _hitTest(screenX, screenY) {
1112
- const { x, y } = screenToWorld(screenX, screenY);
1113
-
1114
- // Check cards (reverse order = topmost first)
1115
- for (let i = _cards.length - 1; i >= 0; i--) {
1116
- const card = _cards[i];
1117
- if (x >= card.x - CARD_W / 2 && x <= card.x + CARD_W / 2 &&
1118
- y >= card.y - CARD_H / 2 && y <= card.y + CARD_H / 2) {
1119
- return { type: 'card', item: card, node: card.node };
1120
- }
1121
- }
1122
-
1123
- // Check category labels — hit zone scales with label size + zoom compensation
1124
- const allRegions = flattenRegions(_regions);
1125
- const mul = state.labelSizeMultiplier || 1;
1126
- for (const region of allRegions) {
1127
- if (!state.labelsVisible) {
1128
- // Labels hidden — small dot hit zone so categories are still clickable
1129
- if (x >= region.cx - 30 && x <= region.cx + 30 &&
1130
- y >= region.cy - 30 && y <= region.cy + 30) {
1131
- return { type: 'region', item: region };
1132
- }
1133
- } else {
1134
- // Compute zoom compensation to match rendered size (capped, same as _drawCategoryLabel)
1135
- const MAX_ZOOM_BOOST = region.isParent ? 2.5 : 2.0;
1136
- const baseFontSize = region.isParent ? 38 : 22;
1137
- const MIN_SCREEN_PX = region.isParent ? 18 : 13;
1138
- const rawFontSize = baseFontSize * mul;
1139
- const screenPx = rawFontSize * _viewport.scale / _dpr;
1140
- const zoomBoost = screenPx < MIN_SCREEN_PX
1141
- ? Math.min(MIN_SCREEN_PX / screenPx, MAX_ZOOM_BOOST)
1142
- : 1;
1143
-
1144
- const fontSize = Math.round(baseFontSize * mul * zoomBoost);
1145
- const hasLogo = _categoryLogos.get(region.name)?.ready || (region.name === 'openclaw' && _openclawLogoReady);
1146
-
1147
- // Hit zone: logo width or text width + padding
1148
- let labelW, labelH;
1149
- if (hasLogo) {
1150
- // Logo has its own zoom compensation (same as _drawCategoryLabel)
1151
- const baseLogo = (region.isParent ? 64 : 32) * mul;
1152
- const minLogoScreen = region.isParent ? 32 : 20;
1153
- const logoScreen = baseLogo * _viewport.scale / _dpr;
1154
- const logoBoost = logoScreen < minLogoScreen
1155
- ? Math.min(minLogoScreen / logoScreen, region.isParent ? 5.0 : 4.0)
1156
- : 1;
1157
- const logoH = baseLogo * logoBoost;
1158
- const logoW = logoH * 3; // conservative aspect estimate
1159
- labelW = Math.max(logoW, 160) + 20;
1160
- labelH = logoH + fontSize * 0.5 + 30;
1161
- } else {
1162
- // Match glass pill sizing: text width + padX on each side, fontSize + padY*2 + count height
1163
- const nameText = region.name.toUpperCase();
1164
- const approxTextW = fontSize * 0.65 * nameText.length;
1165
- const padX = fontSize * (region.isParent ? 0.6 : 0.5);
1166
- const padY = fontSize * (region.isParent ? 0.4 : 0.35);
1167
- const countH = fontSize * 0.5; // always include count space for hit zone
1168
- labelW = approxTextW + padX * 2 + 20; // extra margin for easier clicking
1169
- labelH = fontSize + padY * 2 + countH + 20;
1170
- }
1171
- if (x >= region.cx - labelW / 2 && x <= region.cx + labelW / 2 &&
1172
- y >= region.cy - labelH / 2 && y <= region.cy + labelH / 2) {
1173
- return { type: 'region', item: region };
1174
- }
1175
- }
1176
- }
1177
-
1178
- return null;
1179
- }
1180
-
1181
-
1182
- // ═══════════════════════════════════════════
1183
- // DRAG SYSTEM
1184
- // ═══════════════════════════════════════════
1185
-
1186
- function _buildDragSet(hit) {
1187
- const set = [];
1188
-
1189
- if (hit.type === 'card') {
1190
- // If this card is part of a multi-selection, drag ALL selected cards
1191
- if (state.multiSelected.size > 0 && state.multiSelected.has(hit.node.id)) {
1192
- for (const card of _cards) {
1193
- if (state.multiSelected.has(card.node.id)) {
1194
- set.push({ type: 'card', ref: card });
1195
- }
1196
- }
1197
- } else {
1198
- // Just this card
1199
- set.push({ type: 'card', ref: hit.item });
1200
- }
1201
- } else if (hit.type === 'region') {
1202
- const region = hit.item;
1203
- // The region label itself
1204
- set.push({ type: 'region', ref: region });
1205
-
1206
- // All cards directly on this region
1207
- for (const card of _cards) {
1208
- const cat = card.node.payload?.category;
1209
- if (cat === region.name) {
1210
- set.push({ type: 'card', ref: card });
1211
- }
1212
- }
1213
-
1214
- // If parent, also include children + their cards
1215
- if (region.isParent && region.children) {
1216
- for (const child of region.children) {
1217
- set.push({ type: 'region', ref: child });
1218
- for (const card of _cards) {
1219
- const cat = card.node.payload?.category;
1220
- if (cat === child.name) {
1221
- set.push({ type: 'card', ref: card });
1222
- }
1223
- }
1224
- }
1225
- }
1226
- }
1227
-
1228
- return set;
1229
- }
1230
-
1231
- function _applyDragDelta(dx, dy) {
1232
- for (const item of _pointer.dragSet) {
1233
- if (item.type === 'card') {
1234
- item.ref.x += dx;
1235
- item.ref.y += dy;
1236
- item.ref.pinned = true;
1237
- } else if (item.type === 'region') {
1238
- item.ref.cx += dx;
1239
- item.ref.cy += dy;
1240
- }
1241
- }
1242
- }
1243
-
1244
-
1245
- // ═══════════════════════════════════════════
1246
- // POSITION PERSISTENCE
1247
- // ═══════════════════════════════════════════
1248
-
1249
- function _savePositions() {
1250
- const positions = {};
1251
- for (const card of _cards) {
1252
- positions[card.node.id] = { x: card.x, y: card.y, pinned: card.pinned };
1253
- }
1254
- for (const region of flattenRegions(_regions)) {
1255
- positions[`_cat_${region.name}`] = { x: region.cx, y: region.cy };
1256
- }
1257
- try { storage.setItem(KEYS.NODE_POS_2D, JSON.stringify(positions)); } catch (_) {}
1258
- }
1259
-
1260
- function _schedulePositionSave() {
1261
- if (_posSaveTimer) clearTimeout(_posSaveTimer);
1262
- _posSaveTimer = setTimeout(_savePositions, 2000);
1263
- }
1264
-
1265
-
1266
- // ═══════════════════════════════════════════
1267
- // EVENT HANDLERS
1268
- // ═══════════════════════════════════════════
1269
-
1270
- function _onPointerDown(e) {
1271
- if (_focusPaused || e.button !== 0) return;
1272
- // Skip if Shift (lasso mode handled by lasso.js)
1273
- if (e.shiftKey) return;
1274
-
1275
- const hit = _hitTest(e.clientX, e.clientY);
1276
-
1277
- _pointer.downScreenX = e.clientX;
1278
- _pointer.downScreenY = e.clientY;
1279
- _pointer.lastScreenX = e.clientX;
1280
- _pointer.lastScreenY = e.clientY;
1281
- _pointer.downTime = performance.now();
1282
- _pointer.lastMoveTime = performance.now();
1283
- _pointer.hasMoved = false;
1284
- _panInertia.active = false; // stop coasting on new interaction
1285
-
1286
- if (hit) {
1287
- _pointer.isDragging = true;
1288
- _pointer.isPanning = false;
1289
- _pointer.dragTarget = hit;
1290
- _pointer.dragSet = _buildDragSet(hit);
1291
- _pointer.dragStartWorld = screenToWorld(e.clientX, e.clientY);
1292
- _canvas.setPointerCapture(e.pointerId);
1293
- } else {
1294
- _pointer.isPanning = true;
1295
- _pointer.isDragging = false;
1296
- _pointer.dragTarget = null;
1297
- _canvas.setPointerCapture(e.pointerId);
1298
- }
1299
- }
1300
-
1301
- const DRAG_THRESHOLD = 5; // pixels before drag/pan starts
1302
-
1303
- function _onPointerMove(e) {
1304
- if (_focusPaused) return;
1305
- // Check if we've exceeded the drag threshold before actually moving
1306
- if ((_pointer.isPanning || _pointer.isDragging) && !_pointer.hasMoved) {
1307
- const tdx = e.clientX - _pointer.downScreenX;
1308
- const tdy = e.clientY - _pointer.downScreenY;
1309
- if (Math.abs(tdx) < DRAG_THRESHOLD && Math.abs(tdy) < DRAG_THRESHOLD) return;
1310
- _pointer.hasMoved = true;
1311
- }
1312
-
1313
- if (_pointer.isPanning) {
1314
- const dx = (e.clientX - _pointer.lastScreenX) * _dpr;
1315
- const dy = (e.clientY - _pointer.lastScreenY) * _dpr;
1316
- _viewport.offsetX += dx;
1317
- _viewport.offsetY += dy;
1318
- // Track velocity for inertia
1319
- const now = performance.now();
1320
- const dt = now - _pointer.lastMoveTime || 16;
1321
- _panInertia.vx = dx / dt * 16; // normalize to ~60fps frame
1322
- _panInertia.vy = dy / dt * 16;
1323
- _pointer.lastMoveTime = now;
1324
- _pointer.lastScreenX = e.clientX;
1325
- _pointer.lastScreenY = e.clientY;
1326
- // Kill any active inertia while dragging
1327
- _panInertia.active = false;
1328
- } else if (_pointer.isDragging) {
1329
- const worldNow = screenToWorld(e.clientX, e.clientY);
1330
- const worldPrev = screenToWorld(_pointer.lastScreenX, _pointer.lastScreenY);
1331
- const dx = worldNow.x - worldPrev.x;
1332
- const dy = worldNow.y - worldPrev.y;
1333
- _applyDragDelta(dx, dy);
1334
- _pointer.lastScreenX = e.clientX;
1335
- _pointer.lastScreenY = e.clientY;
1336
- } else {
1337
- // Hover detection
1338
- const hit = _hitTest(e.clientX, e.clientY);
1339
- const newHoverId = (hit && hit.type === 'card') ? hit.node.id : null;
1340
- if (newHoverId !== state.hoveredNodeId) {
1341
- state.hoveredNodeId = newHoverId;
1342
- }
1343
- // Update cursor for both cards and regions
1344
- _canvas.style.cursor = hit ? 'pointer' : 'grab';
1345
- if (newHoverId) emit('node-hover', hit.node);
1346
- else if (!hit) emit('node-hover', null);
1347
- }
1348
- }
1349
-
1350
- function _onPointerUp(e) {
1351
- if (_focusPaused) return;
1352
- const wasDragging = _pointer.isDragging;
1353
- const wasPanning = _pointer.isPanning;
1354
- const moved = _pointer.hasMoved;
1355
- const elapsed = performance.now() - _pointer.downTime;
1356
-
1357
- _pointer.isPanning = false;
1358
- _pointer.isDragging = false;
1359
- _canvas.releasePointerCapture(e.pointerId);
1360
-
1361
- // Start pan inertia if we were panning and had velocity
1362
- if (wasPanning && moved) {
1363
- const speed = Math.sqrt(_panInertia.vx ** 2 + _panInertia.vy ** 2);
1364
- if (speed > 0.5) {
1365
- _panInertia.active = true;
1366
- }
1367
- return;
1368
- }
1369
-
1370
- if (wasDragging && moved) {
1371
- _schedulePositionSave();
1372
- return;
1373
- }
1374
-
1375
- // Click (no significant movement)
1376
- if (!moved || elapsed < 200) {
1377
- const hit = _hitTest(e.clientX, e.clientY);
1378
-
1379
- if (hit && hit.type === 'card') {
1380
- // Ctrl/Cmd+Click: toggle multi-select (same as 3D variant)
1381
- if (e.ctrlKey || e.metaKey) {
1382
- if (state.multiSelected.has(hit.node.id)) {
1383
- state.multiSelected.delete(hit.node.id);
1384
- } else {
1385
- state.multiSelected.add(hit.node.id);
1386
- }
1387
- emit('multiselect:update');
1388
- return;
1389
- }
1390
- // Regular click: clear multi-select, select node
1391
- if (state.multiSelected.size > 0) {
1392
- state.multiSelected.clear();
1393
- emit('multiselect:update');
1394
- }
1395
- state.selectedNodeId = hit.node.id;
1396
- emit('node-selected', hit.node);
1397
- } else if (hit && hit.type === 'region') {
1398
- // Zoom to fit this category region
1399
- _zoomToRegion(hit.item);
1400
- } else {
1401
- // Click background — deselect everything
1402
- if (state.multiSelected.size > 0) {
1403
- state.multiSelected.clear();
1404
- emit('multiselect:update');
1405
- }
1406
- state.selectedNodeId = null;
1407
- emit('background-click');
1408
- emit('node-selected', null);
1409
- }
1410
- }
1411
- }
1412
-
1413
- function _onPointerLeave() {
1414
- if (_focusPaused) return;
1415
- if (state.hoveredNodeId) {
1416
- state.hoveredNodeId = null;
1417
- _canvas.style.cursor = 'grab';
1418
- emit('node-hover', null);
1419
- }
1420
- }
1421
-
1422
- function _onWheel(e) {
1423
- if (_focusPaused) return;
1424
- e.preventDefault();
1425
- // Accumulate zoom target — each tick multiplies by factor
1426
- const zoomFactor = e.deltaY > 0 ? 0.88 : 1.12;
1427
- _smoothZoom.targetScale = Math.max(
1428
- _viewport.minScale,
1429
- Math.min(_viewport.maxScale, (_smoothZoom.active ? _smoothZoom.targetScale : _viewport.scale) * zoomFactor)
1430
- );
1431
- // Update anchor to cursor position (buffer coords)
1432
- _smoothZoom.anchorX = e.clientX * _dpr;
1433
- _smoothZoom.anchorY = e.clientY * _dpr;
1434
- _smoothZoom.active = true;
1435
- }
1436
-
1437
- function _onDblClick(e) {
1438
- if (_focusPaused) return;
1439
- const hit = _hitTest(e.clientX, e.clientY);
1440
- if (hit && hit.type === 'card') {
1441
- // Unpin card
1442
- hit.item.pinned = false;
1443
- _schedulePositionSave();
1444
- } else {
1445
- // Zoom to fit all
1446
- _zoomToFit(400, 60);
1447
- }
1448
- }
1449
-
1450
- function _onContextMenu(e) {
1451
- if (_focusPaused) { e.preventDefault(); return; }
1452
- e.preventDefault();
1453
- const hit = _hitTest(e.clientX, e.clientY);
1454
- if (hit && hit.type === 'card') {
1455
- emit('node-context-menu', { node: hit.node, event: e });
1456
- }
1457
- }
1458
-
1459
-
1460
- // ═══════════════════════════════════════════
1461
- // VIEWPORT NAVIGATION
1462
- // ═══════════════════════════════════════════
1463
-
1464
- function _centerAt(worldX, worldY, duration = 0) {
1465
- const targetOffsetX = _canvas.width / 2 - worldX * _viewport.scale;
1466
- const targetOffsetY = _canvas.height / 2 - worldY * _viewport.scale;
1467
-
1468
- if (duration <= 0) {
1469
- _viewport.offsetX = targetOffsetX;
1470
- _viewport.offsetY = targetOffsetY;
1471
- return;
1472
- }
1473
-
1474
- _animFrom = {
1475
- offsetX: _viewport.offsetX,
1476
- offsetY: _viewport.offsetY,
1477
- scale: _viewport.scale,
1478
- };
1479
- _animTo = {
1480
- offsetX: targetOffsetX,
1481
- offsetY: targetOffsetY,
1482
- scale: _viewport.scale,
1483
- };
1484
- _animStart = performance.now();
1485
- _animDuration = duration;
1486
- _animating = true;
1487
- }
1488
-
1489
- function _zoomToFit(duration = 400, padding = 60) {
1490
- if (_cards.length === 0) return;
1491
-
1492
- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1493
- for (const card of _cards) {
1494
- if (card.x - CARD_W / 2 < minX) minX = card.x - CARD_W / 2;
1495
- if (card.x + CARD_W / 2 > maxX) maxX = card.x + CARD_W / 2;
1496
- if (card.y - CARD_H / 2 < minY) minY = card.y - CARD_H / 2;
1497
- if (card.y + CARD_H / 2 > maxY) maxY = card.y + CARD_H / 2;
1498
- }
1499
-
1500
- const rangeX = (maxX - minX) || 1;
1501
- const rangeY = (maxY - minY) || 1;
1502
- const centerX = (minX + maxX) / 2;
1503
- const centerY = (minY + maxY) / 2;
1504
-
1505
- const scaledPadding = padding * _dpr;
1506
- const availW = _canvas.width - scaledPadding * 2;
1507
- const availH = _canvas.height - scaledPadding * 2;
1508
- const newScale = Math.min(availW / rangeX, availH / rangeY);
1509
- const clampedScale = Math.max(_viewport.minScale, Math.min(_viewport.maxScale, newScale));
1510
-
1511
- if (duration <= 0) {
1512
- _viewport.scale = clampedScale;
1513
- _viewport.offsetX = _canvas.width / 2 - centerX * clampedScale;
1514
- _viewport.offsetY = _canvas.height / 2 - centerY * clampedScale;
1515
- return;
1516
- }
1517
-
1518
- _animFrom = {
1519
- offsetX: _viewport.offsetX,
1520
- offsetY: _viewport.offsetY,
1521
- scale: _viewport.scale,
1522
- };
1523
- _animTo = {
1524
- offsetX: _canvas.width / 2 - centerX * clampedScale,
1525
- offsetY: _canvas.height / 2 - centerY * clampedScale,
1526
- scale: clampedScale,
1527
- };
1528
- _animStart = performance.now();
1529
- _animDuration = duration;
1530
- _animating = true;
1531
- }
1532
-
1533
- function _zoomToRegion(region, duration = 400) {
1534
- const padding = 80 * _dpr;
1535
- const regionR = region.radius || 200;
1536
-
1537
- const rangeX = regionR * 2;
1538
- const rangeY = regionR * 2;
1539
-
1540
- const availW = _canvas.width - padding * 2;
1541
- const availH = _canvas.height - padding * 2;
1542
- const newScale = Math.min(availW / rangeX, availH / rangeY);
1543
- const clampedScale = Math.max(_viewport.minScale, Math.min(_viewport.maxScale, newScale));
1544
-
1545
- _animFrom = {
1546
- offsetX: _viewport.offsetX,
1547
- offsetY: _viewport.offsetY,
1548
- scale: _viewport.scale,
1549
- };
1550
- _animTo = {
1551
- offsetX: _canvas.width / 2 - region.cx * clampedScale,
1552
- offsetY: _canvas.height / 2 - region.cy * clampedScale,
1553
- scale: clampedScale,
1554
- };
1555
- _animStart = performance.now();
1556
- _animDuration = duration;
1557
- _animating = true;
1558
- }
1559
-
1560
-
1561
- // ═══════════════════════════════════════════
1562
- // RESIZE
1563
- // ═══════════════════════════════════════════
1564
-
1565
- function _resizeCanvas() {
1566
- if (!_canvas) return;
1567
- _dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
1568
- const w = window.innerWidth;
1569
- const h = window.innerHeight;
1570
- // Set canvas buffer size to DPR-scaled resolution for crisp rendering
1571
- _canvas.width = Math.round(w * _dpr);
1572
- _canvas.height = Math.round(h * _dpr);
1573
- // CSS display size stays at viewport dimensions
1574
- _canvas.style.width = w + 'px';
1575
- _canvas.style.height = h + 'px';
1576
- }
1577
-
1578
-
1579
- // ═══════════════════════════════════════════
1580
- // PUBLIC API SHIM (force-graph compatible)
1581
- // ═══════════════════════════════════════════
1582
-
1583
- function _getPublicAPI() {
1584
- return {
1585
- centerAt: (x, y, duration) => {
1586
- if (x == null && y == null) {
1587
- // getter mode (used by minimap)
1588
- return {
1589
- x: (_canvas.width / 2 - _viewport.offsetX) / _viewport.scale,
1590
- y: (_canvas.height / 2 - _viewport.offsetY) / _viewport.scale,
1591
- };
1592
- }
1593
- _centerAt(x, y, duration);
1594
- },
1595
- zoom: (level, duration) => {
1596
- if (level == null) return _viewport.scale; // getter
1597
- const newScale = Math.max(_viewport.minScale, Math.min(_viewport.maxScale, level));
1598
- if (duration > 0) {
1599
- _animFrom = { offsetX: _viewport.offsetX, offsetY: _viewport.offsetY, scale: _viewport.scale };
1600
- _animTo = { offsetX: _viewport.offsetX, offsetY: _viewport.offsetY, scale: newScale };
1601
- // Zoom from center
1602
- const cx = _canvas.width / 2;
1603
- const cy = _canvas.height / 2;
1604
- _animTo.offsetX = cx - (cx - _viewport.offsetX) * (newScale / _viewport.scale);
1605
- _animTo.offsetY = cy - (cy - _viewport.offsetY) * (newScale / _viewport.scale);
1606
- _animStart = performance.now();
1607
- _animDuration = duration;
1608
- _animating = true;
1609
- } else {
1610
- _viewport.scale = newScale;
1611
- }
1612
- },
1613
- zoomToFit: (duration, padding) => _zoomToFit(duration, padding),
1614
- graphData: () => ({
1615
- nodes: _cards.map(c => ({
1616
- id: c.node.id,
1617
- x: c.x,
1618
- y: c.y,
1619
- payload: c.node.payload,
1620
- })),
1621
- links: state.allLinks,
1622
- }),
1623
- width: (w) => {
1624
- if (w != null) { _canvas.style.width = w + 'px'; _resizeCanvas(); }
1625
- return _canvas ? Math.round(_canvas.width / _dpr) : 0;
1626
- },
1627
- height: (h) => {
1628
- if (h != null) { _canvas.style.height = h + 'px'; _resizeCanvas(); }
1629
- return _canvas ? Math.round(_canvas.height / _dpr) : 0;
1630
- },
1631
- // No-ops for compatibility
1632
- linkVisibility: (fn) => fn,
1633
- d3ReheatSimulation: () => reheatSimulation(),
1634
- };
1635
- }
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — 2D Canvas Renderer
3
+ // Pure canvas rendering with orbital layout,
4
+ // viewport pan/zoom, hit detection, drag system
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { gfx } from './gfx.js';
8
+ import { computeLayout, resetLayout, flattenRegions, CARD_W, CARD_H } from './layout.js';
9
+ import { drawBackground, initBackground } from './background.js';
10
+ import { state, emit, on } from '../../shared/state.js';
11
+ import { catColor, hexAlpha } from '../../shared/colors.js';
12
+ import { KEYS } from '../../shared/constants.js';
13
+ import { storage } from '../../shared/storage.js';
14
+ import { posStore } from '../../shared/node-positions-store.js';
15
+ import { truncate, normalizeNodes, rafThrottle } from '../../shared/utils.js';
16
+
17
+ // ═══════════════════════════════════════════
18
+ // PRIVATE STATE
19
+ // ═══════════════════════════════════════════
20
+
21
+ let _canvas = null;
22
+ let _ctx = null;
23
+ let _container = null;
24
+ let _animFrameId = null;
25
+ let _callbacks = {};
26
+
27
+ // Layout data
28
+ let _regions = []; // CategoryRegion[] from computeLayout
29
+ let _cards = []; // CardPosition[] from computeLayout
30
+ let _cardById = new Map(); // id → CardPosition
31
+
32
+ // Category logo preload cache
33
+ const _categoryLogos = new Map();
34
+
35
+ // Fallback logo for OpenClaw (hardcoded like 3D variant)
36
+ const _openclawLogo = new Image();
37
+ _openclawLogo.src = '/openclaw-logo-text.png';
38
+ let _openclawLogoReady = false;
39
+ _openclawLogo.onload = () => { _openclawLogoReady = true; };
40
+
41
+ // Graph removal scheduling
42
+ let _graphRemovalTimer = null;
43
+
44
+ // ═══════════════════════════════════════════
45
+ // PERFORMANCE CACHES
46
+ // ═══════════════════════════════════════════
47
+
48
+ // Neighbor adjacency map: id → Set<id> — built once in applyGraphData
49
+ let _neighborMap = new Map();
50
+
51
+ // Resolved links cache — built once in applyGraphData, avoids per-frame ID lookups
52
+ // Each entry: { sourceCard, targetCard, strength, crossCat }
53
+ let _resolvedLinks = [];
54
+ let _resolvedLinksStrong = []; // strength >= 0.4 (shown at LOD 1)
55
+
56
+ // Card text cache: nodeId → { content, lines }
57
+ const _textCache = new Map();
58
+
59
+ // DPR for high-DPI rendering (capped to limit fill rate on high-DPI displays)
60
+ const MAX_DPR = 1.5;
61
+ let _dpr = 1;
62
+
63
+ // Idle throttling — full rate while panning/zooming/animating, reduced when
64
+ // still (background breathing is a 4s sine; 15fps keeps it smooth enough)
65
+ const IDLE_AFTER_MS = 2000;
66
+ const IDLE_FPS = 15;
67
+ let _externallyStopped = false; // stopAnimation() called (focus mode / viz toggle)
68
+ let _lastActivityTime = 0;
69
+ let _lastIdleRenderTime = 0;
70
+
71
+ // ═══════════════════════════════════════════
72
+ // VIEWPORT (pan/zoom state)
73
+ // ═══════════════════════════════════════════
74
+
75
+ const _viewport = {
76
+ offsetX: 0,
77
+ offsetY: 0,
78
+ scale: 0.4,
79
+ minScale: 0.02,
80
+ maxScale: 3.0,
81
+ };
82
+
83
+ // Animation state
84
+ let _animating = false;
85
+ let _animStart = 0;
86
+ let _animDuration = 0;
87
+ let _animFrom = {};
88
+ let _animTo = {};
89
+
90
+ // Smooth zoom state — lerps toward target for fluid feel
91
+ const _smoothZoom = {
92
+ targetScale: 0.4,
93
+ anchorX: 0, // cursor position in buffer coords (zoom pivot)
94
+ anchorY: 0,
95
+ active: false,
96
+ };
97
+
98
+ // ═══════════════════════════════════════════
99
+ // INTERACTION STATE
100
+ // ═══════════════════════════════════════════
101
+
102
+ const _pointer = {
103
+ isPanning: false,
104
+ isDragging: false,
105
+ dragTarget: null, // { type: 'card'|'region', item: CardPosition|Region }
106
+ dragSet: [], // items moving together (rigid-body)
107
+ dragStartWorld: null, // world coords at drag start
108
+ lastScreenX: 0,
109
+ lastScreenY: 0,
110
+ downScreenX: 0,
111
+ downScreenY: 0,
112
+ lastMoveTime: 0, // for velocity tracking
113
+ downTime: 0,
114
+ hasMoved: false,
115
+ };
116
+
117
+ // Position save timer
118
+ let _posSaveTimer = null;
119
+
120
+ // Focus mode — true when visualization is paused (zero-resource mode)
121
+ let _focusPaused = false;
122
+
123
+ // Pan inertia — coasting after mouse release
124
+ const _panInertia = {
125
+ vx: 0,
126
+ vy: 0,
127
+ active: false,
128
+ };
129
+
130
+ // ═══════════════════════════════════════════
131
+ // COORDINATE TRANSFORMS
132
+ // ═══════════════════════════════════════════
133
+
134
+ export function screenToWorld(sx, sy) {
135
+ return {
136
+ x: (sx * _dpr - _viewport.offsetX) / _viewport.scale,
137
+ y: (sy * _dpr - _viewport.offsetY) / _viewport.scale,
138
+ };
139
+ }
140
+
141
+ export function worldToScreen(wx, wy) {
142
+ return {
143
+ x: (wx * _viewport.scale + _viewport.offsetX) / _dpr,
144
+ y: (wy * _viewport.scale + _viewport.offsetY) / _dpr,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Get the visible world-space bounding box (with margin for partially visible items).
150
+ * @param {number} margin Extra world-space pixels around the viewport edges
151
+ * @returns {{ left: number, right: number, top: number, bottom: number }}
152
+ */
153
+ function _getVisibleBounds(margin = 0) {
154
+ const invScale = 1 / _viewport.scale;
155
+ const left = -_viewport.offsetX * invScale - margin;
156
+ const top = -_viewport.offsetY * invScale - margin;
157
+ const right = (_canvas.width - _viewport.offsetX) * invScale + margin;
158
+ const bottom = (_canvas.height - _viewport.offsetY) * invScale + margin;
159
+ return { left, right, top, bottom };
160
+ }
161
+
162
+ // ═══════════════════════════════════════════
163
+ // PUBLIC API: Viewport accessors
164
+ // ═══════════════════════════════════════════
165
+
166
+ export function getViewport() { return { ..._viewport, dpr: _dpr }; }
167
+ export function getAllCards() { return _cards; }
168
+ export function getRegions() { return _regions; }
169
+
170
+
171
+ // ═══════════════════════════════════════════
172
+ // INIT
173
+ // ═══════════════════════════════════════════
174
+
175
+ /**
176
+ * Initialize the 2D canvas renderer.
177
+ * @param {HTMLElement} container The #graph-container element
178
+ * @param {Object} callbacks { onRenderFramePost }
179
+ * @returns {Object} Public API shim (centerAt, zoom, etc.)
180
+ */
181
+ export function initGraph(container, callbacks = {}) {
182
+ _container = container;
183
+ _callbacks = callbacks;
184
+
185
+ // Create or find the main canvas
186
+ _canvas = document.getElementById('canvas-main');
187
+ if (!_canvas) {
188
+ _canvas = document.createElement('canvas');
189
+ _canvas.id = 'canvas-main';
190
+ _canvas.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1;';
191
+ container.appendChild(_canvas);
192
+ }
193
+ _ctx = _canvas.getContext('2d');
194
+
195
+ // Size canvas (DPR-aware, capped for perf)
196
+ _dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
197
+ _resizeCanvas();
198
+
199
+ // Center viewport
200
+ _viewport.offsetX = _canvas.width / 2;
201
+ _viewport.offsetY = _canvas.height / 2;
202
+ _smoothZoom.targetScale = _viewport.scale;
203
+
204
+ // Attach event listeners
205
+ _canvas.addEventListener('pointerdown', _onPointerDown);
206
+ _canvas.addEventListener('pointermove', _onPointerMove);
207
+ _canvas.addEventListener('pointerup', _onPointerUp);
208
+ _canvas.addEventListener('pointerleave', _onPointerLeave);
209
+ _canvas.addEventListener('wheel', _onWheel, { passive: false });
210
+ _canvas.addEventListener('dblclick', _onDblClick);
211
+ _canvas.addEventListener('contextmenu', _onContextMenu);
212
+ window.addEventListener('resize', _scheduleCanvasResize);
213
+
214
+ // Hidden tab → stop the loop entirely; resume on return unless externally stopped
215
+ document.addEventListener('visibilitychange', () => {
216
+ if (document.hidden) {
217
+ if (_animFrameId) { cancelAnimationFrame(_animFrameId); _animFrameId = null; }
218
+ } else if (!_externallyStopped && !_animFrameId && _canvas) {
219
+ _lastActivityTime = performance.now(); // render immediately on return
220
+ _animFrameId = requestAnimationFrame(_renderLoop);
221
+ }
222
+ });
223
+
224
+ // Start render loop
225
+ _animFrameId = requestAnimationFrame(_renderLoop);
226
+
227
+ // Init background (logo preload)
228
+ initBackground();
229
+
230
+ return _getPublicAPI();
231
+ }
232
+
233
+ /**
234
+ * Get the public API object (force-graph compatible shim).
235
+ * @returns {Object|null}
236
+ */
237
+ export function getGraph() {
238
+ if (!_canvas) return null;
239
+ return _getPublicAPI();
240
+ }
241
+
242
+
243
+ // ═══════════════════════════════════════════
244
+ // PUBLIC API: Data & Layout
245
+ // ═══════════════════════════════════════════
246
+
247
+ /**
248
+ * Apply graph data: filters by activeCategories, runs layout, updates render state.
249
+ * Call after data load, category change, or search change.
250
+ */
251
+ export function applyGraphData() {
252
+ if (!_canvas) return;
253
+ _lastActivityTime = performance.now(); // new data → render at full rate
254
+
255
+ // Filter nodes by active categories
256
+ const visibleNodes = state.allNodes.filter(n => {
257
+ const cat = n.payload?.category;
258
+ return cat && state.activeCategories.has(cat);
259
+ });
260
+
261
+ // Compute orbital layout
262
+ const result = computeLayout(visibleNodes, state.categoryMetadata, state.activeCategories);
263
+ _regions = result.regions;
264
+ _cards = result.cards;
265
+
266
+ // Build card lookup
267
+ _cardById.clear();
268
+ for (const card of _cards) {
269
+ _cardById.set(card.node.id, card);
270
+ }
271
+
272
+ // Build neighbor adjacency map + resolved links (O(links) once, not per frame)
273
+ _neighborMap.clear();
274
+ _resolvedLinks = [];
275
+ _resolvedLinksStrong = [];
276
+ for (const link of state.allLinks) {
277
+ const s = typeof link.source === 'object' ? link.source.id : link.source;
278
+ const t = typeof link.target === 'object' ? link.target.id : link.target;
279
+ if (!_neighborMap.has(s)) _neighborMap.set(s, new Set());
280
+ if (!_neighborMap.has(t)) _neighborMap.set(t, new Set());
281
+ _neighborMap.get(s).add(t);
282
+ _neighborMap.get(t).add(s);
283
+
284
+ const sc = _cardById.get(s);
285
+ const tc = _cardById.get(t);
286
+ if (!sc || !tc) continue;
287
+ const crossCat = (sc.node.payload?.category !== tc.node.payload?.category);
288
+ const entry = { sourceCard: sc, targetCard: tc, strength: link.strength || 0.5, crossCat };
289
+ _resolvedLinks.push(entry);
290
+ if (entry.strength >= 0.4) _resolvedLinksStrong.push(entry);
291
+ }
292
+
293
+ // Clear text cache (content may have changed)
294
+ _textCache.clear();
295
+
296
+ // Preload category logos
297
+ const cats = [];
298
+ for (const [name, meta] of Object.entries(state.categoryMetadata)) {
299
+ if (meta.logo) cats.push({ name, logo: meta.logo });
300
+ }
301
+ preloadCategoryLogos(cats);
302
+
303
+ emit('stats-changed');
304
+ }
305
+
306
+ /**
307
+ * Preload category logo images from metadata.
308
+ * @param {Array<{name:string, logo:string}>} categories
309
+ */
310
+ export function preloadCategoryLogos(categories) {
311
+ const names = new Set(categories.map(c => c.name));
312
+ for (const [k] of _categoryLogos) { if (!names.has(k)) _categoryLogos.delete(k); }
313
+ for (const cat of categories) {
314
+ if (!cat.logo) continue;
315
+ const existing = _categoryLogos.get(cat.name);
316
+ if (existing && existing.src === cat.logo) continue;
317
+ const img = new Image();
318
+ const entry = { img, ready: false, src: cat.logo };
319
+ img.onload = () => { entry.ready = true; };
320
+ img.onerror = () => { _categoryLogos.delete(cat.name); };
321
+ img.src = cat.logo;
322
+ _categoryLogos.set(cat.name, entry);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Re-fetch data from API and re-apply layout.
328
+ */
329
+ export async function refreshGraph() {
330
+ const res = await fetch('/api/memories?fields=graph');
331
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
332
+ const data = await res.json();
333
+ state.allNodes = normalizeNodes(data.nodes);
334
+ state.allLinks = data.links;
335
+ applyGraphData();
336
+ emit('data-loaded', data);
337
+ }
338
+
339
+ /**
340
+ * Re-compute layout from scratch (replaces reheatSimulation).
341
+ */
342
+ export function reheatSimulation() {
343
+ resetLayout();
344
+ applyGraphData();
345
+ }
346
+
347
+ /**
348
+ * Schedule a deferred graph data reapply (after deletion animation).
349
+ */
350
+ export function scheduleGraphRemoval(delay = 500) {
351
+ cancelScheduledRemoval();
352
+ _graphRemovalTimer = setTimeout(() => {
353
+ applyGraphData();
354
+ _graphRemovalTimer = null;
355
+ }, delay);
356
+ }
357
+
358
+ export function cancelScheduledRemoval() {
359
+ if (_graphRemovalTimer) {
360
+ clearTimeout(_graphRemovalTimer);
361
+ _graphRemovalTimer = null;
362
+ }
363
+ }
364
+
365
+
366
+ /** Stop the render loop (focus mode). */
367
+ export function stopAnimation() {
368
+ _externallyStopped = true;
369
+ if (_animFrameId) {
370
+ cancelAnimationFrame(_animFrameId);
371
+ _animFrameId = null;
372
+ }
373
+ }
374
+
375
+ /** Restart the render loop after stopping. */
376
+ export function startAnimation() {
377
+ _externallyStopped = false;
378
+ _lastActivityTime = performance.now();
379
+ if (!_animFrameId && _canvas) {
380
+ _animFrameId = requestAnimationFrame(_renderLoop);
381
+ }
382
+ }
383
+
384
+ /** Pause all interaction (focus mode — zero resource consumption). */
385
+ export function pauseInteraction() {
386
+ _focusPaused = true;
387
+ _panInertia.active = false;
388
+ // Reset any in-flight drag/pan so state doesn't stick across focus toggles
389
+ _pointer.isPanning = false;
390
+ _pointer.isDragging = false;
391
+ // Ensure canvas doesn't block pointer events to layers below (whiteboard).
392
+ // This also covers the boot race where viz:toggle fires before _bootComplete.
393
+ if (_canvas) _canvas.classList.add('viz-hidden-2d');
394
+ }
395
+
396
+ /** Resume interaction after focus mode exit. */
397
+ export function resumeInteraction() {
398
+ _focusPaused = false;
399
+ if (_canvas) _canvas.classList.remove('viz-hidden-2d');
400
+ }
401
+
402
+ // ═══════════════════════════════════════════
403
+ // RENDER LOOP
404
+ // ═══════════════════════════════════════════
405
+
406
+ function _renderLoop(time) {
407
+ if (!_canvas || !_ctx) return;
408
+
409
+ // Pending window resize: apply it now (once this frame) and draw this frame
410
+ // even when idle — resizing cleared the canvas.
411
+ const resized = _resizePending;
412
+ if (resized) _resizeCanvas();
413
+
414
+ // Idle gate: while any viewport motion is in flight stay at full rate;
415
+ // after 2s of stillness drop to IDLE_FPS (skip the frame, keep the loop)
416
+ if (_animating || _smoothZoom.active || _panInertia.active) {
417
+ _lastActivityTime = time;
418
+ }
419
+ if (!resized &&
420
+ time - _lastActivityTime > IDLE_AFTER_MS &&
421
+ time - _lastIdleRenderTime < 1000 / IDLE_FPS) {
422
+ _animFrameId = requestAnimationFrame(_renderLoop);
423
+ return;
424
+ }
425
+ _lastIdleRenderTime = time;
426
+
427
+ const w = _canvas.width;
428
+ const h = _canvas.height;
429
+ const ctx = _ctx;
430
+
431
+ // Process viewport animation
432
+ if (_animating) {
433
+ const elapsed = time - _animStart;
434
+ const t = Math.min(elapsed / _animDuration, 1);
435
+ const ease = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2; // easeInOutQuad
436
+
437
+ _viewport.offsetX = _animFrom.offsetX + (_animTo.offsetX - _animFrom.offsetX) * ease;
438
+ _viewport.offsetY = _animFrom.offsetY + (_animTo.offsetY - _animFrom.offsetY) * ease;
439
+ _viewport.scale = _animFrom.scale + (_animTo.scale - _animFrom.scale) * ease;
440
+
441
+ if (t >= 1) {
442
+ _animating = false;
443
+ _smoothZoom.targetScale = _viewport.scale;
444
+ _smoothZoom.active = false;
445
+ }
446
+ }
447
+
448
+ // Process smooth zoom (lerp toward target scale with cursor as pivot)
449
+ if (_smoothZoom.active) {
450
+ const LERP = 0.18;
451
+ const diff = _smoothZoom.targetScale - _viewport.scale;
452
+ if (Math.abs(diff) < 0.0001) {
453
+ _viewport.scale = _smoothZoom.targetScale;
454
+ _smoothZoom.active = false;
455
+ } else {
456
+ const newScale = _viewport.scale + diff * LERP;
457
+ const mx = _smoothZoom.anchorX;
458
+ const my = _smoothZoom.anchorY;
459
+ _viewport.offsetX = mx - (mx - _viewport.offsetX) * (newScale / _viewport.scale);
460
+ _viewport.offsetY = my - (my - _viewport.offsetY) * (newScale / _viewport.scale);
461
+ _viewport.scale = newScale;
462
+ }
463
+ }
464
+
465
+ // Process pan inertia (coasting after mouse release)
466
+ if (_panInertia.active) {
467
+ const FRICTION = 0.92;
468
+ _viewport.offsetX += _panInertia.vx;
469
+ _viewport.offsetY += _panInertia.vy;
470
+ _panInertia.vx *= FRICTION;
471
+ _panInertia.vy *= FRICTION;
472
+ if (Math.abs(_panInertia.vx) < 0.1 && Math.abs(_panInertia.vy) < 0.1) {
473
+ _panInertia.active = false;
474
+ }
475
+ }
476
+
477
+ // ── Compute visible world bounds (with margin for partially visible items) ──
478
+ const cardMargin = Math.max(CARD_W, CARD_H);
479
+ const vb = _getVisibleBounds(cardMargin);
480
+
481
+ // ── LOD thresholds (card screen-space size in CSS pixels) ──
482
+ const cardScreenW = CARD_W * _viewport.scale / _dpr;
483
+ // LOD 0: full card (cardScreenW >= 25px)
484
+ // LOD 1: simple colored rect (12-25px)
485
+ // LOD 2: tiny dot (< 12px)
486
+ const lod = cardScreenW >= 25 ? 0 : cardScreenW >= 12 ? 1 : 2;
487
+
488
+ // ── 1. Background (screen-space) ──
489
+ drawBackground(ctx, time, w, h);
490
+
491
+ // ── 2. Apply pan/zoom transform ──
492
+ ctx.save();
493
+ ctx.translate(_viewport.offsetX, _viewport.offsetY);
494
+ ctx.scale(_viewport.scale, _viewport.scale);
495
+
496
+ // ── 3. Category region glows (culled, skip at LOD 2) ──
497
+ if (gfx.regionGlowOpacity > 0 && lod < 2) {
498
+ for (const region of _regions) {
499
+ if (_isRegionVisible(region, vb)) _drawRegionGlow(ctx, region);
500
+ for (const child of (region.children || [])) {
501
+ if (_isRegionVisible(child, vb)) _drawRegionGlow(ctx, child);
502
+ }
503
+ }
504
+ }
505
+
506
+ // ── 4. Connection lines (batched + culled, LOD-aware, skip at LOD 2) ──
507
+ if (state.linkMode !== 'off' && _resolvedLinks.length > 0 && lod < 2) {
508
+ _drawLinks(ctx, vb, lod);
509
+ }
510
+
511
+ // ── 5. Memory cards (LOD-aware + culled) ──
512
+ if (lod === 2) {
513
+ // LOD 2: tiny dots — batch into single path per color for speed
514
+ _drawCardDots(ctx, vb);
515
+ } else if (lod === 1) {
516
+ // LOD 1: simple colored rectangles, no text/shadows
517
+ _drawCardSimple(ctx, vb);
518
+ } else {
519
+ // LOD 0: full detail
520
+ for (const card of _cards) {
521
+ if (card.x + CARD_W / 2 < vb.left || card.x - CARD_W / 2 > vb.right ||
522
+ card.y + CARD_H / 2 < vb.top || card.y - CARD_H / 2 > vb.bottom) continue;
523
+ _drawCard(ctx, card);
524
+ }
525
+ }
526
+
527
+ // ── 6. Category labels (LOD-aware + culled) ──
528
+ // Child labels hidden at LOD >= 1 (zoomed out too far to be useful)
529
+ // Child labels fade near the LOD boundary for smooth transition
530
+ const childLabelOpacity = lod >= 1 ? 0 : Math.min(1, (cardScreenW - 25) / 15); // fade 25→40px
531
+ for (const region of _regions) {
532
+ if (_isRegionVisible(region, vb)) _drawCategoryLabel(ctx, region, true, 1);
533
+ if (childLabelOpacity > 0) {
534
+ for (const child of (region.children || [])) {
535
+ if (_isRegionVisible(child, vb)) _drawCategoryLabel(ctx, child, false, childLabelOpacity);
536
+ }
537
+ }
538
+ }
539
+
540
+ ctx.restore();
541
+
542
+ // ── 7. Post-render callback (minimap, etc.) ──
543
+ if (_callbacks.onRenderFramePost) {
544
+ _callbacks.onRenderFramePost();
545
+ }
546
+ emit('render-frame-post');
547
+
548
+ _animFrameId = requestAnimationFrame(_renderLoop);
549
+ }
550
+
551
+ /** Check if a region's bounding circle is within the visible bounds */
552
+ function _isRegionVisible(region, vb) {
553
+ const r = (region.radius || 200) + 100; // extra margin for glow/labels
554
+ return region.cx + r > vb.left && region.cx - r < vb.right &&
555
+ region.cy + r > vb.top && region.cy - r < vb.bottom;
556
+ }
557
+
558
+
559
+ // ═══════════════════════════════════════════
560
+ // DRAWING: Region Glow
561
+ // ═══════════════════════════════════════════
562
+
563
+ function _drawRegionGlow(ctx, region) {
564
+ if (!region.radius || region.radius <= 0) return;
565
+ const r = region.radius + 40;
566
+ const grad = ctx.createRadialGradient(region.cx, region.cy, 0, region.cx, region.cy, r);
567
+ grad.addColorStop(0, hexAlpha(region.color, gfx.regionGlowOpacity));
568
+ grad.addColorStop(1, hexAlpha(region.color, 0));
569
+ ctx.fillStyle = grad;
570
+ ctx.beginPath();
571
+ ctx.arc(region.cx, region.cy, r, 0, Math.PI * 2);
572
+ ctx.fill();
573
+ }
574
+
575
+
576
+ // ═══════════════════════════════════════════
577
+ // DRAWING: Category Labels
578
+ // ═══════════════════════════════════════════
579
+
580
+ function _drawCategoryLabel(ctx, region, isParent, labelOpacity = 1) {
581
+ // Respect label visibility toggle
582
+ if (!state.labelsVisible) return;
583
+
584
+ const mul = state.labelSizeMultiplier || 1;
585
+ const logoEntry = _categoryLogos.get(region.name);
586
+ // Fall back to hardcoded OpenClaw logo if no uploaded logo
587
+ const hasLogo = (logoEntry && logoEntry.ready) || (region.name === 'openclaw' && _openclawLogoReady);
588
+ const logoImg = (logoEntry && logoEntry.ready) ? logoEntry.img : _openclawLogo;
589
+
590
+ // ── Zoom compensation: ensure labels stay readable at any zoom ──
591
+ // Capped at 2.5x to prevent labels from growing larger than the gaps between categories
592
+ const MAX_ZOOM_BOOST = isParent ? 2.5 : 2.0;
593
+ const MIN_SCREEN_PX = isParent ? 18 : 13;
594
+ const baseFontSize = isParent ? 38 : 22;
595
+ const rawFontSize = baseFontSize * mul;
596
+ const screenPx = rawFontSize * _viewport.scale / _dpr;
597
+ const zoomBoost = screenPx < MIN_SCREEN_PX
598
+ ? Math.min(MIN_SCREEN_PX / screenPx, MAX_ZOOM_BOOST)
599
+ : 1;
600
+
601
+ // Apply label opacity (for child label fade-out at zoom boundaries)
602
+ const needsOpacity = labelOpacity < 1;
603
+ if (needsOpacity) {
604
+ ctx.save();
605
+ ctx.globalAlpha = labelOpacity;
606
+ }
607
+
608
+ if (isParent) {
609
+ // ── PARENT: logo-first if available, clean text otherwise ──
610
+ const fontSize = Math.round(38 * mul * zoomBoost);
611
+ const nameText = region.name.toUpperCase();
612
+
613
+ // Subtle glow ring
614
+ ctx.save();
615
+ const glowR = 70 * mul * zoomBoost;
616
+ const grad = ctx.createRadialGradient(region.cx, region.cy, glowR * 0.3, region.cx, region.cy, glowR);
617
+ grad.addColorStop(0, hexAlpha(region.color, 0.12));
618
+ grad.addColorStop(1, hexAlpha(region.color, 0));
619
+ ctx.fillStyle = grad;
620
+ ctx.beginPath();
621
+ ctx.arc(region.cx, region.cy, glowR, 0, Math.PI * 2);
622
+ ctx.fill();
623
+ ctx.restore();
624
+
625
+ let cursorY = region.cy; // tracks vertical stacking position
626
+
627
+ if (hasLogo) {
628
+ // Logo with its own zoom compensation (independent of text zoomBoost)
629
+ // Logos are the primary identifier so they need a higher cap and their own min screen size
630
+ const BASE_LOGO_H = 64 * mul;
631
+ const MIN_LOGO_SCREEN = 32; // minimum screen pixels for parent logo
632
+ const logoScreenH = BASE_LOGO_H * _viewport.scale / _dpr;
633
+ const logoBoost = logoScreenH < MIN_LOGO_SCREEN
634
+ ? Math.min(MIN_LOGO_SCREEN / logoScreenH, 5.0)
635
+ : 1;
636
+ const logoH = Math.round(BASE_LOGO_H * logoBoost);
637
+ const aspect = (logoImg.naturalWidth || 1) / (logoImg.naturalHeight || 1);
638
+ const logoW = Math.round(logoH * aspect);
639
+ const logoY = region.cy - logoH / 2;
640
+ ctx.save();
641
+ ctx.globalAlpha = 0.92;
642
+ ctx.drawImage(logoImg,
643
+ region.cx - logoW / 2, logoY,
644
+ logoW, logoH
645
+ );
646
+ ctx.restore();
647
+ cursorY = logoY + logoH + 6;
648
+ } else {
649
+ // No logo — glass pill behind text
650
+ ctx.font = `bold ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
651
+ ctx.textAlign = 'center';
652
+ ctx.textBaseline = 'middle';
653
+
654
+ // Measure text to size the glass pill
655
+ const textW = ctx.measureText(nameText).width;
656
+ const padX = fontSize * 0.6;
657
+ const padY = fontSize * 0.4;
658
+
659
+ // Memory count text (compute now so pill can encompass both)
660
+ const count = region.memories ? region.memories.length : 0;
661
+ const childCount = (region.children || []).reduce((sum, c) => sum + (c.memories ? c.memories.length : 0), 0);
662
+ const total = count + childCount;
663
+ const subSize = Math.round(fontSize * 0.45);
664
+ const countText = total > 0 ? `${total} ${total === 1 ? 'memory' : 'memories'}` : '';
665
+ const countH = total > 0 ? subSize + padY * 0.5 : 0;
666
+
667
+ const pillW = textW + padX * 2;
668
+ const pillH = fontSize + padY * 2 + countH;
669
+ const pillX = region.cx - pillW / 2;
670
+ const pillY = cursorY - fontSize / 2 - padY;
671
+ const pillR = Math.min(14, pillH / 2);
672
+
673
+ // Glass pill — diffuse color-tinted glassmorphic
674
+ ctx.save();
675
+ const col = region.color;
676
+
677
+ // Colored diffuse glow behind pill
678
+ ctx.shadowColor = hexAlpha(col, 0.3);
679
+ ctx.shadowBlur = 44;
680
+ _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
681
+ ctx.fillStyle = 'rgba(4, 5, 8, 0.92)';
682
+ ctx.fill();
683
+ ctx.shadowColor = 'transparent';
684
+ ctx.shadowBlur = 0;
685
+
686
+ // Color-tinted inner wash
687
+ _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
688
+ ctx.fillStyle = hexAlpha(col, 0.1);
689
+ ctx.fill();
690
+
691
+ // Top highlight edge
692
+ const hlGrad = ctx.createLinearGradient(pillX, pillY, pillX, pillY + pillH * 0.35);
693
+ hlGrad.addColorStop(0, 'rgba(255, 255, 255, 0.06)');
694
+ hlGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
695
+ _roundRect(ctx, pillX, pillY, pillW, pillH * 0.35, pillR);
696
+ ctx.fillStyle = hlGrad;
697
+ ctx.fill();
698
+
699
+ // Color-tinted border
700
+ _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
701
+ ctx.strokeStyle = hexAlpha(col, 0.2);
702
+ ctx.lineWidth = 1;
703
+ ctx.stroke();
704
+
705
+ ctx.restore();
706
+
707
+ // Draw name text
708
+ ctx.font = `bold ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
709
+ ctx.textAlign = 'center';
710
+ ctx.textBaseline = 'middle';
711
+ ctx.fillStyle = region.color;
712
+ ctx.fillText(nameText, region.cx, cursorY);
713
+ cursorY += fontSize * 0.5 + 4;
714
+
715
+ // Memory count inside the pill
716
+ if (total > 0) {
717
+ ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
718
+ ctx.fillStyle = hexAlpha(region.color, 0.5);
719
+ ctx.fillText(countText, region.cx, cursorY + subSize * 0.7);
720
+ }
721
+ }
722
+
723
+ // Memory count below (only for logo path — text path handles it inside the pill)
724
+ if (hasLogo) {
725
+ const count = region.memories ? region.memories.length : 0;
726
+ const childCount = (region.children || []).reduce((sum, c) => sum + (c.memories ? c.memories.length : 0), 0);
727
+ const total = count + childCount;
728
+ if (total > 0) {
729
+ const fontSize2 = Math.round(38 * mul * zoomBoost);
730
+ const subSize = Math.round(fontSize2 * 0.45);
731
+ ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
732
+ ctx.fillStyle = hexAlpha(region.color, 0.5);
733
+ ctx.fillText(`${total} ${total === 1 ? 'memory' : 'memories'}`, region.cx, cursorY + subSize * 0.7);
734
+ }
735
+ }
736
+
737
+ } else {
738
+ // ── CHILD: smaller, simpler ──
739
+ const fontSize = Math.round(22 * mul * zoomBoost);
740
+ const nameText = region.name.toUpperCase();
741
+
742
+ let cursorY = region.cy;
743
+
744
+ if (hasLogo) {
745
+ // Logo with its own zoom compensation (independent of text zoomBoost)
746
+ const BASE_LOGO_H = 32 * mul;
747
+ const MIN_LOGO_SCREEN = 20; // minimum screen pixels for child logo
748
+ const logoScreenH = BASE_LOGO_H * _viewport.scale / _dpr;
749
+ const logoBoost = logoScreenH < MIN_LOGO_SCREEN
750
+ ? Math.min(MIN_LOGO_SCREEN / logoScreenH, 4.0)
751
+ : 1;
752
+ const logoH = Math.round(BASE_LOGO_H * logoBoost);
753
+ const aspect = (logoImg.naturalWidth || 1) / (logoImg.naturalHeight || 1);
754
+ const logoW = Math.round(logoH * aspect);
755
+ const logoY = region.cy - logoH / 2;
756
+ ctx.save();
757
+ ctx.globalAlpha = 0.75;
758
+ ctx.drawImage(logoImg,
759
+ region.cx - logoW / 2, logoY,
760
+ logoW, logoH
761
+ );
762
+ ctx.restore();
763
+ cursorY = logoY + logoH + 4;
764
+ } else {
765
+ // No logo — glass pill behind text
766
+ ctx.font = `600 ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
767
+ ctx.textAlign = 'center';
768
+ ctx.textBaseline = 'middle';
769
+
770
+ const textW = ctx.measureText(nameText).width;
771
+ const padX = fontSize * 0.5;
772
+ const padY = fontSize * 0.35;
773
+
774
+ // Count text
775
+ const count = region.memories ? region.memories.length : 0;
776
+ const subSize = Math.round(fontSize * 0.5);
777
+ const countH = count > 0 ? subSize + padY * 0.4 : 0;
778
+
779
+ const pillW = textW + padX * 2;
780
+ const pillH = fontSize + padY * 2 + countH;
781
+ const pillX = region.cx - pillW / 2;
782
+ const pillY = cursorY - fontSize / 2 - padY;
783
+ const pillR = Math.min(10, pillH / 2);
784
+
785
+ // Glass pill — diffuse color-tinted glassmorphic
786
+ ctx.save();
787
+ const col = region.color;
788
+
789
+ // Colored diffuse glow
790
+ ctx.shadowColor = hexAlpha(col, 0.25);
791
+ ctx.shadowBlur = 30;
792
+ _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
793
+ ctx.fillStyle = 'rgba(4, 5, 8, 0.88)';
794
+ ctx.fill();
795
+ ctx.shadowColor = 'transparent';
796
+ ctx.shadowBlur = 0;
797
+
798
+ // Color-tinted inner wash
799
+ _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
800
+ ctx.fillStyle = hexAlpha(col, 0.08);
801
+ ctx.fill();
802
+
803
+ // Color-tinted border
804
+ _roundRect(ctx, pillX, pillY, pillW, pillH, pillR);
805
+ ctx.strokeStyle = hexAlpha(col, 0.18);
806
+ ctx.lineWidth = 1;
807
+ ctx.stroke();
808
+ ctx.restore();
809
+
810
+ // Name text
811
+ ctx.font = `600 ${fontSize}px 'Space Grotesk', 'Inter', sans-serif`;
812
+ ctx.textAlign = 'center';
813
+ ctx.textBaseline = 'middle';
814
+ ctx.fillStyle = hexAlpha(region.color, 0.8);
815
+ ctx.fillText(nameText, region.cx, cursorY);
816
+ cursorY += fontSize * 0.5 + 4;
817
+
818
+ // Count inside pill
819
+ if (count > 0) {
820
+ ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
821
+ ctx.fillStyle = hexAlpha(region.color, 0.4);
822
+ ctx.fillText(`${count}`, region.cx, cursorY + subSize * 0.6);
823
+ }
824
+ }
825
+
826
+ // Memory count (only for logo path)
827
+ if (hasLogo) {
828
+ const count = region.memories ? region.memories.length : 0;
829
+ if (count > 0) {
830
+ const subSize = Math.round(fontSize * 0.5);
831
+ ctx.font = `500 ${subSize}px 'Inter', sans-serif`;
832
+ ctx.fillStyle = hexAlpha(region.color, 0.4);
833
+ ctx.fillText(`${count}`, region.cx, cursorY + subSize * 0.6);
834
+ }
835
+ }
836
+ }
837
+
838
+ if (needsOpacity) ctx.restore();
839
+ }
840
+
841
+
842
+ // Pre-computed card background fill style (used by LOD 0, 1, and 2 drawing functions)
843
+ const _cardBgFill = `rgba(15, 18, 25, ${gfx.cardOpacity})`;
844
+
845
+
846
+ // ═══════════════════════════════════════════
847
+ // DRAWING: LOD 2 — Dots (zoomed far out)
848
+ // ═══════════════════════════════════════════
849
+
850
+ /**
851
+ * Draw all visible cards as tiny colored dots, batched by color.
852
+ * At LOD 2, cards are ~2px circles — no text, no shadows, no borders.
853
+ * Groups by category color for minimal state changes (one fillStyle + fill per color).
854
+ */
855
+ function _drawCardDots(ctx, vb) {
856
+ const hasSearch = state.searchResults !== null;
857
+ // Group visible cards by color
858
+ const byColor = new Map();
859
+ for (const card of _cards) {
860
+ if (card.x < vb.left || card.x > vb.right ||
861
+ card.y < vb.top || card.y > vb.bottom) continue;
862
+ // Skip non-matching cards during search
863
+ if (hasSearch && !state.searchResults.has(card.node.id)) continue;
864
+ const color = catColor(card.node.payload?.category || '');
865
+ let list = byColor.get(color);
866
+ if (!list) { list = []; byColor.set(color, list); }
867
+ list.push(card);
868
+ }
869
+
870
+ const TWO_PI = Math.PI * 2;
871
+ for (const [color, cards] of byColor) {
872
+ ctx.fillStyle = color;
873
+ ctx.beginPath();
874
+ for (const card of cards) {
875
+ const dotR = hasSearch && state.searchResults.has(card.node.id) ? 5 : 3;
876
+ ctx.moveTo(card.x + dotR, card.y);
877
+ ctx.arc(card.x, card.y, dotR, 0, TWO_PI);
878
+ }
879
+ ctx.fill();
880
+ }
881
+ }
882
+
883
+
884
+ // ═══════════════════════════════════════════
885
+ // DRAWING: LOD 1 — Simple Rects (medium zoom)
886
+ // ═══════════════════════════════════════════
887
+
888
+ /**
889
+ * Draw visible cards as simple colored rectangles — no text, no shadows, no font rendering.
890
+ * Much faster than full cards but still shows shape and color coding.
891
+ */
892
+ function _drawCardSimple(ctx, vb) {
893
+ const halfW = CARD_W / 2;
894
+ const halfH = CARD_H / 2;
895
+ const selectedId = state.selectedNodeId;
896
+ const hasSearch = state.searchResults !== null;
897
+
898
+ for (const card of _cards) {
899
+ if (card.x + halfW < vb.left || card.x - halfW > vb.right ||
900
+ card.y + halfH < vb.top || card.y - halfH > vb.bottom) continue;
901
+ // Skip non-matching cards during search
902
+ if (hasSearch && !state.searchResults.has(card.node.id)) continue;
903
+
904
+ const isMatch = hasSearch && state.searchResults.has(card.node.id);
905
+ const scale = isMatch ? 1.3 : 1;
906
+ const w = CARD_W * scale;
907
+ const h = CARD_H * scale;
908
+ const x = card.x - w / 2;
909
+ const y = card.y - h / 2;
910
+ const color = catColor(card.node.payload?.category || '');
911
+
912
+ // Card background
913
+ ctx.fillStyle = _cardBgFill;
914
+ ctx.fillRect(x, y, w, h);
915
+
916
+ // Left color bar (thicker at this LOD for visibility)
917
+ ctx.fillStyle = color;
918
+ ctx.fillRect(x, y, 4, h);
919
+
920
+ // Selection or search highlight
921
+ if (card.node.id === selectedId || isMatch) {
922
+ ctx.strokeStyle = color;
923
+ ctx.lineWidth = isMatch ? 1.5 : 2;
924
+ ctx.strokeRect(x, y, w, h);
925
+ }
926
+ }
927
+ }
928
+
929
+
930
+ // ═══════════════════════════════════════════
931
+ // DRAWING: LOD 0 — Full Detail Cards
932
+ // ═══════════════════════════════════════════
933
+
934
+ function _drawCard(ctx, card) {
935
+ const node = card.node;
936
+ const cat = node.payload?.category || '';
937
+ const color = catColor(cat);
938
+ const importance = node.payload?.importance || 5;
939
+ const content = node.payload?.content || '';
940
+ const isSelected = node.id === state.selectedNodeId;
941
+ const isHovered = node.id === state.hoveredNodeId;
942
+ const isMultiSelected = state.multiSelected.has(node.id);
943
+
944
+ // Opacity for search/focus dimming — use pre-built neighbor map (O(1) lookup)
945
+ let opacity = 1;
946
+ const isSearchMatch = state.searchResults && state.searchResults.has(node.id);
947
+ const isSearchMiss = state.searchResults && !isSearchMatch;
948
+ if (isSearchMiss) return; // Vanish non-matching memories during search
949
+ if (isSearchMatch) opacity = 1; // Full brightness for matches
950
+ if (state.focusedNodeId && state.focusedNodeId !== node.id) {
951
+ const neighbors = _neighborMap.get(state.focusedNodeId);
952
+ if (neighbors && neighbors.has(node.id)) opacity = 0.7;
953
+ else opacity = 0.15;
954
+ }
955
+ if (opacity < 0.05) return;
956
+
957
+ // Search matches get scaled up
958
+ const searchScale = isSearchMatch ? 1.3 : 1;
959
+ const scaledW = CARD_W * searchScale;
960
+ const scaledH = CARD_H * searchScale;
961
+ const x = card.x - scaledW / 2;
962
+ const y = card.y - scaledH / 2;
963
+ const needsShadow = isHovered || isSelected || isMultiSelected || isSearchMatch;
964
+
965
+ // Only save/restore when we actually change global state
966
+ if (opacity < 1 || needsShadow) {
967
+ ctx.save();
968
+ ctx.globalAlpha = opacity;
969
+ }
970
+
971
+ // ── Hover glow (shadow) ──
972
+ if (isSearchMatch && !isHovered && !isSelected) {
973
+ ctx.shadowColor = hexAlpha(color, 0.7);
974
+ ctx.shadowBlur = 14;
975
+ } else if (needsShadow) {
976
+ ctx.shadowColor = isSelected ? color : hexAlpha(color, 0.6);
977
+ ctx.shadowBlur = isSelected ? 16 : 10;
978
+ }
979
+
980
+ // ── Card background ──
981
+ _roundRect(ctx, x, y, scaledW, scaledH, 8);
982
+ ctx.fillStyle = _cardBgFill;
983
+ ctx.fill();
984
+
985
+ // Reset shadow after fill (only if set)
986
+ if (needsShadow) {
987
+ ctx.shadowColor = 'transparent';
988
+ ctx.shadowBlur = 0;
989
+ }
990
+
991
+ // ── Selection/hover border ──
992
+ if (isSelected || isMultiSelected) {
993
+ _roundRect(ctx, x, y, scaledW, scaledH, 8);
994
+ ctx.strokeStyle = color;
995
+ ctx.lineWidth = 1.5;
996
+ ctx.stroke();
997
+ } else if (isHovered) {
998
+ _roundRect(ctx, x, y, scaledW, scaledH, 8);
999
+ ctx.strokeStyle = hexAlpha(color, 0.4);
1000
+ ctx.lineWidth = 1;
1001
+ ctx.stroke();
1002
+ } else if (isSearchMatch) {
1003
+ _roundRect(ctx, x, y, scaledW, scaledH, 8);
1004
+ ctx.strokeStyle = hexAlpha(color, 0.5);
1005
+ ctx.lineWidth = 1;
1006
+ ctx.stroke();
1007
+ }
1008
+
1009
+ // ── Left color bar (simplified — no clip needed for 3px bar) ──
1010
+ ctx.fillStyle = color;
1011
+ ctx.fillRect(x, y + 8, 3, scaledH - 16);
1012
+
1013
+ // ── Category label ──
1014
+ ctx.font = "bold 8px 'Inter', sans-serif";
1015
+ ctx.fillStyle = color;
1016
+ ctx.textAlign = 'left';
1017
+ ctx.textBaseline = 'top';
1018
+ ctx.fillText(cat.toUpperCase(), x + 8, y + 6);
1019
+
1020
+ // ── Content preview (cached text wrapping) ──
1021
+ ctx.font = "10px 'Inter', sans-serif";
1022
+ ctx.fillStyle = 'rgba(200, 206, 220, 0.85)';
1023
+ const lines = _getWrappedText(ctx, node.id, content, scaledW - 16, 3);
1024
+ for (let i = 0; i < lines.length; i++) {
1025
+ ctx.fillText(lines[i], x + 8, y + 20 + i * 13);
1026
+ }
1027
+
1028
+ // ── Importance stars ──
1029
+ if (importance > 0) {
1030
+ ctx.font = "8px 'Inter', sans-serif";
1031
+ ctx.fillStyle = 'rgba(255, 200, 50, 0.6)';
1032
+ ctx.textAlign = 'right';
1033
+ ctx.textBaseline = 'bottom';
1034
+ const stars = importance <= 5 ? '\u2606'.repeat(importance) : '\u2605'.repeat(Math.min(importance, 10));
1035
+ ctx.fillText(stars, x + scaledW - 6, y + scaledH - 4);
1036
+ }
1037
+
1038
+ // ── Pin indicator ──
1039
+ if (card.pinned) {
1040
+ ctx.font = "9px sans-serif";
1041
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
1042
+ ctx.textAlign = 'right';
1043
+ ctx.textBaseline = 'top';
1044
+ ctx.fillText('\uD83D\uDCCC', x + scaledW - 4, y + 4);
1045
+ }
1046
+
1047
+ if (opacity < 1 || needsShadow) {
1048
+ ctx.restore();
1049
+ }
1050
+ }
1051
+
1052
+ /**
1053
+ * Get cached wrapped text lines for a card.
1054
+ * Only re-wraps when content changes.
1055
+ */
1056
+ function _getWrappedText(ctx, nodeId, content, maxWidth, maxLines) {
1057
+ const cached = _textCache.get(nodeId);
1058
+ if (cached && cached.content === content) return cached.lines;
1059
+ const lines = _wrapText(ctx, content, maxWidth, maxLines);
1060
+ _textCache.set(nodeId, { content, lines });
1061
+ return lines;
1062
+ }
1063
+
1064
+
1065
+ // ═══════════════════════════════════════════
1066
+ // DRAWING: Links
1067
+ // ═══════════════════════════════════════════
1068
+
1069
+ function _drawLinks(ctx, vb, lod) {
1070
+ // LOD 1: only draw strong links (strength >= 0.4), reduced opacity
1071
+ const links = lod >= 1 ? _resolvedLinksStrong : _resolvedLinks;
1072
+ const opacity = lod >= 1 ? gfx.linkOpacity * 0.5 : gfx.linkOpacity;
1073
+ ctx.strokeStyle = `rgba(100, 140, 200, ${opacity})`;
1074
+ ctx.lineWidth = 0.5;
1075
+
1076
+ ctx.beginPath();
1077
+ const isIntra = state.linkMode === 'intra';
1078
+ const hasSearch = state.searchResults !== null;
1079
+
1080
+ for (let i = 0, len = links.length; i < len; i++) {
1081
+ const link = links[i];
1082
+ const sc = link.sourceCard;
1083
+ const tc = link.targetCard;
1084
+
1085
+ // Viewport cull: skip if both endpoints are off-screen
1086
+ const sx = sc.x, sy = sc.y, tx = tc.x, ty = tc.y;
1087
+ if ((sx < vb.left && tx < vb.left) || (sx > vb.right && tx > vb.right) ||
1088
+ (sy < vb.top && ty < vb.top) || (sy > vb.bottom && ty > vb.bottom)) continue;
1089
+
1090
+ // Filter by link mode
1091
+ if (isIntra && link.crossCat) continue;
1092
+
1093
+ // Hide links where both endpoints are not search matches
1094
+ if (hasSearch && !state.searchResults.has(sc.node.id) && !state.searchResults.has(tc.node.id)) continue;
1095
+
1096
+ ctx.moveTo(sx, sy);
1097
+ ctx.lineTo(tx, ty);
1098
+ }
1099
+ ctx.stroke();
1100
+ }
1101
+
1102
+
1103
+ // ═══════════════════════════════════════════
1104
+ // DRAWING HELPERS
1105
+ // ═══════════════════════════════════════════
1106
+
1107
+ function _roundRect(ctx, x, y, w, h, r) {
1108
+ ctx.beginPath();
1109
+ ctx.moveTo(x + r, y);
1110
+ ctx.lineTo(x + w - r, y);
1111
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1112
+ ctx.lineTo(x + w, y + h - r);
1113
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1114
+ ctx.lineTo(x + r, y + h);
1115
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1116
+ ctx.lineTo(x, y + r);
1117
+ ctx.quadraticCurveTo(x, y, x + r, y);
1118
+ ctx.closePath();
1119
+ }
1120
+
1121
+ function _wrapText(ctx, text, maxWidth, maxLines) {
1122
+ const words = (text || '').replace(/\n/g, ' ').split(' ');
1123
+ const lines = [];
1124
+ let line = '';
1125
+ for (const word of words) {
1126
+ const test = line + (line ? ' ' : '') + word;
1127
+ if (ctx.measureText(test).width > maxWidth && line) {
1128
+ lines.push(line);
1129
+ if (lines.length >= maxLines) {
1130
+ lines[lines.length - 1] = lines[lines.length - 1].replace(/...$/, '') + '\u2026';
1131
+ return lines;
1132
+ }
1133
+ line = word;
1134
+ } else {
1135
+ line = test;
1136
+ }
1137
+ }
1138
+ if (line) {
1139
+ if (lines.length >= maxLines) {
1140
+ lines[lines.length - 1] += '\u2026';
1141
+ } else {
1142
+ lines.push(line);
1143
+ }
1144
+ }
1145
+ return lines;
1146
+ }
1147
+
1148
+
1149
+ // ═══════════════════════════════════════════
1150
+ // HIT DETECTION
1151
+ // ═══════════════════════════════════════════
1152
+
1153
+ function _hitTest(screenX, screenY) {
1154
+ const { x, y } = screenToWorld(screenX, screenY);
1155
+
1156
+ // Check cards (reverse order = topmost first)
1157
+ for (let i = _cards.length - 1; i >= 0; i--) {
1158
+ const card = _cards[i];
1159
+ if (x >= card.x - CARD_W / 2 && x <= card.x + CARD_W / 2 &&
1160
+ y >= card.y - CARD_H / 2 && y <= card.y + CARD_H / 2) {
1161
+ return { type: 'card', item: card, node: card.node };
1162
+ }
1163
+ }
1164
+
1165
+ // Check category labels — hit zone scales with label size + zoom compensation
1166
+ const allRegions = flattenRegions(_regions);
1167
+ const mul = state.labelSizeMultiplier || 1;
1168
+ for (const region of allRegions) {
1169
+ if (!state.labelsVisible) {
1170
+ // Labels hidden — small dot hit zone so categories are still clickable
1171
+ if (x >= region.cx - 30 && x <= region.cx + 30 &&
1172
+ y >= region.cy - 30 && y <= region.cy + 30) {
1173
+ return { type: 'region', item: region };
1174
+ }
1175
+ } else {
1176
+ // Compute zoom compensation to match rendered size (capped, same as _drawCategoryLabel)
1177
+ const MAX_ZOOM_BOOST = region.isParent ? 2.5 : 2.0;
1178
+ const baseFontSize = region.isParent ? 38 : 22;
1179
+ const MIN_SCREEN_PX = region.isParent ? 18 : 13;
1180
+ const rawFontSize = baseFontSize * mul;
1181
+ const screenPx = rawFontSize * _viewport.scale / _dpr;
1182
+ const zoomBoost = screenPx < MIN_SCREEN_PX
1183
+ ? Math.min(MIN_SCREEN_PX / screenPx, MAX_ZOOM_BOOST)
1184
+ : 1;
1185
+
1186
+ const fontSize = Math.round(baseFontSize * mul * zoomBoost);
1187
+ const hasLogo = _categoryLogos.get(region.name)?.ready || (region.name === 'openclaw' && _openclawLogoReady);
1188
+
1189
+ // Hit zone: logo width or text width + padding
1190
+ let labelW, labelH;
1191
+ if (hasLogo) {
1192
+ // Logo has its own zoom compensation (same as _drawCategoryLabel)
1193
+ const baseLogo = (region.isParent ? 64 : 32) * mul;
1194
+ const minLogoScreen = region.isParent ? 32 : 20;
1195
+ const logoScreen = baseLogo * _viewport.scale / _dpr;
1196
+ const logoBoost = logoScreen < minLogoScreen
1197
+ ? Math.min(minLogoScreen / logoScreen, region.isParent ? 5.0 : 4.0)
1198
+ : 1;
1199
+ const logoH = baseLogo * logoBoost;
1200
+ const logoW = logoH * 3; // conservative aspect estimate
1201
+ labelW = Math.max(logoW, 160) + 20;
1202
+ labelH = logoH + fontSize * 0.5 + 30;
1203
+ } else {
1204
+ // Match glass pill sizing: text width + padX on each side, fontSize + padY*2 + count height
1205
+ const nameText = region.name.toUpperCase();
1206
+ const approxTextW = fontSize * 0.65 * nameText.length;
1207
+ const padX = fontSize * (region.isParent ? 0.6 : 0.5);
1208
+ const padY = fontSize * (region.isParent ? 0.4 : 0.35);
1209
+ const countH = fontSize * 0.5; // always include count space for hit zone
1210
+ labelW = approxTextW + padX * 2 + 20; // extra margin for easier clicking
1211
+ labelH = fontSize + padY * 2 + countH + 20;
1212
+ }
1213
+ if (x >= region.cx - labelW / 2 && x <= region.cx + labelW / 2 &&
1214
+ y >= region.cy - labelH / 2 && y <= region.cy + labelH / 2) {
1215
+ return { type: 'region', item: region };
1216
+ }
1217
+ }
1218
+ }
1219
+
1220
+ return null;
1221
+ }
1222
+
1223
+
1224
+ // ═══════════════════════════════════════════
1225
+ // DRAG SYSTEM
1226
+ // ═══════════════════════════════════════════
1227
+
1228
+ function _buildDragSet(hit) {
1229
+ const set = [];
1230
+
1231
+ if (hit.type === 'card') {
1232
+ // If this card is part of a multi-selection, drag ALL selected cards
1233
+ if (state.multiSelected.size > 0 && state.multiSelected.has(hit.node.id)) {
1234
+ for (const card of _cards) {
1235
+ if (state.multiSelected.has(card.node.id)) {
1236
+ set.push({ type: 'card', ref: card });
1237
+ }
1238
+ }
1239
+ } else {
1240
+ // Just this card
1241
+ set.push({ type: 'card', ref: hit.item });
1242
+ }
1243
+ } else if (hit.type === 'region') {
1244
+ const region = hit.item;
1245
+ // The region label itself
1246
+ set.push({ type: 'region', ref: region });
1247
+
1248
+ // All cards directly on this region
1249
+ for (const card of _cards) {
1250
+ const cat = card.node.payload?.category;
1251
+ if (cat === region.name) {
1252
+ set.push({ type: 'card', ref: card });
1253
+ }
1254
+ }
1255
+
1256
+ // If parent, also include children + their cards
1257
+ if (region.isParent && region.children) {
1258
+ for (const child of region.children) {
1259
+ set.push({ type: 'region', ref: child });
1260
+ for (const card of _cards) {
1261
+ const cat = card.node.payload?.category;
1262
+ if (cat === child.name) {
1263
+ set.push({ type: 'card', ref: card });
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ return set;
1271
+ }
1272
+
1273
+ function _applyDragDelta(dx, dy) {
1274
+ for (const item of _pointer.dragSet) {
1275
+ if (item.type === 'card') {
1276
+ item.ref.x += dx;
1277
+ item.ref.y += dy;
1278
+ item.ref.pinned = true;
1279
+ } else if (item.type === 'region') {
1280
+ item.ref.cx += dx;
1281
+ item.ref.cy += dy;
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+
1287
+ // ═══════════════════════════════════════════
1288
+ // POSITION PERSISTENCE
1289
+ // ═══════════════════════════════════════════
1290
+
1291
+ function _savePositions() {
1292
+ const positions = {};
1293
+ for (const card of _cards) {
1294
+ positions[card.node.id] = { x: card.x, y: card.y, pinned: card.pinned };
1295
+ }
1296
+ for (const region of flattenRegions(_regions)) {
1297
+ positions[`_cat_${region.name}`] = { x: region.cx, y: region.cy };
1298
+ }
1299
+ try { posStore.setItem(KEYS.NODE_POS_2D, JSON.stringify(positions)); } catch (_) {}
1300
+ }
1301
+
1302
+ function _schedulePositionSave() {
1303
+ if (_posSaveTimer) clearTimeout(_posSaveTimer);
1304
+ _posSaveTimer = setTimeout(_savePositions, 2000);
1305
+ }
1306
+
1307
+
1308
+ // ═══════════════════════════════════════════
1309
+ // EVENT HANDLERS
1310
+ // ═══════════════════════════════════════════
1311
+
1312
+ function _onPointerDown(e) {
1313
+ if (_focusPaused || e.button !== 0) return;
1314
+ _lastActivityTime = performance.now();
1315
+ // Skip if Shift (lasso mode handled by lasso.js)
1316
+ if (e.shiftKey) return;
1317
+
1318
+ const hit = _hitTest(e.clientX, e.clientY);
1319
+
1320
+ _pointer.downScreenX = e.clientX;
1321
+ _pointer.downScreenY = e.clientY;
1322
+ _pointer.lastScreenX = e.clientX;
1323
+ _pointer.lastScreenY = e.clientY;
1324
+ _pointer.downTime = performance.now();
1325
+ _pointer.lastMoveTime = performance.now();
1326
+ _pointer.hasMoved = false;
1327
+ _panInertia.active = false; // stop coasting on new interaction
1328
+
1329
+ if (hit) {
1330
+ _pointer.isDragging = true;
1331
+ _pointer.isPanning = false;
1332
+ _pointer.dragTarget = hit;
1333
+ _pointer.dragSet = _buildDragSet(hit);
1334
+ _pointer.dragStartWorld = screenToWorld(e.clientX, e.clientY);
1335
+ _canvas.setPointerCapture(e.pointerId);
1336
+ } else {
1337
+ _pointer.isPanning = true;
1338
+ _pointer.isDragging = false;
1339
+ _pointer.dragTarget = null;
1340
+ _canvas.setPointerCapture(e.pointerId);
1341
+ }
1342
+ }
1343
+
1344
+ const DRAG_THRESHOLD = 5; // pixels before drag/pan starts
1345
+
1346
+ function _onPointerMove(e) {
1347
+ if (_focusPaused) return;
1348
+ // A window/panel/terminal drag that passes over the canvas is not graph
1349
+ // activity — don't wake the render loop or hover-pick under it.
1350
+ if (document.body.classList.contains('ui-interacting')) return;
1351
+ _lastActivityTime = performance.now();
1352
+ // Check if we've exceeded the drag threshold before actually moving
1353
+ if ((_pointer.isPanning || _pointer.isDragging) && !_pointer.hasMoved) {
1354
+ const tdx = e.clientX - _pointer.downScreenX;
1355
+ const tdy = e.clientY - _pointer.downScreenY;
1356
+ if (Math.abs(tdx) < DRAG_THRESHOLD && Math.abs(tdy) < DRAG_THRESHOLD) return;
1357
+ _pointer.hasMoved = true;
1358
+ }
1359
+
1360
+ if (_pointer.isPanning) {
1361
+ const dx = (e.clientX - _pointer.lastScreenX) * _dpr;
1362
+ const dy = (e.clientY - _pointer.lastScreenY) * _dpr;
1363
+ _viewport.offsetX += dx;
1364
+ _viewport.offsetY += dy;
1365
+ // Track velocity for inertia
1366
+ const now = performance.now();
1367
+ const dt = now - _pointer.lastMoveTime || 16;
1368
+ _panInertia.vx = dx / dt * 16; // normalize to ~60fps frame
1369
+ _panInertia.vy = dy / dt * 16;
1370
+ _pointer.lastMoveTime = now;
1371
+ _pointer.lastScreenX = e.clientX;
1372
+ _pointer.lastScreenY = e.clientY;
1373
+ // Kill any active inertia while dragging
1374
+ _panInertia.active = false;
1375
+ } else if (_pointer.isDragging) {
1376
+ const worldNow = screenToWorld(e.clientX, e.clientY);
1377
+ const worldPrev = screenToWorld(_pointer.lastScreenX, _pointer.lastScreenY);
1378
+ const dx = worldNow.x - worldPrev.x;
1379
+ const dy = worldNow.y - worldPrev.y;
1380
+ _applyDragDelta(dx, dy);
1381
+ _pointer.lastScreenX = e.clientX;
1382
+ _pointer.lastScreenY = e.clientY;
1383
+ } else {
1384
+ // Hover detection — _hitTest allocates flattenRegions() + per-region zoom
1385
+ // math, so coalesce to one test per frame (cursor at frame rate is fine).
1386
+ _lastHoverClientX = e.clientX;
1387
+ _lastHoverClientY = e.clientY;
1388
+ _scheduleHover2D();
1389
+ }
1390
+ }
1391
+
1392
+ let _lastHoverClientX = 0, _lastHoverClientY = 0;
1393
+ const _scheduleHover2D = rafThrottle(() => {
1394
+ if (_focusPaused || _pointer.isPanning || _pointer.isDragging) return;
1395
+ const hit = _hitTest(_lastHoverClientX, _lastHoverClientY);
1396
+ const newHoverId = (hit && hit.type === 'card') ? hit.node.id : null;
1397
+ if (newHoverId !== state.hoveredNodeId) {
1398
+ state.hoveredNodeId = newHoverId;
1399
+ }
1400
+ _canvas.style.cursor = hit ? 'pointer' : 'grab';
1401
+ if (newHoverId) emit('node-hover', hit.node);
1402
+ else if (!hit) emit('node-hover', null);
1403
+ });
1404
+
1405
+ function _onPointerUp(e) {
1406
+ if (_focusPaused) return;
1407
+ const wasDragging = _pointer.isDragging;
1408
+ const wasPanning = _pointer.isPanning;
1409
+ const moved = _pointer.hasMoved;
1410
+ const elapsed = performance.now() - _pointer.downTime;
1411
+
1412
+ _pointer.isPanning = false;
1413
+ _pointer.isDragging = false;
1414
+ _canvas.releasePointerCapture(e.pointerId);
1415
+
1416
+ // Start pan inertia if we were panning and had velocity
1417
+ if (wasPanning && moved) {
1418
+ const speed = Math.sqrt(_panInertia.vx ** 2 + _panInertia.vy ** 2);
1419
+ if (speed > 0.5) {
1420
+ _panInertia.active = true;
1421
+ }
1422
+ return;
1423
+ }
1424
+
1425
+ if (wasDragging && moved) {
1426
+ _schedulePositionSave();
1427
+ return;
1428
+ }
1429
+
1430
+ // Click (no significant movement)
1431
+ if (!moved || elapsed < 200) {
1432
+ const hit = _hitTest(e.clientX, e.clientY);
1433
+
1434
+ if (hit && hit.type === 'card') {
1435
+ // Ctrl/Cmd+Click: toggle multi-select (same as 3D variant)
1436
+ if (e.ctrlKey || e.metaKey) {
1437
+ if (state.multiSelected.has(hit.node.id)) {
1438
+ state.multiSelected.delete(hit.node.id);
1439
+ } else {
1440
+ state.multiSelected.add(hit.node.id);
1441
+ }
1442
+ emit('multiselect:update');
1443
+ return;
1444
+ }
1445
+ // Regular click: clear multi-select, select node
1446
+ if (state.multiSelected.size > 0) {
1447
+ state.multiSelected.clear();
1448
+ emit('multiselect:update');
1449
+ }
1450
+ state.selectedNodeId = hit.node.id;
1451
+ emit('node-selected', hit.node);
1452
+ } else if (hit && hit.type === 'region') {
1453
+ // Zoom to fit this category region
1454
+ _zoomToRegion(hit.item);
1455
+ } else {
1456
+ // Click background — deselect everything
1457
+ if (state.multiSelected.size > 0) {
1458
+ state.multiSelected.clear();
1459
+ emit('multiselect:update');
1460
+ }
1461
+ state.selectedNodeId = null;
1462
+ emit('background-click');
1463
+ emit('node-selected', null);
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ function _onPointerLeave() {
1469
+ if (_focusPaused) return;
1470
+ if (state.hoveredNodeId) {
1471
+ state.hoveredNodeId = null;
1472
+ _canvas.style.cursor = 'grab';
1473
+ emit('node-hover', null);
1474
+ }
1475
+ }
1476
+
1477
+ function _onWheel(e) {
1478
+ if (_focusPaused) return;
1479
+ e.preventDefault();
1480
+ // Accumulate zoom target — each tick multiplies by factor
1481
+ const zoomFactor = e.deltaY > 0 ? 0.88 : 1.12;
1482
+ _smoothZoom.targetScale = Math.max(
1483
+ _viewport.minScale,
1484
+ Math.min(_viewport.maxScale, (_smoothZoom.active ? _smoothZoom.targetScale : _viewport.scale) * zoomFactor)
1485
+ );
1486
+ // Update anchor to cursor position (buffer coords)
1487
+ _smoothZoom.anchorX = e.clientX * _dpr;
1488
+ _smoothZoom.anchorY = e.clientY * _dpr;
1489
+ _smoothZoom.active = true;
1490
+ }
1491
+
1492
+ function _onDblClick(e) {
1493
+ if (_focusPaused) return;
1494
+ const hit = _hitTest(e.clientX, e.clientY);
1495
+ if (hit && hit.type === 'card') {
1496
+ // Unpin card
1497
+ hit.item.pinned = false;
1498
+ _schedulePositionSave();
1499
+ } else {
1500
+ // Zoom to fit all
1501
+ _zoomToFit(400, 60);
1502
+ }
1503
+ }
1504
+
1505
+ function _onContextMenu(e) {
1506
+ if (_focusPaused) { e.preventDefault(); return; }
1507
+ e.preventDefault();
1508
+ const hit = _hitTest(e.clientX, e.clientY);
1509
+ if (hit && hit.type === 'card') {
1510
+ emit('node-context-menu', { node: hit.node, event: e });
1511
+ }
1512
+ }
1513
+
1514
+
1515
+ // ═══════════════════════════════════════════
1516
+ // VIEWPORT NAVIGATION
1517
+ // ═══════════════════════════════════════════
1518
+
1519
+ function _centerAt(worldX, worldY, duration = 0) {
1520
+ _lastActivityTime = performance.now();
1521
+ const targetOffsetX = _canvas.width / 2 - worldX * _viewport.scale;
1522
+ const targetOffsetY = _canvas.height / 2 - worldY * _viewport.scale;
1523
+
1524
+ if (duration <= 0) {
1525
+ _viewport.offsetX = targetOffsetX;
1526
+ _viewport.offsetY = targetOffsetY;
1527
+ return;
1528
+ }
1529
+
1530
+ _animFrom = {
1531
+ offsetX: _viewport.offsetX,
1532
+ offsetY: _viewport.offsetY,
1533
+ scale: _viewport.scale,
1534
+ };
1535
+ _animTo = {
1536
+ offsetX: targetOffsetX,
1537
+ offsetY: targetOffsetY,
1538
+ scale: _viewport.scale,
1539
+ };
1540
+ _animStart = performance.now();
1541
+ _animDuration = duration;
1542
+ _animating = true;
1543
+ }
1544
+
1545
+ function _zoomToFit(duration = 400, padding = 60) {
1546
+ if (_cards.length === 0) return;
1547
+
1548
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1549
+ for (const card of _cards) {
1550
+ if (card.x - CARD_W / 2 < minX) minX = card.x - CARD_W / 2;
1551
+ if (card.x + CARD_W / 2 > maxX) maxX = card.x + CARD_W / 2;
1552
+ if (card.y - CARD_H / 2 < minY) minY = card.y - CARD_H / 2;
1553
+ if (card.y + CARD_H / 2 > maxY) maxY = card.y + CARD_H / 2;
1554
+ }
1555
+
1556
+ const rangeX = (maxX - minX) || 1;
1557
+ const rangeY = (maxY - minY) || 1;
1558
+ const centerX = (minX + maxX) / 2;
1559
+ const centerY = (minY + maxY) / 2;
1560
+
1561
+ const scaledPadding = padding * _dpr;
1562
+ const availW = _canvas.width - scaledPadding * 2;
1563
+ const availH = _canvas.height - scaledPadding * 2;
1564
+ const newScale = Math.min(availW / rangeX, availH / rangeY);
1565
+ const clampedScale = Math.max(_viewport.minScale, Math.min(_viewport.maxScale, newScale));
1566
+
1567
+ if (duration <= 0) {
1568
+ _viewport.scale = clampedScale;
1569
+ _viewport.offsetX = _canvas.width / 2 - centerX * clampedScale;
1570
+ _viewport.offsetY = _canvas.height / 2 - centerY * clampedScale;
1571
+ return;
1572
+ }
1573
+
1574
+ _animFrom = {
1575
+ offsetX: _viewport.offsetX,
1576
+ offsetY: _viewport.offsetY,
1577
+ scale: _viewport.scale,
1578
+ };
1579
+ _animTo = {
1580
+ offsetX: _canvas.width / 2 - centerX * clampedScale,
1581
+ offsetY: _canvas.height / 2 - centerY * clampedScale,
1582
+ scale: clampedScale,
1583
+ };
1584
+ _animStart = performance.now();
1585
+ _animDuration = duration;
1586
+ _animating = true;
1587
+ }
1588
+
1589
+ function _zoomToRegion(region, duration = 400) {
1590
+ const padding = 80 * _dpr;
1591
+ const regionR = region.radius || 200;
1592
+
1593
+ const rangeX = regionR * 2;
1594
+ const rangeY = regionR * 2;
1595
+
1596
+ const availW = _canvas.width - padding * 2;
1597
+ const availH = _canvas.height - padding * 2;
1598
+ const newScale = Math.min(availW / rangeX, availH / rangeY);
1599
+ const clampedScale = Math.max(_viewport.minScale, Math.min(_viewport.maxScale, newScale));
1600
+
1601
+ _animFrom = {
1602
+ offsetX: _viewport.offsetX,
1603
+ offsetY: _viewport.offsetY,
1604
+ scale: _viewport.scale,
1605
+ };
1606
+ _animTo = {
1607
+ offsetX: _canvas.width / 2 - region.cx * clampedScale,
1608
+ offsetY: _canvas.height / 2 - region.cy * clampedScale,
1609
+ scale: clampedScale,
1610
+ };
1611
+ _animStart = performance.now();
1612
+ _animDuration = duration;
1613
+ _animating = true;
1614
+ }
1615
+
1616
+
1617
+ // ═══════════════════════════════════════════
1618
+ // RESIZE
1619
+ // ═══════════════════════════════════════════
1620
+
1621
+ function _resizeCanvas() {
1622
+ if (!_canvas) return;
1623
+ _resizePending = false;
1624
+ _dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
1625
+ const w = window.innerWidth;
1626
+ const h = window.innerHeight;
1627
+ // Set canvas buffer size to DPR-scaled resolution for crisp rendering
1628
+ _canvas.width = Math.round(w * _dpr);
1629
+ _canvas.height = Math.round(h * _dpr);
1630
+ // CSS display size stays at viewport dimensions
1631
+ _canvas.style.width = w + 'px';
1632
+ _canvas.style.height = h + 'px';
1633
+ }
1634
+
1635
+ // Window resizes arrive many times per frame while a window or sidepanel is
1636
+ // dragged; each buffer resize clears and reallocates the canvas. Apply at most
1637
+ // once per frame: the render loop applies it right before it draws (so the
1638
+ // cleared canvas is redrawn in the same frame); a stopped loop gets a one-off
1639
+ // frame to apply it.
1640
+ let _resizePending = false;
1641
+ function _scheduleCanvasResize() {
1642
+ if (_resizePending) return;
1643
+ _resizePending = true;
1644
+ if (!_animFrameId) {
1645
+ requestAnimationFrame(() => { if (_resizePending) _resizeCanvas(); });
1646
+ }
1647
+ }
1648
+
1649
+
1650
+ // ═══════════════════════════════════════════
1651
+ // PUBLIC API SHIM (force-graph compatible)
1652
+ // ═══════════════════════════════════════════
1653
+
1654
+ function _getPublicAPI() {
1655
+ return {
1656
+ centerAt: (x, y, duration) => {
1657
+ if (x == null && y == null) {
1658
+ // getter mode (used by minimap)
1659
+ return {
1660
+ x: (_canvas.width / 2 - _viewport.offsetX) / _viewport.scale,
1661
+ y: (_canvas.height / 2 - _viewport.offsetY) / _viewport.scale,
1662
+ };
1663
+ }
1664
+ _centerAt(x, y, duration);
1665
+ },
1666
+ zoom: (level, duration) => {
1667
+ if (level == null) return _viewport.scale; // getter
1668
+ _lastActivityTime = performance.now();
1669
+ const newScale = Math.max(_viewport.minScale, Math.min(_viewport.maxScale, level));
1670
+ if (duration > 0) {
1671
+ _animFrom = { offsetX: _viewport.offsetX, offsetY: _viewport.offsetY, scale: _viewport.scale };
1672
+ _animTo = { offsetX: _viewport.offsetX, offsetY: _viewport.offsetY, scale: newScale };
1673
+ // Zoom from center
1674
+ const cx = _canvas.width / 2;
1675
+ const cy = _canvas.height / 2;
1676
+ _animTo.offsetX = cx - (cx - _viewport.offsetX) * (newScale / _viewport.scale);
1677
+ _animTo.offsetY = cy - (cy - _viewport.offsetY) * (newScale / _viewport.scale);
1678
+ _animStart = performance.now();
1679
+ _animDuration = duration;
1680
+ _animating = true;
1681
+ } else {
1682
+ _viewport.scale = newScale;
1683
+ }
1684
+ },
1685
+ zoomToFit: (duration, padding) => _zoomToFit(duration, padding),
1686
+ graphData: () => ({
1687
+ nodes: _cards.map(c => ({
1688
+ id: c.node.id,
1689
+ x: c.x,
1690
+ y: c.y,
1691
+ payload: c.node.payload,
1692
+ })),
1693
+ links: state.allLinks,
1694
+ }),
1695
+ width: (w) => {
1696
+ if (w != null) { _canvas.style.width = w + 'px'; _resizeCanvas(); }
1697
+ return _canvas ? Math.round(_canvas.width / _dpr) : 0;
1698
+ },
1699
+ height: (h) => {
1700
+ if (h != null) { _canvas.style.height = h + 'px'; _resizeCanvas(); }
1701
+ return _canvas ? Math.round(_canvas.height / _dpr) : 0;
1702
+ },
1703
+ // No-ops for compatibility
1704
+ linkVisibility: (fn) => fn,
1705
+ d3ReheatSimulation: () => reheatSimulation(),
1706
+ };
1707
+ }