@agentdeck/bridge 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
  3. package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
  4. package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
  5. package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
  6. package/assets/fonts/LICENSES.md +22 -0
  7. package/dist/adapters/claude-code.d.ts +6 -0
  8. package/dist/adapters/claude-code.d.ts.map +1 -1
  9. package/dist/adapters/claude-code.js +8 -0
  10. package/dist/adapters/claude-code.js.map +1 -1
  11. package/dist/adapters/openclaw.d.ts +24 -0
  12. package/dist/adapters/openclaw.d.ts.map +1 -1
  13. package/dist/adapters/openclaw.js +291 -35
  14. package/dist/adapters/openclaw.js.map +1 -1
  15. package/dist/adapters/opencode-adapter.d.ts +59 -0
  16. package/dist/adapters/opencode-adapter.d.ts.map +1 -1
  17. package/dist/adapters/opencode-adapter.js +297 -35
  18. package/dist/adapters/opencode-adapter.js.map +1 -1
  19. package/dist/adb-reverse.d.ts +1 -1
  20. package/dist/adb-reverse.js +1 -1
  21. package/dist/antigravity-local.d.ts +1 -0
  22. package/dist/antigravity-local.d.ts.map +1 -1
  23. package/dist/antigravity-local.js +33 -0
  24. package/dist/antigravity-local.js.map +1 -1
  25. package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
  26. package/dist/apme/adapters/claude-hook.js +5 -0
  27. package/dist/apme/adapters/claude-hook.js.map +1 -1
  28. package/dist/apme/adapters/codex-hook.d.ts +17 -0
  29. package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
  30. package/dist/apme/adapters/codex-hook.js +95 -0
  31. package/dist/apme/adapters/codex-hook.js.map +1 -0
  32. package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
  33. package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
  34. package/dist/apme/adapters/codex-turn-manager.js +496 -0
  35. package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
  36. package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
  37. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
  38. package/dist/apme/adapters/openclaw-hook.js +234 -0
  39. package/dist/apme/adapters/openclaw-hook.js.map +1 -0
  40. package/dist/apme/adapters/opencode-hook.d.ts +56 -0
  41. package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
  42. package/dist/apme/adapters/opencode-hook.js +182 -0
  43. package/dist/apme/adapters/opencode-hook.js.map +1 -0
  44. package/dist/apme/adapters/timeline.d.ts.map +1 -1
  45. package/dist/apme/adapters/timeline.js +6 -1
  46. package/dist/apme/adapters/timeline.js.map +1 -1
  47. package/dist/apme/antigravity-transcript.d.ts +18 -0
  48. package/dist/apme/antigravity-transcript.d.ts.map +1 -0
  49. package/dist/apme/antigravity-transcript.js +94 -0
  50. package/dist/apme/antigravity-transcript.js.map +1 -0
  51. package/dist/apme/classify-turn.d.ts +13 -0
  52. package/dist/apme/classify-turn.d.ts.map +1 -0
  53. package/dist/apme/classify-turn.js +49 -0
  54. package/dist/apme/classify-turn.js.map +1 -0
  55. package/dist/apme/claude-transcript-reader.d.ts +10 -0
  56. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  57. package/dist/apme/claude-transcript-reader.js +35 -0
  58. package/dist/apme/claude-transcript-reader.js.map +1 -1
  59. package/dist/apme/collector.d.ts +133 -6
  60. package/dist/apme/collector.d.ts.map +1 -1
  61. package/dist/apme/collector.js +471 -23
  62. package/dist/apme/collector.js.map +1 -1
  63. package/dist/apme/dashboard-html.d.ts.map +1 -1
  64. package/dist/apme/dashboard-html.js +117 -11
  65. package/dist/apme/dashboard-html.js.map +1 -1
  66. package/dist/apme/http.d.ts.map +1 -1
  67. package/dist/apme/http.js +52 -6
  68. package/dist/apme/http.js.map +1 -1
  69. package/dist/apme/index.d.ts +26 -5
  70. package/dist/apme/index.d.ts.map +1 -1
  71. package/dist/apme/index.js +146 -9
  72. package/dist/apme/index.js.map +1 -1
  73. package/dist/apme/judge-detect.d.ts +26 -0
  74. package/dist/apme/judge-detect.d.ts.map +1 -0
  75. package/dist/apme/judge-detect.js +69 -0
  76. package/dist/apme/judge-detect.js.map +1 -0
  77. package/dist/apme/pareto.d.ts +39 -0
  78. package/dist/apme/pareto.d.ts.map +1 -0
  79. package/dist/apme/pareto.js +59 -0
  80. package/dist/apme/pareto.js.map +1 -0
  81. package/dist/apme/recommend.d.ts +3 -0
  82. package/dist/apme/recommend.d.ts.map +1 -1
  83. package/dist/apme/recommend.js +43 -1
  84. package/dist/apme/recommend.js.map +1 -1
  85. package/dist/apme/runner.d.ts +99 -4
  86. package/dist/apme/runner.d.ts.map +1 -1
  87. package/dist/apme/runner.js +743 -72
  88. package/dist/apme/runner.js.map +1 -1
  89. package/dist/apme/sample-to-timeline.d.ts +26 -0
  90. package/dist/apme/sample-to-timeline.d.ts.map +1 -0
  91. package/dist/apme/sample-to-timeline.js +95 -0
  92. package/dist/apme/sample-to-timeline.js.map +1 -0
  93. package/dist/apme/scorers/index.d.ts +47 -0
  94. package/dist/apme/scorers/index.d.ts.map +1 -0
  95. package/dist/apme/scorers/index.js +96 -0
  96. package/dist/apme/scorers/index.js.map +1 -0
  97. package/dist/apme/settings.d.ts +17 -11
  98. package/dist/apme/settings.d.ts.map +1 -1
  99. package/dist/apme/settings.js +30 -9
  100. package/dist/apme/settings.js.map +1 -1
  101. package/dist/apme/store.d.ts +33 -1
  102. package/dist/apme/store.d.ts.map +1 -1
  103. package/dist/apme/store.js +334 -25
  104. package/dist/apme/store.js.map +1 -1
  105. package/dist/awaiting-overlay.d.ts +112 -0
  106. package/dist/awaiting-overlay.d.ts.map +1 -0
  107. package/dist/awaiting-overlay.js +179 -0
  108. package/dist/awaiting-overlay.js.map +1 -0
  109. package/dist/ble-sync-spawn.d.ts +63 -0
  110. package/dist/ble-sync-spawn.d.ts.map +1 -0
  111. package/dist/ble-sync-spawn.js +196 -0
  112. package/dist/ble-sync-spawn.js.map +1 -0
  113. package/dist/bridge-core.d.ts +34 -1
  114. package/dist/bridge-core.d.ts.map +1 -1
  115. package/dist/bridge-core.js +207 -24
  116. package/dist/bridge-core.js.map +1 -1
  117. package/dist/broadcast.d.ts +3 -0
  118. package/dist/broadcast.d.ts.map +1 -0
  119. package/dist/broadcast.js +122 -0
  120. package/dist/broadcast.js.map +1 -0
  121. package/dist/check-deps.d.ts.map +1 -1
  122. package/dist/check-deps.js +25 -9
  123. package/dist/check-deps.js.map +1 -1
  124. package/dist/claude-permission-rules.d.ts +55 -0
  125. package/dist/claude-permission-rules.d.ts.map +1 -0
  126. package/dist/claude-permission-rules.js +197 -0
  127. package/dist/claude-permission-rules.js.map +1 -0
  128. package/dist/cli.d.ts +15 -1
  129. package/dist/cli.d.ts.map +1 -1
  130. package/dist/cli.js +1030 -93
  131. package/dist/cli.js.map +1 -1
  132. package/dist/codex-auth.d.ts +21 -0
  133. package/dist/codex-auth.d.ts.map +1 -1
  134. package/dist/codex-auth.js +52 -1
  135. package/dist/codex-auth.js.map +1 -1
  136. package/dist/codex-output-parser.d.ts +8 -0
  137. package/dist/codex-output-parser.d.ts.map +1 -1
  138. package/dist/codex-output-parser.js +110 -25
  139. package/dist/codex-output-parser.js.map +1 -1
  140. package/dist/codex-rate-limits.d.ts +16 -0
  141. package/dist/codex-rate-limits.d.ts.map +1 -0
  142. package/dist/codex-rate-limits.js +197 -0
  143. package/dist/codex-rate-limits.js.map +1 -0
  144. package/dist/codex-rollout-response.d.ts +31 -0
  145. package/dist/codex-rollout-response.d.ts.map +1 -0
  146. package/dist/codex-rollout-response.js +135 -0
  147. package/dist/codex-rollout-response.js.map +1 -0
  148. package/dist/d200h/image-renderer.d.ts +4 -16
  149. package/dist/d200h/image-renderer.d.ts.map +1 -1
  150. package/dist/d200h/image-renderer.js +46 -212
  151. package/dist/d200h/image-renderer.js.map +1 -1
  152. package/dist/daemon-server.d.ts +85 -0
  153. package/dist/daemon-server.d.ts.map +1 -1
  154. package/dist/daemon-server.js +1975 -159
  155. package/dist/daemon-server.js.map +1 -1
  156. package/dist/daemon.js +15 -6
  157. package/dist/daemon.js.map +1 -1
  158. package/dist/display-dim.d.ts +9 -0
  159. package/dist/display-dim.d.ts.map +1 -0
  160. package/dist/display-dim.js +53 -0
  161. package/dist/display-dim.js.map +1 -0
  162. package/dist/display-monitor.d.ts +15 -2
  163. package/dist/display-monitor.d.ts.map +1 -1
  164. package/dist/display-monitor.js +104 -26
  165. package/dist/display-monitor.js.map +1 -1
  166. package/dist/esp32-serial.d.ts +190 -1
  167. package/dist/esp32-serial.d.ts.map +1 -1
  168. package/dist/esp32-serial.js +1090 -156
  169. package/dist/esp32-serial.js.map +1 -1
  170. package/dist/fallback-task-timeline.d.ts +42 -0
  171. package/dist/fallback-task-timeline.d.ts.map +1 -0
  172. package/dist/fallback-task-timeline.js +119 -0
  173. package/dist/fallback-task-timeline.js.map +1 -0
  174. package/dist/foundation-models-helper.d.ts +11 -0
  175. package/dist/foundation-models-helper.d.ts.map +1 -0
  176. package/dist/foundation-models-helper.js +241 -0
  177. package/dist/foundation-models-helper.js.map +1 -0
  178. package/dist/hook-migration.js +1 -1
  179. package/dist/hook-migration.js.map +1 -1
  180. package/dist/hook-server.d.ts +2 -3
  181. package/dist/hook-server.d.ts.map +1 -1
  182. package/dist/hook-server.js +23 -10
  183. package/dist/hook-server.js.map +1 -1
  184. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
  185. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
  186. package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
  187. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
  188. package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
  189. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
  190. package/dist/idotmatrix/idotmatrix-discover.js +85 -0
  191. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
  192. package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
  193. package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
  194. package/dist/idotmatrix/idotmatrix-settings.js +57 -0
  195. package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
  196. package/dist/index.d.ts.map +1 -1
  197. package/dist/index.js +217 -179
  198. package/dist/index.js.map +1 -1
  199. package/dist/log-stream.d.ts +15 -16
  200. package/dist/log-stream.d.ts.map +1 -1
  201. package/dist/log-stream.js +18 -96
  202. package/dist/log-stream.js.map +1 -1
  203. package/dist/logger.d.ts +3 -1
  204. package/dist/logger.d.ts.map +1 -1
  205. package/dist/logger.js +18 -5
  206. package/dist/logger.js.map +1 -1
  207. package/dist/mdns.d.ts +17 -0
  208. package/dist/mdns.d.ts.map +1 -1
  209. package/dist/mdns.js +52 -18
  210. package/dist/mdns.js.map +1 -1
  211. package/dist/model-catalog.d.ts +1 -1
  212. package/dist/model-catalog.d.ts.map +1 -1
  213. package/dist/model-catalog.js +2 -4
  214. package/dist/model-catalog.js.map +1 -1
  215. package/dist/modules/adb-module.d.ts +1 -1
  216. package/dist/modules/adb-module.d.ts.map +1 -1
  217. package/dist/modules/adb-module.js +4 -3
  218. package/dist/modules/adb-module.js.map +1 -1
  219. package/dist/modules/broadcast-module.d.ts +17 -0
  220. package/dist/modules/broadcast-module.d.ts.map +1 -0
  221. package/dist/modules/broadcast-module.js +26 -0
  222. package/dist/modules/broadcast-module.js.map +1 -0
  223. package/dist/modules/d200h-module.d.ts +22 -0
  224. package/dist/modules/d200h-module.d.ts.map +1 -1
  225. package/dist/modules/d200h-module.js +200 -65
  226. package/dist/modules/d200h-module.js.map +1 -1
  227. package/dist/modules/idotmatrix-module.d.ts +9 -0
  228. package/dist/modules/idotmatrix-module.d.ts.map +1 -0
  229. package/dist/modules/idotmatrix-module.js +46 -0
  230. package/dist/modules/idotmatrix-module.js.map +1 -0
  231. package/dist/modules/index.d.ts +3 -1
  232. package/dist/modules/index.d.ts.map +1 -1
  233. package/dist/modules/index.js +9 -3
  234. package/dist/modules/index.js.map +1 -1
  235. package/dist/modules/pixoo-module.d.ts.map +1 -1
  236. package/dist/modules/pixoo-module.js +14 -4
  237. package/dist/modules/pixoo-module.js.map +1 -1
  238. package/dist/modules/serial-module.d.ts +4 -0
  239. package/dist/modules/serial-module.d.ts.map +1 -1
  240. package/dist/modules/serial-module.js +14 -12
  241. package/dist/modules/serial-module.js.map +1 -1
  242. package/dist/modules/timebox-module.d.ts +9 -0
  243. package/dist/modules/timebox-module.d.ts.map +1 -0
  244. package/dist/modules/timebox-module.js +46 -0
  245. package/dist/modules/timebox-module.js.map +1 -0
  246. package/dist/modules/trmnl-module.d.ts +38 -0
  247. package/dist/modules/trmnl-module.d.ts.map +1 -0
  248. package/dist/modules/trmnl-module.js +111 -0
  249. package/dist/modules/trmnl-module.js.map +1 -0
  250. package/dist/modules/types.d.ts +4 -2
  251. package/dist/modules/types.d.ts.map +1 -1
  252. package/dist/observed-steering.d.ts +93 -0
  253. package/dist/observed-steering.d.ts.map +1 -0
  254. package/dist/observed-steering.js +247 -0
  255. package/dist/observed-steering.js.map +1 -0
  256. package/dist/openclaw-session.d.ts +18 -0
  257. package/dist/openclaw-session.d.ts.map +1 -0
  258. package/dist/openclaw-session.js +34 -0
  259. package/dist/openclaw-session.js.map +1 -0
  260. package/dist/opencode-steering.d.ts +36 -0
  261. package/dist/opencode-steering.d.ts.map +1 -0
  262. package/dist/opencode-steering.js +84 -0
  263. package/dist/opencode-steering.js.map +1 -0
  264. package/dist/output-parser.d.ts +30 -0
  265. package/dist/output-parser.d.ts.map +1 -1
  266. package/dist/output-parser.js +172 -18
  267. package/dist/output-parser.js.map +1 -1
  268. package/dist/passive-observer.d.ts +32 -0
  269. package/dist/passive-observer.d.ts.map +1 -1
  270. package/dist/passive-observer.js +288 -49
  271. package/dist/passive-observer.js.map +1 -1
  272. package/dist/permission-resolver.d.ts +55 -0
  273. package/dist/permission-resolver.d.ts.map +1 -0
  274. package/dist/permission-resolver.js +112 -0
  275. package/dist/permission-resolver.js.map +1 -0
  276. package/dist/pixoo/micro-glyphs.d.ts +18 -0
  277. package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
  278. package/dist/pixoo/micro-glyphs.js +143 -0
  279. package/dist/pixoo/micro-glyphs.js.map +1 -0
  280. package/dist/pixoo/official-dot-glyphs.generated.d.ts +8 -0
  281. package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -0
  282. package/dist/pixoo/official-dot-glyphs.generated.js +247 -0
  283. package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -0
  284. package/dist/pixoo/pixoo-bridge.d.ts +16 -8
  285. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  286. package/dist/pixoo/pixoo-bridge.js +127 -35
  287. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  288. package/dist/pixoo/pixoo-camera.d.ts +19 -3
  289. package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
  290. package/dist/pixoo/pixoo-camera.js +62 -27
  291. package/dist/pixoo/pixoo-camera.js.map +1 -1
  292. package/dist/pixoo/pixoo-client.d.ts +13 -3
  293. package/dist/pixoo/pixoo-client.d.ts.map +1 -1
  294. package/dist/pixoo/pixoo-client.js +51 -22
  295. package/dist/pixoo/pixoo-client.js.map +1 -1
  296. package/dist/pixoo/pixoo-discover.d.ts +37 -0
  297. package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
  298. package/dist/pixoo/pixoo-discover.js +105 -0
  299. package/dist/pixoo/pixoo-discover.js.map +1 -0
  300. package/dist/pixoo/pixoo-renderer.d.ts +27 -3
  301. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  302. package/dist/pixoo/pixoo-renderer.js +533 -135
  303. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  304. package/dist/pixoo/pixoo-settings.d.ts +6 -0
  305. package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
  306. package/dist/pixoo/pixoo-settings.js +9 -0
  307. package/dist/pixoo/pixoo-settings.js.map +1 -1
  308. package/dist/pixoo/pixoo-sprites.d.ts +84 -11
  309. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  310. package/dist/pixoo/pixoo-sprites.js +620 -209
  311. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  312. package/dist/pty-manager.d.ts.map +1 -1
  313. package/dist/pty-manager.js +8 -2
  314. package/dist/pty-manager.js.map +1 -1
  315. package/dist/review-runner.d.ts +113 -0
  316. package/dist/review-runner.d.ts.map +1 -0
  317. package/dist/review-runner.js +428 -0
  318. package/dist/review-runner.js.map +1 -0
  319. package/dist/session-activity.d.ts +14 -0
  320. package/dist/session-activity.d.ts.map +1 -0
  321. package/dist/session-activity.js +160 -0
  322. package/dist/session-activity.js.map +1 -0
  323. package/dist/session-aggregator.d.ts +25 -0
  324. package/dist/session-aggregator.d.ts.map +1 -1
  325. package/dist/session-aggregator.js +48 -9
  326. package/dist/session-aggregator.js.map +1 -1
  327. package/dist/session-focus-relay.d.ts.map +1 -1
  328. package/dist/session-focus-relay.js +7 -1
  329. package/dist/session-focus-relay.js.map +1 -1
  330. package/dist/session-registry.d.ts +74 -3
  331. package/dist/session-registry.d.ts.map +1 -1
  332. package/dist/session-registry.js +140 -8
  333. package/dist/session-registry.js.map +1 -1
  334. package/dist/session-timeline-relay.d.ts.map +1 -1
  335. package/dist/session-timeline-relay.js +13 -4
  336. package/dist/session-timeline-relay.js.map +1 -1
  337. package/dist/session-transcript-timeline.d.ts +49 -0
  338. package/dist/session-transcript-timeline.d.ts.map +1 -0
  339. package/dist/session-transcript-timeline.js +276 -0
  340. package/dist/session-transcript-timeline.js.map +1 -0
  341. package/dist/state-machine.d.ts +6 -1
  342. package/dist/state-machine.d.ts.map +1 -1
  343. package/dist/state-machine.js +86 -28
  344. package/dist/state-machine.js.map +1 -1
  345. package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
  346. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
  347. package/dist/timebox/timebox-daemon-sync.js +135 -0
  348. package/dist/timebox/timebox-daemon-sync.js.map +1 -0
  349. package/dist/timebox/timebox-discover.d.ts +19 -0
  350. package/dist/timebox/timebox-discover.d.ts.map +1 -0
  351. package/dist/timebox/timebox-discover.js +87 -0
  352. package/dist/timebox/timebox-discover.js.map +1 -0
  353. package/dist/timebox/timebox-settings.d.ts +32 -0
  354. package/dist/timebox/timebox-settings.d.ts.map +1 -0
  355. package/dist/timebox/timebox-settings.js +80 -0
  356. package/dist/timebox/timebox-settings.js.map +1 -0
  357. package/dist/timeline-store.d.ts +112 -3
  358. package/dist/timeline-store.d.ts.map +1 -1
  359. package/dist/timeline-store.js +460 -14
  360. package/dist/timeline-store.js.map +1 -1
  361. package/dist/timeline-summarizer.d.ts.map +1 -1
  362. package/dist/timeline-summarizer.js +29 -5
  363. package/dist/timeline-summarizer.js.map +1 -1
  364. package/dist/trmnl/byos-server.d.ts +39 -0
  365. package/dist/trmnl/byos-server.d.ts.map +1 -0
  366. package/dist/trmnl/byos-server.js +301 -0
  367. package/dist/trmnl/byos-server.js.map +1 -0
  368. package/dist/trmnl/frame-cache.d.ts +37 -0
  369. package/dist/trmnl/frame-cache.d.ts.map +1 -0
  370. package/dist/trmnl/frame-cache.js +157 -0
  371. package/dist/trmnl/frame-cache.js.map +1 -0
  372. package/dist/trmnl/image-renderer.d.ts +27 -0
  373. package/dist/trmnl/image-renderer.d.ts.map +1 -0
  374. package/dist/trmnl/image-renderer.js +235 -0
  375. package/dist/trmnl/image-renderer.js.map +1 -0
  376. package/dist/trmnl/trmnl-settings.d.ts +96 -0
  377. package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
  378. package/dist/trmnl/trmnl-settings.js +177 -0
  379. package/dist/trmnl/trmnl-settings.js.map +1 -0
  380. package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
  381. package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
  382. package/dist/trmnl/trmnl-telemetry.js +56 -0
  383. package/dist/trmnl/trmnl-telemetry.js.map +1 -0
  384. package/dist/tui/ansi.d.ts +0 -4
  385. package/dist/tui/ansi.d.ts.map +1 -1
  386. package/dist/tui/ansi.js +14 -7
  387. package/dist/tui/ansi.js.map +1 -1
  388. package/dist/tui/dashboard.d.ts +1 -0
  389. package/dist/tui/dashboard.d.ts.map +1 -1
  390. package/dist/tui/dashboard.js +38 -11
  391. package/dist/tui/dashboard.js.map +1 -1
  392. package/dist/tui/renderer.d.ts +36 -0
  393. package/dist/tui/renderer.d.ts.map +1 -1
  394. package/dist/tui/renderer.js +355 -121
  395. package/dist/tui/renderer.js.map +1 -1
  396. package/dist/tui/terrarium.d.ts.map +1 -1
  397. package/dist/tui/terrarium.js +44 -51
  398. package/dist/tui/terrarium.js.map +1 -1
  399. package/dist/types.d.ts +2 -2
  400. package/dist/types.d.ts.map +1 -1
  401. package/dist/types.js +2 -2
  402. package/dist/types.js.map +1 -1
  403. package/dist/usage-api.d.ts.map +1 -1
  404. package/dist/usage-api.js +7 -1
  405. package/dist/usage-api.js.map +1 -1
  406. package/dist/usage-event.d.ts +10 -3
  407. package/dist/usage-event.d.ts.map +1 -1
  408. package/dist/usage-event.js +98 -13
  409. package/dist/usage-event.js.map +1 -1
  410. package/dist/utility-proxy.d.ts.map +1 -1
  411. package/dist/utility-proxy.js +8 -0
  412. package/dist/utility-proxy.js.map +1 -1
  413. package/dist/utils/project-name.d.ts +3 -0
  414. package/dist/utils/project-name.d.ts.map +1 -1
  415. package/dist/utils/project-name.js +44 -2
  416. package/dist/utils/project-name.js.map +1 -1
  417. package/dist/version-check.d.ts.map +1 -1
  418. package/dist/version-check.js +1 -17
  419. package/dist/version-check.js.map +1 -1
  420. package/dist/windows-service.d.ts +26 -0
  421. package/dist/windows-service.d.ts.map +1 -0
  422. package/dist/windows-service.js +145 -0
  423. package/dist/windows-service.js.map +1 -0
  424. package/dist/ws-server.d.ts +17 -3
  425. package/dist/ws-server.d.ts.map +1 -1
  426. package/dist/ws-server.js +78 -8
  427. package/dist/ws-server.js.map +1 -1
  428. package/fm-helper/AgentDeckFMHelper.swift +112 -0
  429. package/package.json +10 -4
  430. package/scripts/build-fm-helper.mjs +26 -0
  431. package/LICENSE +0 -21
  432. package/dist/apme/tuner.d.ts +0 -64
  433. package/dist/apme/tuner.d.ts.map +0 -1
  434. package/dist/apme/tuner.js +0 -297
  435. package/dist/apme/tuner.js.map +0 -1
