aicodeman 1.35.0 → 1.41.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 (316) hide show
  1. package/README.md +33 -7
  2. package/dist/attachment-registry.d.ts +6 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +9 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +225 -0
  7. package/dist/cli-agent.d.ts.map +1 -0
  8. package/dist/cli-agent.js +767 -0
  9. package/dist/cli-agent.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +8 -32
  12. package/dist/cli.js.map +1 -1
  13. package/dist/codeman-credentials.d.ts +27 -0
  14. package/dist/codeman-credentials.d.ts.map +1 -0
  15. package/dist/codeman-credentials.js +66 -0
  16. package/dist/codeman-credentials.js.map +1 -0
  17. package/dist/config/cli-registry/patterns.d.ts +6 -0
  18. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  19. package/dist/config/cli-registry/patterns.js +13 -0
  20. package/dist/config/cli-registry/patterns.js.map +1 -1
  21. package/dist/config/cli-registry/schema.d.ts +15 -0
  22. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  23. package/dist/config/cli-registry/schema.js +63 -2
  24. package/dist/config/cli-registry/schema.js.map +1 -1
  25. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  26. package/dist/config/cli-registry/stock.js +180 -7
  27. package/dist/config/cli-registry/stock.js.map +1 -1
  28. package/dist/config/cli-registry/types.d.ts +105 -7
  29. package/dist/config/cli-registry/types.d.ts.map +1 -1
  30. package/dist/deepseek-route-config.d.ts +110 -0
  31. package/dist/deepseek-route-config.d.ts.map +1 -0
  32. package/dist/deepseek-route-config.js +477 -0
  33. package/dist/deepseek-route-config.js.map +1 -0
  34. package/dist/git-clone.d.ts.map +1 -1
  35. package/dist/git-clone.js +4 -0
  36. package/dist/git-clone.js.map +1 -1
  37. package/dist/git-workspace-status.d.ts +31 -5
  38. package/dist/git-workspace-status.d.ts.map +1 -1
  39. package/dist/git-workspace-status.js +49 -29
  40. package/dist/git-workspace-status.js.map +1 -1
  41. package/dist/image-watcher.d.ts.map +1 -1
  42. package/dist/image-watcher.js +6 -2
  43. package/dist/image-watcher.js.map +1 -1
  44. package/dist/mcp-sync-targets.d.ts +34 -0
  45. package/dist/mcp-sync-targets.d.ts.map +1 -0
  46. package/dist/mcp-sync-targets.js +55 -0
  47. package/dist/mcp-sync-targets.js.map +1 -0
  48. package/dist/mcp-sync.d.ts +2 -1
  49. package/dist/mcp-sync.d.ts.map +1 -1
  50. package/dist/mcp-sync.js +62 -1
  51. package/dist/mcp-sync.js.map +1 -1
  52. package/dist/model-config-resolvers.d.ts +29 -0
  53. package/dist/model-config-resolvers.d.ts.map +1 -0
  54. package/dist/model-config-resolvers.js +34 -0
  55. package/dist/model-config-resolvers.js.map +1 -0
  56. package/dist/session-display-model.d.ts +110 -0
  57. package/dist/session-display-model.d.ts.map +1 -0
  58. package/dist/session-display-model.js +154 -0
  59. package/dist/session-display-model.js.map +1 -0
  60. package/dist/session.d.ts +145 -8
  61. package/dist/session.d.ts.map +1 -1
  62. package/dist/session.js +290 -39
  63. package/dist/session.js.map +1 -1
  64. package/dist/subagent-watcher.d.ts.map +1 -1
  65. package/dist/subagent-watcher.js +29 -6
  66. package/dist/subagent-watcher.js.map +1 -1
  67. package/dist/templates/case-template.md +2 -0
  68. package/dist/tmux-manager.d.ts +5 -0
  69. package/dist/tmux-manager.d.ts.map +1 -1
  70. package/dist/tmux-manager.js +47 -22
  71. package/dist/tmux-manager.js.map +1 -1
  72. package/dist/tui/tui-client.d.ts +3 -17
  73. package/dist/tui/tui-client.d.ts.map +1 -1
  74. package/dist/tui/tui-client.js +3 -44
  75. package/dist/tui/tui-client.js.map +1 -1
  76. package/dist/types/session.d.ts +28 -1
  77. package/dist/types/session.d.ts.map +1 -1
  78. package/dist/types/session.js.map +1 -1
  79. package/dist/types/tools.d.ts +1 -1
  80. package/dist/types/tools.d.ts.map +1 -1
  81. package/dist/utils/deepseek-cli-resolver.d.ts +8 -0
  82. package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
  83. package/dist/utils/deepseek-cli-resolver.js +18 -2
  84. package/dist/utils/deepseek-cli-resolver.js.map +1 -1
  85. package/dist/utils/index.d.ts +1 -0
  86. package/dist/utils/index.d.ts.map +1 -1
  87. package/dist/utils/index.js +1 -0
  88. package/dist/utils/index.js.map +1 -1
  89. package/dist/utils/process-exit-wait.d.ts +40 -0
  90. package/dist/utils/process-exit-wait.d.ts.map +1 -0
  91. package/dist/utils/process-exit-wait.js +68 -0
  92. package/dist/utils/process-exit-wait.js.map +1 -0
  93. package/dist/web/launch-defaults.d.ts +22 -0
  94. package/dist/web/launch-defaults.d.ts.map +1 -0
  95. package/dist/web/launch-defaults.js +46 -0
  96. package/dist/web/launch-defaults.js.map +1 -0
  97. package/dist/web/paste-image-gc.d.ts +46 -6
  98. package/dist/web/paste-image-gc.d.ts.map +1 -1
  99. package/dist/web/paste-image-gc.js +106 -32
  100. package/dist/web/paste-image-gc.js.map +1 -1
  101. package/dist/web/public/admin-ui.js.gz +0 -0
  102. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  103. package/dist/web/public/app.6f8552e1.js +43 -0
  104. package/dist/web/public/app.6f8552e1.js.br +0 -0
  105. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  106. package/dist/web/public/approvals-ui.js.gz +0 -0
  107. package/dist/web/public/{constants.d95fade9.js → constants.bc9c1f63.js} +1276 -127
  108. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  109. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  110. package/dist/web/public/cron-ui.js.gz +0 -0
  111. package/dist/web/public/entrance-animations.js +491 -43
  112. package/dist/web/public/entrance-animations.js.br +0 -0
  113. package/dist/web/public/entrance-animations.js.gz +0 -0
  114. package/dist/web/public/git-status-ui.js +55 -9
  115. package/dist/web/public/git-status-ui.js.br +0 -0
  116. package/dist/web/public/git-status-ui.js.gz +0 -0
  117. package/dist/web/public/home-sessions.js +13 -9
  118. package/dist/web/public/home-sessions.js.br +0 -0
  119. package/dist/web/public/home-sessions.js.gz +0 -0
  120. package/dist/web/public/host-wake-ui.js.gz +0 -0
  121. package/dist/web/public/i18n.a9840ea2.js +2 -0
  122. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  123. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  124. package/dist/web/public/{image-input.a216cf9e.js → image-input.e00471c6.js} +21 -9
  125. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  126. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  127. package/dist/web/public/index.html +250 -59
  128. package/dist/web/public/index.html.br +0 -0
  129. package/dist/web/public/index.html.gz +0 -0
  130. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  131. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  132. package/dist/web/public/{mobile-handlers.23a85db1.js → mobile-handlers.0ef9c311.js} +20 -0
  133. package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
  134. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  135. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  136. package/dist/web/public/mobile-overview.js.gz +0 -0
  137. package/dist/web/public/mobile.111b187d.css +1 -0
  138. package/dist/web/public/mobile.111b187d.css.br +0 -0
  139. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  140. package/dist/web/public/{notification-manager.50bb87d8.js → notification-manager.feb82c04.js} +19 -2
  141. package/dist/web/public/notification-manager.feb82c04.js.br +0 -0
  142. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  143. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  144. package/dist/web/public/{panels-ui.6a5898d9.js → panels-ui.71846fcb.js} +4 -4
  145. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  146. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  147. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  148. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  149. package/dist/web/public/readmymind-ui.js.gz +0 -0
  150. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  151. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  152. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  153. package/dist/web/public/session-lineage.js +229 -86
  154. package/dist/web/public/session-lineage.js.br +0 -0
  155. package/dist/web/public/session-lineage.js.gz +0 -0
  156. package/dist/web/public/session-ui.f731602c.js +100 -0
  157. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  158. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  159. package/dist/web/public/settings-ui.4e33390f.js +97 -0
  160. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  161. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  162. package/dist/web/public/spreadsheet-preview-worker.js +290 -0
  163. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  164. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  165. package/dist/web/public/spreadsheet-preview.af82fa2f.js +555 -0
  166. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  167. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  168. package/dist/web/public/spreadsheet-xlsx-core.js +1006 -0
  169. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  170. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  171. package/dist/web/public/styles.81a72906.css +1 -0
  172. package/dist/web/public/styles.81a72906.css.br +0 -0
  173. package/dist/web/public/styles.81a72906.css.gz +0 -0
  174. package/dist/web/public/{subagent-windows.e6ca799f.js → subagent-windows.07277a58.js} +7 -5
  175. package/dist/web/public/subagent-windows.07277a58.js.br +0 -0
  176. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  177. package/dist/web/public/sw.js +2 -2
  178. package/dist/web/public/sw.js.br +0 -0
  179. package/dist/web/public/sw.js.gz +0 -0
  180. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  181. package/dist/web/public/{tab-rail-resize.94cd59f0.js → tab-rail-resize.a5867793.js} +1 -1
  182. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  183. package/dist/web/public/{tab-rail-resize.94cd59f0.js.gz → tab-rail-resize.a5867793.js.gz} +0 -0
  184. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +1 -0
  185. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
  186. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  187. package/dist/web/public/terminal-split.js +146 -649
  188. package/dist/web/public/terminal-split.js.br +0 -0
  189. package/dist/web/public/terminal-split.js.gz +0 -0
  190. package/dist/web/public/terminal-tile.js +1688 -0
  191. package/dist/web/public/terminal-tile.js.br +0 -0
  192. package/dist/web/public/terminal-tile.js.gz +0 -0
  193. package/dist/web/public/terminal-ui.b7267005.js +2 -0
  194. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  195. package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
  196. package/dist/web/public/tile-grid.js +2895 -0
  197. package/dist/web/public/tile-grid.js.br +0 -0
  198. package/dist/web/public/tile-grid.js.gz +0 -0
  199. package/dist/web/public/ultracode-panel.js.gz +0 -0
  200. package/dist/web/public/ultracode-windows.js +9 -6
  201. package/dist/web/public/ultracode-windows.js.br +0 -0
  202. package/dist/web/public/ultracode-windows.js.gz +0 -0
  203. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  204. package/dist/web/public/vendor/exceljs.min.js +44 -0
  205. package/dist/web/public/vendor/exceljs.min.js.br +0 -0
  206. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  207. package/dist/web/public/vendor/fflate.min.js +1 -0
  208. package/dist/web/public/vendor/fflate.min.js.br +0 -0
  209. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  210. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  211. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  213. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  214. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  215. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  216. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +4 -0
  217. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
  218. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  219. package/dist/web/public/vendor/xterm.css.gz +0 -0
  220. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  221. package/dist/web/public/{voice-input.c4b51eb6.js → voice-input.2342c37b.js} +64 -10
  222. package/dist/web/public/voice-input.2342c37b.js.br +0 -0
  223. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  224. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  225. package/dist/web/public/webview-tabs.js +9 -1
  226. package/dist/web/public/webview-tabs.js.br +0 -0
  227. package/dist/web/public/webview-tabs.js.gz +0 -0
  228. package/dist/web/routes/cli-registry-routes.d.ts +23 -4
  229. package/dist/web/routes/cli-registry-routes.d.ts.map +1 -1
  230. package/dist/web/routes/cli-registry-routes.js +95 -8
  231. package/dist/web/routes/cli-registry-routes.js.map +1 -1
  232. package/dist/web/routes/file-routes.d.ts +7 -0
  233. package/dist/web/routes/file-routes.d.ts.map +1 -1
  234. package/dist/web/routes/file-routes.js +34 -4
  235. package/dist/web/routes/file-routes.js.map +1 -1
  236. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  237. package/dist/web/routes/git-status-routes.js +33 -6
  238. package/dist/web/routes/git-status-routes.js.map +1 -1
  239. package/dist/web/routes/mcp-sync-routes.d.ts +7 -2
  240. package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -1
  241. package/dist/web/routes/mcp-sync-routes.js +10 -3
  242. package/dist/web/routes/mcp-sync-routes.js.map +1 -1
  243. package/dist/web/routes/reboot-restore-routes.js +1 -1
  244. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  245. package/dist/web/routes/session-routes.d.ts +18 -0
  246. package/dist/web/routes/session-routes.d.ts.map +1 -1
  247. package/dist/web/routes/session-routes.js +154 -69
  248. package/dist/web/routes/session-routes.js.map +1 -1
  249. package/dist/web/routes/status-telemetry-routes.d.ts +3 -0
  250. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  251. package/dist/web/routes/status-telemetry-routes.js +12 -0
  252. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  253. package/dist/web/routes/system-routes.d.ts.map +1 -1
  254. package/dist/web/routes/system-routes.js +13 -0
  255. package/dist/web/routes/system-routes.js.map +1 -1
  256. package/dist/web/schemas.d.ts +30 -0
  257. package/dist/web/schemas.d.ts.map +1 -1
  258. package/dist/web/schemas.js +43 -0
  259. package/dist/web/schemas.js.map +1 -1
  260. package/dist/web/server.d.ts +6 -1
  261. package/dist/web/server.d.ts.map +1 -1
  262. package/dist/web/server.js +47 -21
  263. package/dist/web/server.js.map +1 -1
  264. package/dist/web/session-listener-wiring.d.ts +1 -0
  265. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  266. package/dist/web/session-listener-wiring.js +11 -0
  267. package/dist/web/session-listener-wiring.js.map +1 -1
  268. package/package.json +6 -4
  269. package/scripts/postinstall.js +16 -0
  270. package/skills/codeman/SKILL.md +6 -0
  271. package/README.zh-CN.md +0 -1139
  272. package/dist/web/public/app.b9516c7b.js +0 -43
  273. package/dist/web/public/app.b9516c7b.js.br +0 -0
  274. package/dist/web/public/app.b9516c7b.js.gz +0 -0
  275. package/dist/web/public/constants.d95fade9.js.br +0 -0
  276. package/dist/web/public/constants.d95fade9.js.gz +0 -0
  277. package/dist/web/public/i18n.52f5b7cc.js +0 -1
  278. package/dist/web/public/i18n.52f5b7cc.js.br +0 -0
  279. package/dist/web/public/i18n.52f5b7cc.js.gz +0 -0
  280. package/dist/web/public/image-input.a216cf9e.js.br +0 -0
  281. package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
  282. package/dist/web/public/mobile-handlers.23a85db1.js.br +0 -0
  283. package/dist/web/public/mobile-handlers.23a85db1.js.gz +0 -0
  284. package/dist/web/public/mobile.1aa4952d.css +0 -1
  285. package/dist/web/public/mobile.1aa4952d.css.br +0 -0
  286. package/dist/web/public/mobile.1aa4952d.css.gz +0 -0
  287. package/dist/web/public/notification-manager.50bb87d8.js.br +0 -0
  288. package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
  289. package/dist/web/public/panels-ui.6a5898d9.js.br +0 -0
  290. package/dist/web/public/panels-ui.6a5898d9.js.gz +0 -0
  291. package/dist/web/public/session-ui.6bddf916.js +0 -89
  292. package/dist/web/public/session-ui.6bddf916.js.br +0 -0
  293. package/dist/web/public/session-ui.6bddf916.js.gz +0 -0
  294. package/dist/web/public/settings-ui.6e798b7b.js +0 -97
  295. package/dist/web/public/settings-ui.6e798b7b.js.br +0 -0
  296. package/dist/web/public/settings-ui.6e798b7b.js.gz +0 -0
  297. package/dist/web/public/styles.2d175c60.css +0 -1
  298. package/dist/web/public/styles.2d175c60.css.br +0 -0
  299. package/dist/web/public/styles.2d175c60.css.gz +0 -0
  300. package/dist/web/public/subagent-windows.e6ca799f.js.br +0 -0
  301. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  302. package/dist/web/public/tab-rail-resize.94cd59f0.js.br +0 -0
  303. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js +0 -1
  304. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.br +0 -0
  305. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  306. package/dist/web/public/terminal-ui.a4535311.js +0 -2
  307. package/dist/web/public/terminal-ui.a4535311.js.br +0 -0
  308. package/dist/web/public/terminal-ui.a4535311.js.gz +0 -0
  309. package/dist/web/public/upload.html +0 -155
  310. package/dist/web/public/upload.html.br +0 -0
  311. package/dist/web/public/upload.html.gz +0 -0
  312. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +0 -4
  313. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
  314. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
  315. package/dist/web/public/voice-input.c4b51eb6.js.br +0 -0
  316. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
