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