@@ -18,17 +18,23 @@
18
18
  * 4. Screen-space overlays (danger flash) → output buffer
19
19
  */
20
20
  import { State } from '../types.js';
21
+ import { PASSIVE_OFFLINE_LABEL } from '@agentdeck/shared';
22
+ import { hasOpenClawSession, foldCodexSessionsForDisplay } from '@agentdeck/shared';
23
+ import { layoutOctopuses, layoutCloudCreatures, layoutOpenCodeCreatures, layoutAntigravityCreatures, } from '@agentdeck/shared';
21
24
  import { drawTextCentered } from './pixoo-font.js';
22
- import { COLORS, setPixel, blendPixel, glowPixel, lerpColor, drawOctopus, drawJellyfish, drawOpenCode, drawCrayfish, drawTetra, drawText, getOctopusPaletteForSession, getJellyfishPaletteForSession, getOpenCodePaletteForSession, } from './pixoo-sprites.js';
23
- import { blitWithCamera, updateDirector, } from './pixoo-camera.js';
24
- const W = 64;
25
+ import { COLORS, setPixel, blendPixel, glowPixel, lerpColor, drawOfficialDotGlyph, drawTetra, drawText, getOctopusPaletteForSession, getJellyfishPaletteForSession, getOpenCodePaletteForSession, getAntigravityPaletteForSession, } from './pixoo-sprites.js';
26
+ import { blitWithCamera, quantizeCameraPixels, updateDirector, WORLD_SIZE, ACTIVE_SIZE, } from './pixoo-camera.js';
27
+ import { MICRO_SIZE, paintTimeboxBeacon, } from './micro-glyphs.js';
28
+ import { OFFICIAL_DOT_GLYPHS, OFFICIAL_DOT_GLYPH_SIZE, } from './official-dot-glyphs.generated.js';
29
+ const W = WORLD_SIZE;
30
+ const ACTIVE_OFFSET = (WORLD_SIZE - ACTIVE_SIZE) / 2; // 16
25
31
  // Track last render time for accurate dt calculation