@@ -1,12 +1,13 @@
1
1
  /**
2
- * @fileoverview Entrance animations for the four things that appear when work
2
+ * @fileoverview Entrance animations for the things that appear when work
3
3
  * starts: session TABS, the main TERMINAL pane a session's CLI runs in, floating
4
- * agent WINDOWS, and the CONNECTION LINES tying a window back to its parent tab.
5
- * One picker per surface, plus themes that set all four to a matching look.
4
+ * agent WINDOWS, the CONNECTION LINES tying a window back to its parent tab, and
5
+ * the TILES of the tile grid (tile-grid.js). One picker per surface, plus themes
6
+ * that set all five to a matching look.
6
7
  *
7
8
  * Everything is OFF by default (the `legacy` theme), so an untouched install
8
9
  * behaves exactly as it did before this module existed. Opt in via App Settings
9
- * → Appearance → Entrance Animations.
10
+ * → Animations.
10
11
  *
11
12
  * Four constraints shape the design:
12
13
  *
@@ -34,13 +35,28 @@
34
35
  * once-per-id even though the POST response and the SSE event both call
35
36
  * `_onSessionCreated`.
36
37
  *
38
+ * Tiles are off by default (`settle`, the grid's own quick fade, exactly as
39
+ * before) and switched on in App Settings → Animations → Tile Animations, or
40
+ * preset by a theme. A styled tile plays in two beats that combine two
41
+ * surfaces. The FRAME enters as it mounts, in its own style
42
+ * (TILE_ANIM_STYLES); the SCREEN plays the terminal pane's style when its
43
+ * first capture lands (`.tile-body.term-enter`, the same keyframes as the main
44
+ * pane). The load queue serves one capture at a time, so
45
+ * the screens light up one after another, the focused tile first. The frame
46
+ * styles move transform and opacity only (six tiles animate at once; a blur
47
+ * belongs to the serialized screen beat), and each has its own way out on the
48
+ * closing grid's still copy. A reload restores with `settle` whatever the
49
+ * setting.
50
+ *
37
51
  * Styles are selected by `data-tab-anim` / `data-term-anim` / `data-win-anim` /
38
- * `data-line-anim` on <html>; the keyframes live in styles.css. `?animlab=1`
39
- * opens a floating picker that fakes tabs, a pane replay, a window and a line,
40
- * so styles can be compared without spawning real sessions or agents.
52
+ * `data-line-anim` / `data-tile-anim` on <html>; the keyframes live in
53
+ * styles.css. `?animlab=1` opens a floating picker that fakes tabs, a pane
54
+ * replay, a window and a line, and replays the tile grid in place, so styles
55
+ * can be compared without spawning real sessions or agents.
41
56
  *
42
57
  * @mixin Extends CodemanApp.prototype via Object.assign
43
- * @dependency app.js (tab render pipeline), subagent-windows.js (window + line hooks)
58
+ * @dependency app.js (tab render pipeline), subagent-windows.js (window + line hooks),
59
+ * tile-grid.js (tile mount, reveal and still-copy hooks)
44
60
  * @dependency constants.js (escapeHtml)
45
61
  * @loadorder 12.6 of 16, after webview-tabs.js, before ralph-wizard.js
46
62
  */
