@agentdeck/bridge 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/claude-code.d.ts +11 -27
  3. package/dist/adapters/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/claude-code.js +29 -123
  5. package/dist/adapters/claude-code.js.map +1 -1
  6. package/dist/adapters/codex-cli.d.ts +21 -0
  7. package/dist/adapters/codex-cli.d.ts.map +1 -0
  8. package/dist/adapters/codex-cli.js +53 -0
  9. package/dist/adapters/codex-cli.js.map +1 -0
  10. package/dist/adapters/index.d.ts +4 -0
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +13 -0
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/adapters/monitor.d.ts +34 -0
  15. package/dist/adapters/monitor.d.ts.map +1 -0
  16. package/dist/adapters/monitor.js +86 -0
  17. package/dist/adapters/monitor.js.map +1 -0
  18. package/dist/adapters/openclaw.d.ts +36 -2
  19. package/dist/adapters/openclaw.d.ts.map +1 -1
  20. package/dist/adapters/openclaw.js +329 -59
  21. package/dist/adapters/openclaw.js.map +1 -1
  22. package/dist/adapters/opencode-adapter.d.ts +48 -0
  23. package/dist/adapters/opencode-adapter.d.ts.map +1 -0
  24. package/dist/adapters/opencode-adapter.js +345 -0
  25. package/dist/adapters/opencode-adapter.js.map +1 -0
  26. package/dist/adapters/pty-adapter.d.ts +56 -0
  27. package/dist/adapters/pty-adapter.d.ts.map +1 -0
  28. package/dist/adapters/pty-adapter.js +143 -0
  29. package/dist/adapters/pty-adapter.js.map +1 -0
  30. package/dist/adb-reverse.d.ts +25 -0
  31. package/dist/adb-reverse.d.ts.map +1 -0
  32. package/dist/adb-reverse.js +129 -0
  33. package/dist/adb-reverse.js.map +1 -0
  34. package/dist/antigravity-local.d.ts +7 -0
  35. package/dist/antigravity-local.d.ts.map +1 -0
  36. package/dist/antigravity-local.js +153 -0
  37. package/dist/antigravity-local.js.map +1 -0
  38. package/dist/apme/adapters/claude-hook.d.ts +15 -0
  39. package/dist/apme/adapters/claude-hook.d.ts.map +1 -0
  40. package/dist/apme/adapters/claude-hook.js +85 -0
  41. package/dist/apme/adapters/claude-hook.js.map +1 -0
  42. package/dist/apme/adapters/claude-pty.d.ts +28 -0
  43. package/dist/apme/adapters/claude-pty.d.ts.map +1 -0
  44. package/dist/apme/adapters/claude-pty.js +88 -0
  45. package/dist/apme/adapters/claude-pty.js.map +1 -0
  46. package/dist/apme/adapters/timeline.d.ts +16 -0
  47. package/dist/apme/adapters/timeline.d.ts.map +1 -0
  48. package/dist/apme/adapters/timeline.js +66 -0
  49. package/dist/apme/adapters/timeline.js.map +1 -0
  50. package/dist/apme/classifier.d.ts +53 -0
  51. package/dist/apme/classifier.d.ts.map +1 -0
  52. package/dist/apme/classifier.js +266 -0
  53. package/dist/apme/classifier.js.map +1 -0
  54. package/dist/apme/claude-transcript-reader.d.ts +38 -0
  55. package/dist/apme/claude-transcript-reader.d.ts.map +1 -0
  56. package/dist/apme/claude-transcript-reader.js +127 -0
  57. package/dist/apme/claude-transcript-reader.js.map +1 -0
  58. package/dist/apme/collector.d.ts +108 -0
  59. package/dist/apme/collector.d.ts.map +1 -0
  60. package/dist/apme/collector.js +659 -0
  61. package/dist/apme/collector.js.map +1 -0
  62. package/dist/apme/dashboard-html.d.ts +9 -0
  63. package/dist/apme/dashboard-html.d.ts.map +1 -0
  64. package/dist/apme/dashboard-html.js +416 -0
  65. package/dist/apme/dashboard-html.js.map +1 -0
  66. package/dist/apme/http.d.ts +19 -0
  67. package/dist/apme/http.d.ts.map +1 -0
  68. package/dist/apme/http.js +210 -0
  69. package/dist/apme/http.js.map +1 -0
  70. package/dist/apme/hw-sampler.d.ts +30 -0
  71. package/dist/apme/hw-sampler.d.ts.map +1 -0
  72. package/dist/apme/hw-sampler.js +106 -0
  73. package/dist/apme/hw-sampler.js.map +1 -0
  74. package/dist/apme/index.d.ts +43 -0
  75. package/dist/apme/index.d.ts.map +1 -0
  76. package/dist/apme/index.js +75 -0
  77. package/dist/apme/index.js.map +1 -0
  78. package/dist/apme/outcome.d.ts +48 -0
  79. package/dist/apme/outcome.d.ts.map +1 -0
  80. package/dist/apme/outcome.js +232 -0
  81. package/dist/apme/outcome.js.map +1 -0
  82. package/dist/apme/recommend.d.ts +32 -0
  83. package/dist/apme/recommend.d.ts.map +1 -0
  84. package/dist/apme/recommend.js +41 -0
  85. package/dist/apme/recommend.js.map +1 -0
  86. package/dist/apme/runner.d.ts +94 -0
  87. package/dist/apme/runner.d.ts.map +1 -0
  88. package/dist/apme/runner.js +812 -0
  89. package/dist/apme/runner.js.map +1 -0
  90. package/dist/apme/settings.d.ts +65 -0
  91. package/dist/apme/settings.d.ts.map +1 -0
  92. package/dist/apme/settings.js +97 -0
  93. package/dist/apme/settings.js.map +1 -0
  94. package/dist/apme/store.d.ts +99 -0
  95. package/dist/apme/store.d.ts.map +1 -0
  96. package/dist/apme/store.js +911 -0
  97. package/dist/apme/store.js.map +1 -0
  98. package/dist/apme/tuner.d.ts +64 -0
  99. package/dist/apme/tuner.d.ts.map +1 -0
  100. package/dist/apme/tuner.js +297 -0
  101. package/dist/apme/tuner.js.map +1 -0
  102. package/dist/apme/types.d.ts +11 -0
  103. package/dist/apme/types.d.ts.map +1 -0
  104. package/dist/apme/types.js +10 -0
  105. package/dist/apme/types.js.map +1 -0
  106. package/dist/auth.d.ts +1 -1
  107. package/dist/auth.d.ts.map +1 -1
  108. package/dist/auth.js +15 -5
  109. package/dist/auth.js.map +1 -1
  110. package/dist/bridge-core.d.ts +162 -0
  111. package/dist/bridge-core.d.ts.map +1 -0
  112. package/dist/bridge-core.js +627 -0
  113. package/dist/bridge-core.js.map +1 -0
  114. package/dist/check-deps.d.ts +3 -1
  115. package/dist/check-deps.d.ts.map +1 -1
  116. package/dist/check-deps.js +34 -13
  117. package/dist/check-deps.js.map +1 -1
  118. package/dist/cli.d.ts +3 -0
  119. package/dist/cli.d.ts.map +1 -0
  120. package/dist/cli.js +1235 -0
  121. package/dist/cli.js.map +1 -0
  122. package/dist/codex-auth.d.ts +10 -0
  123. package/dist/codex-auth.d.ts.map +1 -0
  124. package/dist/codex-auth.js +63 -0
  125. package/dist/codex-auth.js.map +1 -0
  126. package/dist/codex-output-parser.d.ts +21 -0
  127. package/dist/codex-output-parser.d.ts.map +1 -0
  128. package/dist/codex-output-parser.js +253 -0
  129. package/dist/codex-output-parser.js.map +1 -0
  130. package/dist/d200h/hid-protocol.d.ts +62 -0
  131. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  132. package/dist/d200h/hid-protocol.js +124 -0
  133. package/dist/d200h/hid-protocol.js.map +1 -0
  134. package/dist/d200h/image-renderer.d.ts +36 -0
  135. package/dist/d200h/image-renderer.d.ts.map +1 -0
  136. package/dist/d200h/image-renderer.js +487 -0
  137. package/dist/d200h/image-renderer.js.map +1 -0
  138. package/dist/daemon-server.d.ts +19 -0
  139. package/dist/daemon-server.d.ts.map +1 -0
  140. package/dist/daemon-server.js +1168 -0
  141. package/dist/daemon-server.js.map +1 -0
  142. package/dist/daemon-ws-client.d.ts +82 -0
  143. package/dist/daemon-ws-client.d.ts.map +1 -0
  144. package/dist/daemon-ws-client.js +179 -0
  145. package/dist/daemon-ws-client.js.map +1 -0
  146. package/dist/daemon.d.ts +10 -0
  147. package/dist/daemon.d.ts.map +1 -0
  148. package/dist/daemon.js +56 -0
  149. package/dist/daemon.js.map +1 -0
  150. package/dist/display-monitor.d.ts +25 -0
  151. package/dist/display-monitor.d.ts.map +1 -0
  152. package/dist/display-monitor.js +143 -0
  153. package/dist/display-monitor.js.map +1 -0
  154. package/dist/esp32-serial.d.ts +112 -0
  155. package/dist/esp32-serial.d.ts.map +1 -0
  156. package/dist/esp32-serial.js +502 -0
  157. package/dist/esp32-serial.js.map +1 -0
  158. package/dist/gateway-probe.d.ts +12 -0
  159. package/dist/gateway-probe.d.ts.map +1 -0
  160. package/dist/gateway-probe.js +39 -0
  161. package/dist/gateway-probe.js.map +1 -0
  162. package/dist/hook-server.d.ts +29 -0
  163. package/dist/hook-server.d.ts.map +1 -1
  164. package/dist/hook-server.js +366 -8
  165. package/dist/hook-server.js.map +1 -1
  166. package/dist/index.d.ts +20 -2
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +1305 -562
  169. package/dist/index.js.map +1 -1
  170. package/dist/log-stream.d.ts +26 -0
  171. package/dist/log-stream.d.ts.map +1 -0
  172. package/dist/log-stream.js +111 -0
  173. package/dist/log-stream.js.map +1 -0
  174. package/dist/logger.d.ts +9 -2
  175. package/dist/logger.d.ts.map +1 -1
  176. package/dist/logger.js +36 -5
  177. package/dist/logger.js.map +1 -1
  178. package/dist/mdns.d.ts +15 -1
  179. package/dist/mdns.d.ts.map +1 -1
  180. package/dist/mdns.js +143 -28
  181. package/dist/mdns.js.map +1 -1
  182. package/dist/mlx-probe.d.ts +14 -0
  183. package/dist/mlx-probe.d.ts.map +1 -0
  184. package/dist/mlx-probe.js +44 -0
  185. package/dist/mlx-probe.js.map +1 -0
  186. package/dist/model-catalog.d.ts +5 -3
  187. package/dist/model-catalog.d.ts.map +1 -1
  188. package/dist/model-catalog.js +45 -29
  189. package/dist/model-catalog.js.map +1 -1
  190. package/dist/modules/adb-module.d.ts +14 -0
  191. package/dist/modules/adb-module.d.ts.map +1 -0
  192. package/dist/modules/adb-module.js +36 -0
  193. package/dist/modules/adb-module.js.map +1 -0
  194. package/dist/modules/d200h-module.d.ts +43 -0
  195. package/dist/modules/d200h-module.d.ts.map +1 -0
  196. package/dist/modules/d200h-module.js +360 -0
  197. package/dist/modules/d200h-module.js.map +1 -0
  198. package/dist/modules/d200h-renderer.d.ts +7 -0
  199. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  200. package/dist/modules/d200h-renderer.js +321 -0
  201. package/dist/modules/d200h-renderer.js.map +1 -0
  202. package/dist/modules/index.d.ts +23 -0
  203. package/dist/modules/index.d.ts.map +1 -0
  204. package/dist/modules/index.js +65 -0
  205. package/dist/modules/index.js.map +1 -0
  206. package/dist/modules/mdns-module.d.ts +12 -0
  207. package/dist/modules/mdns-module.d.ts.map +1 -0
  208. package/dist/modules/mdns-module.js +21 -0
  209. package/dist/modules/mdns-module.js.map +1 -0
  210. package/dist/modules/pixoo-module.d.ts +8 -0
  211. package/dist/modules/pixoo-module.d.ts.map +1 -0
  212. package/dist/modules/pixoo-module.js +24 -0
  213. package/dist/modules/pixoo-module.js.map +1 -0
  214. package/dist/modules/serial-module.d.ts +18 -0
  215. package/dist/modules/serial-module.d.ts.map +1 -0
  216. package/dist/modules/serial-module.js +47 -0
  217. package/dist/modules/serial-module.js.map +1 -0
  218. package/dist/modules/types.d.ts +39 -0
  219. package/dist/modules/types.d.ts.map +1 -0
  220. package/dist/modules/types.js +2 -0
  221. package/dist/modules/types.js.map +1 -0
  222. package/dist/ollama-probe.d.ts +13 -0
  223. package/dist/ollama-probe.d.ts.map +1 -0
  224. package/dist/ollama-probe.js +46 -0
  225. package/dist/ollama-probe.js.map +1 -0
  226. package/dist/opencode-client.d.ts +126 -0
  227. package/dist/opencode-client.d.ts.map +1 -0
  228. package/dist/opencode-client.js +181 -0
  229. package/dist/opencode-client.js.map +1 -0
  230. package/dist/output-parser.d.ts +3 -0
  231. package/dist/output-parser.d.ts.map +1 -1
  232. package/dist/output-parser.js +29 -3
  233. package/dist/output-parser.js.map +1 -1
  234. package/dist/passive-observer.d.ts +38 -0
  235. package/dist/passive-observer.d.ts.map +1 -0
  236. package/dist/passive-observer.js +632 -0
  237. package/dist/passive-observer.js.map +1 -0
  238. package/dist/pixoo/pixoo-bridge.d.ts +50 -0
  239. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -0
  240. package/dist/pixoo/pixoo-bridge.js +280 -0
  241. package/dist/pixoo/pixoo-bridge.js.map +1 -0
  242. package/dist/pixoo/pixoo-camera.d.ts +80 -0
  243. package/dist/pixoo/pixoo-camera.d.ts.map +1 -0
  244. package/dist/pixoo/pixoo-camera.js +271 -0
  245. package/dist/pixoo/pixoo-camera.js.map +1 -0
  246. package/dist/pixoo/pixoo-client.d.ts +67 -0
  247. package/dist/pixoo/pixoo-client.d.ts.map +1 -0
  248. package/dist/pixoo/pixoo-client.js +335 -0
  249. package/dist/pixoo/pixoo-client.js.map +1 -0
  250. package/dist/pixoo/pixoo-font.d.ts +24 -0
  251. package/dist/pixoo/pixoo-font.d.ts.map +1 -0
  252. package/dist/pixoo/pixoo-font.js +121 -0
  253. package/dist/pixoo/pixoo-font.js.map +1 -0
  254. package/dist/pixoo/pixoo-renderer.d.ts +32 -0
  255. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -0
  256. package/dist/pixoo/pixoo-renderer.js +696 -0
  257. package/dist/pixoo/pixoo-renderer.js.map +1 -0
  258. package/dist/pixoo/pixoo-settings.d.ts +10 -0
  259. package/dist/pixoo/pixoo-settings.d.ts.map +1 -0
  260. package/dist/pixoo/pixoo-settings.js +47 -0
  261. package/dist/pixoo/pixoo-settings.js.map +1 -0
  262. package/dist/pixoo/pixoo-sprites.d.ts +187 -0
  263. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -0
  264. package/dist/pixoo/pixoo-sprites.js +926 -0
  265. package/dist/pixoo/pixoo-sprites.js.map +1 -0
  266. package/dist/pty-manager.d.ts +1 -1
  267. package/dist/pty-manager.d.ts.map +1 -1
  268. package/dist/pty-manager.js +36 -15
  269. package/dist/pty-manager.js.map +1 -1
  270. package/dist/session-aggregator.d.ts +41 -0
  271. package/dist/session-aggregator.d.ts.map +1 -0
  272. package/dist/session-aggregator.js +76 -0
  273. package/dist/session-aggregator.js.map +1 -0
  274. package/dist/session-focus-relay.d.ts +35 -0
  275. package/dist/session-focus-relay.d.ts.map +1 -0
  276. package/dist/session-focus-relay.js +127 -0
  277. package/dist/session-focus-relay.js.map +1 -0
  278. package/dist/session-registry.d.ts +88 -1
  279. package/dist/session-registry.d.ts.map +1 -1
  280. package/dist/session-registry.js +222 -15
  281. package/dist/session-registry.js.map +1 -1
  282. package/dist/session-timeline-relay.d.ts +27 -0
  283. package/dist/session-timeline-relay.d.ts.map +1 -0
  284. package/dist/session-timeline-relay.js +124 -0
  285. package/dist/session-timeline-relay.js.map +1 -0
  286. package/dist/state-machine.d.ts +1 -0
  287. package/dist/state-machine.d.ts.map +1 -1
  288. package/dist/state-machine.js +14 -0
  289. package/dist/state-machine.js.map +1 -1
  290. package/dist/terminal-status.d.ts +28 -0
  291. package/dist/terminal-status.d.ts.map +1 -0
  292. package/dist/terminal-status.js +152 -0
  293. package/dist/terminal-status.js.map +1 -0
  294. package/dist/timeline-store.d.ts +22 -0
  295. package/dist/timeline-store.d.ts.map +1 -0
  296. package/dist/timeline-store.js +79 -0
  297. package/dist/timeline-store.js.map +1 -0
  298. package/dist/timeline-summarizer.d.ts +14 -0
  299. package/dist/timeline-summarizer.d.ts.map +1 -0
  300. package/dist/timeline-summarizer.js +154 -0
  301. package/dist/timeline-summarizer.js.map +1 -0
  302. package/dist/tts.d.ts +25 -0
  303. package/dist/tts.d.ts.map +1 -0
  304. package/dist/tts.js +99 -0
  305. package/dist/tts.js.map +1 -0
  306. package/dist/tui/ansi.d.ts +92 -0
  307. package/dist/tui/ansi.d.ts.map +1 -0
  308. package/dist/tui/ansi.js +181 -0
  309. package/dist/tui/ansi.js.map +1 -0
  310. package/dist/tui/dashboard.d.ts +35 -0
  311. package/dist/tui/dashboard.d.ts.map +1 -0
  312. package/dist/tui/dashboard.js +436 -0
  313. package/dist/tui/dashboard.js.map +1 -0
  314. package/dist/tui/gauge.d.ts +15 -0
  315. package/dist/tui/gauge.d.ts.map +1 -0
  316. package/dist/tui/gauge.js +82 -0
  317. package/dist/tui/gauge.js.map +1 -0
  318. package/dist/tui/renderer.d.ts +15 -0
  319. package/dist/tui/renderer.d.ts.map +1 -0
  320. package/dist/tui/renderer.js +758 -0
  321. package/dist/tui/renderer.js.map +1 -0
  322. package/dist/tui/screen.d.ts +27 -0
  323. package/dist/tui/screen.d.ts.map +1 -0
  324. package/dist/tui/screen.js +109 -0
  325. package/dist/tui/screen.js.map +1 -0
  326. package/dist/tui/terrarium.d.ts +96 -0
  327. package/dist/tui/terrarium.d.ts.map +1 -0
  328. package/dist/tui/terrarium.js +912 -0
  329. package/dist/tui/terrarium.js.map +1 -0
  330. package/dist/types.d.ts +4 -2
  331. package/dist/types.d.ts.map +1 -1
  332. package/dist/types.js +3 -2
  333. package/dist/types.js.map +1 -1
  334. package/dist/usage-api.d.ts +9 -2
  335. package/dist/usage-api.d.ts.map +1 -1
  336. package/dist/usage-api.js +224 -42
  337. package/dist/usage-api.js.map +1 -1
  338. package/dist/usage-event.d.ts +12 -0
  339. package/dist/usage-event.d.ts.map +1 -0
  340. package/dist/usage-event.js +78 -0
  341. package/dist/usage-event.js.map +1 -0
  342. package/dist/usage-tracker.d.ts +4 -0
  343. package/dist/usage-tracker.d.ts.map +1 -1
  344. package/dist/usage-tracker.js +17 -0
  345. package/dist/usage-tracker.js.map +1 -1
  346. package/dist/utility-proxy.d.ts +30 -0
  347. package/dist/utility-proxy.d.ts.map +1 -0
  348. package/dist/utility-proxy.js +217 -0
  349. package/dist/utility-proxy.js.map +1 -0
  350. package/dist/utils/project-name.d.ts +22 -0
  351. package/dist/utils/project-name.d.ts.map +1 -0
  352. package/dist/utils/project-name.js +72 -0
  353. package/dist/utils/project-name.js.map +1 -0
  354. package/dist/version-check.d.ts +26 -0
  355. package/dist/version-check.d.ts.map +1 -0
  356. package/dist/version-check.js +225 -0
  357. package/dist/version-check.js.map +1 -0
  358. package/dist/voice-assistant.d.ts +63 -0
  359. package/dist/voice-assistant.d.ts.map +1 -0
  360. package/dist/voice-assistant.js +406 -0
  361. package/dist/voice-assistant.js.map +1 -0
  362. package/dist/voice.d.ts +2 -0
  363. package/dist/voice.d.ts.map +1 -1
  364. package/dist/voice.js +26 -1
  365. package/dist/voice.js.map +1 -1
  366. package/dist/wake-word.d.ts +35 -0
  367. package/dist/wake-word.d.ts.map +1 -0
  368. package/dist/wake-word.js +225 -0
  369. package/dist/wake-word.js.map +1 -0
  370. package/dist/whisper-server-manager.d.ts +1 -1
  371. package/dist/whisper-server-manager.js +1 -1
  372. package/dist/wifi-config.d.ts +24 -0
  373. package/dist/wifi-config.d.ts.map +1 -0
  374. package/dist/wifi-config.js +84 -0
  375. package/dist/wifi-config.js.map +1 -0
  376. package/dist/ws-server.d.ts +12 -0
  377. package/dist/ws-server.d.ts.map +1 -1
  378. package/dist/ws-server.js +81 -3
  379. package/dist/ws-server.js.map +1 -1
  380. package/package.json +22 -13