26
32
  let lastRenderTime = 0;
27
33
  // ===== Layout (world-buffer pixel coords) =====
28
- const SAND_TOP = 54;
29
- const SAND_BOT = 59;
30
- const SUBSTRATE_TOP = 60;
31
- const SURFACE_Y = 2;
34
+ const SAND_TOP = ACTIVE_OFFSET + 54; // 70
35
+ const SAND_BOT = ACTIVE_OFFSET + 59; // 75
36
+ const SUBSTRATE_TOP = ACTIVE_OFFSET + 60; // 76
37
+ const SURFACE_Y = ACTIVE_OFFSET + 2; // 18
32
38
  // ===== Creature World Positions (normalized 0~1) =====
33
39
  const CF_DEFAULT_X = 0.72;
34
40
  const CF_DEFAULT_Y = 0.76; // just above sand line (sitting on ground)
@@ -36,28 +42,74 @@ const CF_DEFAULT_Y = 0.76; // just above sand line (sitting on ground)
36
42
  const PHI = (1 + Math.sqrt(5)) / 2;
37
43
  /** Active creature instances keyed by sessionId. */
38
44
  const creatureInstances = new Map();
39
- /** Agent types that represent coding agents (draw as octopus). */
45
+ /** Agent types that represent coding agents (draw as octopus/robot). */
40
46
  const CODING_AGENTS = new Set(['claude-code']);
41
47
  /** Agent types drawn as jellyfish (cloud creature). */
42
- const JELLYFISH_AGENTS = new Set(['codex-cli']);
48
+ const JELLYFISH_AGENTS = new Set(['codex-cli', 'codex-app']);
43
49
  /** Agent types drawn as nested-square opencode. */
44
50
  const OPENCODE_AGENTS = new Set(['opencode']);