@@ -94,7 +110,6 @@ const LINE_ANIM_STYLES = [
94
110
  */
95
111
  const TERM_ANIM_STYLES = [
96
112
  { key: 'crt', label: 'CRT', blurb: 'Power-on: a hot line that expands to full height.', duration: 560 },
97
- { key: 'boot', label: 'Boot', blurb: 'Flickers on under a green scan sweep.', duration: 760 },
98
113
  { key: 'wipe', label: 'Wipe', blurb: 'Reveals top-to-bottom behind a bright edge.', duration: 520 },
99
114
  { key: 'slide', label: 'Slide up', blurb: 'Rises into place from below.', duration: 420 },
100
115
  { key: 'fade', label: 'Fade', blurb: 'Quiet fade with a touch of scale.', duration: 340 },
@@ -102,30 +117,79 @@ const TERM_ANIM_STYLES = [
102
117
  { key: 'off', label: 'Off', blurb: 'Current behaviour: the pane just appears.', duration: 0 },
103
118
  ];
104
119
 
120
+ /**
121
+ * Tile grid entrance styles, for each tile's FRAME (its screen plays the
122
+ * TERM_ANIM_STYLES style once content lands). Transform and opacity only, plus
123
+ * a wash on ::before: FitAddon reads the untransformed layout box, so a tile
124
+ * still fits once at its final size (#464). `stagger` is the default gap
125
+ * between tiles and `order` the default cascade: `reading` (row by row),
126
+ * `wave` (diagonals from the top left) or `ripple` (outward from the focused
127
+ * tile). `from`: the tile is measured against a source after layout (its tab,
128
+ * the Tiles button), so it is held one frame and timed by _runTileEntrances.
129
+ */
130
+ // prettier-ignore
131
+ const TILE_ANIM_STYLES = [
132
+ { key: 'settle', label: 'Off (default)', blurb: "The grid's own quick fade and settle.", duration: 180, stagger: 24, order: 'reading' },
133
+ { key: 'fly', label: 'Fly from tab', blurb: 'Each tile flies out of its session tab, and back into it on close.', duration: 560, stagger: 55, order: 'reading', from: 'tab' },
134
+ { key: 'deal', label: 'Deal', blurb: 'Dealt out of the Tiles button like cards, gathered back on close.', duration: 600, stagger: 75, order: 'reading', from: 'button' },
135
+ { key: 'crt', label: 'CRT', blurb: 'Powers on as a hot line; switches off to a dot on close.', duration: 560, stagger: 70, order: 'wave' },
136
+ { key: 'beam', label: 'Beam down', blurb: 'A beam draws down from its tab, then the tile materializes.', duration: 620, stagger: 90, order: 'reading', from: 'tab' },
137
+ { key: 'cascade', label: 'Cascade', blurb: 'Swings down from its top edge in a diagonal wave.', duration: 600, stagger: 80, order: 'wave' },
138
+ { key: 'pop', label: 'Pop', blurb: 'Springs open, rippling out from the focused tile.', duration: 480, stagger: 70, order: 'ripple' },
139
+ { key: 'soft', label: 'Soft', blurb: 'Drifts in slowly, rippling out from the focused tile.', duration: 620, stagger: 60, order: 'ripple' },
140
+ { key: 'off', label: 'None', blurb: 'Tiles just appear.', duration: 0, stagger: 0, order: 'reading' },
141
+ ];
142
+
143
+ /** Cascade orders for the tile grid; `auto` is each style's own. */
144
+ const TILE_ANIM_ORDERS = [
145
+ { key: 'auto', label: 'Style default' },
146
+ { key: 'reading', label: 'Reading order' },
147
+ { key: 'wave', label: 'Diagonal wave' },
148
+ { key: 'ripple', label: 'Ripple from focus' },
149
+ ];
150
+
105
151
  /** How long a `beam` window waits before materializing. Just under the line draw. */
106
152
  const BEAM_HOLD_MS = 360;
107
153
 
154
+ /** Gap between tiles leaving, in reading order, so the last copy ends last. */
155
+ const TILE_EXIT_STAGGER_MS = 35;
156
+
157
+ /** Tile exit durations per style (styles.css `tile-leave-*`); the rest use the default fade. */
158
+ const TILE_EXIT_MS = { fly: 460, deal: 520, crt: 520, beam: 480, cascade: 480, pop: 380, soft: 520 };
159
+
108
160
  /** One-click combinations that read as a single look. */
109
161
  const ANIM_THEMES = [
110
- { key: 'terminal', label: 'Terminal', tab: 'crt', win: 'crt', line: 'draw', term: 'crt' },
111
- { key: 'beamdown', label: 'Beam down', tab: 'crt', win: 'beam', line: 'draw', term: 'wipe' },
112
- { key: 'softfocus', label: 'Soft focus', tab: 'blur', win: 'blur', line: 'blur', term: 'blur' },
113
- { key: 'quiet', label: 'Quiet', tab: 'slide', win: 'materialize', line: 'fade', term: 'fade' },
114
- { key: 'playful', label: 'Playful', tab: 'pop', win: 'pop', line: 'packet', term: 'slide' },
115
- { key: 'legacy', label: 'Legacy', tab: 'off', win: 'fly', line: 'off', term: 'off' },
162
+ { key: 'terminal', label: 'Terminal', tab: 'crt', win: 'crt', line: 'draw', term: 'crt', tile: 'crt' },
163
+ { key: 'beamdown', label: 'Beam down', tab: 'crt', win: 'beam', line: 'draw', term: 'wipe', tile: 'beam' },
164
+ { key: 'launch', label: 'Launch', tab: 'pop', win: 'fly', line: 'packet', term: 'fade', tile: 'fly' },
165
+ { key: 'softfocus', label: 'Soft focus', tab: 'blur', win: 'blur', line: 'blur', term: 'blur', tile: 'soft' },
166
+ { key: 'quiet', label: 'Quiet', tab: 'slide', win: 'materialize', line: 'fade', term: 'fade', tile: 'settle' },
167
+ { key: 'playful', label: 'Playful', tab: 'pop', win: 'pop', line: 'packet', term: 'slide', tile: 'deal' },
168
+ { key: 'legacy', label: 'Legacy', tab: 'off', win: 'fly', line: 'off', term: 'off', tile: 'settle' },
116
169
  ];
117
170
 
171
+ /**
172
+ * The surfaces a theme is recognised by. A theme also PRESETS the tile style
173
+ * when it is picked, but the tile style is its own setting (App Settings →
174
+ * Animations → Tile Animations, off by default), so changing it afterwards
175
+ * does not turn the theme into "Custom".
176
+ */
177
+ const ANIM_SURFACES = ['tab', 'win', 'line', 'term'];
178
+
118
179
  /**
119
180
  * Defaults are the `legacy` theme: every entrance OFF, and agent windows on the
120
181
  * `fly` behaviour Codeman already had before this module existed. So a user who
121
182
  * never opens the picker sees exactly the pre-existing UI, and each mark/apply
122
- * hook short-circuits on its first line. Opt in via App Settings → Appearance →
123
- * Entrance Animations, which persists to the localStorage keys below.
183
+ * hook short-circuits on its first line. Opt in via App Settings → Animations,
184
+ * which persists to the localStorage keys below.
124
185
  */
125
186
  const TAB_ANIM_DEFAULT = 'off';
126
187
  const WIN_ANIM_DEFAULT = 'fly';
127
188
  const LINE_ANIM_DEFAULT = 'off';
128
189
  const TERM_ANIM_DEFAULT = 'off';
190
+ /** The grid's own fade and settle, unchanged for anyone who never picks a theme. */
191
+ const TILE_ANIM_DEFAULT = 'settle';
192
+ const TILE_ANIM_ORDER_DEFAULT = 'auto';
129
193
  const TAB_ANIM_STAGGER_DEFAULT = 90;
130
194
  /** A new id joins the current cascade if it arrives within this of the last one. */
131
195
  const TAB_ANIM_BATCH_WINDOW_MS = 600;
@@ -135,6 +199,8 @@ const ANIM_KEYS = {
135
199
  win: 'codeman:winAnim',
136
200
  line: 'codeman:lineAnim',
137
201
  term: 'codeman:termAnim',
202
+ tile: 'codeman:tileAnim',
203
+ tileOrder: 'codeman:tileAnimOrder',
138
204
  termSwitch: 'codeman:termAnimOnSwitch',
139
205
  stagger: 'codeman:tabAnimStagger',
140
206
  speed: 'codeman:tabAnimSpeed',
@@ -164,6 +230,10 @@ Object.assign(CodemanApp.prototype, {
164
230
  this.setWinAnimStyle(pick('winanim', WIN_ANIM_STYLES, ANIM_KEYS.win, WIN_ANIM_DEFAULT), { persist: false });
165
231
  this.setLineAnimStyle(pick('lineanim', LINE_ANIM_STYLES, ANIM_KEYS.line, LINE_ANIM_DEFAULT), { persist: false });
166
232
  this.setTermAnimStyle(pick('termanim', TERM_ANIM_STYLES, ANIM_KEYS.term, TERM_ANIM_DEFAULT), { persist: false });
233
+ // Off (the grid's own `settle`) until chosen: a theme saved before tiles
234
+ // were a surface gives them nothing new.
235
+ this.setTileAnimStyle(pick('tileanim', TILE_ANIM_STYLES, ANIM_KEYS.tile, TILE_ANIM_DEFAULT), { persist: false });
236
+ this.setTileAnimOrder(this._animRead(ANIM_KEYS.tileOrder, TILE_ANIM_ORDER_DEFAULT), { persist: false });
167
237
  this.setTermAnimOnSwitch(this._animRead(ANIM_KEYS.termSwitch, '0') === '1', { persist: false });
168
238
 
169
239
  this.setTabAnimStagger(Number(this._animRead(ANIM_KEYS.stagger, TAB_ANIM_STAGGER_DEFAULT)), { persist: false });
@@ -219,6 +289,18 @@ Object.assign(CodemanApp.prototype, {
219
289
  this._setAnimStyle('_termAnimStyle', key, TERM_ANIM_STYLES, TERM_ANIM_DEFAULT, 'data-term-anim', ANIM_KEYS.term, persist);
220
290
  },
221
291
 
292
+ setTileAnimStyle(key, { persist = true } = {}) {
293
+ // prettier-ignore
294
+ this._setAnimStyle('_tileAnimStyle', key, TILE_ANIM_STYLES, TILE_ANIM_DEFAULT, 'data-tile-anim', ANIM_KEYS.tile, persist);
295
+ },
296
+
297
+ /** The tile cascade: `auto` (the style's own) or a TILE_ANIM_ORDERS key. */
298
+ setTileAnimOrder(key, { persist = true } = {}) {
299
+ this._tileAnimOrder = TILE_ANIM_ORDERS.some((o) => o.key === key) ? key : TILE_ANIM_ORDER_DEFAULT;
300
+ if (persist) this._animWrite(ANIM_KEYS.tileOrder, this._tileAnimOrder);
301
+ this._syncAnimLab?.();
302
+ },
303
+
222
304
  /** Replay the terminal entrance on every tab switch, not just on a new session. */
223
305
  setTermAnimOnSwitch(on, { persist = true } = {}) {
224
306
  this._termAnimOnSwitch = !!on;
@@ -234,22 +316,29 @@ Object.assign(CodemanApp.prototype, {
234
316
  this.setWinAnimStyle(theme.win);
235
317
  this.setLineAnimStyle(theme.line);
236
318
  this.setTermAnimStyle(theme.term);
319
+ this.setTileAnimStyle(theme.tile);
237
320
  this._syncEntranceAnimSetting?.();
238
321
  },
239
322
 
240
- /** The theme matching the four current styles, or 'custom' for a lab mix. */
323
+ /** The current style of each surface, keyed as ANIM_SURFACES. */
324
+ _currentAnimStyles() {
325
+ return {
326
+ tab: this._tabAnimStyle,
327
+ win: this._winAnimStyle,
328
+ line: this._lineAnimStyle,
329
+ term: this._termAnimStyle,
330
+ tile: this._tileAnimStyle,
331
+ };
332
+ },
333
+
334
+ /** The theme matching the current tab, window, line and pane styles, or 'custom' for a lab mix. */
241
335
  currentAnimTheme() {
242
- const match = ANIM_THEMES.find(
243
- (t) =>
244
- t.tab === this._tabAnimStyle &&
245
- t.win === this._winAnimStyle &&
246
- t.line === this._lineAnimStyle &&
247
- t.term === this._termAnimStyle
248
- );
336
+ const current = this._currentAnimStyles();
337
+ const match = ANIM_THEMES.find((t) => ANIM_SURFACES.every((k) => t[k] === current[k]));
249
338
  return match ? match.key : 'custom';
250
339
  },
251
340
 
252
- // ── App Settings picker ───────────────────────────────────────────────────
341
+ // ── App Settings → Animations ─────────────────────────────────────────────
253
342
  //
254
343
  // Wired straight to setAnimTheme() rather than through saveAppSettings(): the
255
344
  // styles live in their own localStorage keys, so they stay per-device and never
@@ -257,16 +346,40 @@ Object.assign(CodemanApp.prototype, {
257
346
 
258
347
  _syncEntranceAnimSetting() {
259
348
  const sel = document.getElementById('appSettingsEntranceAnim');
260
- if (!sel) return;
261
- sel.value = this.currentAnimTheme();
262
- if (!sel.dataset.bound) {
263
- sel.dataset.bound = '1';
264
- sel.addEventListener('change', () => {
265
- // 'custom' is a readout of a lab mix, not something you can select into.
266
- if (sel.value === 'custom') sel.value = this.currentAnimTheme();
267
- else this.setAnimTheme(sel.value);
349
+ if (sel) {
350
+ sel.value = this.currentAnimTheme();
351
+ if (!sel.dataset.bound) {
352
+ sel.dataset.bound = '1';
353
+ sel.addEventListener('change', () => {
354
+ // 'custom' is a readout of a lab mix, not something you can select into.
355
+ if (sel.value === 'custom') sel.value = this.currentAnimTheme();
356
+ else this.setAnimTheme(sel.value);
357
+ });
358
+ }
359
+ }
360
+ // App Settings → Animations → Animation Lab. Settings has no unsaved-edit
361
+ // tracking, so this closes it as Cancel does (the row says so).
362
+ const labBtn = document.getElementById('appSettingsOpenAnimLab');
363
+ if (labBtn && !labBtn.dataset.bound) {
364
+ labBtn.dataset.bound = '1';
365
+ labBtn.addEventListener('click', () => {
366
+ this.closeAppSettings?.();
367
+ this.openAnimLab();
268
368
  });
269
369
  }
370
+ // Tile Animations: its own row, off (`settle`) by default. A theme picked
371
+ // above presets it; picked here, it applies to tiles alone.
372
+ const tileSel = document.getElementById('appSettingsTileAnim');
373
+ if (tileSel) {
374
+ tileSel.value = this._tileAnimStyle || TILE_ANIM_DEFAULT;
375
+ if (!tileSel.dataset.bound) {
376
+ tileSel.dataset.bound = '1';
377
+ tileSel.addEventListener('change', () => {
378
+ this.setTileAnimStyle(tileSel.value);
379
+ this._syncEntranceAnimSetting();
380
+ });
381
+ }
382
+ }
270
383
  },
271
384
 
272
385
  setTabAnimStagger(ms, { persist = true } = {}) {
@@ -303,6 +416,10 @@ Object.assign(CodemanApp.prototype, {
303
416
  return this._styleDuration(TERM_ANIM_STYLES, this._termAnimStyle);
304
417
  },
305
418
 
419
+ _tileAnimDuration() {
420
+ return this._styleDuration(TILE_ANIM_STYLES, this._tileAnimStyle);
421
+ },
422
+
306
423
  // ── Tabs ──────────────────────────────────────────────────────────────────
307
424
 
308
425
  /** Queue a session id to animate on its next render. Idempotent per id. */
@@ -501,6 +618,317 @@ Object.assign(CodemanApp.prototype, {
501
618
  }
502
619
  },
503
620
 
621
+ // ── Tile grid ─────────────────────────────────────────────────────────────
622
+
623
+ /** The frame style a tile mounted now enters with (tile-grid.js _mountTile). */
624
+ tileEntranceStyle() {
625
+ return this._tileAnimStyle || TILE_ANIM_DEFAULT;
626
+ },
627
+
628
+ /**
629
+ * Holds a just-mounted tile (`.tile--enter-hold`: invisible, not animating)
630
+ * until the next frame, when its cell is final and _runTileEntrances can
631
+ * order it and measure it against its source. `setBackstop(ms)` arms the
632
+ * mount's own timer that ends the entrance should animationend never come
633
+ * (a hidden browser tab, a zoomed grid hiding the tile); armed long at once,
634
+ * so a frame that never comes cannot strand a tile invisible.
635
+ */
636
+ _stageTileEntrance(el, sessionId, setBackstop) {
637
+ el.classList.add('tile--enter-themed', 'tile--enter-hold');
638
+ (this._tileEnterQueue ||= []).push({ el, sessionId, setBackstop });
639
+ setBackstop(4000);
640
+ if (!this._tileEnterRaf) this._tileEnterRaf = requestAnimationFrame(() => this._runTileEntrances());
641
+ },
642
+
643
+ /**
644
+ * One frame after the tiles mounted, every cell is final (openTileGrid packs,
645
+ * then a stored grid moves its tiles back). Each held tile gets its delay
646
+ * from the cascade order and, for `fly`/`deal`, the offset that starts it on
647
+ * its tab or the Tiles button (FLIP: transform only, so the fit it already
648
+ * did at its real size stands). `beam` draws its lines instead.
649
+ */
650
+ _runTileEntrances() {
651
+ this._tileEnterRaf = 0;
652
+ const queue = (this._tileEnterQueue || []).filter((q) => q.el.isConnected);
653
+ this._tileEnterQueue = [];
654
+ if (queue.length === 0) return;
655
+ const def = TILE_ANIM_STYLES.find((s) => s.key === this._tileAnimStyle) || TILE_ANIM_STYLES[0];
656
+ const speed = this._animSpeed || 1;
657
+ const stagger = def.stagger / speed;
658
+ const duration = this._tileAnimDuration();
659
+ const hold = def.key === 'beam' ? BEAM_HOLD_MS / speed : 0;
660
+ const order = this._tileAnimOrder && this._tileAnimOrder !== 'auto' ? this._tileAnimOrder : def.order;
661
+ const ranks = this._tileEnterRanks(
662
+ queue.map((q) => q.sessionId),
663
+ order
664
+ );
665
+ const beams = [];
666
+ queue.forEach((item, k) => {
667
+ const { el, sessionId } = item;
668
+ const delay = ranks[k] * stagger;
669
+ el.style.setProperty('--tile-enter-delay', `${Math.round(delay + hold)}ms`);
670
+ if (def.from) {
671
+ const to = el.getBoundingClientRect();
672
+ const from = this._tileSourceRect(def.from, sessionId);
673
+ if (from && to.width > 0 && to.height > 0) {
674
+ if (def.key === 'beam') beams.push({ from, to, delay });
675
+ else this._setTileFlight(el, from, to, def.key, ranks[k], '--tile-from');
676
+ if (def.from === 'tab') this._flashTileSourceTab(sessionId, delay);
677
+ }
678
+ }
679
+ el.classList.remove('tile--enter-hold');
680
+ // When the frame lands, for a screen whose content arrives earlier.
681
+ el._tileEnterEndsAt = performance.now() + delay + hold + duration;
682
+ item.setBackstop(delay + hold + duration + 900);
683
+ });
684
+ if (beams.length > 0) this._drawTileBeams(beams);
685
+ },
686
+
687
+ /**
688
+ * Cascade steps for `ids` (tiles in this batch) by their cells: `reading`
689
+ * row by row, `wave` by diagonal (row + column), `ripple` by distance from
690
+ * the focused tile. Equal keys share a step, so a diagonal lands together.
691
+ */
692
+ _tileEnterRanks(ids, order) {
693
+ const grid = this._tileGrid;
694
+ const cols = Math.max(1, grid?.cols || 1);
695
+ const cellOf = (id) => (Array.isArray(grid?.cells) ? grid.cells.indexOf(id) : -1);
696
+ const pos = ids.map((id, k) => {
697
+ const c = cellOf(id);
698
+ return c < 0 ? { cell: k, row: 0, col: k } : { cell: c, row: Math.floor(c / cols), col: c % cols };
699
+ });
700
+ let keys;
701
+ if (order === 'wave') {
702
+ keys = pos.map((p) => p.row + p.col);
703
+ } else if (order === 'ripple') {
704
+ const f = cellOf(grid?.focusedId);
705
+ const fr = f < 0 ? 0 : Math.floor(f / cols);
706
+ const fc = f < 0 ? 0 : f % cols;
707
+ keys = pos.map((p) => Math.abs(p.row - fr) + Math.abs(p.col - fc));
708
+ } else {
709
+ keys = pos.map((p) => p.cell);
710
+ }
711
+ const steps = [...new Set(keys)].sort((a, b) => a - b);
712
+ return keys.map((v) => steps.indexOf(v));
713
+ },
714
+
715
+ /** On-screen rect of a tile's source: its session tab (`tab`), else the Tiles button. */
716
+ _tileSourceRect(kind, sessionId) {
717
+ const visible = (node) => {
718
+ const r = node?.getBoundingClientRect?.();
719
+ if (!r || !(r.width > 0 && r.height > 0)) return null;
720
+ return r.bottom > 0 && r.right > 0 && r.top < window.innerHeight && r.left < window.innerWidth ? r : null;
721
+ };
722
+ if (kind === 'tab') {
723
+ const tab = document.querySelector(`.session-tab[data-id="${CSS.escape(sessionId)}"]`);
724
+ const r = visible(tab);
725
+ if (r) return r;
726
+ }
727
+ return visible(document.querySelector('.btn-tile-grid'));
728
+ },
729
+
730
+ /**
731
+ * The transform that puts a tile laid out at `to` onto `from`, as custom
732
+ * properties `<prefix>-x/-y/-sx/-sy/-rot` for the keyframes: the tab's own
733
+ * size for `fly` (it grows out of it), a small card turned a little for
734
+ * `deal`.
735
+ */
736
+ _setTileFlight(el, from, to, kind, rank, prefix) {
737
+ const dx = from.left + from.width / 2 - (to.left + to.width / 2);
738
+ const dy = from.top + from.height / 2 - (to.top + to.height / 2);
739
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
740
+ let sx;
741
+ let sy;
742
+ let rot = 0;
743
+ if (kind === 'deal') {
744
+ sx = sy = clamp((from.width * 1.6) / to.width, 0.04, 0.3);
745
+ rot = [-14, 10, -7, 13, -11, 8][rank % 6];
746
+ } else {
747
+ sx = clamp(from.width / to.width, 0.02, 1);
748
+ sy = clamp(from.height / to.height, 0.02, 1);
749
+ }
750
+ el.style.setProperty(`${prefix}-x`, `${Math.round(dx)}px`);
751
+ el.style.setProperty(`${prefix}-y`, `${Math.round(dy)}px`);
752
+ el.style.setProperty(`${prefix}-sx`, sx.toFixed(4));
753
+ el.style.setProperty(`${prefix}-sy`, sy.toFixed(4));
754
+ el.style.setProperty(`${prefix}-rot`, `${rot}deg`);
755
+ },
756
+
757
+ /** The tab a tile leaves from glows as it goes (`fly`, `beam`). */
758
+ _flashTileSourceTab(sessionId, delay) {
759
+ const tab = document.querySelector(`.session-tab[data-id="${CSS.escape(sessionId)}"]`);
760
+ if (!tab) return;
761
+ tab.style.setProperty('--tab-launch-delay', `${Math.round(delay)}ms`);
762
+ tab.classList.remove('tab-launch');
763
+ void tab.offsetWidth;
764
+ tab.classList.add('tab-launch');
765
+ const onEnd = (e) => {
766
+ if (e.target === tab && /^tab-launch/.test(e.animationName || '')) done();
767
+ };
768
+ const done = () => {
769
+ clearTimeout(timer);
770
+ tab.removeEventListener('animationend', onEnd);
771
+ tab.classList.remove('tab-launch');
772
+ tab.style.removeProperty('--tab-launch-delay');
773
+ };
774
+ const timer = setTimeout(done, delay + 900);
775
+ tab.addEventListener('animationend', onEnd);
776
+ },
777
+
778
+ /**
779
+ * `beam`: a line draws from each tile's tab (or the Tiles button) down into
780
+ * the middle of its tile, in the connection-line look, with a packet riding it
781
+ * when the line style is `packet`; the tile materializes as it lands. Its
782
+ * own overlay: the agent lines' one is rebuilt from scratch on every redraw.
783
+ * The overlay goes once every line has faded.
784
+ */
785
+ _drawTileBeams(beams) {
786
+ const ns = 'http://www.w3.org/2000/svg';
787
+ let svg = document.getElementById('tileBeamLines');
788
+ if (!svg) {
789
+ svg = document.createElementNS(ns, 'svg');
790
+ svg.id = 'tileBeamLines';
791
+ svg.setAttribute('class', 'connection-lines-svg tile-beam-lines');
792
+ svg.setAttribute('aria-hidden', 'true');
793
+ document.body.appendChild(svg);
794
+ }
795
+ const packet = this._lineAnimStyle === 'packet';
796
+ let last = 0;
797
+ for (const { from, to, delay } of beams) {
798
+ const x1 = from.left + from.width / 2;
799
+ const y1 = from.bottom;
800
+ // Into the tile's middle: its top edge sits right under the tab strip,
801
+ // so a beam aimed there ran sideways along the strip instead of down.
802
+ const x2 = to.left + to.width / 2;
803
+ const y2 = to.top + to.height / 2;
804
+ const midY = (y1 + y2) / 2;
805
+ const path = document.createElementNS(ns, 'path');
806
+ path.setAttribute('d', `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`);
807
+ path.setAttribute('class', 'connection-line tile-beam-line');
808
+ svg.appendChild(path);
809
+ const len = Math.max(1, Math.round(path.getTotalLength()));
810
+ path.style.setProperty('--line-len', `${len}px`);
811
+ path.style.setProperty('--line-enter-delay', `${Math.round(delay)}ms`);
812
+ if (packet) {
813
+ const dot = path.cloneNode(false);
814
+ dot.setAttribute('class', 'connection-line-packet');
815
+ svg.appendChild(dot);
816
+ }
817
+ last = Math.max(last, delay);
818
+ }
819
+ clearTimeout(this._tileBeamTimer);
820
+ this._tileBeamTimer = setTimeout(() => svg.remove(), last + 1500 / (this._animSpeed || 1));
821
+ },
822
+
823
+ /**
824
+ * A tile's screen lights up when its first capture lands (tile-grid.js load
825
+ * queue), in the terminal pane's style: the same keyframes as the main pane,
826
+ * on `.tile-body`. Transform, opacity and clip-path (and `blur`'s filter, one
827
+ * tile at a time, since the queue serves one capture at a time), so the
828
+ * xterm inside keeps its size and its fit.
829
+ */
830
+ playTileScreenEntrance(body) {
831
+ if (!body || (this._termAnimStyle || TERM_ANIM_DEFAULT) === 'off') return;
832
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) return;
833
+ body._codemanScreenDone?.();
834
+ // Content that lands while the frame is still flying in waits (held at
835
+ // its first keyframe, so hidden) until the frame has nearly landed: two
836
+ // beats, frame then screen, rather than both at once.
837
+ const frame = body.closest?.('.tile');
838
+ const endsAt = frame?.classList.contains('tile--entering') ? frame._tileEnterEndsAt || 0 : 0;
839
+ const wait = Math.max(0, Math.round(endsAt - performance.now() - 120 / (this._animSpeed || 1)));
840
+ body.style.setProperty('--tile-screen-delay', `${wait}ms`);
841
+ body.classList.remove('term-enter');
842
+ void body.offsetWidth;
843
+ body.classList.add('term-enter');
844
+ let timer = null;
845
+ const done = (e) => {
846
+ if (e && (e.target !== body || e.pseudoElement)) return;
847
+ clearTimeout(timer);
848
+ body.removeEventListener('animationend', done);
849
+ body.removeEventListener('animationcancel', done);
850
+ body.classList.remove('term-enter');
851
+ body.style.removeProperty('--tile-screen-delay');
852
+ if (body._codemanScreenDone === done) body._codemanScreenDone = null;
853
+ };
854
+ body._codemanScreenDone = done;
855
+ body.addEventListener('animationend', done);
856
+ body.addEventListener('animationcancel', done);
857
+ // Backstop, as the main pane's: a backgrounded tab never fires animationend.
858
+ timer = setTimeout(() => done(), wait + this._termAnimDuration() + 900);
859
+ },
860
+
861
+ /**
862
+ * The closing grid's still copy (tile-grid.js _ghostTileGrid) leaves the
863
+ * frame style's own way: `fly` back into each tab, `deal` gathered into the
864
+ * Tiles button, `crt` switched off to a dot, and so on. Copies leave in
865
+ * reading order, so the last one ends last (the layer goes on its
866
+ * animationend). Re-forming the grid (`now`) keeps the plain fade: that copy
867
+ * covers tiles that stay. Returns how long the slowest copy takes, for the
868
+ * layer's fallback timer, or 0 for the default fade.
869
+ */
870
+ _stageTileExit(copies, { now = false } = {}) {
871
+ const style = this._tileAnimStyle || TILE_ANIM_DEFAULT;
872
+ const ms = TILE_EXIT_MS[style];
873
+ if (now || !ms || copies.length === 0) return 0;
874
+ const speed = this._animSpeed || 1;
875
+ const def = TILE_ANIM_STYLES.find((s) => s.key === style);
876
+ copies.forEach(({ ghost, el, sessionId }, k) => {
877
+ ghost.style.setProperty('--tile-exit-delay', `${Math.round((k * TILE_EXIT_STAGGER_MS) / speed)}ms`);
878
+ if (style !== 'fly' && style !== 'deal') return;
879
+ const at = el.getBoundingClientRect();
880
+ const target = this._tileSourceRect(def.from, sessionId);
881
+ if (target && at.width > 0 && at.height > 0) this._setTileFlight(ghost, target, at, style, k, '--tile-to');
882
+ });
883
+ return (ms + copies.length * TILE_EXIT_STAGGER_MS) / speed + 250;
884
+ },
885
+
886
+ /**
887
+ * Lab: replay the open grid's entrance IN PLACE (frames re-enter, screens
888
+ * light up again in queue order): no remount, reconnect or resize. With the
889
+ * grid closed it opens it, the real path.
890
+ */
891
+ _demoTiles() {
892
+ const grid = this._tileGrid;
893
+ if (!grid?.open) {
894
+ if (this.canOpenTileGrid?.()) this.toggleTileGrid?.();
895
+ else this.showToast?.('The tile grid needs a window at least 1180 px wide', 'info');
896
+ return;
897
+ }
898
+ if (!this._tileMotionAllowed?.()) return;
899
+ const ids = grid.ids.slice();
900
+ ids.forEach((id, k) => {
901
+ const entry = grid.tiles.get(id);
902
+ if (entry) this._replayTileEntrance?.(entry.el, id, k);
903
+ });
904
+ // The screens, as the load queue would land them: focused first, then reading order.
905
+ const speed = this._animSpeed || 1;
906
+ const lead = Math.min(this._tileAnimDuration() * 0.55, 420);
907
+ const order = [grid.focusedId, ...ids.filter((id) => id !== grid.focusedId)].filter(Boolean);
908
+ clearTimeout(this._tileDemoTimer);
909
+ const timers = order.map((id, k) =>
910
+ setTimeout(() => this.playTileScreenEntrance(grid.tiles.get(id)?.body), lead + (k * 140) / speed)
911
+ );
912
+ this._tileDemoTimers?.forEach(clearTimeout);
913
+ this._tileDemoTimers = timers;
914
+ },
915
+
916
+ /** Lab: close the grid with its exit, then open it again with its entrance (the real paths). */
917
+ _demoTilesRoundTrip() {
918
+ if (!this._tileGrid?.open) {
919
+ this._demoTiles();
920
+ return;
921
+ }
922
+ this.toggleTileGrid?.();
923
+ clearTimeout(this._tileDemoTimer);
924
+ this._tileDemoTimer = setTimeout(
925
+ () => {
926
+ if (!this._tileGrid?.open) this.toggleTileGrid?.();
927
+ },
928
+ 1400 / (this._animSpeed || 1)
929
+ );
930
+ },
931
+
504
932
  // ── Lab (compare styles without spawning sessions or agents) ───────────────
505
933
 
506
934
  /** Floating picker: switch styles per surface and replay fake entrances. */
@@ -538,6 +966,12 @@ Object.assign(CodemanApp.prototype, {
538
966
  </label>
539
967
  ${group('Agent windows', WIN_ANIM_STYLES, 'win')}
540
968
  ${group('Connection lines', LINE_ANIM_STYLES, 'line')}
969
+ ${group('Tile grid (frames; screens use the pane style)', TILE_ANIM_STYLES, 'tile')}
970
+ <label class="anim-lab-select">Tile order
971
+ <select data-select="tileOrder">
972
+ ${TILE_ANIM_ORDERS.map((o) => `<option value="${o.key}">${escapeHtml(o.label)}</option>`).join('')}
973
+ </select>
974
+ </label>
541
975
  </div>
542
976
  <label class="anim-lab-range">Tab stagger <output data-out="stagger"></output>
543
977
  <input type="range" data-range="stagger" min="0" max="260" step="10">
@@ -552,6 +986,11 @@ Object.assign(CodemanApp.prototype, {
552
986
  <button type="button" data-demo="window">Window</button>
553
987
  <button type="button" data-demo="all">All</button>
554
988
  </div>
989
+ <div class="anim-lab-demo">
990
+ <span>Tiles</span>
991
+ <button type="button" data-demo="tiles">Replay</button>
992
+ <button type="button" data-demo="tiles-roundtrip">Close + reopen</button>
993
+ </div>
555
994
  <p class="anim-lab-hint">Fake tabs, window and line, removed after the run. Real launches use the same timing.</p>
556
995
  `;
557
996
  document.body.appendChild(panel);
@@ -570,11 +1009,17 @@ Object.assign(CodemanApp.prototype, {
570
1009
  if (attr === 'tab') this.setTabAnimStyle(style);
571
1010
  else if (attr === 'win') this.setWinAnimStyle(style);
572
1011
  else if (attr === 'term') this.setTermAnimStyle(style);
1012
+ else if (attr === 'tile') this.setTileAnimStyle(style);
573
1013
  else this.setLineAnimStyle(style);
574
1014
  this._syncAnimLab();
575
- this.demoEntrance({ tab: 'tabs', term: 'term' }[attr] || 'all');
1015
+ this._syncEntranceAnimSetting?.();
1016
+ this.demoEntrance({ tab: 'tabs', term: 'term', tile: 'tiles' }[attr] || 'all');
576
1017
  });
577
1018
  });
1019
+ panel.querySelector('select[data-select="tileOrder"]').addEventListener('change', (e) => {
1020
+ this.setTileAnimOrder(e.target.value);
1021
+ this.demoEntrance('tiles');
1022
+ });
578
1023
  panel.querySelector('input[data-check="termSwitch"]').addEventListener('change', (e) => {
579
1024
  this.setTermAnimOnSwitch(e.target.checked);
580
1025
  });
@@ -601,19 +1046,16 @@ Object.assign(CodemanApp.prototype, {
601
1046
  _syncAnimLab() {
602
1047
  const panel = document.getElementById('animLab');
603
1048
  if (!panel) return;
604
- const current = {
605
- tab: this._tabAnimStyle,
606
- win: this._winAnimStyle,
607
- line: this._lineAnimStyle,
608
- term: this._termAnimStyle,
609
- };
1049
+ const current = this._currentAnimStyles();
610
1050
  panel.querySelectorAll('.anim-lab-style').forEach((btn) => {
611
1051
  btn.classList.toggle('selected', current[btn.dataset.attr] === btn.dataset.style);
612
1052
  });
613
1053
  panel.querySelectorAll('button[data-theme]').forEach((btn) => {
614
1054
  const t = ANIM_THEMES.find((x) => x.key === btn.dataset.theme);
615
- btn.classList.toggle('selected', !!t && ['tab', 'win', 'line', 'term'].every((k) => t[k] === current[k]));
1055
+ btn.classList.toggle('selected', !!t && ANIM_SURFACES.every((k) => t[k] === current[k]));
616
1056
  });
1057
+ const order = panel.querySelector('select[data-select="tileOrder"]');
1058
+ if (order) order.value = this._tileAnimOrder || TILE_ANIM_ORDER_DEFAULT;
617
1059
  const check = panel.querySelector('input[data-check="termSwitch"]');
618
1060
  if (check) check.checked = !!this._termAnimOnSwitch;
619
1061
  panel.querySelector('input[data-range="stagger"]').value = String(this._tabAnimStagger);
@@ -647,10 +1089,15 @@ Object.assign(CodemanApp.prototype, {
647
1089
  return svg;
648
1090
  },
649
1091
 
650
- /** @param {'tabs'|'term'|'window'|'all'} what */
1092
+ /** @param {'tabs'|'term'|'window'|'all'|'tiles'|'tiles-roundtrip'} what */
651
1093
  demoEntrance(what = 'all') {
652
1094
  this._clearEntranceDemo();
653
1095
 
1096
+ if (what === 'tiles') return this._demoTiles();
1097
+ if (what === 'tiles-roundtrip') return this._demoTilesRoundTrip();
1098
+ // With the grid open, the "pane" is every tile's screen.
1099
+ if (what === 'term' && this._tileGrid?.open) return this._demoTiles();
1100
+
654
1101
  // The pane is a real, shared element rather than a throwaway, so replay it
655
1102
  // through the same entry point a real launch uses (bypassing the owed-id
656
1103
  // check, which only exists to keep background sessions from hijacking it).
@@ -692,6 +1139,7 @@ Object.assign(CodemanApp.prototype, {
692
1139
  <span class="tab-number">${base + i + 1}</span>
693
1140
  <span class="tab-status idle" aria-hidden="true"></span>
694
1141
  <span class="tab-info"><span class="tab-name-row">
1142
+ <span class="tab-harness run-mode-dot claude" aria-hidden="true"></span>
695
1143
  <span class="tab-name">w${base + i + 1}-demo</span>
696
1144
  </span></span>`;
697
1145
  container.appendChild(tab);