aicodeman 0.2.9 → 0.3.1

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 (374) hide show
  1. package/README.md +118 -4
  2. package/dist/ai-idle-checker.d.ts.map +1 -1
  3. package/dist/ai-idle-checker.js +3 -2
  4. package/dist/ai-idle-checker.js.map +1 -1
  5. package/dist/ai-plan-checker.d.ts.map +1 -1
  6. package/dist/ai-plan-checker.js +3 -2
  7. package/dist/ai-plan-checker.js.map +1 -1
  8. package/dist/bash-tool-parser.d.ts +2 -3
  9. package/dist/bash-tool-parser.d.ts.map +1 -1
  10. package/dist/bash-tool-parser.js +14 -31
  11. package/dist/bash-tool-parser.js.map +1 -1
  12. package/dist/config/ai-defaults.d.ts +16 -0
  13. package/dist/config/ai-defaults.d.ts.map +1 -0
  14. package/dist/config/ai-defaults.js +16 -0
  15. package/dist/config/ai-defaults.js.map +1 -0
  16. package/dist/config/auth-config.d.ts +19 -0
  17. package/dist/config/auth-config.d.ts.map +1 -0
  18. package/dist/config/auth-config.js +28 -0
  19. package/dist/config/auth-config.js.map +1 -0
  20. package/dist/config/exec-timeout.d.ts +10 -0
  21. package/dist/config/exec-timeout.d.ts.map +1 -0
  22. package/dist/config/exec-timeout.js +10 -0
  23. package/dist/config/exec-timeout.js.map +1 -0
  24. package/dist/config/map-limits.d.ts +4 -0
  25. package/dist/config/map-limits.d.ts.map +1 -1
  26. package/dist/config/map-limits.js +7 -0
  27. package/dist/config/map-limits.js.map +1 -1
  28. package/dist/config/server-timing.d.ts +42 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +57 -0
  31. package/dist/config/server-timing.js.map +1 -0
  32. package/dist/config/team-config.d.ts +16 -0
  33. package/dist/config/team-config.d.ts.map +1 -0
  34. package/dist/config/team-config.js +16 -0
  35. package/dist/config/team-config.js.map +1 -0
  36. package/dist/config/terminal-limits.d.ts +18 -0
  37. package/dist/config/terminal-limits.d.ts.map +1 -0
  38. package/dist/config/terminal-limits.js +18 -0
  39. package/dist/config/terminal-limits.js.map +1 -0
  40. package/dist/config/tunnel-config.d.ts +27 -0
  41. package/dist/config/tunnel-config.d.ts.map +1 -0
  42. package/dist/config/tunnel-config.js +36 -0
  43. package/dist/config/tunnel-config.js.map +1 -0
  44. package/dist/hooks-config.d.ts +21 -6
  45. package/dist/hooks-config.d.ts.map +1 -1
  46. package/dist/hooks-config.js +28 -12
  47. package/dist/hooks-config.js.map +1 -1
  48. package/dist/image-watcher.d.ts +4 -4
  49. package/dist/image-watcher.d.ts.map +1 -1
  50. package/dist/image-watcher.js +17 -30
  51. package/dist/image-watcher.js.map +1 -1
  52. package/dist/index.js +1 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/plan-orchestrator.d.ts +2 -24
  55. package/dist/plan-orchestrator.d.ts.map +1 -1
  56. package/dist/plan-orchestrator.js.map +1 -1
  57. package/dist/prompts/planner.d.ts +7 -8
  58. package/dist/prompts/planner.d.ts.map +1 -1
  59. package/dist/prompts/planner.js +7 -8
  60. package/dist/prompts/planner.js.map +1 -1
  61. package/dist/prompts/research-agent.d.ts +6 -4
  62. package/dist/prompts/research-agent.d.ts.map +1 -1
  63. package/dist/prompts/research-agent.js +6 -4
  64. package/dist/prompts/research-agent.js.map +1 -1
  65. package/dist/push-store.d.ts +1 -1
  66. package/dist/push-store.d.ts.map +1 -1
  67. package/dist/push-store.js +4 -12
  68. package/dist/push-store.js.map +1 -1
  69. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  70. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  71. package/dist/ralph-fix-plan-watcher.js +326 -0
  72. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  73. package/dist/ralph-loop.d.ts +14 -4
  74. package/dist/ralph-loop.d.ts.map +1 -1
  75. package/dist/ralph-loop.js +14 -4
  76. package/dist/ralph-loop.js.map +1 -1
  77. package/dist/ralph-plan-tracker.d.ts +201 -0
  78. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  79. package/dist/ralph-plan-tracker.js +325 -0
  80. package/dist/ralph-plan-tracker.js.map +1 -0
  81. package/dist/ralph-stall-detector.d.ts +84 -0
  82. package/dist/ralph-stall-detector.d.ts.map +1 -0
  83. package/dist/ralph-stall-detector.js +139 -0
  84. package/dist/ralph-stall-detector.js.map +1 -0
  85. package/dist/ralph-status-parser.d.ts +141 -0
  86. package/dist/ralph-status-parser.d.ts.map +1 -0
  87. package/dist/ralph-status-parser.js +478 -0
  88. package/dist/ralph-status-parser.js.map +1 -0
  89. package/dist/ralph-tracker.d.ts +218 -692
  90. package/dist/ralph-tracker.d.ts.map +1 -1
  91. package/dist/ralph-tracker.js +389 -1723
  92. package/dist/ralph-tracker.js.map +1 -1
  93. package/dist/respawn-adaptive-timing.d.ts +61 -0
  94. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  95. package/dist/respawn-adaptive-timing.js +105 -0
  96. package/dist/respawn-adaptive-timing.js.map +1 -0
  97. package/dist/respawn-controller.d.ts +35 -115
  98. package/dist/respawn-controller.d.ts.map +1 -1
  99. package/dist/respawn-controller.js +167 -607
  100. package/dist/respawn-controller.js.map +1 -1
  101. package/dist/respawn-health.d.ts +54 -0
  102. package/dist/respawn-health.d.ts.map +1 -0
  103. package/dist/respawn-health.js +183 -0
  104. package/dist/respawn-health.js.map +1 -0
  105. package/dist/respawn-metrics.d.ts +81 -0
  106. package/dist/respawn-metrics.d.ts.map +1 -0
  107. package/dist/respawn-metrics.js +198 -0
  108. package/dist/respawn-metrics.js.map +1 -0
  109. package/dist/respawn-patterns.d.ts +45 -0
  110. package/dist/respawn-patterns.d.ts.map +1 -0
  111. package/dist/respawn-patterns.js +125 -0
  112. package/dist/respawn-patterns.js.map +1 -0
  113. package/dist/session-auto-ops.d.ts +89 -0
  114. package/dist/session-auto-ops.d.ts.map +1 -0
  115. package/dist/session-auto-ops.js +224 -0
  116. package/dist/session-auto-ops.js.map +1 -0
  117. package/dist/session-cli-builder.d.ts +62 -0
  118. package/dist/session-cli-builder.d.ts.map +1 -0
  119. package/dist/session-cli-builder.js +121 -0
  120. package/dist/session-cli-builder.js.map +1 -0
  121. package/dist/session-manager.d.ts +17 -5
  122. package/dist/session-manager.d.ts.map +1 -1
  123. package/dist/session-manager.js +17 -5
  124. package/dist/session-manager.js.map +1 -1
  125. package/dist/session-task-cache.d.ts +52 -0
  126. package/dist/session-task-cache.d.ts.map +1 -0
  127. package/dist/session-task-cache.js +90 -0
  128. package/dist/session-task-cache.js.map +1 -0
  129. package/dist/session.d.ts +23 -41
  130. package/dist/session.d.ts.map +1 -1
  131. package/dist/session.js +79 -317
  132. package/dist/session.js.map +1 -1
  133. package/dist/state-store.d.ts +19 -9
  134. package/dist/state-store.d.ts.map +1 -1
  135. package/dist/state-store.js +29 -30
  136. package/dist/state-store.js.map +1 -1
  137. package/dist/subagent-watcher.d.ts +26 -7
  138. package/dist/subagent-watcher.d.ts.map +1 -1
  139. package/dist/subagent-watcher.js +47 -64
  140. package/dist/subagent-watcher.js.map +1 -1
  141. package/dist/team-watcher.d.ts.map +1 -1
  142. package/dist/team-watcher.js +2 -5
  143. package/dist/team-watcher.js.map +1 -1
  144. package/dist/tmux-manager.d.ts.map +1 -1
  145. package/dist/tmux-manager.js +1 -2
  146. package/dist/tmux-manager.js.map +1 -1
  147. package/dist/tunnel-manager.d.ts +26 -0
  148. package/dist/tunnel-manager.d.ts.map +1 -1
  149. package/dist/tunnel-manager.js +126 -7
  150. package/dist/tunnel-manager.js.map +1 -1
  151. package/dist/types/api.d.ts +108 -0
  152. package/dist/types/api.d.ts.map +1 -0
  153. package/dist/types/api.js +98 -0
  154. package/dist/types/api.js.map +1 -0
  155. package/dist/types/app-state.d.ts +117 -0
  156. package/dist/types/app-state.d.ts.map +1 -0
  157. package/dist/types/app-state.js +76 -0
  158. package/dist/types/app-state.js.map +1 -0
  159. package/dist/types/common.d.ts +79 -0
  160. package/dist/types/common.d.ts.map +1 -0
  161. package/dist/types/common.js +17 -0
  162. package/dist/types/common.js.map +1 -0
  163. package/dist/types/index.d.ts +66 -0
  164. package/dist/types/index.d.ts.map +1 -0
  165. package/dist/types/index.js +66 -0
  166. package/dist/types/index.js.map +1 -0
  167. package/dist/types/lifecycle.d.ts +28 -0
  168. package/dist/types/lifecycle.d.ts.map +1 -0
  169. package/dist/types/lifecycle.js +16 -0
  170. package/dist/types/lifecycle.js.map +1 -0
  171. package/dist/types/plan.d.ts +45 -0
  172. package/dist/types/plan.d.ts.map +1 -0
  173. package/dist/types/plan.js +18 -0
  174. package/dist/types/plan.js.map +1 -0
  175. package/dist/types/push.d.ts +36 -0
  176. package/dist/types/push.d.ts.map +1 -0
  177. package/dist/types/push.js +18 -0
  178. package/dist/types/push.js.map +1 -0
  179. package/dist/types/ralph.d.ts +262 -0
  180. package/dist/types/ralph.d.ts.map +1 -0
  181. package/dist/types/ralph.js +70 -0
  182. package/dist/types/ralph.js.map +1 -0
  183. package/dist/types/respawn.d.ts +271 -0
  184. package/dist/types/respawn.d.ts.map +1 -0
  185. package/dist/types/respawn.js +26 -0
  186. package/dist/types/respawn.js.map +1 -0
  187. package/dist/types/run-summary.d.ts +96 -0
  188. package/dist/types/run-summary.d.ts.map +1 -0
  189. package/dist/types/run-summary.js +37 -0
  190. package/dist/types/run-summary.js.map +1 -0
  191. package/dist/types/session.d.ts +152 -0
  192. package/dist/types/session.d.ts.map +1 -0
  193. package/dist/types/session.js +27 -0
  194. package/dist/types/session.js.map +1 -0
  195. package/dist/types/task.d.ts +72 -0
  196. package/dist/types/task.d.ts.map +1 -0
  197. package/dist/types/task.js +19 -0
  198. package/dist/types/task.js.map +1 -0
  199. package/dist/types/teams.d.ts +73 -0
  200. package/dist/types/teams.d.ts.map +1 -0
  201. package/dist/types/teams.js +23 -0
  202. package/dist/types/teams.js.map +1 -0
  203. package/dist/types/tools.d.ts +61 -0
  204. package/dist/types/tools.d.ts.map +1 -0
  205. package/dist/types/tools.js +20 -0
  206. package/dist/types/tools.js.map +1 -0
  207. package/dist/types.d.ts +8 -1134
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/types.js +8 -210
  210. package/dist/types.js.map +1 -1
  211. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  212. package/dist/utils/claude-cli-resolver.js +1 -2
  213. package/dist/utils/claude-cli-resolver.js.map +1 -1
  214. package/dist/utils/debouncer.d.ts +111 -0
  215. package/dist/utils/debouncer.d.ts.map +1 -0
  216. package/dist/utils/debouncer.js +162 -0
  217. package/dist/utils/debouncer.js.map +1 -0
  218. package/dist/utils/index.d.ts +3 -2
  219. package/dist/utils/index.d.ts.map +1 -1
  220. package/dist/utils/index.js +3 -2
  221. package/dist/utils/index.js.map +1 -1
  222. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  223. package/dist/utils/opencode-cli-resolver.js +1 -2
  224. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  225. package/dist/utils/string-similarity.d.ts +0 -57
  226. package/dist/utils/string-similarity.d.ts.map +1 -1
  227. package/dist/utils/string-similarity.js +3 -18
  228. package/dist/utils/string-similarity.js.map +1 -1
  229. package/dist/web/middleware/auth.d.ts +31 -0
  230. package/dist/web/middleware/auth.d.ts.map +1 -0
  231. package/dist/web/middleware/auth.js +154 -0
  232. package/dist/web/middleware/auth.js.map +1 -0
  233. package/dist/web/ports/auth-port.d.ts +18 -0
  234. package/dist/web/ports/auth-port.d.ts.map +1 -0
  235. package/dist/web/ports/auth-port.js +6 -0
  236. package/dist/web/ports/auth-port.js.map +1 -0
  237. package/dist/web/ports/config-port.d.ts +28 -0
  238. package/dist/web/ports/config-port.d.ts.map +1 -0
  239. package/dist/web/ports/config-port.js +6 -0
  240. package/dist/web/ports/config-port.js.map +1 -0
  241. package/dist/web/ports/event-port.d.ts +13 -0
  242. package/dist/web/ports/event-port.d.ts.map +1 -0
  243. package/dist/web/ports/event-port.js +6 -0
  244. package/dist/web/ports/event-port.js.map +1 -0
  245. package/dist/web/ports/index.d.ts +14 -0
  246. package/dist/web/ports/index.d.ts.map +1 -0
  247. package/dist/web/ports/index.js +9 -0
  248. package/dist/web/ports/index.js.map +1 -0
  249. package/dist/web/ports/infra-port.d.ts +36 -0
  250. package/dist/web/ports/infra-port.d.ts.map +1 -0
  251. package/dist/web/ports/infra-port.js +6 -0
  252. package/dist/web/ports/infra-port.js.map +1 -0
  253. package/dist/web/ports/respawn-port.d.ts +20 -0
  254. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  255. package/dist/web/ports/respawn-port.js +6 -0
  256. package/dist/web/ports/respawn-port.js.map +1 -0
  257. package/dist/web/ports/session-port.d.ts +15 -0
  258. package/dist/web/ports/session-port.d.ts.map +1 -0
  259. package/dist/web/ports/session-port.js +6 -0
  260. package/dist/web/ports/session-port.js.map +1 -0
  261. package/dist/web/public/api-client.js +82 -0
  262. package/dist/web/public/api-client.js.br +0 -0
  263. package/dist/web/public/api-client.js.gz +0 -0
  264. package/dist/web/public/app.js +117 -201
  265. package/dist/web/public/app.js.br +0 -0
  266. package/dist/web/public/app.js.gz +0 -0
  267. package/dist/web/public/constants.js +365 -0
  268. package/dist/web/public/constants.js.br +0 -0
  269. package/dist/web/public/constants.js.gz +0 -0
  270. package/dist/web/public/index.html +15 -3
  271. package/dist/web/public/index.html.br +0 -0
  272. package/dist/web/public/index.html.gz +0 -0
  273. package/dist/web/public/keyboard-accessory.js +302 -0
  274. package/dist/web/public/keyboard-accessory.js.br +0 -0
  275. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  276. package/dist/web/public/mobile-handlers.js +491 -0
  277. package/dist/web/public/mobile-handlers.js.br +0 -0
  278. package/dist/web/public/mobile-handlers.js.gz +0 -0
  279. package/dist/web/public/mobile.css.gz +0 -0
  280. package/dist/web/public/notification-manager.js +472 -0
  281. package/dist/web/public/notification-manager.js.br +0 -0
  282. package/dist/web/public/notification-manager.js.gz +0 -0
  283. package/dist/web/public/ralph-wizard.js +33 -9
  284. package/dist/web/public/ralph-wizard.js.br +0 -0
  285. package/dist/web/public/ralph-wizard.js.gz +0 -0
  286. package/dist/web/public/styles.css.gz +0 -0
  287. package/dist/web/public/subagent-windows.js +1149 -0
  288. package/dist/web/public/subagent-windows.js.br +0 -0
  289. package/dist/web/public/subagent-windows.js.gz +0 -0
  290. package/dist/web/public/sw.js +15 -0
  291. package/dist/web/public/sw.js.br +0 -0
  292. package/dist/web/public/sw.js.gz +0 -0
  293. package/dist/web/public/upload.html.gz +0 -0
  294. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  295. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  296. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  297. package/dist/web/public/vendor/xterm-zerolag-input.js +4 -0
  298. package/dist/web/public/vendor/xterm-zerolag-input.js.br +0 -0
  299. package/dist/web/public/vendor/xterm-zerolag-input.js.gz +0 -0
  300. package/dist/web/public/vendor/xterm.css.gz +0 -0
  301. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  302. package/dist/web/public/voice-input.js +882 -0
  303. package/dist/web/public/voice-input.js.br +0 -0
  304. package/dist/web/public/voice-input.js.gz +0 -0
  305. package/dist/web/route-helpers.d.ts +38 -0
  306. package/dist/web/route-helpers.d.ts.map +1 -0
  307. package/dist/web/route-helpers.js +144 -0
  308. package/dist/web/route-helpers.js.map +1 -0
  309. package/dist/web/routes/case-routes.d.ts +9 -0
  310. package/dist/web/routes/case-routes.d.ts.map +1 -0
  311. package/dist/web/routes/case-routes.js +426 -0
  312. package/dist/web/routes/case-routes.js.map +1 -0
  313. package/dist/web/routes/file-routes.d.ts +8 -0
  314. package/dist/web/routes/file-routes.d.ts.map +1 -0
  315. package/dist/web/routes/file-routes.js +337 -0
  316. package/dist/web/routes/file-routes.js.map +1 -0
  317. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  318. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  319. package/dist/web/routes/hook-event-routes.js +57 -0
  320. package/dist/web/routes/hook-event-routes.js.map +1 -0
  321. package/dist/web/routes/index.d.ts +16 -0
  322. package/dist/web/routes/index.d.ts.map +1 -0
  323. package/dist/web/routes/index.js +16 -0
  324. package/dist/web/routes/index.js.map +1 -0
  325. package/dist/web/routes/mux-routes.d.ts +8 -0
  326. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  327. package/dist/web/routes/mux-routes.js +32 -0
  328. package/dist/web/routes/mux-routes.js.map +1 -0
  329. package/dist/web/routes/plan-routes.d.ts +9 -0
  330. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  331. package/dist/web/routes/plan-routes.js +385 -0
  332. package/dist/web/routes/plan-routes.js.map +1 -0
  333. package/dist/web/routes/push-routes.d.ts +8 -0
  334. package/dist/web/routes/push-routes.d.ts.map +1 -0
  335. package/dist/web/routes/push-routes.js +49 -0
  336. package/dist/web/routes/push-routes.js.map +1 -0
  337. package/dist/web/routes/ralph-routes.d.ts +9 -0
  338. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  339. package/dist/web/routes/ralph-routes.js +485 -0
  340. package/dist/web/routes/ralph-routes.js.map +1 -0
  341. package/dist/web/routes/respawn-routes.d.ts +8 -0
  342. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  343. package/dist/web/routes/respawn-routes.js +270 -0
  344. package/dist/web/routes/respawn-routes.js.map +1 -0
  345. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  346. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  347. package/dist/web/routes/scheduled-routes.js +51 -0
  348. package/dist/web/routes/scheduled-routes.js.map +1 -0
  349. package/dist/web/routes/session-routes.d.ts +9 -0
  350. package/dist/web/routes/session-routes.d.ts.map +1 -0
  351. package/dist/web/routes/session-routes.js +751 -0
  352. package/dist/web/routes/session-routes.js.map +1 -0
  353. package/dist/web/routes/system-routes.d.ts +9 -0
  354. package/dist/web/routes/system-routes.d.ts.map +1 -0
  355. package/dist/web/routes/system-routes.js +699 -0
  356. package/dist/web/routes/system-routes.js.map +1 -0
  357. package/dist/web/routes/team-routes.d.ts +8 -0
  358. package/dist/web/routes/team-routes.d.ts.map +1 -0
  359. package/dist/web/routes/team-routes.js +14 -0
  360. package/dist/web/routes/team-routes.js.map +1 -0
  361. package/dist/web/schemas.d.ts +43 -3
  362. package/dist/web/schemas.d.ts.map +1 -1
  363. package/dist/web/schemas.js +6 -2
  364. package/dist/web/schemas.js.map +1 -1
  365. package/dist/web/server.d.ts +35 -15
  366. package/dist/web/server.d.ts.map +1 -1
  367. package/dist/web/server.js +563 -3971
  368. package/dist/web/server.js.map +1 -1
  369. package/dist/web/sse-events.d.ts +361 -0
  370. package/dist/web/sse-events.d.ts.map +1 -0
  371. package/dist/web/sse-events.js +396 -0
  372. package/dist/web/sse-events.js.map +1 -0
  373. package/package.json +2 -1
  374. package/scripts/postinstall.js +58 -0