45
- // Y positions by state idle nearly on sand, active higher up
46
- const IDLE_Y = 0.78; // just above sand line (sleeping on ground)
47
- const WORKING_Y = 0.42; // mid-water (working/starburst)
48
- const ASKING_Y = 0.38; // slightly higher (room for "?" bubble)
49
- function stateY(state) {
50
- if (state === 'processing')
51
- return WORKING_Y;
52
- if (state === 'awaiting')
53
- return ASKING_Y;
54
- return IDLE_Y;
51
+ /** Agent types drawn as the Antigravity peak/arc mark. */
52
+ const ANTIGRAVITY_AGENTS = new Set(['antigravity']);
53
+ function clamp(value, min, max) {
54
+ return Math.min(max, Math.max(min, value));
55
+ }
56
+ /**
57
+ * Vertical stratification per creature type, applied on top of the band's base
58
+ * Y. Each type keeps its own water layer so a mixed Claude+Codex+OpenCode tank
59
+ * stays readable: clouds ride near the surface while working and sink to answer,
60
+ * octopuses hold mid-water, and everything settles to the sand when idle.
61
+ *
62
+ * Mirror of `stateY(_:kind:baseY:)` in apple/AgentDeck/Daemon/Modules/PixooRenderer.swift.
63
+ */
64
+ function stateYForType(state, creatureType, baseY) {
65
+ switch (creatureType) {
66
+ case 'octopus':
67
+ if (state === 'processing')
68
+ return clamp(baseY, 0.40, 0.54);
69
+ if (state === 'awaiting')
70
+ return clamp(baseY - 0.04, 0.35, 0.48);
71
+ return 0.80;
72
+ case 'jellyfish':
73
+ if (state === 'processing')
74
+ return clamp(baseY, 0.16, 0.28);
75
+ if (state === 'awaiting')
76
+ return clamp(baseY + 0.26, 0.52, 0.64);
77
+ return clamp(baseY + 0.40, 0.80, 0.82);
78
+ case 'opencode':
79
+ if (state === 'processing')
80
+ return clamp(baseY - 0.02, 0.20, 0.34);
81
+ if (state === 'awaiting')
82
+ return clamp(baseY + 0.22, 0.50, 0.62);
83
+ return clamp(baseY + 0.36, 0.79, 0.81);
84
+ case 'antigravity':
85
+ if (state === 'processing')
86
+ return clamp(baseY - 0.04, 0.16, 0.30);
87
+ if (state === 'awaiting')
88
+ return clamp(baseY + 0.22, 0.46, 0.54);
89
+ return clamp(baseY + 0.34, 0.56, 0.64);
90
+ }
91
+ }
92
+ function slotsForType(creatureType, count) {
93
+ switch (creatureType) {
94
+ case 'octopus': return layoutOctopuses(count);
95
+ case 'jellyfish': return layoutCloudCreatures(count);
96
+ case 'opencode': return layoutOpenCodeCreatures(count);
97
+ case 'antigravity': return layoutAntigravityCreatures(count);
98
+ }
99
+ }
100
+ const FALLBACK_SLOT = { x: 0.38, y: 0.42, scale: 1.0 };
101
+ function slotAt(slots, index) {
102
+ if (slots.length === 0)
103
+ return FALLBACK_SLOT;
104
+ return slots[Math.min(index, slots.length - 1)];
55
105
  }
56
106
  /** Check if agent type gets a creature. */
57
107
  function isCreatureAgent(agentType) {
58
- return CODING_AGENTS.has(agentType) || JELLYFISH_AGENTS.has(agentType) || OPENCODE_AGENTS.has(agentType);
108
+ return CODING_AGENTS.has(agentType) || JELLYFISH_AGENTS.has(agentType) || OPENCODE_AGENTS.has(agentType) || ANTIGRAVITY_AGENTS.has(agentType);
59
109
  }
60
110
  function creatureTypeFor(agentType) {
111
+ if (ANTIGRAVITY_AGENTS.has(agentType))
112
+ return 'antigravity';
61
113
  if (JELLYFISH_AGENTS.has(agentType))
62
114
  return 'jellyfish';
63
115
  if (OPENCODE_AGENTS.has(agentType))
@@ -65,11 +117,15 @@ function creatureTypeFor(agentType) {
65
117
  return 'octopus';
66
118
  }
67
119
  function syncCreatures(sessions, stateEvent) {
68
- // Determine which sessions are alive creature agents (octopus or jellyfish)
120
+ // Determine which sessions are alive creature agents (octopus or jellyfish).
121
+ // Codex rows fold by project first: each Claude Code rescue/stop-gate spawns a
122
+ // fresh codex thread, so without folding one workspace lights up 4-5 cloud
123
+ // marks at once. Octopus / opencode are NOT folded — running several of those
124
+ // against one project is a deliberate user pattern.
69
125
  const aliveCoding = [];
70
126
  if (sessions) {
71
- for (const s of sessions) {
72
- if (s.alive && s.agentType && isCreatureAgent(s.agentType)) {
127
+ for (const s of foldCodexSessionsForDisplay(sessions.filter(s => s.alive))) {
128
+ if (s.agentType && isCreatureAgent(s.agentType)) {
73
129
  aliveCoding.push({ id: s.id, agentType: s.agentType, state: s.state ?? 'idle' });
74
130
  }
75
131
  }
@@ -89,45 +145,70 @@ function syncCreatures(sessions, stateEvent) {
89
145
  creatureInstances.delete(id);
90
146
  }
91
147
  }
148
+ // Each agent type owns an X band and wraps to a second/third row as it fills,
149
+ // so a mixed tank clusters by agent instead of interleaving along one line.
150
+ const typeCounts = new Map();
151
+ for (const s of aliveCoding) {
152
+ const t = creatureTypeFor(s.agentType);
153
+ typeCounts.set(t, (typeCounts.get(t) ?? 0) + 1);
154
+ }
155
+ const slotsByType = new Map();
156
+ for (const [type, count] of typeCounts)
157
+ slotsByType.set(type, slotsForType(type, count));
158
+ const typeIndices = new Map();
159
+ // Band base Y per session, kept so the `_primary` refresh below can re-derive
160
+ // its stratified Y from the band instead of compounding onto an already
161
+ // stratified worldY.
162
+ const baseYById = new Map();
92
163
  // Add/update creatures
93
164
  for (let i = 0; i < aliveCoding.length; i++) {
94
165
  const s = aliveCoding[i];
95
166
  const existing = creatureInstances.get(s.id);
96
167
  const sessionState = mapSessionState(s.state);
168
+ const creatureType = creatureTypeFor(s.agentType);
169
+ const slotIndex = typeIndices.get(creatureType) ?? 0;
170
+ typeIndices.set(creatureType, slotIndex + 1);
171
+ const slot = slotAt(slotsByType.get(creatureType) ?? [], slotIndex);
172
+ const x = slot.x;
173
+ baseYById.set(s.id, slot.y);
174
+ const worldY = stateYForType(sessionState, creatureType, slot.y);
97
175
  if (existing) {
98
176
  existing.state = sessionState;
99
177
  existing.agentType = s.agentType;
100
- existing.creatureType = creatureTypeFor(s.agentType);
101
- // Update Y position based on state (X stays fixed)
102
- existing.worldY = stateY(sessionState);
178
+ existing.creatureType = creatureType;
179
+ existing.worldX = x; // Update X dynamically to maintain even spacing
180
+ existing.worldY = worldY;
181
+ existing.sizeScale = slot.scale;
103
182
  }
104
183
  else {
105
- // Golden ratio X distribution, Y by state
106
- const x = aliveCoding.length === 1
107
- ? 0.38 // single session: classic center-left
108
- : 0.15 + ((i * PHI) % 1) * 0.70;
109
184
  creatureInstances.set(s.id, {
110
185
  sessionId: s.id,
111
186
  agentType: s.agentType,
112
- creatureType: creatureTypeFor(s.agentType),
187
+ creatureType,
113
188
  state: sessionState,
114
189
  worldX: x,
115
- worldY: stateY(sessionState),
190
+ worldY,
116
191
  phaseOffset: i * 5,
192
+ sizeScale: slot.scale,
117
193
  });
118
194
  }
119
195
  }
120
- // Override primary session state from stateEvent (more precise than polling)
121
- // Only when stateEvent is from a creature agent daemon/openclaw report stale IDLE
196
+ // Refresh the synthetic `_primary` creature from stateEvent it *is* the
197
+ // stateEvent, so this keeps it precise before the first sessions_list lands.
198
+ //
199
+ // Real sessions are deliberately left alone. stateEvent.sessionId can name a
200
+ // row that folding has already collapsed into a representative, and stamping
201
+ // that member's state onto the representative would downgrade a group the fold
202
+ // resolved to `processing` down to the one idle sibling the event came from.
203
+ // The sessions list is daemon-pushed and already carries per-session state.
122
204
  const aType = stateEvent?.agentType;
123
205
  const isCreature = isCreatureAgent(aType ?? '');
124
- if (stateEvent && isCreature && aliveCoding.length > 0) {
125
- const primaryId = aliveCoding[0].id;
126
- const primary = creatureInstances.get(primaryId);
206
+ if (stateEvent && isCreature) {
207
+ const primary = creatureInstances.get('_primary');
127
208
  if (primary) {
128
209
  const st = simplifiedState(stateEvent.state ?? State.IDLE);
129
210
  primary.state = st;
130
- primary.worldY = stateY(st);
211
+ primary.worldY = stateYForType(st, primary.creatureType, baseYById.get('_primary') ?? primary.worldY);
131
212
  }
132
213
  }
133
214
  }
@@ -187,7 +268,7 @@ function waterColorAt(palette, surfaceY, y) {
187
268
  const waterDepth = SAND_TOP - surfaceY;
188
269
  if (waterDepth <= 0)
189
270
  return palette.deep;
190
- const t = (y - surfaceY) / waterDepth;
271
+ const t = Math.max(0, (y - surfaceY) / waterDepth);
191
272
  if (t < 0.25)
192
273
  return lerpColor(palette.surface, palette.light, t / 0.25);
193
274
  if (t < 0.6)
@@ -200,8 +281,8 @@ function initTetras() {
200
281
  const result = [];
201
282
  for (let i = 0; i < NUM_TETRAS; i++) {
202
283
  result.push({
203
- x: 12 + Math.random() * 40,
204
- y: 20 + Math.random() * 25,
284
+ x: 12 + Math.random() * (WORLD_SIZE - 24),
285
+ y: SURFACE_Y + 4 + Math.random() * 22,
205
286
  heading: Math.random() > 0.5 ? 1 : -1,
206
287
  speed: 0.08 + Math.random() * 0.12, // slower — prevents teleporting at ~1fps
207
288
  phase: Math.random() * Math.PI * 2,
@@ -213,11 +294,11 @@ function initTetras() {
213
294
  function updateTetras(frame, surfaceY, maxY) {
214
295
  if (!tetras)
215
296
  tetras = initTetras();
216
- // Two school centers via Lissajous (meet and diverge every ~25s)
217
- const sc0X = 24 + Math.sin(frame * 0.02) * 16;
218
- const sc0Y = Math.max(surfaceY + 8, 22) + Math.cos(frame * 0.015) * 8;
219
- const sc1X = 40 + Math.sin(frame * 0.0175 + 2) * 16;
220
- const sc1Y = Math.max(surfaceY + 8, 24) + Math.cos(frame * 0.0225 + 1) * 8;
297
+ // Two school centers via Lissajous (meet and diverge every ~25s), scaled to WORLD_SIZE
298
+ const sc0X = (WORLD_SIZE / 2 - 12) + Math.sin(frame * 0.02) * 24;
299
+ const sc0Y = Math.max(surfaceY + 8, SURFACE_Y + 12) + Math.cos(frame * 0.015) * 8;
300
+ const sc1X = (WORLD_SIZE / 2 + 12) + Math.sin(frame * 0.0175 + 2) * 24;
301
+ const sc1Y = Math.max(surfaceY + 8, SURFACE_Y + 14) + Math.cos(frame * 0.0225 + 1) * 8;
221
302
  const centers = [{ x: sc0X, y: sc0Y }, { x: sc1X, y: sc1Y }];
222
303
  for (const t of tetras) {
223
304
  const sc = centers[t.schoolId];
@@ -228,9 +309,9 @@ function updateTetras(frame, surfaceY, maxY) {
228
309
  t.y += dy * 0.025 + Math.sin(frame * 0.05 + t.phase) * 0.2;
229
310
  // Boundary
230
311
  const minY = surfaceY + 3;
231
- if (t.x < 3 || t.x > 61) {
312
+ if (t.x < 3 || t.x > WORLD_SIZE - 3) {
232
313
  t.heading *= -1;
233
- t.x = Math.max(3, Math.min(61, t.x));
314
+ t.x = Math.max(3, Math.min(WORLD_SIZE - 3, t.x));
234
315
  }
235
316
  if (t.y < minY)
236
317
  t.y = minY;
@@ -247,12 +328,17 @@ function getSchoolCenter() {
247
328
  sx += t.x;
248
329
  sy += t.y;
249
330
  }
250
- return { x: sx / tetras.length / W, y: sy / tetras.length / W };
331
+ const avgX = sx / tetras.length;
332
+ const avgY = sy / tetras.length;
333
+ return {
334
+ x: (avgX - ACTIVE_OFFSET) / ACTIVE_SIZE,
335
+ y: (avgY - ACTIVE_OFFSET) / ACTIVE_SIZE,
336
+ };
251
337
  }
252
338
  let bubbles = [];
253
339
  function spawnBubble() {
254
340
  return {
255
- x: 4 + Math.random() * 56,
341
+ x: 4 + Math.random() * (WORLD_SIZE - 8),
256
342
  y: SAND_TOP - 1 - Math.random() * 4,
257
343
  speed: 0.3 + Math.random() * 0.4,
258
344
  wobblePhase: Math.random() * Math.PI * 2,
@@ -275,7 +361,7 @@ let dataParticles = [];
275
361
  function updateDataParticles(frame, surfaceY, active) {
276
362
  if (active && frame % 6 === 0) { // spawn half as often
277
363
  dataParticles.push({
278
- x: 10 + Math.random() * 44,
364
+ x: 10 + Math.random() * (WORLD_SIZE - 20),
279
365
  y: surfaceY + 2 + Math.random() * 3,
280
366
  vy: 0.2 + Math.random() * 0.15,
281
367
  life: 60 + Math.random() * 40,
@@ -293,12 +379,20 @@ function updateDataParticles(frame, surfaceY, active) {
293
379
  }
294
380
  // ===== Seaweed =====
295
381
  const SEAWEED_POSITIONS = [
382
+ // Outer left edge (padding region)
296
383
  { x: 2, h: 13, phase: 0 },
297
384
  { x: 5, h: 9, phase: 1.2 },
298
385
  { x: 8, h: 6, phase: 2.5 },
299
- { x: 55, h: 12, phase: 0.8 },
300
- { x: 58, h: 8, phase: 1.9 },
301
- { x: 61, h: 7, phase: 3.1 },
386
+ // Active region left edge
387
+ { x: ACTIVE_OFFSET + 2, h: 11, phase: 0.5 },
388
+ { x: ACTIVE_OFFSET + 5, h: 7, phase: 1.7 },
389
+ // Active region right edge
390
+ { x: ACTIVE_OFFSET + 58, h: 8, phase: 2.2 },
391
+ { x: ACTIVE_OFFSET + 61, h: 10, phase: 0.9 },
392
+ // Outer right edge (padding region)
393
+ { x: WORLD_SIZE - 9, h: 12, phase: 0.8 },
394
+ { x: WORLD_SIZE - 6, h: 8, phase: 1.9 },
395
+ { x: WORLD_SIZE - 3, h: 7, phase: 3.1 },
302
396
  ];
303
397
  function drawSeaweed(buf, frame, surfaceY) {
304
398
  for (const sw of SEAWEED_POSITIONS) {
@@ -320,9 +414,11 @@ function drawSeaweed(buf, frame, surfaceY) {
320
414
  // ===== Light Rays =====
321
415
  function drawLightRays(buf, frame, surfaceY) {
322
416
  const rays = [
323
- { baseX: 15 + Math.sin(frame * 0.02) * 5, angle: 0.15 },
324
- { baseX: 35 + Math.sin(frame * 0.015 + 1) * 6, angle: -0.1 },
325
- { baseX: 50 + Math.sin(frame * 0.025 + 2) * 4, angle: 0.2 },
417
+ { baseX: 8 + Math.sin(frame * 0.018) * 3, angle: 0.1 }, // left padding ray
418
+ { baseX: ACTIVE_OFFSET + 15 + Math.sin(frame * 0.02) * 5, angle: 0.15 },
419
+ { baseX: ACTIVE_OFFSET + 35 + Math.sin(frame * 0.015 + 1) * 6, angle: -0.1 },
420
+ { baseX: ACTIVE_OFFSET + 50 + Math.sin(frame * 0.025 + 2) * 4, angle: 0.2 },
421
+ { baseX: WORLD_SIZE - 10 + Math.sin(frame * 0.022) * 3, angle: -0.15 }, // right padding ray
326
422
  ];
327
423
  for (const ray of rays) {
328
424
  const depth = SAND_TOP - surfaceY;
@@ -385,7 +481,8 @@ function drawTerrain(buf) {
385
481
  setPixel(buf, x, y, color);
386
482
  }
387
483
  }
388
- const gravelPositions = [8, 15, 22, 29, 37, 44, 51, 57];
484
+ // Distribute gravel positions across the entire 96 width
485
+ const gravelPositions = [4, 12, 20, 24, 31, 38, 45, 53, 60, 67, 74, 80, 87, 92];
389
486
  for (const gx of gravelPositions)
390
487
  setPixel(buf, gx, SAND_TOP, COLORS.gravel);
391
488
  for (let y = SUBSTRATE_TOP; y < W; y++) {
@@ -395,9 +492,11 @@ function drawTerrain(buf) {
395
492
  }
396
493
  }
397
494
  const rocks = [
398
- { x: 12, y: SAND_BOT, w: 4, h: 2 },
399
- { x: 30, y: SAND_BOT + 1, w: 3, h: 2 },
400
- { x: 48, y: SAND_BOT, w: 5, h: 3 },
495
+ { x: 4, y: SAND_BOT + 1, w: 3, h: 2 }, // left padding rock
496
+ { x: ACTIVE_OFFSET + 12, y: SAND_BOT, w: 4, h: 2 },
497
+ { x: ACTIVE_OFFSET + 30, y: SAND_BOT + 1, w: 3, h: 2 },
498
+ { x: ACTIVE_OFFSET + 48, y: SAND_BOT, w: 5, h: 3 },
499
+ { x: WORLD_SIZE - 8, y: SAND_BOT, w: 4, h: 2 }, // right padding rock
401
500
  ];
402
501
  for (const r of rocks) {
403
502
  for (let dy = 0; dy < r.h; dy++) {
@@ -438,7 +537,7 @@ function simplifiedState(state) {
438
537
  }
439
538
  // ===== Usage HUD Helpers =====
440
539
  /** Gauge bar color based on usage percentage. */
441
- function gaugeColor(pct, animFrame) {
540
+ function gaugeColor(pct, animFrame, brand) {
442
541
  if (pct >= 90) {
443
542
  // Red with pulse
444
543
  const pulse = (Math.sin(animFrame * 0.2) + 1) * 0.3;
@@ -446,12 +545,10 @@ function gaugeColor(pct, animFrame) {
446
545
  }
447
546
  if (pct >= 70)
448
547
  return COLORS.stateAwaiting; // amber
449
- if (pct >= 50)
450
- return [0x00, 0xC8, 0xB4]; // teal
451
- return COLORS.stateProcessing; // blue
548
+ return brand;
452
549
  }
453
550
  /** Pixoo HUD reset time: "1h23", "4d6", "59m". */
454
- function formatResetDetailed(resetsAt) {
551
+ export function formatResetDetailed(resetsAt) {
455
552
  if (!resetsAt)
456
553
  return '';
457
554
  const ms = new Date(resetsAt).getTime() - Date.now();
@@ -472,81 +569,348 @@ function formatResetDetailed(resetsAt) {
472
569
  return `${hours}h`;
473
570
  return `${mins}m`;
474
571
  }
475
- /** Draw Usage HUD in screen space (bottom right, zoom-independent).
476
- * Single row at rows 57-63:
477
- * - 7d absent: full-width, right-aligned (original behavior)
478
- * - 7d present: left half [0..30]=5h | right half [32..63]=7d
572
+ /** Draw Usage HUD in screen space (bottom, zoom-independent).
573
+ * Each provider uses the same seven-pixel band with its official creature
574
+ * silhouette, full-height usage fill, percentage and reset countdown. When both are present Claude occupies
575
+ * rows 50-56 and Codex rows 57-63; a lone provider stays on rows 57-63.
576
+ * Primary/5h is the left zone and secondary/7d is the right zone.
577
+ * When Codex only reports 7d, its subscription date occupies the otherwise
578
+ * empty primary zone.
479
579
  */
480
580
  function drawUsageHUD(buf, usageEvent, animFrame) {
481
- // Hide the HUD entirely when upstream flags the data as stale — otherwise
482
- // an old "12%" frozen on-screen looks authoritative to the user even though
483
- // the CLI daemon isn't live to produce a fresh fetch. Matches the collapse
484
- // behavior on every other surface (macOS dashboard, plugin, Android, D200H).
485
- if (!usageEvent || usageEvent.fiveHourPercent == null)
581
+ if (!usageEvent)
486
582
  return;
487
- if (usageEvent.usageStale === true)
583
+ const providers = [];
584
+ if (usageEvent.usageStale !== true && usageEvent.fiveHourPercent != null) {
585
+ providers.push({
586
+ glyph: 'claudeCode', brand: [255, 112, 76],
587
+ primary: { percent: usageEvent.fiveHourPercent, resetsAt: usageEvent.fiveHourResetsAt },
588
+ secondary: usageEvent.sevenDayPercent == null ? undefined : {
589
+ percent: usageEvent.sevenDayPercent, resetsAt: usageEvent.sevenDayResetsAt,
590
+ },
591
+ });
592
+ }
593
+ // A window counts as renderable only when it is both fresh AND carries a
594
+ // number. "Not stale but no percent" is a malformed frame, not a zero — the
595
+ // gauge must stay absent rather than draw an authoritative-looking 0%.
596
+ // Mirror of `freshCodexWindow` in apple/AgentDeck/Daemon/Modules/PixooRenderer.swift.
597
+ const freshCodexWindow = (w) => w && w.stale !== true && w.usedPercent != null
598
+ ? { percent: w.usedPercent, resetsAt: w.resetsAt }
599
+ : undefined;
600
+ const codexPrimaryWindow = freshCodexWindow(usageEvent.codexRateLimits?.primary);
601
+ const codexSecondaryWindow = freshCodexWindow(usageEvent.codexRateLimits?.secondary);
602
+ if (codexPrimaryWindow || codexSecondaryWindow) {
603
+ providers.push({
604
+ glyph: 'codex', brand: [126, 116, 255],
605
+ primary: codexPrimaryWindow,
606
+ secondary: codexSecondaryWindow,
607
+ subscriptionUntil: usageEvent.codexSubscriptionActiveUntil,
608
+ });
609
+ }
610
+ if (providers.length === 0)
488
611
  return;
489
- const textY = 58;
490
- const bgTop = textY - 1;
491
- const bgBot = textY + 5;
492
- // Full-width dark base — hides sand/terrain at HUD rows regardless of camera zoom
493
- for (let y = bgTop; y <= bgBot; y++) {
494
- for (let x = 0; x < 64; x++) {
495
- blendPixel(buf, x, y, COLORS.black, 0.55);
612
+ const timeColor = [0x60, 0x70, 0x80];
613
+ const firstY = providers.length > 1 ? 50 : 57;
614
+ function drawCreatureMarker(provider, rowY) {
615
+ const mask = OFFICIAL_DOT_GLYPHS[provider.glyph];
616
+ const sourceSize = OFFICIAL_DOT_GLYPH_SIZE;
617
+ // Sample the canonical square canvas instead of cropping its occupied
618
+ // bounds. Claude's source silhouette is intentionally wider than it is
619
+ // tall; cropping and stretching those bounds to 7x7 turns the robot into
620
+ // a square. The 9px slot leaves one physical LED on both sides.
621
+ const markerSize = 7;
622
+ const markerX = 1;
623
+ for (let dy = 0; dy < 7; dy++) {
624
+ const sy = Math.min(sourceSize - 1, Math.floor((dy + 0.5) * sourceSize / markerSize));
625
+ for (let dx = 0; dx < markerSize; dx++) {
626
+ const sx = Math.min(sourceSize - 1, Math.floor((dx + 0.5) * sourceSize / markerSize));
627
+ const alpha = mask[sy * sourceSize + sx];
628
+ if (alpha <= 16)
629
+ continue;
630
+ const intensity = alpha >= 96 ? 1 : 0.56;
631
+ setPixel(buf, markerX + dx, rowY + dy, lerpColor(COLORS.black, provider.brand, intensity));
632
+ }
496
633
  }
497
634
  }
498
- const timeColor = [0x60, 0x70, 0x80];
499
- /** Render a zone: usage background fill + two-color text (pct + time). */
500
- function renderZone(pctText, timeText, pct, leftX, rightX) {
501
- const color = gaugeColor(pct, animFrame);
502
- const zoneW = rightX - leftX + 1;
503
- const fillW = Math.round(zoneW * Math.max(0, Math.min(100, pct)) / 100);
504
- // Background fill proportional to usage
505
- for (let y = bgTop; y <= bgBot; y++) {
506
- for (let x = leftX; x < leftX + fillW; x++) {
507
- blendPixel(buf, x, y, color, 0.35);
635
+ function fittedReset(resetsAt, pctText, zoneWidth) {
636
+ const detailed = formatResetDetailed(resetsAt);
637
+ if (!detailed)
638
+ return '';
639
+ const maxChars = Math.floor(zoneWidth / 4);
640
+ if (pctText.length + detailed.length <= maxChars)
641
+ return detailed;
642
+ const unit = detailed.match(/^(\d+)[dhm]/);
643
+ return unit ? `${unit[1]}${unit[0].at(-1)}` : detailed.slice(0, Math.max(0, maxChars - pctText.length));
644
+ }
645
+ function subscriptionDate(until) {
646
+ const value = until?.trim();
647
+ if (!value)
648
+ return '';
649
+ const iso = value.match(/^\d{4}-(\d{1,2})-(\d{1,2})/);
650
+ if (iso)
651
+ return `${Number(iso[1])}/${Number(iso[2])}`;
652
+ const compact = value.match(/^~?(\d{1,2})\/(\d{1,2})$/);
653
+ return compact ? `${Number(compact[1])}/${Number(compact[2])}` : '';
654
+ }
655
+ function drawCenteredLabel(text, leftX, rightX, rowY) {
656
+ if (!text)
657
+ return;
658
+ const textWidth = text.length * 4 - 1;
659
+ const startX = leftX + Math.max(0, Math.floor((rightX - leftX - textWidth) / 2));
660
+ drawText(buf, text, startX + textWidth, rowY + 1, timeColor);
661
+ }
662
+ function renderWindow(window, leftX, rightX, rowY, brand) {
663
+ const pct = Math.max(0, Math.min(100, window.percent));
664
+ const color = gaugeColor(pct, animFrame, brand);
665
+ const zoneWidth = rightX - leftX;
666
+ const fillWidth = Math.round(zoneWidth * pct / 100);
667
+ // TC001-style fill: the used portion is a dim full-height color field, so
668
+ // the gauge reads at a glance while bright text remains crisp above it.
669
+ for (let y = rowY; y < rowY + 7; y++) {
670
+ for (let x = leftX; x < leftX + fillWidth; x++)
671
+ blendPixel(buf, x, y, color, 0.24);
672
+ }
673
+ const pctText = `${Math.round(pct)}%`;
674
+ const resetText = fittedReset(window.resetsAt, pctText, zoneWidth);
675
+ if (resetText) {
676
+ drawText(buf, resetText, rightX, rowY + 1, timeColor);
677
+ drawText(buf, pctText, rightX - resetText.length * 4, rowY + 1, color);
678
+ }
679
+ else {
680
+ drawText(buf, pctText, rightX, rowY + 1, color);
681
+ }
682
+ }
683
+ providers.forEach((provider, index) => {
684
+ const rowY = firstY + index * 7;
685
+ for (let y = rowY; y < rowY + 7; y++) {
686
+ for (let x = 0; x < 64; x++) {
687
+ blendPixel(buf, x, y, COLORS.black, 0.70);
688
+ blendPixel(buf, x, y, provider.brand, 0.08);
508
689
  }
509
690
  }
510
- // Two-color text: time (dimmed) right-aligned, then pct (gauge color) to its left
511
- if (timeText) {
512
- drawText(buf, timeText, rightX, textY, timeColor);
513
- const timeW = timeText.length * 4; // 3px glyph + 1px gap per char
514
- drawText(buf, pctText, rightX - timeW, textY, color);
691
+ // A slightly denser identity dock lets the official mark join the same
692
+ // color band without placing quota fill behind the silhouette itself.
693
+ for (let y = rowY; y < rowY + 7; y++) {
694
+ for (let x = 0; x < 9; x++)
695
+ blendPixel(buf, x, y, provider.brand, 0.12);
696
+ }
697
+ drawCreatureMarker(provider, rowY);
698
+ if (provider.primary && provider.secondary) {
699
+ for (let y = rowY + 1; y < rowY + 6; y++)
700
+ blendPixel(buf, 36, y, provider.brand, 0.28);
701
+ renderWindow(provider.primary, 9, 36, rowY, provider.brand);
702
+ renderWindow(provider.secondary, 37, 64, rowY, provider.brand);
703
+ }
704
+ else if (!provider.primary && provider.secondary && subscriptionDate(provider.subscriptionUntil)) {
705
+ for (let y = rowY + 1; y < rowY + 6; y++)
706
+ blendPixel(buf, 36, y, provider.brand, 0.28);
707
+ drawCenteredLabel(subscriptionDate(provider.subscriptionUntil), 9, 36, rowY);
708
+ renderWindow(provider.secondary, 37, 64, rowY, provider.brand);
515
709
  }
516
710
  else {
517
- drawText(buf, pctText, rightX, textY, color);
711
+ const only = provider.primary ?? provider.secondary;
712
+ if (only)
713
+ renderWindow(only, 9, 64, rowY, provider.brand);
518
714
  }
715
+ });
716
+ }
717
+ /**
718
+ * Render the Timebox Mini's native 11×11 Agent Beacon, then nearest-scale into
719
+ * the requested output. A stable 9×9 official mark carries identity while the
720
+ * one-pixel perimeter rail alone carries state and motion.
721
+ */
722
+ function renderMicroFrame(outputBuf, size, animFrame, stateEvent, sessions, usagePct) {
723
+ // Presence-driven SSOT: the crayfish renders iff the daemon emitted an
724
+ // OpenClaw session — never from raw gateway flags. The daemon emits it iff
725
+ // the Gateway is authenticated, so reachability/error alone won't draw it.
726
+ const hasGateway = hasOpenClawSession(sessions ?? []);
727
+ const gatewayHasError = stateEvent?.gatewayHasError ?? false;
728
+ // Pick the dominant creature: awaiting (most urgent) → processing → idle.
729
+ const byPriority = (c) => c.state === 'awaiting' ? 0 : c.state === 'processing' ? 1 : 2;
730
+ const dominant = [...creatureInstances.values()].sort((a, b) => byPriority(a) - byPriority(b))[0];
731
+ // When the only creature is the gateway crayfish, its routing state still
732
+ // drives the background (no dominant creature instance exists for OpenClaw).
733
+ const routing = sessions?.some((s) => s.agentType === 'openclaw' && s.state === 'processing') ?? false;
734
+ const aggregate = gatewayHasError || usagePct >= 90 ? 'error'
735
+ : dominant?.state === 'awaiting' ? 'awaiting'
736
+ : (dominant?.state === 'processing' || (!dominant && routing)) ? 'processing'
737
+ : 'idle';
738
+ const base = new Uint8Array(MICRO_SIZE * MICRO_SIZE * 3);
739
+ let creature = null;
740
+ if (dominant) {
741
+ creature =
742
+ dominant.agentType === 'antigravity' ? 'antigravity'
743
+ : dominant.creatureType === 'jellyfish' ? 'jellyfish'
744
+ : dominant.creatureType === 'opencode' ? 'opencode'
745
+ : 'octopus';
519
746
  }
520
- const pct5 = usageEvent.fiveHourPercent;
521
- if (usageEvent.sevenDayPercent == null) {
522
- // Single full-width zone
523
- const r5 = formatResetDetailed(usageEvent.fiveHourResetsAt);
524
- renderZone(`${Math.round(pct5)}%`, r5, pct5, 0, 63);
525
- return;
747
+ else if (hasGateway) {
748
+ creature = 'crayfish';
749
+ }
750
+ paintTimeboxBeacon(base, creature, aggregate, animFrame);
751
+ // Scale the 11×11 base into the size×size output (1:1 when size === 11).
752
+ for (let y = 0; y < size; y++) {
753
+ const sy = Math.min(MICRO_SIZE - 1, Math.floor((y * MICRO_SIZE) / size));
754
+ for (let x = 0; x < size; x++) {
755
+ const sx = Math.min(MICRO_SIZE - 1, Math.floor((x * MICRO_SIZE) / size));
756
+ const s = (sy * MICRO_SIZE + sx) * 3;
757
+ const d = (y * size + x) * 3;
758
+ outputBuf[d] = base[s];
759
+ outputBuf[d + 1] = base[s + 1];
760
+ outputBuf[d + 2] = base[s + 2];
761
+ }
526
762
  }
527
- // Two-column layout: [5h | 7d]
528
- const pct7 = usageEvent.sevenDayPercent;
529
- const r5 = formatResetDetailed(usageEvent.fiveHourResetsAt);
530
- const r7 = formatResetDetailed(usageEvent.sevenDayResetsAt);
531
- renderZone(`${Math.round(pct5)}%`, r5, pct5, 0, 30);
532
- renderZone(`${Math.round(pct7)}%`, r7, pct7, 32, 63);
763
+ }
764
+ /** Native 32×32 iDotMatrix identity stage. The panel gets saturated official
765
+ * marks on a quiet field plus four fixed telemetry rails; no 64px scene is
766
+ * downsampled, so cutouts remain physical pixels. */
767
+ function renderCompact32Frame(outputBuf, animFrame, stateEvent, sessions, usageEvent) {
768
+ const set = (x, y, color) => setPixel(outputBuf, x, y, color);
769
+ const state = String(stateEvent?.state ?? State.IDLE);
770
+ for (let y = 0; y < 28; y++) {
771
+ const t = y / 27;
772
+ const color = [2 + Math.round(5 * t), 7 + Math.round(10 * t), 18 + Math.round(18 * t)];
773
+ for (let x = 0; x < 32; x++)
774
+ set(x, y, color);
775
+ }
776
+ const surface = state.startsWith('awaiting') ? [255, 190, 45]
777
+ : state === 'processing' ? [50, 225, 255] : [38, 210, 145];
778
+ for (let x = 0; x < 32; x++) {
779
+ if ((x + Math.floor(animFrame / 4)) % (state === 'processing' ? 3 : 6) === 0)
780
+ set(x, 2, surface);
781
+ }
782
+ const glyphFor = (kind) => kind === 'jellyfish' ? 'codex' : kind === 'opencode' ? 'openCode'
783
+ : kind === 'antigravity' ? 'antigravity' : 'claudeCode';
784
+ const priority = (s) => s === 'awaiting' ? 0 : s === 'processing' ? 1 : 2;
785
+ const marks = [...creatureInstances.values()].map((c) => ({ glyph: glyphFor(c.creatureType), state: c.state }));
786
+ if (hasOpenClawSession(sessions ?? [])) {
787
+ const routing = sessions?.some((s) => s.agentType === 'openclaw' && s.state === 'processing') ?? false;
788
+ marks.push({ glyph: 'openClaw', state: routing ? 'processing' : 'idle' });
789
+ }
790
+ marks.sort((a, b) => priority(a.state) - priority(b.state));
791
+ marks.splice(3);
792
+ const slots = marks.length === 1 ? [{ x: 16, y: 14, size: 18 }]
793
+ : marks.length === 2 ? [{ x: 9, y: 14, size: 13 }, { x: 23, y: 14, size: 13 }]
794
+ : [{ x: 6, y: 14, size: 10 }, { x: 16, y: 14, size: 10 }, { x: 26, y: 14, size: 10 }];
795
+ const antigravityBands = [
796
+ [92, 214, 77], [245, 203, 36], [255, 132, 16], [255, 82, 65],
797
+ [183, 92, 182], [102, 111, 225], [36, 126, 255],
798
+ ];
799
+ const baseColor = (glyph, sx) => {
800
+ if (glyph === 'claudeCode')
801
+ return [255, 112, 76];
802
+ if (glyph === 'codex')
803
+ return [126, 116, 255];
804
+ if (glyph === 'openCode')
805
+ return [255, 246, 248];
806
+ if (glyph === 'openClaw')
807
+ return [255, 67, 84];
808
+ return antigravityBands[Math.min(antigravityBands.length - 1, Math.floor(sx * antigravityBands.length / OFFICIAL_DOT_GLYPH_SIZE))];
809
+ };
810
+ marks.forEach((mark, index) => {
811
+ const slot = slots[index];
812
+ const mask = OFFICIAL_DOT_GLYPHS[mark.glyph];
813
+ const bob = mark.state === 'processing' ? Math.round(Math.sin((animFrame + index * 5) * 0.28)) : 0;
814
+ const x0 = slot.x - Math.floor(slot.size / 2);
815
+ const y0 = slot.y - Math.floor(slot.size / 2) + bob;
816
+ for (let dy = 0; dy < slot.size; dy++) {
817
+ const sy = Math.min(OFFICIAL_DOT_GLYPH_SIZE - 1, Math.floor(dy * OFFICIAL_DOT_GLYPH_SIZE / slot.size));
818
+ for (let dx = 0; dx < slot.size; dx++) {
819
+ const sx = Math.min(OFFICIAL_DOT_GLYPH_SIZE - 1, Math.floor(dx * OFFICIAL_DOT_GLYPH_SIZE / slot.size));
820
+ const alpha = mask[sy * OFFICIAL_DOT_GLYPH_SIZE + sx] / 255;
821
+ if (alpha <= 0.04)
822
+ continue;
823
+ const color = baseColor(mark.glyph, sx);
824
+ blendPixel(outputBuf, x0 + dx + 1, y0 + dy + 1, [0, 0, 0], alpha * 0.55);
825
+ if (alpha > 0.42) {
826
+ blendPixel(outputBuf, x0 + dx - 1, y0 + dy, color, 0.055);
827
+ blendPixel(outputBuf, x0 + dx + 1, y0 + dy, color, 0.055);
828
+ }
829
+ const coverage = Math.min(1, Math.pow(alpha, 0.72) * 1.12);
830
+ const light = 1.08 - dy / Math.max(1, slot.size - 1) * 0.12;
831
+ const lit = [
832
+ Math.min(255, Math.round(color[0] * light)),
833
+ Math.min(255, Math.round(color[1] * light)),
834
+ Math.min(255, Math.round(color[2] * light)),
835
+ ];
836
+ blendPixel(outputBuf, x0 + dx, y0 + dy, lit, coverage);
837
+ }
838
+ }
839
+ if (mark.glyph === 'openClaw') {
840
+ set(x0 + Math.round(9.05 / 24 * slot.size), y0 + Math.round(7.63 / 24 * slot.size), [0, 229, 204]);
841
+ set(x0 + Math.round(15.38 / 24 * slot.size), y0 + Math.round(7.63 / 24 * slot.size), [0, 229, 204]);
842
+ }
843
+ if (mark.state === 'processing') {
844
+ for (let spark = 0; spark < 3; spark++) {
845
+ const angle = animFrame * 0.24 + spark * Math.PI * 2 / 3;
846
+ const radius = slot.size / 2 + 1;
847
+ set(Math.round(slot.x + Math.cos(angle) * radius), Math.round(slot.y + bob + Math.sin(angle) * radius), [110, 235, 255]);
848
+ }
849
+ }
850
+ else if (mark.state === 'awaiting') {
851
+ set(Math.min(31, x0 + slot.size), Math.max(2, y0), [255, 190, 45]);
852
+ set(Math.min(31, x0 + slot.size), Math.max(2, y0 + 1), [255, 190, 45]);
853
+ }
854
+ });
855
+ if (marks.length === 0) {
856
+ for (const [x, y] of [[14, 12], [15, 11], [16, 12], [17, 11], [18, 12], [15, 14], [16, 15], [17, 14]]) {
857
+ set(x, y, [76, 206, 220]);
858
+ }
859
+ }
860
+ const primary = usageEvent?.codexRateLimits?.primary?.stale === true
861
+ ? undefined : usageEvent?.codexRateLimits?.primary?.usedPercent;
862
+ const secondary = usageEvent?.codexRateLimits?.secondary?.stale === true
863
+ ? undefined : usageEvent?.codexRateLimits?.secondary?.usedPercent;
864
+ const telemetry = [
865
+ [usageEvent?.fiveHourPercent, [42, 220, 154]],
866
+ [usageEvent?.sevenDayPercent, [54, 154, 255]],
867
+ [primary, [185, 86, 255]],
868
+ [secondary, [104, 116, 255]],
869
+ ];
870
+ telemetry.forEach(([raw, brand], row) => {
871
+ const y = 28 + row;
872
+ for (let x = 0; x < 32; x++)
873
+ set(x, y, [5, 8, 14]);
874
+ if (raw == null)
875
+ return;
876
+ const pct = Math.max(0, Math.min(100, raw));
877
+ const color = pct >= 90 ? [255, 58, 72] : pct >= 70 ? [255, 183, 38] : brand;
878
+ set(0, y, brand);
879
+ set(1, y, brand);
880
+ const width = Math.round(pct / 100 * 29);
881
+ for (let x = 3; x < 3 + width; x++)
882
+ set(x, y, color);
883
+ });
533
884
  }
534
885
  /**
535
- * Render a complete 64×64 frame with camera system.
536
- * Returns 12,288-byte RGB buffer.
886
+ * Render a complete frame with camera system.
887
+ * Returns RGB buffer.
888
+ *
889
+ * `layout='micro'` renders the Timebox Mini Agent Beacon;
890
+ * `'standard'` is the full terrarium.
537
891
  */
538
- export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
892
+ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs, size = 64, layout = 'standard') {
539
893
  const worldBuf = new Uint8Array(W * W * 3);
540
- const outputBuf = new Uint8Array(W * W * 3);
894
+ const outputBuf = new Uint8Array(size * size * 3);
541
895
  const animFrame = getAnimFrame(timeOverrideMs);
896
+ if (layout === 'micro') {
897
+ // Still sync creature instances so dominant-creature selection reflects live state.
898
+ syncCreatures(sessions, stateEvent);
899
+ renderMicroFrame(outputBuf, size, animFrame, stateEvent, sessions, usageEvent?.fiveHourPercent ?? 0);
900
+ return outputBuf;
901
+ }
902
+ if (size === 32) {
903
+ syncCreatures(sessions, stateEvent);
904
+ renderCompact32Frame(outputBuf, animFrame, stateEvent, sessions, usageEvent);
905
+ return outputBuf;
906
+ }
542
907
  const state = stateEvent?.state ?? State.IDLE;
543
908
  const usagePct = usageEvent?.fiveHourPercent ?? 0;
544
909
  const surfaceY = SURFACE_Y;
545
910
  const palette = ZONE_BLUE; // Water stays blue — usage shown only via HUD gauge
546
- // Gateway creature is gated by authenticated connection, not reachability.
547
- const hasGateway = (stateEvent?.gatewayConnected ?? false)
548
- || (stateEvent?.gatewayHasError ?? false)
549
- || (sessions?.some(s => s.agentType === 'openclaw') ?? false);
911
+ // Presence-driven SSOT: crayfish renders iff the daemon emitted an OpenClaw
912
+ // session (authenticated), not from raw gateway reachability/error flags.
913
+ const hasGateway = hasOpenClawSession(sessions ?? []);
550
914
  // === Sync creature instances ===
551
915
  syncCreatures(sessions, stateEvent);
552
916
  // === Build active creatures list for camera ===
@@ -571,7 +935,19 @@ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
571
935
  const dt = lastRenderTime > 0 ? Math.min(5, (now - lastRenderTime) / 1000) : 1.0;
572
936
  lastRenderTime = now;
573
937
  const schoolPos = getSchoolCenter();
574
- const camera = updateDirector(dt, activeCreatures, crayfishRouting, hasGateway ? { x: cfX, y: cfY } : null, schoolPos);
938
+ let camera = updateDirector(dt, activeCreatures, crayfishRouting, hasGateway ? { x: cfX, y: cfY } : null, schoolPos);
939
+ // Adaptive zoom out when multiple sessions are active to increase spacing & breathing room
940
+ const activeSessionCount = creatureInstances.size;
941
+ if (activeSessionCount > 1 && camera.zoom === 1.0) {
942
+ camera.zoom = Math.max(0.78, 1.0 - (activeSessionCount - 1) * 0.11);
943
+ }
944
+ camera.width = size; // Set camera target resolution width
945
+ // Snap the camera center to whole device pixels so a fixed sprite cell (a
946
+ // creature eye) doesn't sub-step as the camera lerps. Applied to the single
947
+ // camera shared by the background blit and every creature draw → they stay
948
+ // pixel-aligned. Must run after the adaptive-zoom tweak above so the final
949
+ // zoom is what we quantize against.
950
+ camera = quantizeCameraPixels(camera);
575
951
  // ========================================
576
952
  // Phase 1: Render environment → world buffer
577
953
  // ========================================
@@ -636,20 +1012,16 @@ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
636
1012
  const spriteState = c.state === 'processing' ? 'working'
637
1013
  : c.state === 'awaiting' ? 'asking'
638
1014
  : 'idle'; // IDLE → idle (limbs move, body color preserved)
639
- if (c.creatureType === 'jellyfish') {
640
- drawJellyfish(outputBuf, c.worldX, c.worldY, spriteState, animFrame + c.phaseOffset, camera, getJellyfishPaletteForSession(sessionToneIndex));
641
- }
642
- else if (c.creatureType === 'opencode') {
643
- drawOpenCode(outputBuf, c.worldX, c.worldY, spriteState, animFrame + c.phaseOffset, camera, getOpenCodePaletteForSession(sessionToneIndex));
644
- }
645
- else {
646
- drawOctopus(outputBuf, c.worldX, c.worldY, spriteState, animFrame + c.phaseOffset, camera, getOctopusPaletteForSession(sessionToneIndex));
647
- }
1015
+ const glyph = c.creatureType === 'jellyfish' ? 'codex'
1016
+ : c.creatureType === 'opencode' ? 'openCode'
1017
+ : c.creatureType === 'antigravity' ? 'antigravity'
1018
+ : 'claudeCode';
1019
+ drawOfficialDotGlyph(outputBuf, glyph, c.worldX, c.worldY, spriteState, animFrame + c.phaseOffset, camera, sessionToneIndex, c.sizeScale);
648
1020
  }
649
1021
  // Crayfish — always drawn when gateway available; IDLE = sitting (subtle breathing only)
650
1022
  if (hasGateway) {
651
1023
  const gatewayHasError = stateEvent?.gatewayHasError ?? false;
652
- drawCrayfish(outputBuf, cfX, cfY, crayfishRouting, animFrame, camera, gatewayHasError);
1024
+ drawOfficialDotGlyph(outputBuf, 'openClaw', cfX, cfY, crayfishRouting ? 'working' : 'idle', animFrame, camera, 0, 1.0, gatewayHasError);
653
1025
  }
654
1026
  // ========================================
655
1027
  // Phase 4: Screen-space overlays
@@ -657,8 +1029,8 @@ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
657
1029
  // Danger flash (>90% usage)
658
1030
  if (usagePct >= 90) {
659
1031
  const flashIntensity = (Math.sin(animFrame * 0.2) + 1) * 0.08;
660
- for (let y = 0; y < W; y++) {
661
- for (let x = 0; x < W; x++) {
1032
+ for (let y = 0; y < size; y++) {
1033
+ for (let x = 0; x < size; x++) {
662
1034
  glowPixel(outputBuf, x, y, COLORS.stateError, flashIntensity);
663
1035
  }
664
1036
  }
@@ -670,9 +1042,14 @@ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
670
1042
  for (let i = 0; i < Math.min(sessionCount, 6); i++) {
671
1043
  const dotX = 1 + i * 3; // 2px dot + 1px gap
672
1044
  const c = orderedCreatures[i];
1045
+ // Color the dot by agent type so OpenCode is distinguishable, not painted as an octopus.
673
1046
  const dotColor = c.creatureType === 'jellyfish'
674
1047
  ? getJellyfishPaletteForSession(i).body
675
- : getOctopusPaletteForSession(i).body;
1048
+ : c.creatureType === 'opencode'
1049
+ ? getOpenCodePaletteForSession(i).outer
1050
+ : c.creatureType === 'antigravity'
1051
+ ? getAntigravityPaletteForSession(i).yellow
1052
+ : getOctopusPaletteForSession(i).body;
676
1053
  setPixel(outputBuf, dotX, 1, dotColor);
677
1054
  setPixel(outputBuf, dotX + 1, 1, dotColor);
678
1055
  setPixel(outputBuf, dotX, 2, dotColor);
@@ -686,11 +1063,32 @@ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
686
1063
  // ===== Disconnected Frame =====
687
1064
  /** Render a static black frame with centered grey "OFFLINE" text. */
688
1065
  export function renderDisconnectedFrame() {
689
- const buf = new Uint8Array(W * W * 3); // black
690
- drawTextCentered(buf, 29, 'OFFLINE', '#555555');
1066
+ const buf = new Uint8Array(64 * 64 * 3); // black
1067
+ drawTextCentered(buf, 29, PASSIVE_OFFLINE_LABEL, '#555555');
691
1068
  return buf;
692
1069
  }
693
1070
  // ===== Preview API (re-export camera controls) =====
1071
+ /**
1072
+ * Introspection seam: the creature set `renderFrame` would draw for this input,
1073
+ * without rasterizing anything. Counting marks back out of a 64×64 buffer is
1074
+ * far too fragile to assert on, so tests and the preview CLI read the resolved
1075
+ * world placement from here instead.
1076
+ *
1077
+ * This runs the same `syncCreatures` the renderer runs, so it mutates the module's
1078
+ * creature state exactly as a real frame would.
1079
+ */
1080
+ export function getCreatureLayoutSnapshot(sessions, stateEvent) {
1081
+ syncCreatures(sessions, stateEvent);
1082
+ return [...creatureInstances.values()].map(c => ({
1083
+ sessionId: c.sessionId,
1084
+ agentType: c.agentType,
1085
+ creatureType: c.creatureType,
1086
+ state: c.state,
1087
+ worldX: c.worldX,
1088
+ worldY: c.worldY,
1089
+ sizeScale: c.sizeScale,
1090
+ }));
1091
+ }
694
1092
  export { setZone, setOverride, resetDirector } from './pixoo-camera.js';
695
1093
  export { ZONES } from './pixoo-camera.js';
696
1094
  //# sourceMappingURL=pixoo-renderer.js.map