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,1149 @@
1
+ /**
2
+ * @fileoverview Subagent floating window management mixed into CodemanApp.prototype.
3
+ *
4
+ * Extends CodemanApp with methods for managing floating terminal windows that display
5
+ * Claude Code background agent (subagent) output. Each subagent window has its own
6
+ * xterm.js terminal instance, drag/resize handles, minimize/close controls, and
7
+ * connection lines drawn to the parent session tab.
8
+ *
9
+ * Key functionality:
10
+ * - Tab badge dropdown showing minimized agents per session
11
+ * - Minimize/restore/permanently-close lifecycle for subagent windows
12
+ * - Cross-browser state persistence (localStorage + server-backed PUT /api/subagent-window-states)
13
+ * - Window state saved on every minimize/restore/close action
14
+ *
15
+ * @mixin Extends CodemanApp.prototype via Object.assign
16
+ * @dependency app.js (CodemanApp class, this.subagents, this.subagentWindows, this.minimizedSubagents)
17
+ * @dependency constants.js (escapeHtml)
18
+ * @loadorder 9 of 9 — loaded last, after api-client.js
19
+ */
20
+
21
+ // Codeman — Subagent window management for CodemanApp
22
+ // Loaded after app.js (needs CodemanApp class defined)
23
+
24
+ Object.assign(CodemanApp.prototype, {
25
+ // Render subagent badge with dropdown for minimized agents on a tab
26
+ renderSubagentTabBadge(sessionId, minimizedAgents) {
27
+ if (!minimizedAgents || minimizedAgents.size === 0) return '';
28
+
29
+ const agentItems = [];
30
+ for (const agentId of minimizedAgents) {
31
+ const agent = this.subagents.get(agentId);
32
+ const displayName = agent?.description || agentId.substring(0, 12);
33
+ const truncatedName = displayName.length > 25 ? displayName.substring(0, 25) + '…' : displayName;
34
+ const statusClass = agent?.status || 'idle';
35
+ agentItems.push(`
36
+ <div class="subagent-dropdown-item" onclick="event.stopPropagation(); app.restoreMinimizedSubagent('${escapeHtml(agentId)}', '${escapeHtml(sessionId)}')" title="Click to restore">
37
+ <span class="subagent-dropdown-status ${statusClass}"></span>
38
+ <span class="subagent-dropdown-name">${escapeHtml(truncatedName)}</span>
39
+ <span class="subagent-dropdown-close" onclick="event.stopPropagation(); app.permanentlyCloseMinimizedSubagent('${escapeHtml(agentId)}', '${escapeHtml(sessionId)}')" title="Dismiss">&times;</span>
40
+ </div>
41
+ `);
42
+ }
43
+
44
+ // Compact badge - shows on hover, click to pin open
45
+ const count = minimizedAgents.size;
46
+ const label = count === 1 ? 'AGENT' : `AGENTS (${count})`;
47
+ return `
48
+ <span class="tab-subagent-badge"
49
+ onmouseenter="app.showSubagentDropdown(this)"
50
+ onmouseleave="app.scheduleHideSubagentDropdown(this)"
51
+ onclick="event.stopPropagation(); app.pinSubagentDropdown(this);">
52
+ <span class="subagent-label">${label}</span>
53
+ <div class="subagent-dropdown" onmouseenter="app.cancelHideSubagentDropdown()" onmouseleave="app.scheduleHideSubagentDropdown(this.parentElement)">
54
+ ${agentItems.join('')}
55
+ </div>
56
+ </span>
57
+ `;
58
+ },
59
+
60
+ // Restore a minimized subagent window
61
+ restoreMinimizedSubagent(agentId, sessionId) {
62
+ // Remove from minimized set
63
+ const minimizedAgents = this.minimizedSubagents.get(sessionId);
64
+ if (minimizedAgents) {
65
+ minimizedAgents.delete(agentId);
66
+ if (minimizedAgents.size === 0) {
67
+ this.minimizedSubagents.delete(sessionId);
68
+ }
69
+ }
70
+
71
+ // Restore the window
72
+ this.restoreSubagentWindow(agentId);
73
+
74
+ // Re-render tabs to update badge
75
+ this.renderSessionTabs();
76
+
77
+ // Persist the state change
78
+ this.saveSubagentWindowStates();
79
+ },
80
+
81
+ // Permanently close a minimized subagent (remove from DOM and minimized set)
82
+ permanentlyCloseMinimizedSubagent(agentId, sessionId) {
83
+ // Remove from minimized set
84
+ const minimizedAgents = this.minimizedSubagents.get(sessionId);
85
+ if (minimizedAgents) {
86
+ minimizedAgents.delete(agentId);
87
+ if (minimizedAgents.size === 0) {
88
+ this.minimizedSubagents.delete(sessionId);
89
+ }
90
+ }
91
+
92
+ // Force close the window (removes from DOM)
93
+ this.forceCloseSubagentWindow(agentId);
94
+
95
+ // Re-render tabs to update badge
96
+ this.renderSessionTabs();
97
+ this.updateConnectionLines();
98
+
99
+ // Persist the state change
100
+ this.saveSubagentWindowStates();
101
+ },
102
+
103
+ // ═══════════════════════════════════════════════════════════════
104
+ // Subagent Window State Persistence
105
+ // ═══════════════════════════════════════════════════════════════
106
+
107
+ /**
108
+ * Save subagent window states (minimized/open) to server for cross-browser persistence.
109
+ * Called when a window is minimized, restored, or auto-minimized on completion.
110
+ */
111
+ async saveSubagentWindowStates() {
112
+ // Build state object: which agents are minimized per session
113
+ const minimizedState = {};
114
+ for (const [sessionId, agentIds] of this.minimizedSubagents) {
115
+ minimizedState[sessionId] = Array.from(agentIds);
116
+ }
117
+
118
+ // Also track which windows are open (not minimized)
119
+ const openWindows = [];
120
+ for (const [agentId, windowData] of this.subagentWindows) {
121
+ if (!windowData.minimized) {
122
+ openWindows.push({
123
+ agentId,
124
+ position: windowData.position || null
125
+ });
126
+ }
127
+ }
128
+
129
+ const windowStates = { minimized: minimizedState, open: openWindows };
130
+
131
+ // Save to localStorage for quick restore
132
+ localStorage.setItem('codeman-subagent-window-states', JSON.stringify(windowStates));
133
+
134
+ // Save to server for cross-browser persistence
135
+ try {
136
+ await this._apiPut('/api/subagent-window-states', windowStates);
137
+ } catch (err) {
138
+ console.error('Failed to save subagent window states to server:', err);
139
+ }
140
+ },
141
+
142
+ /**
143
+ * Restore subagent window states after loading subagents.
144
+ * Opens windows that were open before, keeps minimized ones minimized.
145
+ * IMPORTANT: Parent associations are loaded from subagentParentMap BEFORE this is called.
146
+ */
147
+ async restoreSubagentWindowStates() {
148
+ const states = await this.loadSubagentWindowStates();
149
+
150
+ // Restore minimized state using the PERSISTENT parent map
151
+ // Skip old agents from previous runs to avoid confusion
152
+ const cutoffTime = Date.now() - 10 * 60 * 1000; // 10 minutes
153
+ for (const [savedSessionId, agentIds] of Object.entries(states.minimized || {})) {
154
+ if (Array.isArray(agentIds) && agentIds.length > 0) {
155
+ for (const agentId of agentIds) {
156
+ const agent = this.subagents.get(agentId);
157
+ if (!agent) continue; // Agent no longer exists
158
+
159
+ // Skip completed or old agents
160
+ const agentStartTime = agent.startedAt || 0;
161
+ if (agent.status === 'completed' || agentStartTime < cutoffTime) continue;
162
+
163
+ // Use the PERSISTENT parent map (THE source of truth)
164
+ // Fall back to saved sessionId only if it exists in current sessions
165
+ const parentFromMap = this.subagentParentMap.get(agentId);
166
+ const correctSessionId = parentFromMap ||
167
+ (this.sessions.has(savedSessionId) ? savedSessionId : null);
168
+
169
+ if (correctSessionId) {
170
+ // Ensure the parent map has this association
171
+ if (!parentFromMap && this.sessions.has(savedSessionId)) {
172
+ this.setAgentParentSessionId(agentId, savedSessionId);
173
+ }
174
+
175
+ if (!this.minimizedSubagents.has(correctSessionId)) {
176
+ this.minimizedSubagents.set(correctSessionId, new Set());
177
+ }
178
+ this.minimizedSubagents.get(correctSessionId).add(agentId);
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ // Restore open windows (for recent, non-completed agents only)
185
+ const now = Date.now();
186
+ const maxAgeMs = 10 * 60 * 1000; // 10 minutes - don't restore windows for old agents
187
+ for (const { agentId, position } of (states.open || [])) {
188
+ const agent = this.subagents.get(agentId);
189
+ // Only restore window if agent exists, is recent, and is still active/idle
190
+ const agentAge = agent?.startedAt ? now - agent.startedAt : Infinity;
191
+ if (agent && agent.status !== 'completed' && agentAge < maxAgeMs) {
192
+ this.openSubagentWindow(agentId);
193
+ // Restore position if saved (with viewport bounds check)
194
+ if (position) {
195
+ const windowData = this.subagentWindows.get(agentId);
196
+ if (windowData && windowData.element) {
197
+ // Parse position values and clamp to viewport
198
+ let left = parseInt(position.left, 10) || 50;
199
+ let top = parseInt(position.top, 10) || WINDOW_INITIAL_TOP_PX;
200
+ const viewportWidth = window.innerWidth;
201
+ const viewportHeight = window.innerHeight;
202
+ const windowWidth = 420;
203
+ const windowHeight = 350;
204
+ left = Math.max(10, Math.min(left, viewportWidth - windowWidth - 10));
205
+ top = Math.max(10, Math.min(top, viewportHeight - windowHeight - 10));
206
+ windowData.element.style.left = `${left}px`;
207
+ windowData.element.style.top = `${top}px`;
208
+ windowData.position = { left: `${left}px`, top: `${top}px` };
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ this.renderSessionTabs(); // Update tab badges
215
+ this.saveSubagentWindowStates(); // Persist corrected mappings
216
+
217
+ // Update connection lines after all windows are restored (use rAF to ensure DOM is ready)
218
+ requestAnimationFrame(() => {
219
+ this.updateConnectionLines();
220
+ });
221
+ },
222
+
223
+ // ═══════════════════════════════════════════════════════════════
224
+ // Subagent Connection Lines
225
+ // ═══════════════════════════════════════════════════════════════
226
+ //
227
+ // Connection lines are drawn from agent windows to their parent TABs.
228
+ // The parent TAB is determined by the PERSISTENT subagentParentMap.
229
+ // This map stores agentId -> sessionId, where sessionId is the tab's data-id.
230
+
231
+ updateConnectionLines() {
232
+ // Coalesce multiple calls — uses background scheduler priority to avoid
233
+ // competing with terminal writes for main thread time
234
+ if (!this._connectionLinesScheduled) {
235
+ this._connectionLinesScheduled = true;
236
+ scheduleBackground(() => {
237
+ this._connectionLinesScheduled = false;
238
+ this._updateConnectionLinesImmediate();
239
+ });
240
+ }
241
+ },
242
+
243
+ _updateConnectionLinesImmediate() {
244
+ const svg = document.getElementById('connectionLines');
245
+ if (!svg) return;
246
+
247
+ // Check if Ralph wizard modal is open
248
+ const wizardModal = document.getElementById('ralphWizardModal');
249
+ const wizardOpen = wizardModal?.classList.contains('active');
250
+ const wizardContent = wizardOpen ? wizardModal.querySelector('.modal-content') : null;
251
+
252
+ // Collect visible regular subagent windows
253
+ const visibleSubagentWindows = [];
254
+ for (const [agentId, windowInfo] of this.subagentWindows) {
255
+ if (windowInfo.minimized || windowInfo.hidden) continue;
256
+ const win = windowInfo.element;
257
+ if (!win) continue;
258
+ visibleSubagentWindows.push({ agentId, windowInfo, win });
259
+ }
260
+
261
+ // Get plan subagent windows as array for distribution
262
+ const planSubagentArray = Array.from(this.planSubagents.entries())
263
+ .filter(([, data]) => data.element)
264
+ .map(([id, data]) => ({ id, ...data }));
265
+
266
+ // === PHASE 1: Batch all layout reads (getBoundingClientRect) ===
267
+ // Reading layout properties forces the browser to calculate layout.
268
+ // By batching all reads before any writes, we avoid repeated forced reflows.
269
+ const rects = new Map();
270
+
271
+ // Read all subagent window rects
272
+ for (const { agentId, win } of visibleSubagentWindows) {
273
+ rects.set('sub:' + agentId, win.getBoundingClientRect());
274
+ }
275
+
276
+ // Read all plan subagent rects
277
+ for (const planAgent of planSubagentArray) {
278
+ rects.set('plan:' + planAgent.id, planAgent.element.getBoundingClientRect());
279
+ }
280
+
281
+ // Read wizard rect (if open)
282
+ let wizardRect = null;
283
+ if (wizardOpen && wizardContent) {
284
+ wizardRect = wizardContent.getBoundingClientRect();
285
+ }
286
+
287
+ // Read tab rects for normal mode (only tabs that are actually needed)
288
+ if (!wizardOpen) {
289
+ for (const { agentId } of visibleSubagentWindows) {
290
+ const parentSessionId = this.subagentParentMap.get(agentId);
291
+ if (!parentSessionId || rects.has('tab:' + parentSessionId)) continue;
292
+ const tab = document.querySelector(`.session-tab[data-id="${parentSessionId}"]`);
293
+ if (tab) rects.set('tab:' + parentSessionId, tab.getBoundingClientRect());
294
+ }
295
+ }
296
+
297
+ // Read plan window rects for wizard-to-plan lines
298
+ if (wizardOpen && wizardContent && this.planSubagents.size > 0 && !this.planAgentsMinimized) {
299
+ for (const [agentId, windowData] of this.planSubagents) {
300
+ if (!windowData.element) continue;
301
+ const key = 'planwin:' + agentId;
302
+ if (!rects.has(key)) rects.set(key, windowData.element.getBoundingClientRect());
303
+ }
304
+ }
305
+
306
+ // === PHASE 2: DOM writes using cached rects (no more layout reads) ===
307
+ svg.innerHTML = '';
308
+
309
+ for (const { agentId } of visibleSubagentWindows) {
310
+ const winRect = rects.get('sub:' + agentId);
311
+
312
+ // If wizard is open with plan subagents, connect regular subagents to plan subagent windows
313
+ if (wizardOpen && wizardContent && planSubagentArray.length > 0) {
314
+ // Find the nearest plan subagent window to connect to
315
+ let nearestPlanAgent = null;
316
+ let nearestDistance = Infinity;
317
+
318
+ for (const planAgent of planSubagentArray) {
319
+ const planRect = rects.get('plan:' + planAgent.id);
320
+ const planCenterX = planRect.left + planRect.width / 2;
321
+ const planCenterY = planRect.top + planRect.height / 2;
322
+ const winCenterX = winRect.left + winRect.width / 2;
323
+ const winCenterY = winRect.top + winRect.height / 2;
324
+ const distance = Math.hypot(planCenterX - winCenterX, planCenterY - winCenterY);
325
+
326
+ if (distance < nearestDistance) {
327
+ nearestDistance = distance;
328
+ nearestPlanAgent = planAgent;
329
+ }
330
+ }
331
+
332
+ if (nearestPlanAgent) {
333
+ const planRect = rects.get('plan:' + nearestPlanAgent.id);
334
+
335
+ // Draw line from plan subagent window to regular subagent window
336
+ let x1, y1, x2, y2;
337
+ const planCenterX = planRect.left + planRect.width / 2;
338
+ const winCenterX = winRect.left + winRect.width / 2;
339
+
340
+ if (winCenterX < planCenterX) {
341
+ x1 = planRect.left;
342
+ y1 = planRect.top + planRect.height / 2;
343
+ x2 = winRect.right;
344
+ y2 = winRect.top + winRect.height / 2;
345
+ } else {
346
+ x1 = planRect.right;
347
+ y1 = planRect.top + planRect.height / 2;
348
+ x2 = winRect.left;
349
+ y2 = winRect.top + winRect.height / 2;
350
+ }
351
+
352
+ const midX = (x1 + x2) / 2;
353
+ const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
354
+
355
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
356
+ line.setAttribute('d', path);
357
+ line.setAttribute('class', 'connection-line plan-to-subagent-line');
358
+ line.setAttribute('data-agent-id', agentId);
359
+ line.setAttribute('data-plan-agent-id', nearestPlanAgent.id);
360
+ svg.appendChild(line);
361
+ }
362
+ } else if (wizardOpen && wizardContent) {
363
+ // Wizard open but no plan subagents - connect directly to wizard
364
+ const winCenterX = winRect.left + winRect.width / 2;
365
+ const wizardCenterX = wizardRect.left + wizardRect.width / 2;
366
+
367
+ let x1, y1, x2, y2;
368
+
369
+ if (winCenterX < wizardCenterX) {
370
+ x1 = wizardRect.left;
371
+ y1 = wizardRect.top + wizardRect.height / 2;
372
+ x2 = winRect.right;
373
+ y2 = winRect.top + winRect.height / 2;
374
+ } else {
375
+ x1 = wizardRect.right;
376
+ y1 = wizardRect.top + wizardRect.height / 2;
377
+ x2 = winRect.left;
378
+ y2 = winRect.top + winRect.height / 2;
379
+ }
380
+
381
+ const midX = (x1 + x2) / 2;
382
+ const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
383
+
384
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
385
+ line.setAttribute('d', path);
386
+ line.setAttribute('class', 'connection-line wizard-connection');
387
+ line.setAttribute('data-agent-id', agentId);
388
+ svg.appendChild(line);
389
+ } else {
390
+ // NORMAL MODE: Connect agent window to its parent TAB
391
+ // Use the PERSISTENT subagentParentMap as the ONLY source of truth
392
+ const parentSessionId = this.subagentParentMap.get(agentId);
393
+
394
+ if (!parentSessionId) {
395
+ // No parent known yet - skip this agent
396
+ continue;
397
+ }
398
+
399
+ const tabRect = rects.get('tab:' + parentSessionId);
400
+ if (!tabRect) {
401
+ // Tab not in DOM (might be scrolled out or session closed)
402
+ continue;
403
+ }
404
+
405
+ // Draw curved line from TAB bottom-center to window top-center
406
+ const x1 = tabRect.left + tabRect.width / 2;
407
+ const y1 = tabRect.bottom;
408
+ const x2 = winRect.left + winRect.width / 2;
409
+ const y2 = winRect.top;
410
+
411
+ // Bezier curve control points for smooth curve
412
+ const midY = (y1 + y2) / 2;
413
+ const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
414
+
415
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
416
+ line.setAttribute('d', path);
417
+ line.setAttribute('class', 'connection-line');
418
+ line.setAttribute('data-agent-id', agentId);
419
+ line.setAttribute('data-parent-tab', parentSessionId);
420
+ svg.appendChild(line);
421
+ }
422
+ }
423
+
424
+ // Draw lines from wizard to plan subagent windows (Opus agents during plan generation)
425
+ // Skip if agents are minimized to tab
426
+ if (wizardOpen && wizardContent && this.planSubagents.size > 0 && !this.planAgentsMinimized) {
427
+ for (const [agentId] of this.planSubagents) {
428
+ const winRect = rects.get('planwin:' + agentId);
429
+ if (!winRect) continue;
430
+
431
+ // Determine which side of wizard the window is on
432
+ const winCenterX = winRect.left + winRect.width / 2;
433
+ const wizardCenterX = wizardRect.left + wizardRect.width / 2;
434
+
435
+ let x1, y1, x2, y2;
436
+
437
+ if (winCenterX < wizardCenterX) {
438
+ x1 = wizardRect.left;
439
+ y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50);
440
+ x2 = winRect.right;
441
+ y2 = winRect.top + winRect.height / 2;
442
+ } else {
443
+ x1 = wizardRect.right;
444
+ y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50);
445
+ x2 = winRect.left;
446
+ y2 = winRect.top + winRect.height / 2;
447
+ }
448
+
449
+ const midX = (x1 + x2) / 2;
450
+ const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
451
+
452
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
453
+ line.setAttribute('d', path);
454
+ line.setAttribute('class', 'connection-line wizard-connection plan-subagent-line');
455
+ line.setAttribute('data-plan-agent-id', agentId);
456
+ svg.appendChild(line);
457
+ }
458
+ }
459
+ },
460
+
461
+ // ═══════════════════════════════════════════════════════════════
462
+ // Subagent Floating Windows
463
+ // ═══════════════════════════════════════════════════════════════
464
+
465
+ openSubagentWindow(agentId) {
466
+ // If window already exists, focus it
467
+ if (this.subagentWindows.has(agentId)) {
468
+ const existing = this.subagentWindows.get(agentId);
469
+ const agent = this.subagents.get(agentId);
470
+ const settings = this.loadAppSettingsFromStorage();
471
+ const activeTabOnly = settings.subagentActiveTabOnly ?? true;
472
+
473
+ // If window is hidden (different tab) and activeTabOnly is enabled, switch to parent tab
474
+ if (existing.hidden && agent?.parentSessionId && activeTabOnly) {
475
+ this.selectSession(agent.parentSessionId);
476
+ return;
477
+ }
478
+
479
+ // If not activeTabOnly mode, just show the window
480
+ if (existing.hidden && !activeTabOnly) {
481
+ existing.element.style.display = 'flex';
482
+ existing.hidden = false;
483
+ }
484
+
485
+ existing.element.style.zIndex = ++this.subagentWindowZIndex;
486
+ if (existing.minimized) {
487
+ this.restoreSubagentWindow(agentId);
488
+ }
489
+ return;
490
+ }
491
+
492
+ const agent = this.subagents.get(agentId);
493
+ if (!agent) return;
494
+
495
+ // Only open windows for agents that belong to a Codeman-managed session tab.
496
+ // Agents from external Claude sessions (not tracked by Codeman) should not pop up.
497
+ if (agent.sessionId) {
498
+ const hasMatchingTab = Array.from(this.sessions.values()).some(s => s.claudeSessionId === agent.sessionId);
499
+ if (!hasMatchingTab) return;
500
+ }
501
+
502
+ // Calculate final position - grid layout to avoid overlaps
503
+ const windowCount = this.subagentWindows.size;
504
+ const isMobile = MobileDetection.getDeviceType() === 'mobile';
505
+ const mobileCardHeight = 110;
506
+ const mobileCardGap = 4;
507
+ const windowWidth = isMobile ? window.innerWidth : 420;
508
+ const windowHeight = isMobile ? mobileCardHeight : 350;
509
+ const gap = 20;
510
+ const viewportWidth = window.innerWidth;
511
+ const viewportHeight = window.innerHeight;
512
+
513
+ let finalX = 0;
514
+ let finalY = 0;
515
+
516
+ if (isMobile) {
517
+ // Mobile: stack compact cards. Count visible (non-minimized) windows.
518
+ let visibleCount = 0;
519
+ for (const [, data] of this.subagentWindows) {
520
+ if (!data.minimized && !data.hidden) visibleCount++;
521
+ }
522
+ finalX = 4;
523
+ const keyboardUp = typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible;
524
+ if (keyboardUp) {
525
+ // Keyboard visible: stack from bottom above toolbar
526
+ const toolbarHeight = 40;
527
+ const bottomOffset = toolbarHeight + visibleCount * (mobileCardHeight + mobileCardGap);
528
+ finalY = viewportHeight - bottomOffset - mobileCardHeight;
529
+ } else {
530
+ // Keyboard hidden: stack from top below header with spacing
531
+ const headerHeight = document.querySelector('.header')?.offsetHeight || 36;
532
+ const topStart = headerHeight + 8;
533
+ finalY = topStart + visibleCount * (mobileCardHeight + mobileCardGap);
534
+ }
535
+ } else {
536
+ // Check if Ralph wizard modal is open - if so, position windows on the sides
537
+ const wizardModal = document.getElementById('ralphWizardModal');
538
+ const wizardOpen = wizardModal?.classList.contains('active');
539
+
540
+ let startX, startY, maxCols;
541
+
542
+ if (wizardOpen) {
543
+ // Wizard is ~720px wide, centered. Position windows on left/right sides
544
+ const wizardWidth = 720;
545
+ const centerX = viewportWidth / 2;
546
+ const wizardLeft = centerX - wizardWidth / 2;
547
+ const wizardRight = centerX + wizardWidth / 2;
548
+
549
+ // Alternate between left and right sides of the wizard
550
+ const leftSideSpace = wizardLeft - 20;
551
+ const rightSideSpace = viewportWidth - wizardRight - 20;
552
+
553
+ if (windowCount % 2 === 0 && rightSideSpace >= windowWidth) {
554
+ // Even windows go to the right
555
+ startX = wizardRight + 20;
556
+ maxCols = Math.floor(rightSideSpace / (windowWidth + gap)) || 1;
557
+ } else if (leftSideSpace >= windowWidth) {
558
+ // Odd windows go to the left
559
+ startX = Math.max(10, wizardLeft - windowWidth - 20);
560
+ maxCols = 1; // Usually only room for 1 column on left
561
+ } else {
562
+ // Not enough side space, use right side
563
+ startX = wizardRight + 20;
564
+ maxCols = 1;
565
+ }
566
+ startY = 80; // Start higher when wizard is open
567
+ } else {
568
+ // Normal positioning
569
+ startX = 50;
570
+ startY = WINDOW_INITIAL_TOP_PX;
571
+ maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1;
572
+ }
573
+
574
+ const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1;
575
+ const col = windowCount % maxCols;
576
+ const row = Math.floor(windowCount / maxCols) % maxRows; // Wrap rows to stay in viewport
577
+ finalX = startX + col * (windowWidth + gap);
578
+ finalY = startY + row * (windowHeight + gap);
579
+
580
+ // Ensure window stays within viewport bounds
581
+ finalX = Math.max(10, Math.min(finalX, viewportWidth - windowWidth - 10));
582
+ finalY = Math.max(10, Math.min(finalY, viewportHeight - windowHeight - 10));
583
+ }
584
+
585
+ // Get parent session from PERSISTENT map (THE source of truth for tab connections)
586
+ const parentSessionId = this.subagentParentMap.get(agentId) || agent.parentSessionId;
587
+ let parentSessionName = null;
588
+
589
+ if (parentSessionId) {
590
+ const parentSession = this.sessions.get(parentSessionId);
591
+ if (parentSession) {
592
+ parentSessionName = this.getSessionName(parentSession);
593
+ // Ensure the agent object is also updated for consistency
594
+ if (!agent.parentSessionId) {
595
+ agent.parentSessionId = parentSessionId;
596
+ agent.parentSessionName = parentSessionName;
597
+ this.subagents.set(agentId, agent);
598
+ }
599
+ }
600
+ }
601
+
602
+ // Get parent TAB element for spawn animation
603
+ const parentTab = parentSessionId
604
+ ? document.querySelector(`.session-tab[data-id="${parentSessionId}"]`)
605
+ : null;
606
+
607
+ // Create window element
608
+ const win = document.createElement('div');
609
+ win.className = 'subagent-window';
610
+ win.id = `subagent-window-${agentId}`;
611
+ win.style.zIndex = ++this.subagentWindowZIndex;
612
+
613
+ // Build parent header if we have parent info
614
+ const parentHeader = parentSessionId && parentSessionName
615
+ ? `<div class="subagent-window-parent" data-parent-session="${parentSessionId}">
616
+ <span class="parent-label">from</span>
617
+ <span class="parent-name" onclick="app.selectSession('${escapeHtml(parentSessionId)}')">${escapeHtml(parentSessionName)}</span>
618
+ </div>`
619
+ : '';
620
+
621
+ const teammateInfo = this.getTeammateInfo(agent);
622
+ const windowTitle = teammateInfo ? teammateInfo.name : (agent.description || agentId.substring(0, 7));
623
+ const maxTitleLen = isMobile ? 30 : 50;
624
+ const truncatedTitle = windowTitle.length > maxTitleLen ? windowTitle.substring(0, maxTitleLen) + '...' : windowTitle;
625
+ const modelBadge = agent.modelShort
626
+ ? `<span class="subagent-model-badge ${agent.modelShort}">${agent.modelShort}</span>`
627
+ : '';
628
+ win.innerHTML = `
629
+ <div class="subagent-window-header">
630
+ <div class="subagent-window-title" title="${escapeHtml(agent.description || agentId)}">
631
+ <span class="icon">🤖</span>
632
+ <span class="id">${escapeHtml(truncatedTitle)}</span>
633
+ ${modelBadge}
634
+ <span class="status ${agent.status}">${agent.status}</span>
635
+ </div>
636
+ <div class="subagent-window-actions">
637
+ <button onclick="app.closeSubagentWindow('${escapeHtml(agentId)}')" title="Minimize to tab">─</button>
638
+ </div>
639
+ </div>
640
+ ${parentHeader}
641
+ <div class="subagent-window-body" id="subagent-window-body-${agentId}">
642
+ <div class="subagent-empty">Loading activity...</div>
643
+ </div>
644
+ `;
645
+
646
+ // If we have a parent tab, start window at tab position for spawn animation
647
+ if (isMobile) {
648
+ // Mobile: position using top (keyboard-aware positioning calculated above)
649
+ win.style.top = `${finalY}px`;
650
+ win.style.bottom = 'auto';
651
+ } else if (parentTab) {
652
+ const tabRect = parentTab.getBoundingClientRect();
653
+ win.style.left = `${tabRect.left}px`;
654
+ win.style.top = `${tabRect.bottom}px`;
655
+ win.style.transform = 'scale(0.3)';
656
+ win.style.opacity = '0';
657
+ win.classList.add('spawning');
658
+ } else {
659
+ // No parent tab, just position normally (desktop/tablet)
660
+ win.style.left = `${finalX}px`;
661
+ win.style.top = `${finalY}px`;
662
+ }
663
+
664
+ document.body.appendChild(win);
665
+
666
+ // Make draggable (returns listener refs for cleanup)
667
+ const dragListeners = this.makeWindowDraggable(win, win.querySelector('.subagent-window-header'));
668
+
669
+ // Check if this window should be visible based on settings
670
+ // Use the PERSISTENT parent map for accurate tab-based visibility
671
+ const settings = this.loadAppSettingsFromStorage();
672
+ const activeTabOnly = settings.subagentActiveTabOnly ?? true;
673
+ let shouldHide = false;
674
+ if (activeTabOnly) {
675
+ const storedParent = this.subagentParentMap.get(agentId);
676
+ const hasKnownParent = storedParent || agent.parentSessionId;
677
+ const parentId = storedParent || agent.parentSessionId;
678
+ const isForActiveSession = !hasKnownParent || parentId === this.activeSessionId;
679
+ shouldHide = !isForActiveSession;
680
+ }
681
+
682
+ // Store reference (including drag listeners for cleanup)
683
+ this.subagentWindows.set(agentId, {
684
+ element: win,
685
+ minimized: false,
686
+ hidden: shouldHide,
687
+ dragListeners, // Store for cleanup to prevent memory leaks
688
+ });
689
+
690
+ // Hide window if not for active session
691
+ if (shouldHide) {
692
+ win.style.display = 'none';
693
+ }
694
+
695
+ // Render content — check if this teammate has a tmux pane
696
+ const paneInfo = teammateInfo ? this.teammatePanesByName.get(teammateInfo.name) : null;
697
+ if (paneInfo) {
698
+ this.initTeammateTerminal(agentId, paneInfo, win);
699
+ } else {
700
+ this.renderSubagentWindowContent(agentId);
701
+ }
702
+
703
+ // Focus on click
704
+ win.addEventListener('mousedown', () => {
705
+ win.style.zIndex = ++this.subagentWindowZIndex;
706
+ });
707
+
708
+ // Update connection lines when window is resized
709
+ const resizeObserver = new ResizeObserver(() => {
710
+ this.updateConnectionLines();
711
+ });
712
+ resizeObserver.observe(win);
713
+
714
+ // Store observer for cleanup
715
+ this.subagentWindows.get(agentId).resizeObserver = resizeObserver;
716
+
717
+ // Animate to final position if spawning from tab (desktop only)
718
+ if (parentTab && !isMobile) {
719
+ requestAnimationFrame(() => {
720
+ win.style.transition = 'all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)';
721
+ win.style.left = `${finalX}px`;
722
+ win.style.top = `${finalY}px`;
723
+ win.style.transform = 'scale(1)';
724
+ win.style.opacity = '1';
725
+
726
+ // Clean up after animation
727
+ setTimeout(() => {
728
+ win.style.transition = '';
729
+ win.classList.remove('spawning');
730
+ this.updateConnectionLines();
731
+ }, 400);
732
+ });
733
+ } else {
734
+ // No animation (mobile uses CSS positioning), just update connection lines
735
+ this.updateConnectionLines();
736
+ }
737
+
738
+ // Persist the state change (new window opened)
739
+ this.saveSubagentWindowStates();
740
+ },
741
+
742
+ closeSubagentWindow(agentId) {
743
+ const windowData = this.subagentWindows.get(agentId);
744
+ if (!windowData) return;
745
+
746
+ const agent = this.subagents.get(agentId);
747
+
748
+ // Get parent from PERSISTENT map (THE source of truth)
749
+ // Fall back to agent's parentSessionId, then to active session
750
+ const storedParent = this.subagentParentMap.get(agentId);
751
+ let parentSessionId = storedParent || agent?.parentSessionId || this.activeSessionId;
752
+
753
+ // If we don't have a stored parent yet, store it now
754
+ if (!storedParent && parentSessionId && this.sessions.has(parentSessionId)) {
755
+ this.setAgentParentSessionId(agentId, parentSessionId);
756
+ }
757
+
758
+ // Always minimize to tab
759
+ windowData.element.style.display = 'none';
760
+ windowData.minimized = true;
761
+
762
+ // Track minimized agent for the session (use the TAB's session ID)
763
+ if (parentSessionId) {
764
+ if (!this.minimizedSubagents.has(parentSessionId)) {
765
+ this.minimizedSubagents.set(parentSessionId, new Set());
766
+ }
767
+ this.minimizedSubagents.get(parentSessionId).add(agentId);
768
+
769
+ // Update tab badge to show minimized agents
770
+ this.renderSessionTabs();
771
+ }
772
+
773
+ // Persist the state change
774
+ this.saveSubagentWindowStates();
775
+ this.updateConnectionLines();
776
+ // Restack remaining visible mobile windows to fill the gap
777
+ this.relayoutMobileSubagentWindows();
778
+ },
779
+
780
+ /** Reposition all visible mobile subagent windows (called on keyboard show/hide). */
781
+ relayoutMobileSubagentWindows() {
782
+ if (MobileDetection.getDeviceType() !== 'mobile') return;
783
+ const mobileCardHeight = 110;
784
+ const mobileCardGap = 4;
785
+ const keyboardUp = typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible;
786
+ let idx = 0;
787
+ for (const [, data] of this.subagentWindows) {
788
+ if (data.minimized || data.hidden) continue;
789
+ const el = data.element;
790
+ // Reset left to proper position (drag may have set an arbitrary value)
791
+ el.style.left = '4px';
792
+ if (keyboardUp) {
793
+ // Stack from bottom above toolbar
794
+ const bottomPx = 40 + idx * (mobileCardHeight + mobileCardGap);
795
+ el.style.bottom = `${bottomPx}px`;
796
+ el.style.top = 'auto';
797
+ } else {
798
+ // Stack from top below header
799
+ const headerHeight = document.querySelector('.header')?.offsetHeight || 36;
800
+ const topPx = headerHeight + 8 + idx * (mobileCardHeight + mobileCardGap);
801
+ el.style.top = `${topPx}px`;
802
+ el.style.bottom = 'auto';
803
+ }
804
+ idx++;
805
+ }
806
+ },
807
+
808
+ // Clean up ALL floating windows (called during handleInit to prevent memory leaks on reconnect)
809
+ cleanupAllFloatingWindows() {
810
+ // Clean up all subagent windows with their ResizeObservers and drag listeners
811
+ for (const [agentId, windowData] of this.subagentWindows) {
812
+ if (windowData.resizeObserver) {
813
+ windowData.resizeObserver.disconnect();
814
+ }
815
+ if (windowData.dragListeners) {
816
+ document.removeEventListener('mousemove', windowData.dragListeners.move);
817
+ document.removeEventListener('mouseup', windowData.dragListeners.up);
818
+ if (windowData.dragListeners.touchMove) {
819
+ document.removeEventListener('touchmove', windowData.dragListeners.touchMove);
820
+ document.removeEventListener('touchend', windowData.dragListeners.up);
821
+ document.removeEventListener('touchcancel', windowData.dragListeners.up);
822
+ }
823
+ }
824
+ windowData.element.remove();
825
+ }
826
+ this.subagentWindows.clear();
827
+
828
+ // Clean up all teammate terminals
829
+ for (const [, termData] of this.teammateTerminals) {
830
+ if (termData.resizeObserver) termData.resizeObserver.disconnect();
831
+ if (termData.terminal) {
832
+ try { termData.terminal.dispose(); } catch {}
833
+ }
834
+ }
835
+ this.teammateTerminals.clear();
836
+ this.teammatePanesByName.clear();
837
+
838
+ // Clean up all log viewer windows with their EventSources and drag listeners
839
+ for (const [windowId, data] of this.logViewerWindows) {
840
+ if (data.eventSource) {
841
+ data.eventSource.close();
842
+ }
843
+ if (data.dragListeners) {
844
+ document.removeEventListener('mousemove', data.dragListeners.move);
845
+ document.removeEventListener('mouseup', data.dragListeners.up);
846
+ if (data.dragListeners.touchMove) {
847
+ document.removeEventListener('touchmove', data.dragListeners.touchMove);
848
+ document.removeEventListener('touchend', data.dragListeners.up);
849
+ document.removeEventListener('touchcancel', data.dragListeners.up);
850
+ }
851
+ }
852
+ data.element.remove();
853
+ }
854
+ this.logViewerWindows.clear();
855
+
856
+ // Clean up plan subagent windows (wizard agents)
857
+ if (this.planSubagents) {
858
+ for (const [agentId, windowData] of this.planSubagents) {
859
+ if (windowData.dragListeners) {
860
+ document.removeEventListener('mousemove', windowData.dragListeners.move);
861
+ document.removeEventListener('mouseup', windowData.dragListeners.up);
862
+ }
863
+ if (windowData.element) {
864
+ windowData.element.remove();
865
+ }
866
+ }
867
+ this.planSubagents.clear();
868
+ }
869
+
870
+ // Clean up all image popup windows with their drag listeners
871
+ for (const [imageId, popupData] of this.imagePopups) {
872
+ if (popupData.dragListeners) {
873
+ document.removeEventListener('mousemove', popupData.dragListeners.move);
874
+ document.removeEventListener('mouseup', popupData.dragListeners.up);
875
+ if (popupData.dragListeners.touchMove) {
876
+ document.removeEventListener('touchmove', popupData.dragListeners.touchMove);
877
+ document.removeEventListener('touchend', popupData.dragListeners.up);
878
+ document.removeEventListener('touchcancel', popupData.dragListeners.up);
879
+ }
880
+ }
881
+ popupData.element.remove();
882
+ }
883
+ this.imagePopups.clear();
884
+
885
+ // Clear orphaned plan generation state
886
+ this.activePlanOrchestratorId = null;
887
+ this._planProgressHandler = null;
888
+ this.planGenerationStopped = true;
889
+ if (this.planGenerationAbortController) {
890
+ this.planGenerationAbortController.abort();
891
+ this.planGenerationAbortController = null;
892
+ }
893
+
894
+ // Clean up wizard-specific timers (leak fix: not cleared on SSE reconnect)
895
+ if (this.wizardMinimizedTimer) {
896
+ clearInterval(this.wizardMinimizedTimer);
897
+ this.wizardMinimizedTimer = null;
898
+ }
899
+
900
+ // Clean up wizard drag listeners (leak fix: document-level handlers)
901
+ this.cleanupWizardDragging();
902
+
903
+ // Deactivate focus trap if wizard was open (leak fix: keydown listener)
904
+ if (this.activeFocusTrap) {
905
+ this.activeFocusTrap.deactivate();
906
+ this.activeFocusTrap = null;
907
+ }
908
+
909
+ // Clean up team tasks panel drag listeners
910
+ if (this.teamTasksDragListeners) {
911
+ document.removeEventListener('mousemove', this.teamTasksDragListeners.move);
912
+ document.removeEventListener('mouseup', this.teamTasksDragListeners.up);
913
+ if (this.teamTasksDragListeners.touchMove) {
914
+ document.removeEventListener('touchmove', this.teamTasksDragListeners.touchMove);
915
+ document.removeEventListener('touchend', this.teamTasksDragListeners.up);
916
+ document.removeEventListener('touchcancel', this.teamTasksDragListeners.up);
917
+ }
918
+ this.teamTasksDragListeners = null;
919
+ }
920
+
921
+ // Clear minimized agents tracking
922
+ this.minimizedSubagents.clear();
923
+
924
+ // Update monitor panel
925
+ this.renderMonitorPlanAgents();
926
+
927
+ // Update connection lines (should be empty now)
928
+ this.updateConnectionLines();
929
+ },
930
+
931
+ restoreSubagentWindow(agentId) {
932
+ const windowData = this.subagentWindows.get(agentId);
933
+ const agent = this.subagents.get(agentId);
934
+
935
+ // If window doesn't exist but agent does, recreate it
936
+ if (!windowData && agent) {
937
+ this.openSubagentWindow(agentId);
938
+ return;
939
+ }
940
+
941
+ if (windowData) {
942
+ const settings = this.loadAppSettingsFromStorage();
943
+ const activeTabOnly = settings.subagentActiveTabOnly ?? true;
944
+
945
+ // Get parent from PERSISTENT map (THE source of truth)
946
+ const storedParent = this.subagentParentMap.get(agentId);
947
+ const parentSessionId = storedParent || agent?.parentSessionId;
948
+
949
+ // Determine if we should show the window
950
+ let shouldShow = true;
951
+ if (activeTabOnly) {
952
+ // Only restore if the window belongs to the active session (or has no parent)
953
+ shouldShow = !parentSessionId || parentSessionId === this.activeSessionId;
954
+ }
955
+
956
+ if (shouldShow) {
957
+ windowData.element.style.display = 'flex';
958
+ windowData.element.style.zIndex = ++this.subagentWindowZIndex;
959
+ windowData.hidden = false;
960
+ }
961
+ windowData.minimized = false;
962
+ this.updateConnectionLines();
963
+ // Restack all visible mobile windows so restored ones don't overlap
964
+ this.relayoutMobileSubagentWindows();
965
+ }
966
+ },
967
+
968
+ // Returns drag listener references for cleanup (prevents memory leaks)
969
+ makeWindowDraggable(win, handle) {
970
+ let isDragging = false;
971
+ let startX, startY, startLeft, startTop;
972
+ let dragUpdateScheduled = false;
973
+
974
+ const startDrag = (clientX, clientY) => {
975
+ isDragging = true;
976
+ startX = clientX;
977
+ startY = clientY;
978
+ startLeft = parseInt(win.style.left) || win.getBoundingClientRect().left;
979
+ startTop = parseInt(win.style.top) || win.getBoundingClientRect().top;
980
+ // On drag start, switch from bottom-positioned to top-positioned so left/top work
981
+ win.style.bottom = 'auto';
982
+ };
983
+
984
+ const moveDrag = (clientX, clientY) => {
985
+ if (!isDragging) return;
986
+ const dx = clientX - startX;
987
+ const dy = clientY - startY;
988
+ // Constrain to viewport bounds
989
+ const winWidth = win.offsetWidth || 420;
990
+ const winHeight = win.offsetHeight || 350;
991
+ const maxX = window.innerWidth - winWidth - 4;
992
+ const maxY = window.innerHeight - winHeight - 4;
993
+ const newLeft = Math.max(4, Math.min(startLeft + dx, maxX));
994
+ const newTop = Math.max(4, Math.min(startTop + dy, maxY));
995
+ win.style.left = `${newLeft}px`;
996
+ win.style.top = `${newTop}px`;
997
+ // Throttle connection line updates during drag
998
+ if (!dragUpdateScheduled) {
999
+ dragUpdateScheduled = true;
1000
+ requestAnimationFrame(() => {
1001
+ this.updateConnectionLines();
1002
+ dragUpdateScheduled = false;
1003
+ });
1004
+ }
1005
+ };
1006
+
1007
+ const endDrag = () => {
1008
+ if (isDragging) {
1009
+ isDragging = false;
1010
+ // Save position after drag ends
1011
+ this.saveSubagentWindowStates();
1012
+ }
1013
+ };
1014
+
1015
+ // Named handle-level listeners (stored for explicit cleanup on window close)
1016
+ const handleMouseDown = (e) => {
1017
+ if (e.target.tagName === 'BUTTON') return;
1018
+ startDrag(e.clientX, e.clientY);
1019
+ e.preventDefault();
1020
+ };
1021
+ const handleTouchStart = (e) => {
1022
+ if (e.target.tagName === 'BUTTON') return;
1023
+ const touch = e.touches[0];
1024
+ startDrag(touch.clientX, touch.clientY);
1025
+ };
1026
+
1027
+ handle.addEventListener('mousedown', handleMouseDown);
1028
+ handle.addEventListener('touchstart', handleTouchStart, { passive: true });
1029
+
1030
+ // Store references to document-level listeners so they can be removed on window close
1031
+ const moveListener = (e) => {
1032
+ moveDrag(e.clientX, e.clientY);
1033
+ };
1034
+
1035
+ const touchMoveListener = (e) => {
1036
+ if (!isDragging) return;
1037
+ e.preventDefault(); // Prevent page scroll while dragging
1038
+ const touch = e.touches[0];
1039
+ moveDrag(touch.clientX, touch.clientY);
1040
+ };
1041
+
1042
+ const upListener = () => {
1043
+ endDrag();
1044
+ };
1045
+
1046
+ document.addEventListener('mousemove', moveListener);
1047
+ document.addEventListener('mouseup', upListener);
1048
+ document.addEventListener('touchmove', touchMoveListener, { passive: false });
1049
+ document.addEventListener('touchend', upListener);
1050
+ document.addEventListener('touchcancel', upListener);
1051
+
1052
+ // Return all listener references for cleanup (both handle-level and document-level)
1053
+ return {
1054
+ move: moveListener,
1055
+ up: upListener,
1056
+ touchMove: touchMoveListener,
1057
+ handle,
1058
+ handleMouseDown,
1059
+ handleTouchStart,
1060
+ };
1061
+ },
1062
+
1063
+ // Show subagent dropdown on hover
1064
+ showSubagentDropdown(badgeEl) {
1065
+ this.cancelHideSubagentDropdown();
1066
+ const dropdown = badgeEl.querySelector('.subagent-dropdown');
1067
+ if (!dropdown || dropdown.classList.contains('open')) return;
1068
+
1069
+ // Close other dropdowns first
1070
+ document.querySelectorAll('.subagent-dropdown.open').forEach(d => {
1071
+ d.classList.remove('open', 'pinned');
1072
+ if (d.parentElement === document.body && d._originalParent) {
1073
+ d._originalParent.appendChild(d);
1074
+ }
1075
+ });
1076
+
1077
+ // Move to body to escape clipping
1078
+ dropdown._originalParent = badgeEl;
1079
+ document.body.appendChild(dropdown);
1080
+
1081
+ // Position below badge
1082
+ const rect = badgeEl.getBoundingClientRect();
1083
+ dropdown.style.top = `${rect.bottom + 2}px`;
1084
+ dropdown.style.left = `${rect.left + rect.width / 2}px`;
1085
+ dropdown.style.transform = 'translateX(-50%)';
1086
+ dropdown.classList.add('open');
1087
+ },
1088
+
1089
+ // Schedule hide after delay (allows moving mouse to dropdown)
1090
+ scheduleHideSubagentDropdown(badgeEl) {
1091
+ this._subagentHideTimeout = setTimeout(() => {
1092
+ const dropdown = badgeEl?.querySelector?.('.subagent-dropdown') ||
1093
+ document.querySelector('.subagent-dropdown.open');
1094
+ if (dropdown && !dropdown.classList.contains('pinned')) {
1095
+ dropdown.classList.remove('open');
1096
+ if (dropdown._originalParent) {
1097
+ dropdown._originalParent.appendChild(dropdown);
1098
+ }
1099
+ }
1100
+ }, 150);
1101
+ },
1102
+
1103
+ // Cancel scheduled hide
1104
+ cancelHideSubagentDropdown() {
1105
+ if (this._subagentHideTimeout) {
1106
+ clearTimeout(this._subagentHideTimeout);
1107
+ this._subagentHideTimeout = null;
1108
+ }
1109
+ },
1110
+
1111
+ // Pin dropdown open on click (stays until clicking outside)
1112
+ pinSubagentDropdown(badgeEl) {
1113
+ const dropdown = document.querySelector('.subagent-dropdown.open');
1114
+ if (!dropdown) {
1115
+ this.showSubagentDropdown(badgeEl);
1116
+ // On mobile/touch, pin immediately so onmouseleave doesn't close it
1117
+ const openedDropdown = document.querySelector('.subagent-dropdown.open');
1118
+ if (openedDropdown) {
1119
+ openedDropdown.classList.add('pinned');
1120
+ const closeHandler = (e) => {
1121
+ if (!badgeEl.contains(e.target) && !openedDropdown.contains(e.target)) {
1122
+ openedDropdown.classList.remove('open', 'pinned');
1123
+ if (openedDropdown._originalParent) {
1124
+ openedDropdown._originalParent.appendChild(openedDropdown);
1125
+ }
1126
+ document.removeEventListener('click', closeHandler);
1127
+ }
1128
+ };
1129
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
1130
+ }
1131
+ return;
1132
+ }
1133
+ dropdown.classList.toggle('pinned');
1134
+
1135
+ if (dropdown.classList.contains('pinned')) {
1136
+ // Close on outside click
1137
+ const closeHandler = (e) => {
1138
+ if (!badgeEl.contains(e.target) && !dropdown.contains(e.target)) {
1139
+ dropdown.classList.remove('open', 'pinned');
1140
+ if (dropdown._originalParent) {
1141
+ dropdown._originalParent.appendChild(dropdown);
1142
+ }
1143
+ document.removeEventListener('click', closeHandler);
1144
+ }
1145
+ };
1146
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
1147
+ }
1148
+ },
1149
+ });