@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,758 @@
1
+ /**
2
+ * TUI Dashboard renderer — layout calculation + panel rendering.
3
+ *
4
+ * Width invariant: every output line is exactly `cols` characters wide (visually).
5
+ * Box structure:
6
+ * Wide: ┌─leftW─┬─rightW─┐ where leftW + rightW + 3 = cols
7
+ * Std/Nar: ┌──w────┐ where w + 2 = cols
8
+ * Split: │ lh │ rh │ where lh + rh + 3 = cols
9
+ */
10
+ import { cursor, screen as screenCodes, RESET, colors, box, hLine, sgr, stateColor, stateIcon, truncText, padRight, visLen, terminalCaps, centerText, fg, } from './ansi.js';
11
+ import { blockGauge, resetTimeStr, formatTokens } from './gauge.js';
12
+ import { sortSessions, assignDisplayNames } from '@agentdeck/shared';
13
+ // ===== Layout Breakpoints =====
14
+ export function getLayout(cols, rows) {
15
+ if (cols >= 120)
16
+ return 'wide';
17
+ if (cols >= 80)
18
+ return 'standard';
19
+ return 'narrow';
20
+ }
21
+ export function shouldShowTerrarium(cols, rows) {
22
+ if (cols < 60)
23
+ return false;
24
+ if (rows < 16)
25
+ return false;
26
+ return true;
27
+ }
28
+ // ===== Border Line Builder =====
29
+ function borderFill(prefix, suffix, targetWidth) {
30
+ const fillLen = Math.max(0, targetWidth - visLen(prefix) - visLen(suffix));
31
+ return prefix + `${colors.border}${hLine(fillLen)}${RESET}` + suffix;
32
+ }
33
+ // ===== Pixel Font (4 wide × 6 tall → 4×3 half-block) =====
34
+ const FONT = {
35
+ A: ['.##.', '#..#', '####', '#..#', '#..#', '....'],
36
+ G: ['.###', '#...', '#.##', '#..#', '.##.', '....'],
37
+ E: ['####', '#...', '###.', '#...', '####', '....'],
38
+ N: ['#..#', '##.#', '#.##', '#..#', '#..#', '....'],
39
+ T: ['####', '.##.', '.##.', '.##.', '.##.', '....'],
40
+ D: ['###.', '#..#', '#..#', '#..#', '###.', '....'],
41
+ C: ['.###', '#...', '#...', '#...', '.###', '....'],
42
+ K: ['#..#', '#.#.', '##..', '#.#.', '#..#', '....'],
43
+ };
44
+ /** Render a word in half-block pixel font. Returns 3 terminal lines. */
45
+ function renderPixelFont(word) {
46
+ const result = ['', '', ''];
47
+ for (let li = 0; li < word.length; li++) {
48
+ if (li > 0) {
49
+ result[0] += ' ';
50
+ result[1] += ' ';
51
+ result[2] += ' ';
52
+ }
53
+ const pixels = FONT[word[li]];
54
+ if (!pixels)
55
+ continue;
56
+ for (let hr = 0; hr < 3; hr++) {
57
+ const topRow = pixels[hr * 2];
58
+ const botRow = pixels[hr * 2 + 1];
59
+ for (let col = 0; col < 4; col++) {
60
+ const top = topRow[col] === '#';
61
+ const bot = botRow[col] === '#';
62
+ if (top && bot)
63
+ result[hr] += '\u2588'; // █
64
+ else if (top)
65
+ result[hr] += '\u2580'; // ▀
66
+ else if (bot)
67
+ result[hr] += '\u2584'; // ▄
68
+ else
69
+ result[hr] += ' ';
70
+ }
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ // Pre-render logo lines — "AGENT" + "DECK" stacked, sky blue
76
+ const LOGO_AGENT = renderPixelFont('AGENT'); // 3 lines, 24 chars wide
77
+ const LOGO_DECK = renderPixelFont('DECK'); // 3 lines, 19 chars wide
78
+ // ===== Timeline Icons =====
79
+ function typeIcon(type) {
80
+ if (!terminalCaps.unicode) {
81
+ switch (type) {
82
+ case 'chat_start': return '>';
83
+ case 'chat_end': return '=';
84
+ case 'chat_response': return ':';
85
+ case 'tool_request':
86
+ case 'tool_exec': return '*';
87
+ case 'tool_resolved': return '+';
88
+ case 'error': return 'x';
89
+ case 'model_call':
90
+ case 'model_response': return 'm';
91
+ case 'memory_recall': return 'r';
92
+ case 'scheduled': return 's';
93
+ case 'user_action': return 'u';
94
+ case 'eval_result': return '#';
95
+ default: return '*';
96
+ }
97
+ }
98
+ switch (type) {
99
+ case 'chat_start':
100
+ case 'user_action': return '\u25B6';
101
+ case 'chat_end': return '\u25A0';
102
+ case 'chat_response': return '\u25A1';
103
+ case 'tool_request':
104
+ case 'tool_exec': return '\u25C6';
105
+ case 'tool_resolved': return '\u2713';
106
+ case 'error': return '\u2717';
107
+ case 'model_call':
108
+ case 'model_response': return '\u25C8';
109
+ case 'memory_recall': return '\u25CC';
110
+ case 'scheduled': return '\u25D1';
111
+ case 'eval_result': return '\u2605'; // ★
112
+ default: return '\u25C6';
113
+ }
114
+ }
115
+ function typeColor(type) {
116
+ switch (type) {
117
+ case 'chat_start':
118
+ case 'user_action': return colors.chat;
119
+ case 'chat_end':
120
+ case 'chat_response': return colors.end;
121
+ case 'tool_request':
122
+ case 'tool_exec':
123
+ case 'tool_resolved': return colors.tool;
124
+ case 'error': return colors.errorTl;
125
+ case 'model_call':
126
+ case 'model_response': return sgr(35);
127
+ case 'memory_recall': return sgr(33);
128
+ case 'eval_result': return sgr(33); // yellow/amber
129
+ default: return colors.dim;
130
+ }
131
+ }
132
+ const SPINNER_FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
133
+ export function spinner(frame) {
134
+ return SPINNER_FRAMES[Math.floor(frame / 2) % SPINNER_FRAMES.length];
135
+ }
136
+ // ===== Creature Icon Helper =====
137
+ function creatureEmoji(agentType) {
138
+ if (!terminalCaps.emoji) {
139
+ if (agentType === 'daemon')
140
+ return 'D';
141
+ if (agentType === 'openclaw')
142
+ return 'C';
143
+ if (agentType === 'codex-cli')
144
+ return 'X';
145
+ if (agentType === 'opencode')
146
+ return 'O';
147
+ return '*';
148
+ }
149
+ if (agentType === 'daemon')
150
+ return '\u2699\uFE0F'; // ⚙️
151
+ if (agentType === 'openclaw')
152
+ return '\uD83E\uDD9E'; // 🦞
153
+ if (agentType === 'codex-cli')
154
+ return '\u2601'; // ☁ (cloud — matches creature)
155
+ if (agentType === 'opencode')
156
+ return '\u25A3'; // ▣ (nested square — matches creature)
157
+ return '\u273B'; // ✻ (teardrop-spoked asterisk — Claude sparkle)
158
+ }
159
+ function creatureBrandColor(agentType) {
160
+ if (!terminalCaps.trueColor)
161
+ return '';
162
+ switch (agentType) {
163
+ case 'claude-code': return fg(192, 112, 88); // terracotta
164
+ case 'openclaw': return fg(255, 77, 77); // red
165
+ case 'codex-cli': return fg(177, 167, 255); // indigo
166
+ case 'opencode': return fg(241, 236, 236); // warm gray (#F1ECEC)
167
+ default: return '';
168
+ }
169
+ }
170
+ function compactStateLabel(state) {
171
+ switch (state) {
172
+ case 'processing': return 'PROC';
173
+ case 'awaiting_permission': return 'PERM';
174
+ case 'awaiting_option': return 'OPT';
175
+ case 'awaiting_diff': return 'DIFF';
176
+ case 'disconnected': return 'DISC';
177
+ case 'idle':
178
+ default:
179
+ return 'IDLE';
180
+ }
181
+ }
182
+ function currentSessionSummary(state, width) {
183
+ const parts = [];
184
+ if (state.projectName)
185
+ parts.push(state.projectName);
186
+ if (state.modelName)
187
+ parts.push(state.modelName);
188
+ if (state.state)
189
+ parts.push(compactStateLabel(state.state));
190
+ if (parts.length === 0)
191
+ return 'No session';
192
+ return truncText(parts.join(' · '), width);
193
+ }
194
+ // stateRank, sortSessions imported from @agentdeck/shared
195
+ function sessionHotkeyLabel(index) {
196
+ if (index === null || index > 8)
197
+ return '·';
198
+ return String(index + 1);
199
+ }
200
+ function sessionHotkeyIndex(session, nextIndex) {
201
+ if (!session.port || session.controlMode === 'observed')
202
+ return null;
203
+ return nextIndex;
204
+ }
205
+ function observedDetailParts(session) {
206
+ if (!session)
207
+ return [];
208
+ const parts = [];
209
+ if (session.controlMode === 'observed')
210
+ parts.push('observed');
211
+ if (typeof session.contextPercent === 'number')
212
+ parts.push(`${Math.round(session.contextPercent)}% ctx`);
213
+ if (session.currentTask)
214
+ parts.push(session.currentTask);
215
+ return parts;
216
+ }
217
+ // ===== Panel Renderers =====
218
+ function renderAgentLines(state, maxWidth, useLogo) {
219
+ const lines = [];
220
+ // Big pixel-font logo (AGENT + DECK stacked, sky blue)
221
+ const logoColor = terminalCaps.trueColor ? fg(100, 180, 255) : colors.header; // sky blue
222
+ if (useLogo && maxWidth >= 24) {
223
+ for (const l of LOGO_AGENT)
224
+ lines.push(`${logoColor}${l}${RESET}`);
225
+ for (const l of LOGO_DECK)
226
+ lines.push(`${logoColor} ${l}${RESET}`);
227
+ }
228
+ else if (useLogo && maxWidth >= 10) {
229
+ lines.push(`${logoColor} AgentDeck${RESET}`);
230
+ }
231
+ lines.push('');
232
+ // Session list
233
+ const renderSession = (proj, model, sessState, agentType, hotkeyIndex, session) => {
234
+ const col = stateColor(sessState);
235
+ const hotkey = `${colors.dim}[${sessionHotkeyLabel(hotkeyIndex)}]${RESET}`;
236
+ const name = truncText(proj, maxWidth - 16);
237
+ const emoji = `${creatureBrandColor(agentType)}${creatureEmoji(agentType)}${RESET}`;
238
+ const secondary = [model, ...observedDetailParts(session), compactStateLabel(sessState)]
239
+ .filter(Boolean)
240
+ .join(' - ');
241
+ lines.push(` ${hotkey} ${emoji} ${col}${name}${RESET}`);
242
+ lines.push(`${colors.dim} ${truncText(secondary, maxWidth - 4)}${RESET}`);
243
+ };
244
+ // #N suffix via shared assignDisplayNames
245
+ // Daemon mode: sessions list already contains all agents (including virtual OpenClaw).
246
+ // Also applies when daemon relays OpenClaw (agentType='openclaw' but sessions has the entry).
247
+ // Session bridge mode: sessions has siblings only — prepend self.
248
+ const isDaemonLike = state.agentType === 'daemon' ||
249
+ (state.agentType && state.sessions.some(s => s.agentType === state.agentType));
250
+ if (isDaemonLike) {
251
+ const sorted = sortSessions(state.sessions);
252
+ const displayed = assignDisplayNames(sorted.map(s => ({
253
+ id: s.id, projectName: s.projectName || 'unknown', agentType: s.agentType, state: s.state,
254
+ modelName: s.modelName,
255
+ port: s.port, controlMode: s.controlMode,
256
+ currentTask: s.currentTask,
257
+ contextPercent: s.contextPercent,
258
+ totalTokens: s.totalTokens,
259
+ })));
260
+ let focusableIndex = 0;
261
+ for (const d of displayed) {
262
+ const hotkeyIndex = sessionHotkeyIndex(d.session, focusableIndex);
263
+ if (hotkeyIndex !== null)
264
+ focusableIndex += 1;
265
+ renderSession(d.displayName, d.session.modelName, d.session.state || 'idle', d.session.agentType, hotkeyIndex, d.session);
266
+ }
267
+ }
268
+ else if (state.state) {
269
+ // Self (primary session)
270
+ renderSession(state.projectName || 'unknown', state.modelName ?? undefined, state.state, state.agentType ?? undefined, null);
271
+ // Siblings — include self as virtual entry for correct #N numbering
272
+ const sorted = sortSessions(state.sessions);
273
+ const selfEntry = {
274
+ id: '__self__', projectName: state.projectName || 'unknown',
275
+ agentType: state.agentType ?? undefined, state: state.state,
276
+ };
277
+ const allForNumbering = [selfEntry, ...sorted.map(s => ({
278
+ id: s.id, projectName: s.projectName || 'unknown', agentType: s.agentType, state: s.state,
279
+ modelName: s.modelName,
280
+ port: s.port, controlMode: s.controlMode,
281
+ currentTask: s.currentTask,
282
+ contextPercent: s.contextPercent,
283
+ totalTokens: s.totalTokens,
284
+ }))];
285
+ const displayed = assignDisplayNames(allForNumbering);
286
+ // Skip the self entry (index 0), render siblings with hotkey indices
287
+ let focusableIndex = 0;
288
+ for (let i = 1; i < displayed.length; i++) {
289
+ const d = displayed[i];
290
+ const sessionInfo = d.session;
291
+ const hotkeyIndex = sessionHotkeyIndex(sessionInfo, focusableIndex);
292
+ if (hotkeyIndex !== null)
293
+ focusableIndex += 1;
294
+ renderSession(d.displayName, d.session.modelName, d.session.state || 'idle', d.session.agentType, hotkeyIndex, sessionInfo);
295
+ }
296
+ }
297
+ // Virtual OpenClaw entry when gateway detected but not in sessions list
298
+ const hasOcSession = state.sessions.some(s => s.agentType === 'openclaw' || s.agentType === 'gateway');
299
+ if (state.gatewayAvailable && !hasOcSession) {
300
+ renderSession('OpenClaw', undefined, state.crayfishRouting ? 'processing' : 'idle', 'openclaw', null);
301
+ }
302
+ // Gateway error warning
303
+ if (state.gatewayHasError) {
304
+ lines.push(`${colors.error} \u26A0 Gateway Error${RESET}`);
305
+ }
306
+ // Voice assistant indicator
307
+ if (state.voiceAssistantState && state.voiceAssistantState !== 'disabled' && state.voiceAssistantState !== 'idle') {
308
+ lines.push('');
309
+ if (state.voiceAssistantState === 'listening') {
310
+ lines.push(` ${colors.idle}\uD83C\uDFA4 Listening...${RESET}`);
311
+ }
312
+ else if (state.voiceAssistantState === 'processing') {
313
+ const text = state.voiceAssistantText ? truncText(state.voiceAssistantText, maxWidth - 6) : '...';
314
+ lines.push(` ${colors.processing}\uD83C\uDFA4 ${text}${RESET}`);
315
+ }
316
+ else if (state.voiceAssistantState === 'speaking') {
317
+ lines.push(` ${sgr(32)}\uD83D\uDD0A Speaking...${RESET}`);
318
+ }
319
+ }
320
+ lines.push('');
321
+ if (state.usage) {
322
+ const u = state.usage;
323
+ if (u.inputTokens || u.outputTokens) {
324
+ lines.push(`${colors.dim} Tokens: ${formatTokens(u.inputTokens)}/${formatTokens(u.outputTokens)}${RESET}`);
325
+ }
326
+ if (u.estimatedCostUsd) {
327
+ lines.push(`${colors.dim} Cost: $${u.estimatedCostUsd.toFixed(2)}${RESET}`);
328
+ }
329
+ }
330
+ return lines;
331
+ }
332
+ // ===== Status Panel: LIMITS | MODELS =====
333
+ function renderStatusLimitsLines(state, width) {
334
+ const lines = [];
335
+ const u = state.usage;
336
+ if (!u)
337
+ return lines;
338
+ const gaugeW = Math.min(10, Math.floor(width * 0.3));
339
+ if (u.fiveHourPercent !== undefined) {
340
+ const pct = Math.round(u.fiveHourPercent);
341
+ lines.push(` 5h [${blockGauge(pct, gaugeW)}] ${pct}%`);
342
+ const reset = resetTimeStr(u.fiveHourResetsAt);
343
+ if (reset)
344
+ lines.push(`${colors.dim} ${reset}${RESET}`);
345
+ }
346
+ if (u.sevenDayPercent !== undefined) {
347
+ const pct = Math.round(u.sevenDayPercent);
348
+ lines.push(` 7d [${blockGauge(pct, gaugeW)}] ${pct}%`);
349
+ const reset = resetTimeStr(u.sevenDayResetsAt);
350
+ if (reset)
351
+ lines.push(`${colors.dim} ${reset}${RESET}`);
352
+ }
353
+ if (state.currentTool) {
354
+ lines.push(` ${colors.tool}${truncText(state.currentTool, width - 2)}${RESET}`);
355
+ }
356
+ return lines;
357
+ }
358
+ function renderStatusModelsLines(state, width) {
359
+ const lines = [];
360
+ const u = state.usage;
361
+ if (state.modelName) {
362
+ const dot = u?.oauthConnected ? `${colors.idle}\u25CF${RESET}` : `${colors.dim}\u25CB${RESET}`;
363
+ lines.push(` ${dot} ${truncText(state.modelName, width - 4)}`);
364
+ }
365
+ lines.push(...renderOauthCatalogLines(state.modelCatalog, u?.oauthConnected, width));
366
+ lines.push(...renderOllamaSummaryLines(u?.ollamaStatus, width));
367
+ return lines;
368
+ }
369
+ function renderStatusLines(state, width) {
370
+ const lines = [];
371
+ const u = state.usage;
372
+ if (u) {
373
+ const gaugeW = Math.min(12, Math.floor(width * 0.15));
374
+ if (u.fiveHourPercent !== undefined) {
375
+ const pct = Math.round(u.fiveHourPercent);
376
+ lines.push(` 5h [${blockGauge(pct, gaugeW)}] ${pct}% ${colors.dim}${resetTimeStr(u.fiveHourResetsAt)}${RESET}`);
377
+ }
378
+ if (u.sevenDayPercent !== undefined) {
379
+ const pct = Math.round(u.sevenDayPercent);
380
+ lines.push(` 7d [${blockGauge(pct, gaugeW)}] ${pct}% ${colors.dim}${resetTimeStr(u.sevenDayResetsAt)}${RESET}`);
381
+ }
382
+ }
383
+ if (state.currentTool)
384
+ lines.push(` ${colors.tool}Tool: ${truncText(state.currentTool, width - 8)}${RESET}`);
385
+ if (state.modelName)
386
+ lines.push(`${colors.dim} Model: ${state.modelName}${RESET}`);
387
+ lines.push(...renderOauthCatalogLines(state.modelCatalog, u?.oauthConnected, width));
388
+ const ollamaLines = renderOllamaSummaryLines(u?.ollamaStatus, width);
389
+ lines.push(...ollamaLines);
390
+ return lines;
391
+ }
392
+ function renderOauthCatalogLines(modelCatalog, oauthConnected, width) {
393
+ const lines = [];
394
+ const models = modelCatalog.filter((m) => m.available).map((m) => m.name);
395
+ if (models.length > 0) {
396
+ return wrapCommaList(' OAuth: ', models, width);
397
+ }
398
+ if (oauthConnected === true) {
399
+ lines.push(`${colors.dim}${truncText(' OAuth: connected', width)}${RESET}`);
400
+ }
401
+ else if (oauthConnected === false) {
402
+ lines.push(`${colors.dim}${truncText(' OAuth: disconnected', width)}${RESET}`);
403
+ }
404
+ return lines;
405
+ }
406
+ function wrapCommaList(prefix, items, width) {
407
+ const lines = [];
408
+ const indent = ' '.repeat(prefix.length);
409
+ let current = prefix;
410
+ for (let i = 0; i < items.length; i++) {
411
+ const chunk = i === 0 ? items[i] : `, ${items[i]}`;
412
+ if (visLen(current) + visLen(chunk) <= width) {
413
+ current += chunk;
414
+ continue;
415
+ }
416
+ if (visLen(current) > visLen(prefix)) {
417
+ lines.push(`${colors.dim}${padRight(current, width)}${RESET}`.trimEnd());
418
+ current = indent + items[i];
419
+ continue;
420
+ }
421
+ lines.push(`${colors.dim}${truncText(current + chunk, width)}${RESET}`);
422
+ current = indent;
423
+ }
424
+ if (visLen(current.trim()) > 0) {
425
+ lines.push(`${colors.dim}${truncText(current, width)}${RESET}`);
426
+ }
427
+ return lines;
428
+ }
429
+ function renderOllamaSummaryLines(ollamaStatus, width) {
430
+ if (!ollamaStatus)
431
+ return [];
432
+ if (!ollamaStatus.available || ollamaStatus.models.length === 0) {
433
+ return [`${colors.dim}${truncText(' Ollama: stopped', width)}${RESET}`];
434
+ }
435
+ return ollamaStatus.models.map((m) => {
436
+ const size = m.sizeVram > 0 ? m.sizeVram : m.size;
437
+ const sizeText = size > 0 ? ` ${(size / 1e9).toFixed(1)}G` : '';
438
+ return `${colors.dim}${truncText(` Ollama: ${m.name}${sizeText}`, width)}${RESET}`;
439
+ });
440
+ }
441
+ function renderTimelineLines(state, width, maxLines, scrollOffset) {
442
+ const lines = [];
443
+ const entries = state.timeline;
444
+ if (entries.length === 0) {
445
+ lines.push(`${colors.dim} No events yet${RESET}`);
446
+ return lines;
447
+ }
448
+ const start = Math.max(0, entries.length - maxLines - scrollOffset);
449
+ const end = Math.min(entries.length, start + maxLines);
450
+ for (let i = start; i < end; i++) {
451
+ const e = entries[i];
452
+ const time = new Date(e.ts);
453
+ const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
454
+ const raw = truncText(e.raw, width - 10);
455
+ const pending = e.status === 'pending' ? `${colors.dim} [PENDING]${RESET}` : '';
456
+ lines.push(` ${colors.dim}${timeStr}${RESET} ${typeColor(e.type)}${typeIcon(e.type)}${RESET} ${raw}${pending}`);
457
+ }
458
+ return lines;
459
+ }
460
+ function timelineHeader(state, width, maxLines, scrollOffset) {
461
+ const total = state.timeline.length;
462
+ if (total === 0)
463
+ return `${colors.header} TIMELINE${RESET}`;
464
+ const shown = Math.min(total, maxLines);
465
+ const offset = scrollOffset > 0 ? ` · -${scrollOffset}` : '';
466
+ return truncText(`${colors.header} TIMELINE${RESET}${colors.dim} ${shown}/${total}${offset}${RESET}`, width);
467
+ }
468
+ function renderHelpOverlay(state, cols, rows) {
469
+ const boxW = Math.min(cols - 4, 78);
470
+ const boxH = Math.min(rows - 4, 18);
471
+ const left = Math.max(1, Math.floor((cols - boxW) / 2));
472
+ const top = Math.max(1, Math.floor((rows - boxH) / 2));
473
+ const lines = [
474
+ `${colors.header}AgentDeck TUI Help${RESET}`,
475
+ '',
476
+ `${colors.bold}Navigation${RESET}`,
477
+ ' q quit dashboard',
478
+ ' ? / h toggle help',
479
+ ' ↑ ↓ scroll timeline',
480
+ ' j / k vim-style timeline scroll',
481
+ ' 1-9 connect to listed session',
482
+ ' Esc close help',
483
+ '',
484
+ `${colors.bold}Reading The UI${RESET}`,
485
+ ' Header current project, model, and state',
486
+ ' Sessions busiest first, numbered for switching',
487
+ ' STATUS limits, models, OAuth, Ollama',
488
+ ' TIMELINE shown/total and scroll offset',
489
+ '',
490
+ `${colors.bold}Terminal${RESET}`,
491
+ ` Unicode: ${terminalCaps.unicode ? 'on' : 'fallback'} Color: ${terminalCaps.trueColor ? 'truecolor' : '16-color'} Emoji: ${terminalCaps.emoji ? 'on' : 'fallback'}`,
492
+ ];
493
+ let output = cursor.moveTo(1, 1) + screenCodes.clear;
494
+ output += cursor.moveTo(top, left) + `${colors.border}${box.tl}${hLine(boxW - 2)}${box.tr}${RESET}`;
495
+ for (let i = 0; i < boxH - 2; i++) {
496
+ const content = lines[i] ?? '';
497
+ output += cursor.moveTo(top + 1 + i, left) +
498
+ `${colors.border}${box.v}${RESET}` +
499
+ padRight(i === 0 ? centerText(content, boxW - 2) : content, boxW - 2) +
500
+ `${colors.border}${box.v}${RESET}`;
501
+ }
502
+ output += cursor.moveTo(top + boxH - 1, left) + `${colors.border}${box.bl}${hLine(boxW - 2)}${box.br}${RESET}`;
503
+ output += cursor.moveTo(Math.min(rows, top + boxH), left) +
504
+ `${colors.dim}Press ? or Esc to return${RESET}`;
505
+ return output;
506
+ }
507
+ // ===== Output Helper =====
508
+ /** Write buf lines to screen. Last row reserved for q quit hint. */
509
+ function flushBuf(buf, cols, rows, footerHint) {
510
+ const maxBoxRows = rows - 1;
511
+ let output = cursor.moveTo(1, 1);
512
+ const limit = Math.min(buf.length, maxBoxRows);
513
+ for (let i = 0; i < limit; i++) {
514
+ output += cursor.moveTo(i + 1, 1) + screenCodes.clearLine + buf[i];
515
+ }
516
+ for (let i = limit; i < maxBoxRows; i++) {
517
+ output += cursor.moveTo(i + 1, 1) + screenCodes.clearLine;
518
+ }
519
+ // q quit on last row
520
+ output += cursor.moveTo(rows, 1) + screenCodes.clearLine +
521
+ ` ${colors.dimCyan}${truncText(footerHint, cols - 2)}${RESET}`;
522
+ return output;
523
+ }
524
+ // ===== Main Render =====
525
+ export function renderDashboard(state, cols, rows, terrariumLines, frame, scrollOffset) {
526
+ if (state.helpVisible) {
527
+ return renderHelpOverlay(state, cols, rows);
528
+ }
529
+ const layout = getLayout(cols, rows);
530
+ if (cols < 40 || rows < 10) {
531
+ return cursor.moveTo(1, 1) + screenCodes.clear +
532
+ `Resize terminal to at least 60\u00D716 (current: ${cols}\u00D7${rows})`;
533
+ }
534
+ const connIcon = state.connectionStatus === 'connected' ? `${colors.idle}\u25CF` :
535
+ state.connectionStatus === 'reconnecting' ? `${colors.processing}\u25D4` :
536
+ `${colors.disconnected}\u25CB`;
537
+ const staleTag = state.isStale ? ` ${colors.error}[STALE]${RESET}` : '';
538
+ const spinnerStr = state.state === 'processing'
539
+ ? ` ${colors.processing}${spinner(frame)}${RESET}` : '';
540
+ const footerHint = state.sessions.length > 0
541
+ ? 'q quit ↑↓/j k scroll 1-9 switch session'
542
+ : 'q quit ↑↓/j k scroll';
543
+ if (layout === 'wide')
544
+ return renderWideLayout(state, cols, rows, terrariumLines, frame, scrollOffset, connIcon, staleTag, spinnerStr, footerHint);
545
+ if (layout === 'standard')
546
+ return renderStandardLayout(state, cols, rows, terrariumLines, frame, scrollOffset, connIcon, staleTag, spinnerStr, footerHint);
547
+ return renderNarrowLayout(state, cols, rows, frame, scrollOffset, connIcon, staleTag, spinnerStr, footerHint);
548
+ }
549
+ // ===== Wide Layout =====
550
+ function renderWideLayout(state, cols, rows, terrariumLines, frame, scrollOffset, connIcon, staleTag, spinnerStr, footerHint) {
551
+ const leftW = Math.max(20, Math.floor(cols * 0.22));
552
+ const rightW = cols - leftW - 3;
553
+ const buf = [];
554
+ const summary = currentSessionSummary(state, Math.max(16, rightW - 16));
555
+ // Top border
556
+ const topLeft = `${colors.border}${box.tl}${RESET}`;
557
+ const topMid = `${colors.border}${box.tee}${box.h} ${summary} ${RESET}${connIcon}${RESET}${spinnerStr}${staleTag} `;
558
+ const topRight = `${colors.border}${box.tr}${RESET}`;
559
+ const leftFillLen = Math.max(0, leftW + 1 - visLen(topLeft));
560
+ const rightFillLen = Math.max(0, rightW + 2 - visLen(topMid) - visLen(topRight));
561
+ buf.push(topLeft + `${colors.border}${hLine(leftFillLen)}${RESET}` + topMid + `${colors.border}${hLine(rightFillLen)}${RESET}` + topRight);
562
+ const agentLines = renderAgentLines(state, leftW - 2, true);
563
+ // Status: LIMITS | MODELS
564
+ const tH = terrariumLines.length;
565
+ const statusLimitW = Math.floor(rightW * 0.4);
566
+ const statusModelW = rightW - statusLimitW - 1;
567
+ const limitsLines = renderStatusLimitsLines(state, statusLimitW);
568
+ const modelsLines = renderStatusModelsLines(state, statusModelW);
569
+ const statusH = Math.max(3, Math.max(limitsLines.length, modelsLines.length) + 1);
570
+ const boxContentRows = rows - 3; // top border + bottom border + q quit row
571
+ const timelineH = Math.max(3, boxContentRows - tH - statusH - 2);
572
+ const tlLines = renderTimelineLines(state, rightW - 2, timelineH - 1, scrollOffset);
573
+ for (let r = 0; r < boxContentRows; r++) {
574
+ const leftContent = padRight(r < agentLines.length ? agentLines[r] : '', leftW);
575
+ let rightContent = '';
576
+ if (r < tH) {
577
+ rightContent = terrariumLines[r] || '';
578
+ }
579
+ else if (r === tH) {
580
+ rightContent = `${colors.border}${hLine(statusLimitW)}${RESET}` +
581
+ `${colors.border}\u252C${RESET}` +
582
+ `${colors.border}${hLine(statusModelW)}${RESET}`;
583
+ }
584
+ else if (r < tH + 1 + statusH) {
585
+ const si = r - tH - 1;
586
+ if (si === 0) {
587
+ rightContent = padRight(`${colors.header} LIMITS${RESET}`, statusLimitW) +
588
+ `${colors.border}${box.v}${RESET}` +
589
+ padRight(`${colors.header} MODELS${RESET}`, statusModelW);
590
+ }
591
+ else {
592
+ const li = si - 1;
593
+ rightContent = padRight(li < limitsLines.length ? limitsLines[li] : '', statusLimitW) +
594
+ `${colors.border}${box.v}${RESET}` +
595
+ padRight(li < modelsLines.length ? modelsLines[li] : '', statusModelW);
596
+ }
597
+ }
598
+ else if (r === tH + 1 + statusH) {
599
+ rightContent = `${colors.border}${hLine(rightW)}${RESET}`;
600
+ }
601
+ else {
602
+ const ti = r - tH - statusH - 2;
603
+ if (ti === 0)
604
+ rightContent = timelineHeader(state, rightW, timelineH - 1, scrollOffset);
605
+ else
606
+ rightContent = ti - 1 < tlLines.length ? tlLines[ti - 1] : '';
607
+ }
608
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(leftContent, leftW)}` +
609
+ `${colors.border}${box.v}${RESET}${padRight(rightContent, rightW)}` +
610
+ `${colors.border}${box.v}${RESET}`);
611
+ }
612
+ buf.push(`${colors.border}${box.bl}${hLine(leftW)}${box.bTee}${hLine(rightW)}${box.br}${RESET}`);
613
+ return flushBuf(buf, cols, rows, footerHint);
614
+ }
615
+ // ===== Standard Layout =====
616
+ function renderStandardLayout(state, cols, rows, terrariumLines, frame, scrollOffset, connIcon, staleTag, spinnerStr, footerHint) {
617
+ const w = cols - 2;
618
+ const buf = [];
619
+ const summary = currentSessionSummary(state, Math.max(16, w - 18));
620
+ buf.push(borderFill(`${colors.border}${box.tl}${box.h} ${summary} ${RESET}${connIcon}${RESET}${spinnerStr}${staleTag} `, `${colors.border}${box.tr}${RESET}`, cols));
621
+ for (const tl of terrariumLines) {
622
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(tl, w)}${colors.border}${box.v}${RESET}`);
623
+ }
624
+ const leftHalf = Math.floor(w / 2);
625
+ const rightHalf = w - leftHalf - 1;
626
+ const splitPrefix = `${colors.border}${box.lTee}${box.h} STATUS ${RESET}`;
627
+ const splitMid = `${colors.border}${box.tee}${RESET}`;
628
+ const splitSuffix = `${colors.border}${box.rTee}${RESET}`;
629
+ buf.push(splitPrefix + `${colors.border}${hLine(Math.max(0, leftHalf + 1 - visLen(splitPrefix)))}${RESET}` +
630
+ splitMid + `${colors.border}${hLine(Math.max(0, rightHalf + 2 - visLen(splitMid) - visLen(splitSuffix)))}${RESET}` + splitSuffix);
631
+ const statusLines = renderStatusLines(state, leftHalf - 1);
632
+ const agentCompact = renderAgentCompactLines(state, rightHalf - 1);
633
+ const pairRows = Math.max(statusLines.length, agentCompact.length, 3);
634
+ for (let r = 0; r < pairRows; r++) {
635
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(r < statusLines.length ? statusLines[r] : '', leftHalf)}` +
636
+ `${colors.border}${box.v}${RESET}${padRight(r < agentCompact.length ? agentCompact[r] : '', rightHalf)}` +
637
+ `${colors.border}${box.v}${RESET}`);
638
+ }
639
+ buf.push(`${colors.border}${box.lTee}${hLine(leftHalf)}${box.bTee}${hLine(rightHalf)}${box.rTee}${RESET}`);
640
+ const tlAvailable = Math.max(2, rows - buf.length - 2);
641
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(timelineHeader(state, w, tlAvailable, scrollOffset), w)}` +
642
+ `${colors.border}${box.v}${RESET}`);
643
+ const tlLines = renderTimelineLines(state, w - 1, tlAvailable, scrollOffset);
644
+ for (const tl of tlLines) {
645
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(tl, w)}${colors.border}${box.v}${RESET}`);
646
+ }
647
+ buf.push(`${colors.border}${box.bl}${hLine(w)}${box.br}${RESET}`);
648
+ return flushBuf(buf, cols, rows, footerHint);
649
+ }
650
+ function renderAgentCompactLines(state, width) {
651
+ const lines = [];
652
+ const isDaemonLikeCompact = state.agentType === 'daemon' ||
653
+ (state.agentType && state.sessions.some(s => s.agentType === state.agentType));
654
+ if (isDaemonLikeCompact) {
655
+ let focusableIndex = 0;
656
+ for (const s of sortSessions(state.sessions)) {
657
+ const renderInfo = s;
658
+ const st = s.state || 'idle';
659
+ const col = stateColor(st);
660
+ const emoji = `${creatureBrandColor(s.agentType)}${creatureEmoji(s.agentType)}${RESET}`;
661
+ const status = `${col}${stateIcon(st)} ${compactStateLabel(st)}${RESET}`;
662
+ const detail = renderInfo.currentTask || s.modelName;
663
+ const model = detail ? `${colors.dim} · ${truncText(detail, Math.max(8, width - 24))}${RESET}` : '';
664
+ const marker = renderInfo.controlMode === 'observed' ? `${colors.dim} · obs${RESET}` : '';
665
+ const project = truncText(s.projectName || '?', Math.max(8, width - 20));
666
+ const hotkeyIndex = sessionHotkeyIndex(renderInfo, focusableIndex);
667
+ if (hotkeyIndex !== null)
668
+ focusableIndex += 1;
669
+ lines.push(` ${colors.dim}[${sessionHotkeyLabel(hotkeyIndex)}]${RESET} ${emoji} ${project} ${status}${marker}${model}`);
670
+ }
671
+ }
672
+ else if (state.state) {
673
+ // Self first
674
+ const col = stateColor(state.state);
675
+ const proj = truncText(state.projectName || '?', Math.max(8, width - 16));
676
+ const status = `${col}${stateIcon(state.state)} ${compactStateLabel(state.state)}${RESET}`;
677
+ const model = state.modelName ? `${colors.dim} · ${truncText(state.modelName, Math.max(8, width - 24))}${RESET}` : '';
678
+ const emoji = `${creatureBrandColor(state.agentType ?? undefined)}${creatureEmoji(state.agentType ?? undefined)}${RESET}`;
679
+ lines.push(` ${emoji} ${proj} ${status}${model}`);
680
+ // Siblings
681
+ let focusableIndex = 0;
682
+ for (const s of sortSessions(state.sessions)) {
683
+ const renderInfo = s;
684
+ const st = s.state || 'idle';
685
+ const sCol = stateColor(st);
686
+ const sEmoji = `${creatureBrandColor(s.agentType)}${creatureEmoji(s.agentType)}${RESET}`;
687
+ const sStatus = `${sCol}${stateIcon(st)} ${compactStateLabel(st)}${RESET}`;
688
+ const detail = renderInfo.currentTask || s.modelName;
689
+ const sModel = detail ? `${colors.dim} · ${truncText(detail, Math.max(8, width - 24))}${RESET}` : '';
690
+ const marker = renderInfo.controlMode === 'observed' ? `${colors.dim} · obs${RESET}` : '';
691
+ const sProject = truncText(s.projectName || '?', Math.max(8, width - 20));
692
+ const hotkeyIndex = sessionHotkeyIndex(renderInfo, focusableIndex);
693
+ if (hotkeyIndex !== null)
694
+ focusableIndex += 1;
695
+ lines.push(` ${colors.dim}[${sessionHotkeyLabel(hotkeyIndex)}]${RESET} ${sEmoji} ${sProject} ${sStatus}${marker}${sModel}`);
696
+ }
697
+ }
698
+ // Virtual OpenClaw entry
699
+ const hasOcCompact = state.sessions.some(s => s.agentType === 'openclaw' || s.agentType === 'gateway');
700
+ if (state.gatewayAvailable && !hasOcCompact) {
701
+ const ocState = state.crayfishRouting ? 'processing' : 'idle';
702
+ const ocCol = stateColor(ocState);
703
+ const ocEmoji = `${creatureBrandColor('openclaw')}${creatureEmoji('openclaw')}${RESET}`;
704
+ lines.push(` ${ocEmoji} OpenClaw ${ocCol}${stateIcon(ocState)}${ocState.toUpperCase()}${RESET}`);
705
+ }
706
+ // Gateway error warning
707
+ if (state.gatewayHasError) {
708
+ lines.push(`${colors.error} \u26A0 Gateway Error${RESET}`);
709
+ }
710
+ // Voice assistant indicator (compact)
711
+ if (state.voiceAssistantState && state.voiceAssistantState !== 'disabled' && state.voiceAssistantState !== 'idle') {
712
+ if (state.voiceAssistantState === 'listening') {
713
+ lines.push(` ${colors.idle}\uD83C\uDFA4 Listening...${RESET}`);
714
+ }
715
+ else if (state.voiceAssistantState === 'processing') {
716
+ const text = state.voiceAssistantText ? truncText(state.voiceAssistantText, width - 6) : '...';
717
+ lines.push(` ${colors.processing}\uD83C\uDFA4 ${text}${RESET}`);
718
+ }
719
+ else if (state.voiceAssistantState === 'speaking') {
720
+ lines.push(` ${sgr(32)}\uD83D\uDD0A Speaking...${RESET}`);
721
+ }
722
+ }
723
+ if (state.usage) {
724
+ const u = state.usage;
725
+ const parts = [];
726
+ if (u.inputTokens || u.outputTokens)
727
+ parts.push(`${formatTokens(u.inputTokens)}/${formatTokens(u.outputTokens)}`);
728
+ if (u.estimatedCostUsd)
729
+ parts.push(`$${u.estimatedCostUsd.toFixed(2)}`);
730
+ if (parts.length > 0)
731
+ lines.push(`${colors.dim} ${parts.join(' ')}${RESET}`);
732
+ }
733
+ return lines;
734
+ }
735
+ // ===== Narrow Layout =====
736
+ function renderNarrowLayout(state, cols, rows, frame, scrollOffset, connIcon, staleTag, spinnerStr, footerHint) {
737
+ const w = cols - 2;
738
+ const buf = [];
739
+ const summary = currentSessionSummary(state, Math.max(12, w - 18));
740
+ buf.push(borderFill(`${colors.border}${box.tl}${box.h} ${summary} ${RESET}${connIcon}${RESET}${spinnerStr}${staleTag} `, `${colors.border}${box.tr}${RESET}`, cols));
741
+ for (const al of renderAgentCompactLines(state, w - 1)) {
742
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(al, w)}${colors.border}${box.v}${RESET}`);
743
+ }
744
+ buf.push(`${colors.border}${box.lTee}${hLine(w)}${box.rTee}${RESET}`);
745
+ for (const sl of renderStatusLines(state, w - 1)) {
746
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(sl, w)}${colors.border}${box.v}${RESET}`);
747
+ }
748
+ const tlAvailable = Math.max(2, rows - buf.length - 2);
749
+ buf.push(`${colors.border}${box.lTee}${hLine(w)}${box.rTee}${RESET}`);
750
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(timelineHeader(state, w, tlAvailable, scrollOffset), w)}${colors.border}${box.v}${RESET}`);
751
+ const tlLines = renderTimelineLines(state, w - 1, tlAvailable, scrollOffset);
752
+ for (const tl of tlLines) {
753
+ buf.push(`${colors.border}${box.v}${RESET}${padRight(tl, w)}${colors.border}${box.v}${RESET}`);
754
+ }
755
+ buf.push(`${colors.border}${box.bl}${hLine(w)}${box.br}${RESET}`);
756
+ return flushBuf(buf, cols, rows, footerHint);
757
+ }
758
+ //# sourceMappingURL=renderer.js.map