@@ -0,0 +1,696 @@
1
+ /**
2
+ * Pixoo64 Frame Renderer — camera-based animated terrarium.
3
+ *
4
+ * No text. All information encoded visually:
5
+ *
6
+ * Water color ↔ usage zone (blue → teal → amber → red)
7
+ * Waves ↔ agent state (calm=IDLE, choppy=PROC, golden pulse=AWAITING)
8
+ * Bubbles ↔ activity density
9
+ * Creatures ↔ sessions + gateway
10
+ * Particles ↔ data flow during processing
11
+ * Surface glow ↔ state color (green / blue / amber)
12
+ * Camera zoom ↔ state-driven focus (wide, octopus close-up, crayfish, school, surface)
13
+ *
14
+ * Rendering pipeline:
15
+ * 1. Environment → 64×64 world buffer (water, terrain, effects)
16
+ * 2. blitWithCamera() → output buffer (crop + scale by camera zoom/pan)
17
+ * 3. Scaled creatures → output buffer (HD grid sprites with camera-aware sizing)
18
+ * 4. Screen-space overlays (danger flash) → output buffer
19
+ */
20
+ import { State } from '../types.js';
21
+ 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
+ // Track last render time for accurate dt calculation
26
+ let lastRenderTime = 0;
27
+ // ===== 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;
32
+ // ===== Creature World Positions (normalized 0~1) =====
33
+ const CF_DEFAULT_X = 0.72;
34
+ const CF_DEFAULT_Y = 0.76; // just above sand line (sitting on ground)
35
+ /** Golden ratio constant for position distribution. */
36
+ const PHI = (1 + Math.sqrt(5)) / 2;
37
+ /** Active creature instances keyed by sessionId. */
38
+ const creatureInstances = new Map();
39
+ /** Agent types that represent coding agents (draw as octopus). */
40
+ const CODING_AGENTS = new Set(['claude-code']);
41
+ /** Agent types drawn as jellyfish (cloud creature). */
42
+ const JELLYFISH_AGENTS = new Set(['codex-cli']);
43
+ /** Agent types drawn as nested-square opencode. */
44
+ 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;
55
+ }
56
+ /** Check if agent type gets a creature. */
57
+ function isCreatureAgent(agentType) {
58
+ return CODING_AGENTS.has(agentType) || JELLYFISH_AGENTS.has(agentType) || OPENCODE_AGENTS.has(agentType);
59
+ }
60
+ function creatureTypeFor(agentType) {
61
+ if (JELLYFISH_AGENTS.has(agentType))
62
+ return 'jellyfish';
63
+ if (OPENCODE_AGENTS.has(agentType))
64
+ return 'opencode';
65
+ return 'octopus';
66
+ }
67
+ function syncCreatures(sessions, stateEvent) {
68
+ // Determine which sessions are alive creature agents (octopus or jellyfish)
69
+ const aliveCoding = [];
70
+ if (sessions) {
71
+ for (const s of sessions) {
72
+ if (s.alive && s.agentType && isCreatureAgent(s.agentType)) {
73
+ aliveCoding.push({ id: s.id, agentType: s.agentType, state: s.state ?? 'idle' });
74
+ }
75
+ }
76
+ }
77
+ // If sessions data has never been received, use stateEvent as single session (only for creature agents)
78
+ const stateAgentType = (stateEvent?.agentType ?? 'claude-code');
79
+ if (sessions === null && aliveCoding.length === 0 && stateEvent && isCreatureAgent(stateAgentType)) {
80
+ aliveCoding.push({
81
+ id: '_primary',
82
+ agentType: stateAgentType,
83
+ state: simplifiedState(stateEvent.state ?? State.IDLE),
84
+ });
85
+ }
86
+ // Remove creatures for dead sessions
87
+ for (const id of creatureInstances.keys()) {
88
+ if (!aliveCoding.some(s => s.id === id)) {
89
+ creatureInstances.delete(id);
90
+ }
91
+ }
92
+ // Add/update creatures
93
+ for (let i = 0; i < aliveCoding.length; i++) {
94
+ const s = aliveCoding[i];
95
+ const existing = creatureInstances.get(s.id);
96
+ const sessionState = mapSessionState(s.state);
97
+ if (existing) {
98
+ existing.state = sessionState;
99
+ 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);
103
+ }
104
+ 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
+ creatureInstances.set(s.id, {
110
+ sessionId: s.id,
111
+ agentType: s.agentType,
112
+ creatureType: creatureTypeFor(s.agentType),
113
+ state: sessionState,
114
+ worldX: x,
115
+ worldY: stateY(sessionState),
116
+ phaseOffset: i * 5,
117
+ });
118
+ }
119
+ }
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
122
+ const aType = stateEvent?.agentType;
123
+ const isCreature = isCreatureAgent(aType ?? '');
124
+ if (stateEvent && isCreature && aliveCoding.length > 0) {
125
+ const primaryId = aliveCoding[0].id;
126
+ const primary = creatureInstances.get(primaryId);
127
+ if (primary) {
128
+ const st = simplifiedState(stateEvent.state ?? State.IDLE);
129
+ primary.state = st;
130
+ primary.worldY = stateY(st);
131
+ }
132
+ }
133
+ }
134
+ function mapSessionState(state) {
135
+ if (state === 'processing')
136
+ return 'processing';
137
+ if (state === 'awaiting' || state === 'awaiting_option' || state === 'awaiting_permission' || state === 'awaiting_diff')
138
+ return 'awaiting';
139
+ return 'idle';
140
+ }
141
+ const ZONE_BLUE = {
142
+ surface: COLORS.waterSurface, light: COLORS.waterLight,
143
+ mid: COLORS.waterMid, deep: COLORS.waterDeep,
144
+ };
145
+ const ZONE_TEAL = {
146
+ surface: COLORS.waterTealSurface, light: COLORS.waterTealLight,
147
+ mid: COLORS.waterTealMid, deep: COLORS.waterTealDeep,
148
+ };
149
+ const ZONE_AMBER = {
150
+ surface: COLORS.waterAmberSurface, light: COLORS.waterAmberLight,
151
+ mid: COLORS.waterAmberMid, deep: COLORS.waterAmberDeep,
152
+ };
153
+ const ZONE_RED = {
154
+ surface: COLORS.waterRedSurface, light: COLORS.waterRedLight,
155
+ mid: COLORS.waterRedMid, deep: COLORS.waterRedDeep,
156
+ };
157
+ function getWaterPalette(pct) {
158
+ if (pct < 50) {
159
+ const t = pct / 50;
160
+ return {
161
+ surface: lerpColor(ZONE_BLUE.surface, ZONE_TEAL.surface, t),
162
+ light: lerpColor(ZONE_BLUE.light, ZONE_TEAL.light, t),
163
+ mid: lerpColor(ZONE_BLUE.mid, ZONE_TEAL.mid, t),
164
+ deep: lerpColor(ZONE_BLUE.deep, ZONE_TEAL.deep, t),
165
+ };
166
+ }
167
+ else if (pct < 75) {
168
+ const t = (pct - 50) / 25;
169
+ return {
170
+ surface: lerpColor(ZONE_TEAL.surface, ZONE_AMBER.surface, t),
171
+ light: lerpColor(ZONE_TEAL.light, ZONE_AMBER.light, t),
172
+ mid: lerpColor(ZONE_TEAL.mid, ZONE_AMBER.mid, t),
173
+ deep: lerpColor(ZONE_TEAL.deep, ZONE_AMBER.deep, t),
174
+ };
175
+ }
176
+ else {
177
+ const t = (pct - 75) / 25;
178
+ return {
179
+ surface: lerpColor(ZONE_AMBER.surface, ZONE_RED.surface, t),
180
+ light: lerpColor(ZONE_AMBER.light, ZONE_RED.light, t),
181
+ mid: lerpColor(ZONE_AMBER.mid, ZONE_RED.mid, t),
182
+ deep: lerpColor(ZONE_AMBER.deep, ZONE_RED.deep, t),
183
+ };
184
+ }
185
+ }
186
+ function waterColorAt(palette, surfaceY, y) {
187
+ const waterDepth = SAND_TOP - surfaceY;
188
+ if (waterDepth <= 0)
189
+ return palette.deep;
190
+ const t = (y - surfaceY) / waterDepth;
191
+ if (t < 0.25)
192
+ return lerpColor(palette.surface, palette.light, t / 0.25);
193
+ if (t < 0.6)
194
+ return lerpColor(palette.light, palette.mid, (t - 0.25) / 0.35);
195
+ return lerpColor(palette.mid, palette.deep, (t - 0.6) / 0.4);
196
+ }
197
+ const NUM_TETRAS = 14;
198
+ let tetras = null;
199
+ function initTetras() {
200
+ const result = [];
201
+ for (let i = 0; i < NUM_TETRAS; i++) {
202
+ result.push({
203
+ x: 12 + Math.random() * 40,
204
+ y: 20 + Math.random() * 25,
205
+ heading: Math.random() > 0.5 ? 1 : -1,
206
+ speed: 0.08 + Math.random() * 0.12, // slower — prevents teleporting at ~1fps
207
+ phase: Math.random() * Math.PI * 2,
208
+ schoolId: i < 7 ? 0 : 1,
209
+ });
210
+ }
211
+ return result;
212
+ }
213
+ function updateTetras(frame, surfaceY, maxY) {
214
+ if (!tetras)
215
+ 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;
221
+ const centers = [{ x: sc0X, y: sc0Y }, { x: sc1X, y: sc1Y }];
222
+ for (const t of tetras) {
223
+ const sc = centers[t.schoolId];
224
+ const dx = sc.x - t.x;
225
+ const dy = sc.y - t.y;
226
+ // Cohesion + individual motion (stronger cohesion for tighter schooling)
227
+ t.x += dx * 0.025 + t.heading * (t.speed * 0.5);
228
+ t.y += dy * 0.025 + Math.sin(frame * 0.05 + t.phase) * 0.2;
229
+ // Boundary
230
+ const minY = surfaceY + 3;
231
+ if (t.x < 3 || t.x > 61) {
232
+ t.heading *= -1;
233
+ t.x = Math.max(3, Math.min(61, t.x));
234
+ }
235
+ if (t.y < minY)
236
+ t.y = minY;
237
+ if (t.y > maxY)
238
+ t.y = maxY;
239
+ }
240
+ }
241
+ /** Average position of all tetras (normalized 0~1). */
242
+ function getSchoolCenter() {
243
+ if (!tetras || tetras.length === 0)
244
+ return { x: 0.5, y: 0.4 };
245
+ let sx = 0, sy = 0;
246
+ for (const t of tetras) {
247
+ sx += t.x;
248
+ sy += t.y;
249
+ }
250
+ return { x: sx / tetras.length / W, y: sy / tetras.length / W };
251
+ }
252
+ let bubbles = [];
253
+ function spawnBubble() {
254
+ return {
255
+ x: 4 + Math.random() * 56,
256
+ y: SAND_TOP - 1 - Math.random() * 4,
257
+ speed: 0.3 + Math.random() * 0.4,
258
+ wobblePhase: Math.random() * Math.PI * 2,
259
+ bright: Math.random() > 0.6,
260
+ };
261
+ }
262
+ function updateBubbles(frame, surfaceY, density) {
263
+ const maxBubbles = Math.round(density);
264
+ while (bubbles.length < maxBubbles)
265
+ bubbles.push(spawnBubble());
266
+ for (const b of bubbles) {
267
+ b.y -= (b.speed * 0.5);
268
+ b.x += Math.sin(frame * 0.075 + b.wobblePhase) * 0.15;
269
+ }
270
+ bubbles = bubbles.filter(b => b.y > surfaceY + 1);
271
+ while (bubbles.length > maxBubbles + 4)
272
+ bubbles.shift();
273
+ }
274
+ let dataParticles = [];
275
+ function updateDataParticles(frame, surfaceY, active) {
276
+ if (active && frame % 6 === 0) { // spawn half as often
277
+ dataParticles.push({
278
+ x: 10 + Math.random() * 44,
279
+ y: surfaceY + 2 + Math.random() * 3,
280
+ vy: 0.2 + Math.random() * 0.15,
281
+ life: 60 + Math.random() * 40,
282
+ green: Math.random() > 0.6,
283
+ });
284
+ }
285
+ for (const p of dataParticles) {
286
+ p.y += p.vy;
287
+ p.x += Math.sin(frame * 0.1 + p.x * 0.3) * 0.2;
288
+ p.life--;
289
+ }
290
+ dataParticles = dataParticles.filter(p => p.life > 0 && p.y < SAND_TOP - 1 && p.y > surfaceY);
291
+ if (dataParticles.length > 16)
292
+ dataParticles.splice(0, dataParticles.length - 16);
293
+ }
294
+ // ===== Seaweed =====
295
+ const SEAWEED_POSITIONS = [
296
+ { x: 2, h: 13, phase: 0 },
297
+ { x: 5, h: 9, phase: 1.2 },
298
+ { 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 },
302
+ ];
303
+ function drawSeaweed(buf, frame, surfaceY) {
304
+ for (const sw of SEAWEED_POSITIONS) {
305
+ const maxHeight = Math.min(sw.h, SAND_TOP - surfaceY - 2);
306
+ if (maxHeight <= 0)
307
+ continue;
308
+ for (let i = 0; i < maxHeight; i++) {
309
+ const swayAmount = (i / maxHeight) * 1.5;
310
+ const sway = Math.round(Math.sin(frame * 0.06 + sw.phase + i * 0.4) * swayAmount);
311
+ const color = i % 3 === 0 ? COLORS.seaweedLight
312
+ : i % 2 === 0 ? COLORS.seaweed : COLORS.seaweedDark;
313
+ const px = sw.x + sway;
314
+ const py = SAND_TOP - 1 - i;
315
+ if (py > surfaceY)
316
+ setPixel(buf, px, py, color);
317
+ }
318
+ }
319
+ }
320
+ // ===== Light Rays =====
321
+ function drawLightRays(buf, frame, surfaceY) {
322
+ 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 },
326
+ ];
327
+ for (const ray of rays) {
328
+ const depth = SAND_TOP - surfaceY;
329
+ for (let d = 2; d < depth - 2; d++) {
330
+ const y = surfaceY + d;
331
+ const x = Math.round(ray.baseX + d * ray.angle);
332
+ const fadeIn = Math.min(1, d / 6);
333
+ const fadeOut = Math.max(0, 1 - d / depth);
334
+ const alpha = fadeIn * fadeOut * 0.2;
335
+ if (alpha > 0.02) {
336
+ glowPixel(buf, x, y, COLORS.lightRay, alpha);
337
+ glowPixel(buf, x - 1, y, COLORS.lightRay, alpha * 0.4);
338
+ glowPixel(buf, x + 1, y, COLORS.lightRay, alpha * 0.4);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ // ===== Caustics =====
344
+ function drawCaustics(buf, frame, surfaceY) {
345
+ if (surfaceY >= SAND_TOP - 3)
346
+ return;
347
+ for (let x = 1; x < W - 1; x++) {
348
+ const pattern = Math.sin(x * 0.5 + frame * 0.05) * Math.cos(x * 0.3 - frame * 0.035);
349
+ if (pattern > 0.5) {
350
+ const intensity = (pattern - 0.5) * 0.4;
351
+ glowPixel(buf, x, SAND_TOP, COLORS.caustic, intensity);
352
+ glowPixel(buf, x, SAND_TOP + 1, COLORS.caustic, intensity * 0.5);
353
+ }
354
+ }
355
+ }
356
+ // ===== Surface Waves =====
357
+ function drawSurface(buf, frame, surfaceY, palette, state) {
358
+ const shimmerColor = state === State.PROCESSING ? COLORS.stateProcessing
359
+ : state === State.AWAITING_OPTION || state === State.AWAITING_PERMISSION || state === State.AWAITING_DIFF
360
+ ? COLORS.stateAwaiting
361
+ : COLORS.stateIdle;
362
+ const waveSpeed = state === State.PROCESSING ? 0.125 : 0.05;
363
+ const waveAmp = state === State.PROCESSING ? 1.5 : 0.8;
364
+ const shimmerIntensity = state === State.PROCESSING ? 0.35
365
+ : (state === State.AWAITING_OPTION || state === State.AWAITING_PERMISSION || state === State.AWAITING_DIFF)
366
+ ? 0.25 + Math.sin(frame * 0.15) * 0.15
367
+ : 0.15;
368
+ for (let x = 0; x < W; x++) {
369
+ const wave = Math.sin(x * 0.25 + frame * waveSpeed) * waveAmp;
370
+ const wy = surfaceY + Math.round(wave);
371
+ blendPixel(buf, x, wy, palette.surface, 0.8);
372
+ if (wave > waveAmp * 0.3)
373
+ glowPixel(buf, x, wy, shimmerColor, shimmerIntensity);
374
+ if (wave > waveAmp * 0.6 && (Math.floor(x + frame)) % 5 === 0) {
375
+ glowPixel(buf, x, wy, COLORS.white, 0.15);
376
+ }
377
+ }
378
+ }
379
+ // ===== Terrain =====
380
+ function drawTerrain(buf) {
381
+ for (let y = SAND_TOP; y <= SAND_BOT; y++) {
382
+ for (let x = 0; x < W; x++) {
383
+ const noise = ((x * 7 + y * 13) % 11);
384
+ const color = noise < 3 ? COLORS.sandLight : noise < 7 ? COLORS.sand : COLORS.sandDark;
385
+ setPixel(buf, x, y, color);
386
+ }
387
+ }
388
+ const gravelPositions = [8, 15, 22, 29, 37, 44, 51, 57];
389
+ for (const gx of gravelPositions)
390
+ setPixel(buf, gx, SAND_TOP, COLORS.gravel);
391
+ for (let y = SUBSTRATE_TOP; y < W; y++) {
392
+ for (let x = 0; x < W; x++) {
393
+ const noise = ((x * 11 + y * 7) % 13);
394
+ setPixel(buf, x, y, noise < 4 ? COLORS.rockLight : COLORS.rock);
395
+ }
396
+ }
397
+ 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 },
401
+ ];
402
+ for (const r of rocks) {
403
+ for (let dy = 0; dy < r.h; dy++) {
404
+ for (let dx = 0; dx < r.w; dx++) {
405
+ const edge = dx === 0 || dx === r.w - 1 || dy === 0;
406
+ setPixel(buf, r.x + dx, r.y + dy, edge ? COLORS.rockLight : COLORS.rock);
407
+ }
408
+ }
409
+ }
410
+ }
411
+ // ===== Main Render =====
412
+ // Time-based animation frame — ensures consistent speed regardless of who/how often
413
+ // calls renderFrame() (device push vs preview endpoint won't interfere).
414
+ // ~10 units/sec (100ms interval) to match 10fps loop.
415
+ function getAnimFrame(timeOverrideMs) {
416
+ return Math.floor((timeOverrideMs ?? Date.now()) / 100);
417
+ }
418
+ function creatureState(state) {
419
+ switch (state) {
420
+ case State.IDLE: return 'idle';
421
+ case State.PROCESSING: return 'working';
422
+ case State.AWAITING_OPTION:
423
+ case State.AWAITING_PERMISSION:
424
+ case State.AWAITING_DIFF:
425
+ return 'asking';
426
+ default: return 'idle';
427
+ }
428
+ }
429
+ function simplifiedState(state) {
430
+ switch (state) {
431
+ case State.PROCESSING: return 'processing';
432
+ case State.AWAITING_OPTION:
433
+ case State.AWAITING_PERMISSION:
434
+ case State.AWAITING_DIFF:
435
+ return 'awaiting';
436
+ default: return 'idle';
437
+ }
438
+ }
439
+ // ===== Usage HUD Helpers =====
440
+ /** Gauge bar color based on usage percentage. */
441
+ function gaugeColor(pct, animFrame) {
442
+ if (pct >= 90) {
443
+ // Red with pulse
444
+ const pulse = (Math.sin(animFrame * 0.2) + 1) * 0.3;
445
+ return lerpColor(COLORS.stateError, COLORS.white, pulse);
446
+ }
447
+ if (pct >= 70)
448
+ return COLORS.stateAwaiting; // amber
449
+ if (pct >= 50)
450
+ return [0x00, 0xC8, 0xB4]; // teal
451
+ return COLORS.stateProcessing; // blue
452
+ }
453
+ /** Pixoo HUD reset time: "1h23", "4d6", "59m". */
454
+ function formatResetDetailed(resetsAt) {
455
+ if (!resetsAt)
456
+ return '';
457
+ const ms = new Date(resetsAt).getTime() - Date.now();
458
+ if (ms <= 0)
459
+ return '0m';
460
+ const totalMins = Math.max(1, Math.ceil(ms / 60000));
461
+ const hours = Math.floor(totalMins / 60);
462
+ const days = Math.floor(hours / 24);
463
+ const remHours = hours % 24;
464
+ const mins = totalMins % 60;
465
+ if (days > 0 && remHours > 0)
466
+ return `${days}d${remHours}`;
467
+ if (days > 0)
468
+ return `${days}d`;
469
+ if (hours > 0 && mins > 0)
470
+ return `${hours}h${mins}`;
471
+ if (hours > 0)
472
+ return `${hours}h`;
473
+ return `${mins}m`;
474
+ }
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
479
+ */
480
+ 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)
486
+ return;
487
+ if (usageEvent.usageStale === true)
488
+ 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);
496
+ }
497
+ }
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);
508
+ }
509
+ }
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);
515
+ }
516
+ else {
517
+ drawText(buf, pctText, rightX, textY, color);
518
+ }
519
+ }
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;
526
+ }
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);
533
+ }
534
+ /**
535
+ * Render a complete 64×64 frame with camera system.
536
+ * Returns 12,288-byte RGB buffer.
537
+ */
538
+ export function renderFrame(stateEvent, usageEvent, sessions, timeOverrideMs) {
539
+ const worldBuf = new Uint8Array(W * W * 3);
540
+ const outputBuf = new Uint8Array(W * W * 3);
541
+ const animFrame = getAnimFrame(timeOverrideMs);
542
+ const state = stateEvent?.state ?? State.IDLE;
543
+ const usagePct = usageEvent?.fiveHourPercent ?? 0;
544
+ const surfaceY = SURFACE_Y;
545
+ 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);
550
+ // === Sync creature instances ===
551
+ syncCreatures(sessions, stateEvent);
552
+ // === Build active creatures list for camera ===
553
+ const activeCreatures = [];
554
+ for (const c of creatureInstances.values()) {
555
+ if (c.state === 'awaiting') {
556
+ activeCreatures.push({ x: c.worldX, y: c.worldY, priority: 0 });
557
+ }
558
+ else if (c.state === 'processing') {
559
+ activeCreatures.push({ x: c.worldX, y: c.worldY, priority: 1 });
560
+ }
561
+ }
562
+ // Crayfish routing
563
+ const cfX = CF_DEFAULT_X;
564
+ const cfY = CF_DEFAULT_Y;
565
+ const crayfishRouting = hasGateway && (sessions?.some(s => s.agentType === 'openclaw' && s.state === 'processing') ?? false);
566
+ if (crayfishRouting) {
567
+ activeCreatures.push({ x: cfX, y: cfY, priority: 2 });
568
+ }
569
+ // === Update camera director ===
570
+ const now = timeOverrideMs ?? Date.now();
571
+ const dt = lastRenderTime > 0 ? Math.min(5, (now - lastRenderTime) / 1000) : 1.0;
572
+ lastRenderTime = now;
573
+ const schoolPos = getSchoolCenter();
574
+ const camera = updateDirector(dt, activeCreatures, crayfishRouting, hasGateway ? { x: cfX, y: cfY } : null, schoolPos);
575
+ // ========================================
576
+ // Phase 1: Render environment → world buffer
577
+ // ========================================
578
+ // Water body
579
+ for (let y = 0; y < SAND_TOP; y++) {
580
+ const color = waterColorAt(palette, surfaceY, y);
581
+ for (let x = 0; x < W; x++)
582
+ setPixel(worldBuf, x, y, color);
583
+ }
584
+ // Terrain
585
+ drawTerrain(worldBuf);
586
+ // Light rays
587
+ drawLightRays(worldBuf, animFrame, surfaceY);
588
+ // Caustics
589
+ drawCaustics(worldBuf, animFrame, surfaceY);
590
+ // Seaweed
591
+ drawSeaweed(worldBuf, animFrame, surfaceY);
592
+ // Effective state: prefer creature-derived state over stateEvent (daemon may be stale)
593
+ const anyCreatureProcessing = [...creatureInstances.values()].some(c => c.state === 'processing');
594
+ const anyCreatureAwaiting = [...creatureInstances.values()].some(c => c.state === 'awaiting');
595
+ const effectiveState = anyCreatureProcessing ? State.PROCESSING
596
+ : anyCreatureAwaiting ? State.AWAITING_OPTION
597
+ : state;
598
+ // Bubbles
599
+ const bubbleDensity = effectiveState === State.PROCESSING ? 10 : effectiveState === State.IDLE ? 3 : 5;
600
+ updateBubbles(animFrame, surfaceY, bubbleDensity);
601
+ for (const b of bubbles) {
602
+ const bx = Math.round(b.x);
603
+ const by = Math.round(b.y);
604
+ blendPixel(worldBuf, bx, by, b.bright ? COLORS.bubbleBright : COLORS.bubble, 0.6);
605
+ }
606
+ // Data particles (spawn when any creature is processing)
607
+ const anyProcessing = [...creatureInstances.values()].some(c => c.state === 'processing');
608
+ updateDataParticles(animFrame, surfaceY, anyProcessing);
609
+ for (const p of dataParticles) {
610
+ const fadeAlpha = Math.min(1, p.life / 10);
611
+ const color = p.green ? COLORS.dataParticleGreen : COLORS.dataParticle;
612
+ glowPixel(worldBuf, Math.round(p.x), Math.round(p.y), color, 0.5 * fadeAlpha);
613
+ }
614
+ // Tetras — update always
615
+ const tetraMaxY = SAND_TOP - 3;
616
+ updateTetras(animFrame, surfaceY, tetraMaxY);
617
+ // Surface waves — use effectiveState so daemon doesn't suppress wave animation
618
+ drawSurface(worldBuf, animFrame, surfaceY, palette, effectiveState);
619
+ // ========================================
620
+ // Phase 2: Blit world → output with camera
621
+ // ========================================
622
+ blitWithCamera(worldBuf, outputBuf, camera);
623
+ // ========================================
624
+ // Phase 3: Draw scaled creatures → output
625
+ // ========================================
626
+ // Tetras (always drawn — camera-scaled)
627
+ if (tetras) {
628
+ for (const t of tetras) {
629
+ drawTetra(outputBuf, t.x / W, t.y / W, t.heading, camera);
630
+ }
631
+ }
632
+ // Creature instances — octopus or jellyfish based on agent type
633
+ const creatureOrder = [...creatureInstances.keys()];
634
+ for (const c of creatureInstances.values()) {
635
+ const sessionToneIndex = creatureOrder.indexOf(c.sessionId);
636
+ const spriteState = c.state === 'processing' ? 'working'
637
+ : c.state === 'awaiting' ? 'asking'
638
+ : '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
+ }
648
+ }
649
+ // Crayfish — always drawn when gateway available; IDLE = sitting (subtle breathing only)
650
+ if (hasGateway) {
651
+ const gatewayHasError = stateEvent?.gatewayHasError ?? false;
652
+ drawCrayfish(outputBuf, cfX, cfY, crayfishRouting, animFrame, camera, gatewayHasError);
653
+ }
654
+ // ========================================
655
+ // Phase 4: Screen-space overlays
656
+ // ========================================
657
+ // Danger flash (>90% usage)
658
+ if (usagePct >= 90) {
659
+ 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++) {
662
+ glowPixel(outputBuf, x, y, COLORS.stateError, flashIntensity);
663
+ }
664
+ }
665
+ }
666
+ // Session count indicator (top-left, screen-space) — colored dots when 2+ sessions
667
+ const sessionCount = creatureInstances.size;
668
+ if (sessionCount >= 2) {
669
+ const orderedCreatures = [...creatureInstances.values()];
670
+ for (let i = 0; i < Math.min(sessionCount, 6); i++) {
671
+ const dotX = 1 + i * 3; // 2px dot + 1px gap
672
+ const c = orderedCreatures[i];
673
+ const dotColor = c.creatureType === 'jellyfish'
674
+ ? getJellyfishPaletteForSession(i).body
675
+ : getOctopusPaletteForSession(i).body;
676
+ setPixel(outputBuf, dotX, 1, dotColor);
677
+ setPixel(outputBuf, dotX + 1, 1, dotColor);
678
+ setPixel(outputBuf, dotX, 2, dotColor);
679
+ setPixel(outputBuf, dotX + 1, 2, dotColor);
680
+ }
681
+ }
682
+ // Usage HUD (bottom-right, screen-space)
683
+ drawUsageHUD(outputBuf, usageEvent, animFrame);
684
+ return outputBuf;
685
+ }
686
+ // ===== Disconnected Frame =====
687
+ /** Render a static black frame with centered grey "OFFLINE" text. */
688
+ export function renderDisconnectedFrame() {
689
+ const buf = new Uint8Array(W * W * 3); // black
690
+ drawTextCentered(buf, 29, 'OFFLINE', '#555555');
691
+ return buf;
692
+ }
693
+ // ===== Preview API (re-export camera controls) =====
694
+ export { setZone, setOverride, resetDirector } from './pixoo-camera.js';
695
+ export { ZONES } from './pixoo-camera.js';
696
+ //# sourceMappingURL=pixoo-renderer.js.map