@@ -0,0 +1,472 @@
1
+ /**
2
+ * @fileoverview Five-layer notification system for session events and alerts.
3
+ *
4
+ * The NotificationManager class implements five notification layers:
5
+ * 1. In-app notification drawer (slide-out panel with grouped notifications)
6
+ * 2. Tab title flash (alternating "(*) Codeman" when tab is hidden)
7
+ * 3. Browser Notification API (desktop push with auto-close after 8s)
8
+ * 4. Web Push via service worker (OS-level notifications when tab is closed)
9
+ * 5. Audio alerts (Web Audio API beep, user-opt-in)
10
+ *
11
+ * Features:
12
+ * - Per-event-type preferences (enabled, browser, audio, push) with v1→v4 migration
13
+ * - Device-specific defaults (notifications disabled on mobile by default)
14
+ * - 5s notification grouping window to batch rapid-fire events
15
+ * - 100-notification cap with oldest eviction
16
+ * - Rate limiting: 3s between browser notifications
17
+ * - Visibility tracking (pauses title flash when tab becomes visible)
18
+ * - iOS Safari bfcache support via pageshow event
19
+ *
20
+ * @class NotificationManager
21
+ * @param {CodemanApp} app - Reference to the main app instance
22
+ *
23
+ * @dependency constants.js (STUCK_THRESHOLD_DEFAULT_MS, timing constants)
24
+ * @dependency mobile-handlers.js (MobileDetection.getDeviceType for device-specific defaults)
25
+ * @loadorder 4 of 9 — loaded after voice-input.js, before keyboard-accessory.js
26
+ */
27
+
28
+ // Codeman — Multi-layer notification system
29
+ // Loaded after mobile-handlers.js, before app.js
30
+
31
+ // Notification Manager - Multi-layer browser notification system
32
+ class NotificationManager {
33
+ constructor(app) {
34
+ this.app = app;
35
+ this.notifications = [];
36
+ this.unreadCount = 0;
37
+ this.isTabVisible = !document.hidden;
38
+ this.isDrawerOpen = false;
39
+ this.originalTitle = document.title;
40
+ this.titleFlashInterval = null;
41
+ this.titleFlashState = false;
42
+ this.lastBrowserNotifTime = 0;
43
+ this.audioCtx = null;
44
+ this.renderScheduled = false;
45
+
46
+ // Debounce grouping: Map<key, {notification, timeout}>
47
+ this.groupingMap = new Map();
48
+
49
+ // Load preferences
50
+ this.preferences = this.loadPreferences();
51
+
52
+ // Visibility tracking
53
+ document.addEventListener('visibilitychange', () => {
54
+ this.isTabVisible = !document.hidden;
55
+ if (this.isTabVisible) {
56
+ this.onTabVisible();
57
+ }
58
+ });
59
+ // iOS Safari: pageshow fires on back-forward cache restore (bfcache)
60
+ window.addEventListener('pageshow', (e) => {
61
+ if (e.persisted) {
62
+ this.isTabVisible = true;
63
+ this.onTabVisible();
64
+ }
65
+ });
66
+ }
67
+
68
+ loadPreferences() {
69
+ const defaultEventTypes = {
70
+ permission_prompt: { enabled: true, browser: true, audio: true, push: false },
71
+ elicitation_dialog: { enabled: true, browser: true, audio: true, push: false },
72
+ idle_prompt: { enabled: true, browser: true, audio: false, push: false },
73
+ stop: { enabled: true, browser: false, audio: false, push: false },
74
+ session_error: { enabled: true, browser: true, audio: false, push: false },
75
+ respawn_cycle: { enabled: true, browser: false, audio: false, push: false },
76
+ token_milestone: { enabled: true, browser: false, audio: false, push: false },
77
+ ralph_complete: { enabled: true, browser: true, audio: true, push: false },
78
+ subagent_spawn: { enabled: false, browser: false, audio: false, push: false },
79
+ subagent_complete: { enabled: false, browser: false, audio: false, push: false },
80
+ };
81
+
82
+ // Device-specific defaults: mobile has notifications disabled by default
83
+ const isMobile = MobileDetection.getDeviceType() === 'mobile';
84
+ const defaults = {
85
+ enabled: !isMobile, // Disabled on mobile by default
86
+ browserNotifications: !isMobile,
87
+ audioAlerts: false,
88
+ stuckThresholdMs: STUCK_THRESHOLD_DEFAULT_MS,
89
+ // Legacy urgency muting (keep for backwards compat)
90
+ muteCritical: false,
91
+ muteWarning: false,
92
+ muteInfo: false,
93
+ // Per-event-type preferences
94
+ eventTypes: defaultEventTypes,
95
+ _version: 4,
96
+ };
97
+ try {
98
+ const storageKey = this.getStorageKey();
99
+ const saved = localStorage.getItem(storageKey);
100
+ if (saved) {
101
+ const prefs = JSON.parse(saved);
102
+ // Migrate: v1 had browserNotifications defaulting to false
103
+ if (!prefs._version || prefs._version < 2) {
104
+ prefs.browserNotifications = true;
105
+ prefs._version = 2;
106
+ }
107
+ // Migrate: v2 -> v3 adds eventTypes
108
+ if (prefs._version < 3) {
109
+ prefs.eventTypes = defaultEventTypes;
110
+ prefs._version = 3;
111
+ localStorage.setItem(storageKey, JSON.stringify(prefs));
112
+ }
113
+ // Migrate: v3 -> v4 adds push field to all eventTypes
114
+ if (prefs._version < 4) {
115
+ if (prefs.eventTypes) {
116
+ for (const key of Object.keys(prefs.eventTypes)) {
117
+ if (prefs.eventTypes[key] && prefs.eventTypes[key].push === undefined) {
118
+ prefs.eventTypes[key].push = false;
119
+ }
120
+ }
121
+ }
122
+ prefs._version = 4;
123
+ localStorage.setItem(storageKey, JSON.stringify(prefs));
124
+ }
125
+ // Merge with defaults to ensure all eventTypes exist
126
+ return {
127
+ ...defaults,
128
+ ...prefs,
129
+ eventTypes: { ...defaultEventTypes, ...prefs.eventTypes },
130
+ };
131
+ }
132
+ } catch (_e) { /* ignore */ }
133
+ return defaults;
134
+ }
135
+
136
+ // Get storage key for notification prefs (device-specific)
137
+ getStorageKey() {
138
+ const isMobile = MobileDetection.getDeviceType() === 'mobile';
139
+ return isMobile ? 'codeman-notification-prefs-mobile' : 'codeman-notification-prefs';
140
+ }
141
+
142
+ savePreferences() {
143
+ localStorage.setItem(this.getStorageKey(), JSON.stringify(this.preferences));
144
+ }
145
+
146
+ notify({ urgency, category, sessionId, sessionName, title, message }) {
147
+ if (!this.preferences.enabled) return;
148
+
149
+ // Map notification categories to eventType preference keys
150
+ const categoryToEventType = {
151
+ 'hook-permission': 'permission_prompt',
152
+ 'hook-elicitation': 'elicitation_dialog',
153
+ 'hook-idle': 'idle_prompt',
154
+ 'hook-stop': 'stop',
155
+ 'session-error': 'session_error',
156
+ 'session-crash': 'session_error',
157
+ 'session-stuck': 'idle_prompt',
158
+ 'respawn-blocked': 'respawn_cycle',
159
+ 'auto-accept': 'respawn_cycle',
160
+ 'auto-clear': 'respawn_cycle',
161
+ 'ralph-complete': 'ralph_complete',
162
+ 'circuit-breaker': 'respawn_cycle',
163
+ 'exit-gate': 'ralph_complete',
164
+ 'subagent-spawn': 'subagent_spawn',
165
+ 'subagent-complete': 'subagent_complete',
166
+ 'hook-teammate-idle': 'idle_prompt',
167
+ 'hook-task-completed': 'stop',
168
+ };
169
+ const eventTypeKey = categoryToEventType[category] || category;
170
+
171
+ // Check per-event-type preferences first
172
+ const eventPref = this.preferences.eventTypes?.[eventTypeKey];
173
+ let shouldBrowserNotify = false;
174
+ let shouldAudioAlert = false;
175
+
176
+ if (eventPref) {
177
+ // Event type found - use its specific preferences
178
+ if (!eventPref.enabled) return;
179
+ shouldBrowserNotify = eventPref.browser && this.preferences.browserNotifications;
180
+ shouldAudioAlert = eventPref.audio && this.preferences.audioAlerts;
181
+ } else {
182
+ // Fall back to urgency-based muting for unknown categories
183
+ if (urgency === 'critical' && this.preferences.muteCritical) return;
184
+ if (urgency === 'warning' && this.preferences.muteWarning) return;
185
+ if (urgency === 'info' && this.preferences.muteInfo) return;
186
+ // Default browser/audio behavior based on urgency
187
+ shouldBrowserNotify = this.preferences.browserNotifications &&
188
+ (urgency === 'critical' || urgency === 'warning' || !this.isTabVisible);
189
+ shouldAudioAlert = urgency === 'critical' && this.preferences.audioAlerts;
190
+ }
191
+
192
+ // Grouping: same category+session within 5s updates count instead of new entry
193
+ const groupKey = `${category}:${sessionId || 'global'}`;
194
+ const existing = this.groupingMap.get(groupKey);
195
+ if (existing) {
196
+ existing.notification.count = (existing.notification.count || 1) + 1;
197
+ existing.notification.message = message;
198
+ existing.notification.timestamp = Date.now();
199
+ clearTimeout(existing.timeout);
200
+ existing.timeout = setTimeout(() => this.groupingMap.delete(groupKey), GROUPING_TIMEOUT_MS);
201
+ this.scheduleRender();
202
+ return;
203
+ }
204
+
205
+ const notification = {
206
+ id: Date.now() + '-' + Math.random().toString(36).slice(2, 7),
207
+ urgency,
208
+ category,
209
+ sessionId,
210
+ sessionName,
211
+ title,
212
+ message,
213
+ timestamp: Date.now(),
214
+ read: false,
215
+ count: 1,
216
+ };
217
+
218
+ // Add to log (cap at NOTIFICATION_LIST_CAP)
219
+ this.notifications.unshift(notification);
220
+ if (this.notifications.length > NOTIFICATION_LIST_CAP) this.notifications.pop();
221
+
222
+ // Track for grouping
223
+ const timeout = setTimeout(() => this.groupingMap.delete(groupKey), GROUPING_TIMEOUT_MS);
224
+ this.groupingMap.set(groupKey, { notification, timeout });
225
+
226
+ // Update unread
227
+ this.unreadCount++;
228
+ this.updateBadge();
229
+ this.scheduleRender();
230
+
231
+ // Layer 2: Tab title (when tab unfocused)
232
+ if (!this.isTabVisible) {
233
+ this.updateTabTitle();
234
+ }
235
+
236
+ // Layer 3: Browser notification
237
+ if (shouldBrowserNotify) {
238
+ this.sendBrowserNotif(title, message, category, sessionId);
239
+ }
240
+
241
+ // Layer 4: Audio alert
242
+ if (shouldAudioAlert) {
243
+ this.playAudioAlert();
244
+ }
245
+ }
246
+
247
+ // Layer 1: Drawer rendering
248
+ scheduleRender() {
249
+ if (this.renderScheduled) return;
250
+ this.renderScheduled = true;
251
+ requestAnimationFrame(() => {
252
+ this.renderScheduled = false;
253
+ this.renderDrawer();
254
+ });
255
+ }
256
+
257
+ renderDrawer() {
258
+ const list = document.getElementById('notifList');
259
+ const empty = document.getElementById('notifEmpty');
260
+ if (!list || !empty) return;
261
+
262
+ if (this.notifications.length === 0) {
263
+ list.style.display = 'none';
264
+ empty.style.display = 'flex';
265
+ return;
266
+ }
267
+
268
+ list.style.display = 'block';
269
+ empty.style.display = 'none';
270
+
271
+ list.innerHTML = this.notifications.map(n => {
272
+ const urgencyClass = `notif-item-${n.urgency}`;
273
+ const readClass = n.read ? '' : ' unread';
274
+ const countLabel = n.count > 1 ? `<span class="notif-item-count">&times;${n.count}</span>` : '';
275
+ const sessionChip = n.sessionName ? `<span class="notif-item-session">${escapeHtml(n.sessionName)}</span>` : '';
276
+ return `<div class="notif-item ${urgencyClass}${readClass}" data-notif-id="${n.id}" data-session-id="${n.sessionId || ''}" onclick="app.notificationManager.clickNotification('${escapeHtml(n.id)}')">
277
+ <div class="notif-item-header">
278
+ <span class="notif-item-title">${escapeHtml(n.title)}${countLabel}</span>
279
+ <span class="notif-item-time">${this.relativeTime(n.timestamp)}</span>
280
+ </div>
281
+ <div class="notif-item-message">${escapeHtml(n.message)}</div>
282
+ ${sessionChip}
283
+ </div>`;
284
+ }).join('');
285
+ }
286
+
287
+ // Layer 2: Tab title with unread count
288
+ updateTabTitle() {
289
+ if (this.unreadCount > 0 && !this.isTabVisible) {
290
+ if (!this.titleFlashInterval) {
291
+ this.titleFlashInterval = setInterval(() => {
292
+ this.titleFlashState = !this.titleFlashState;
293
+ document.title = this.titleFlashState
294
+ ? `\u26A0\uFE0F (${this.unreadCount}) Codeman`
295
+ : this.originalTitle;
296
+ }, TITLE_FLASH_INTERVAL_MS);
297
+ // Set immediately
298
+ document.title = `\u26A0\uFE0F (${this.unreadCount}) Codeman`;
299
+ }
300
+ }
301
+ }
302
+
303
+ stopTitleFlash() {
304
+ if (this.titleFlashInterval) {
305
+ clearInterval(this.titleFlashInterval);
306
+ this.titleFlashInterval = null;
307
+ this.titleFlashState = false;
308
+ document.title = this.originalTitle;
309
+ }
310
+ }
311
+
312
+ // Layer 3: Web Notification API
313
+ sendBrowserNotif(title, body, tag, sessionId) {
314
+ if (!this.preferences.browserNotifications) return;
315
+ if (typeof Notification === 'undefined') return;
316
+ if (Notification.permission === 'default') {
317
+ // Auto-request on first notification attempt
318
+ Notification.requestPermission().then(result => {
319
+ if (result === 'granted') {
320
+ // Re-send this notification now that we have permission
321
+ this.sendBrowserNotif(title, body, tag, sessionId);
322
+ }
323
+ });
324
+ return;
325
+ }
326
+ if (Notification.permission !== 'granted') return;
327
+
328
+ // Rate limit
329
+ const now = Date.now();
330
+ if (now - this.lastBrowserNotifTime < BROWSER_NOTIF_RATE_LIMIT_MS) return;
331
+ this.lastBrowserNotifTime = now;
332
+
333
+ const notif = new Notification(`Codeman: ${title}`, {
334
+ body,
335
+ tag, // Groups same-tag notifications
336
+ icon: '/favicon.ico',
337
+ silent: true, // We handle audio ourselves
338
+ });
339
+
340
+ notif.onclick = () => {
341
+ window.focus();
342
+ if (sessionId && this.app.sessions.has(sessionId)) {
343
+ this.app.selectSession(sessionId);
344
+ }
345
+ notif.close();
346
+ };
347
+
348
+ // Auto-close
349
+ setTimeout(() => notif.close(), AUTO_CLOSE_NOTIFICATION_MS);
350
+ }
351
+
352
+ async requestPermission() {
353
+ if (typeof Notification === 'undefined') {
354
+ this.app.showToast('Browser notifications not supported', 'warning');
355
+ return;
356
+ }
357
+ const result = await Notification.requestPermission();
358
+ const statusEl = document.getElementById('notifPermissionStatus');
359
+ if (statusEl) statusEl.textContent = `Status: ${result}`;
360
+ if (result === 'granted') {
361
+ this.preferences.browserNotifications = true;
362
+ this.savePreferences();
363
+ this.app.showToast('Notifications enabled', 'success');
364
+ } else {
365
+ this.app.showToast(`Permission ${result}`, 'warning');
366
+ }
367
+ }
368
+
369
+ // Layer 4: Audio alert via Web Audio API
370
+ playAudioAlert() {
371
+ try {
372
+ if (!this.audioCtx) {
373
+ this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
374
+ }
375
+ if (this.audioCtx.state === 'suspended') {
376
+ this.audioCtx.resume();
377
+ }
378
+ const ctx = this.audioCtx;
379
+ const oscillator = ctx.createOscillator();
380
+ const gain = ctx.createGain();
381
+ oscillator.connect(gain);
382
+ gain.connect(ctx.destination);
383
+ oscillator.type = 'sine';
384
+ oscillator.frequency.setValueAtTime(660, ctx.currentTime);
385
+ gain.gain.setValueAtTime(0.15, ctx.currentTime);
386
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
387
+ oscillator.start(ctx.currentTime);
388
+ oscillator.stop(ctx.currentTime + 0.15);
389
+ } catch (_e) { /* Audio not available */ }
390
+ }
391
+
392
+ // UI interactions
393
+ toggleDrawer() {
394
+ const drawer = document.getElementById('notifDrawer');
395
+ if (!drawer) return;
396
+ this.isDrawerOpen = !this.isDrawerOpen;
397
+ drawer.classList.toggle('open', this.isDrawerOpen);
398
+ if (this.isDrawerOpen) {
399
+ this.renderDrawer();
400
+ }
401
+ }
402
+
403
+ clickNotification(notifId) {
404
+ const notif = this.notifications.find(n => n.id === notifId);
405
+ if (!notif) return;
406
+
407
+ // Mark as read
408
+ if (!notif.read) {
409
+ notif.read = true;
410
+ this.unreadCount = Math.max(0, this.unreadCount - 1);
411
+ this.updateBadge();
412
+ }
413
+
414
+ // Switch to session if available
415
+ if (notif.sessionId && this.app.sessions.has(notif.sessionId)) {
416
+ this.app.selectSession(notif.sessionId);
417
+ this.toggleDrawer();
418
+ }
419
+
420
+ this.scheduleRender();
421
+ }
422
+
423
+ markAllRead() {
424
+ this.notifications.forEach(n => { n.read = true; });
425
+ this.unreadCount = 0;
426
+ this.updateBadge();
427
+ this.stopTitleFlash();
428
+ this.scheduleRender();
429
+ }
430
+
431
+ clearAll() {
432
+ this.notifications = [];
433
+ this.unreadCount = 0;
434
+ this.updateBadge();
435
+ this.stopTitleFlash();
436
+ this.scheduleRender();
437
+ }
438
+
439
+ updateBadge() {
440
+ const badge = document.getElementById('notifBadge');
441
+ if (!badge) return;
442
+ if (this.unreadCount > 0) {
443
+ badge.style.display = 'flex';
444
+ badge.textContent = this.unreadCount > 99 ? '99+' : String(this.unreadCount);
445
+ } else {
446
+ badge.style.display = 'none';
447
+ }
448
+ }
449
+
450
+ onTabVisible() {
451
+ this.stopTitleFlash();
452
+ // If drawer is open, mark all as read
453
+ if (this.isDrawerOpen) {
454
+ this.markAllRead();
455
+ }
456
+ // Re-fit terminal and send resize to PTY so this client's dimensions win.
457
+ // Fixes broken layout when switching between desktop and mobile on the same session.
458
+ if (this.app?.fitAddon && this.app?.activeSessionId) {
459
+ this.app.fitAddon.fit();
460
+ this.app.sendResize(this.app.activeSessionId);
461
+ }
462
+ }
463
+
464
+ // Utilities
465
+ relativeTime(ts) {
466
+ const diff = Math.floor((Date.now() - ts) / 1000);
467
+ if (diff < 60) return 'now';
468
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
469
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
470
+ return `${Math.floor(diff / 86400)}d ago`;
471
+ }
472
+ }
@@ -1,10 +1,30 @@
1
1
  /**
2
- * Ralph Loop Wizard — extracted from app.js for maintainability.
3
- * Extends CodemanApp.prototype with wizard methods.
4
- * Loaded after app.js in index.html.
2
+ * @fileoverview Ralph Loop Wizard — multi-step modal for configuring autonomous task loops.
3
+ *
4
+ * Extends CodemanApp.prototype with wizard methods for the Ralph Loop setup flow:
5
+ * Step 1: Task description, completion phrase, iteration limit, case selection
6
+ * Step 2: AI-powered plan generation (optional) with research agent → planner agent pipeline
7
+ * Step 3: Respawn configuration (idle timeout, kickstart prompt, auto-clear/init)
8
+ * Step 4: Review and launch
9
+ *
10
+ * Features:
11
+ * - Plan generation via POST /api/sessions/:id/plan/generate with SSE progress streaming
12
+ * - Existing @fix_plan.md detection and reuse
13
+ * - Plan detail level selection (brief/detailed/comprehensive)
14
+ * - Case selector population from /api/cases
15
+ * - Focus trap for modal accessibility
16
+ * - Abort controller for cancelling in-flight plan generation
17
+ *
18
+ * @mixin Extends CodemanApp.prototype via Object.assign
19
+ * @dependency app.js (CodemanApp class must be defined)
20
+ * @dependency keyboard-accessory.js (FocusTrap class for modal focus management)
21
+ * @dependency constants.js (escapeHtml)
22
+ * @loadorder 7 of 9 — loaded after app.js, before api-client.js
5
23
  */
6
24
 
7
- // ========== Ralph Loop Wizard ==========
25
+ // ═══════════════════════════════════════════════════════════════
26
+ // Ralph Loop Wizard
27
+ // ═══════════════════════════════════════════════════════════════
8
28
 
9
29
  Object.assign(CodemanApp.prototype, {
10
30
 
@@ -361,7 +381,7 @@ Object.assign(CodemanApp.prototype, {
361
381
  prompt += 'Output `<promise>BLOCKED</promise>` with explanation';
362
382
 
363
383
  // Show preview with highlighting (escape first, then apply formatting)
364
- const escapedPrompt = this.escapeHtml(prompt);
384
+ const escapedPrompt = escapeHtml(prompt);
365
385
  const highlightedPrompt = escapedPrompt
366
386
  .replace(/&lt;promise&gt;/g, '<span class="preview-highlight">&lt;promise&gt;')
367
387
  .replace(/&lt;\/promise&gt;/g, '&lt;/promise&gt;</span>')
@@ -388,7 +408,9 @@ Object.assign(CodemanApp.prototype, {
388
408
  }
389
409
  },
390
410
 
391
- // ========== Plan Generation ==========
411
+ // ═══════════════════════════════════════════════════════════════
412
+ // Plan Generation
413
+ // ═══════════════════════════════════════════════════════════════
392
414
 
393
415
  resetPlanGenerationUI() {
394
416
  // Hide all plan generation states
@@ -724,7 +746,7 @@ Object.assign(CodemanApp.prototype, {
724
746
  <input type="checkbox" class="plan-item-checkbox" ${item.enabled ? 'checked' : ''}
725
747
  onchange="app.togglePlanItem(${index})">
726
748
  ${item.priority ? `<span class="plan-item-priority-badge">${item.priority}</span>` : ''}
727
- <span class="plan-item-text">${this.escapeHtml(item.content)}</span>
749
+ <span class="plan-item-text">${escapeHtml(item.content)}</span>
728
750
  `;
729
751
  fragment.appendChild(row);
730
752
  });
@@ -780,7 +802,9 @@ Object.assign(CodemanApp.prototype, {
780
802
  this.closePlanSubagentWindows();
781
803
  },
782
804
 
783
- // ========== Plan Subagent Windows ==========
805
+ // ═══════════════════════════════════════════════════════════════
806
+ // Plan Subagent Windows
807
+ // ═══════════════════════════════════════════════════════════════
784
808
 
785
809
  handlePlanSubagentEvent(event) {
786
810
  if (this.planGenerationStopped) return;
@@ -847,7 +871,7 @@ Object.assign(CodemanApp.prototype, {
847
871
  <div class="plan-subagent-header">
848
872
  <span>
849
873
  <span class="plan-subagent-icon">${typeIcons[agentType] || '🤖'}</span>
850
- <span class="plan-subagent-title">${typeLabels[agentType] || this.escapeHtml(agentType)}</span>
874
+ <span class="plan-subagent-title">${typeLabels[agentType] || escapeHtml(agentType)}</span>
851
875
  </span>
852
876
  <span class="plan-subagent-model">${model}</span>
853
877
  </div>
Binary file
Binary file
Binary file