botmux 2.107.0 → 2.108.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 (515) hide show
  1. package/README.en.md +17 -14
  2. package/README.md +19 -14
  3. package/dist/adapters/backend/sandbox.d.ts +3 -1
  4. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  5. package/dist/adapters/backend/sandbox.js +19 -3
  6. package/dist/adapters/backend/sandbox.js.map +1 -1
  7. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/tmux-backend.js +16 -1
  9. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  10. package/dist/adapters/backend/tmux-pipe-backend.d.ts +24 -15
  11. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/tmux-pipe-backend.js +75 -43
  13. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  14. package/dist/adapters/cli/genius.d.ts.map +1 -1
  15. package/dist/adapters/cli/genius.js +9 -15
  16. package/dist/adapters/cli/genius.js.map +1 -1
  17. package/dist/adapters/cli/grok.d.ts +73 -0
  18. package/dist/adapters/cli/grok.d.ts.map +1 -0
  19. package/dist/adapters/cli/grok.js +298 -0
  20. package/dist/adapters/cli/grok.js.map +1 -0
  21. package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
  22. package/dist/adapters/cli/oh-my-pi.js +133 -12
  23. package/dist/adapters/cli/oh-my-pi.js.map +1 -1
  24. package/dist/adapters/cli/registry.d.ts +2 -1
  25. package/dist/adapters/cli/registry.d.ts.map +1 -1
  26. package/dist/adapters/cli/registry.js +3 -1
  27. package/dist/adapters/cli/registry.js.map +1 -1
  28. package/dist/adapters/cli/shared-hints.d.ts +8 -4
  29. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  30. package/dist/adapters/cli/shared-hints.js +8 -4
  31. package/dist/adapters/cli/shared-hints.js.map +1 -1
  32. package/dist/adapters/cli/types.d.ts +18 -16
  33. package/dist/adapters/cli/types.d.ts.map +1 -1
  34. package/dist/adapters/hook-installer.d.ts +5 -4
  35. package/dist/adapters/hook-installer.d.ts.map +1 -1
  36. package/dist/adapters/hook-installer.js +42 -0
  37. package/dist/adapters/hook-installer.js.map +1 -1
  38. package/dist/cli/send-dispatch.d.ts +21 -0
  39. package/dist/cli/send-dispatch.d.ts.map +1 -1
  40. package/dist/cli/send-dispatch.js +159 -0
  41. package/dist/cli/send-dispatch.js.map +1 -1
  42. package/dist/cli/session-list-liveness.d.ts +22 -0
  43. package/dist/cli/session-list-liveness.d.ts.map +1 -0
  44. package/dist/cli/session-list-liveness.js +22 -0
  45. package/dist/cli/session-list-liveness.js.map +1 -0
  46. package/dist/cli.d.ts.map +1 -1
  47. package/dist/cli.js +182 -33
  48. package/dist/cli.js.map +1 -1
  49. package/dist/core/ask-types.d.ts +4 -0
  50. package/dist/core/ask-types.d.ts.map +1 -1
  51. package/dist/core/ask-types.js +10 -0
  52. package/dist/core/ask-types.js.map +1 -1
  53. package/dist/core/cost-calculator.d.ts.map +1 -1
  54. package/dist/core/cost-calculator.js +35 -13
  55. package/dist/core/cost-calculator.js.map +1 -1
  56. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  57. package/dist/core/dashboard-ipc-server.js +89 -16
  58. package/dist/core/dashboard-ipc-server.js.map +1 -1
  59. package/dist/core/dashboard-rows.d.ts.map +1 -1
  60. package/dist/core/dashboard-rows.js +6 -2
  61. package/dist/core/dashboard-rows.js.map +1 -1
  62. package/dist/core/dashboard-url.d.ts +6 -5
  63. package/dist/core/dashboard-url.d.ts.map +1 -1
  64. package/dist/core/dashboard-url.js +11 -8
  65. package/dist/core/dashboard-url.js.map +1 -1
  66. package/dist/core/maintenance.d.ts +11 -8
  67. package/dist/core/maintenance.d.ts.map +1 -1
  68. package/dist/core/maintenance.js +45 -20
  69. package/dist/core/maintenance.js.map +1 -1
  70. package/dist/core/per-bot-env.d.ts.map +1 -1
  71. package/dist/core/per-bot-env.js +4 -0
  72. package/dist/core/per-bot-env.js.map +1 -1
  73. package/dist/core/session-discovery.d.ts.map +1 -1
  74. package/dist/core/session-discovery.js +4 -0
  75. package/dist/core/session-discovery.js.map +1 -1
  76. package/dist/core/skills/cli-admin-command.d.ts.map +1 -1
  77. package/dist/core/skills/cli-admin-command.js +8 -2
  78. package/dist/core/skills/cli-admin-command.js.map +1 -1
  79. package/dist/core/skills/im-command.d.ts.map +1 -1
  80. package/dist/core/skills/im-command.js +3 -1
  81. package/dist/core/skills/im-command.js.map +1 -1
  82. package/dist/core/skills/sources.d.ts +33 -1
  83. package/dist/core/skills/sources.d.ts.map +1 -1
  84. package/dist/core/skills/sources.js +97 -0
  85. package/dist/core/skills/sources.js.map +1 -1
  86. package/dist/core/skills/types.d.ts +8 -0
  87. package/dist/core/skills/types.d.ts.map +1 -1
  88. package/dist/core/terminal-url.d.ts.map +1 -1
  89. package/dist/core/terminal-url.js +12 -2
  90. package/dist/core/terminal-url.js.map +1 -1
  91. package/dist/core/terminal-write-auth.d.ts +34 -0
  92. package/dist/core/terminal-write-auth.d.ts.map +1 -0
  93. package/dist/core/terminal-write-auth.js +76 -0
  94. package/dist/core/terminal-write-auth.js.map +1 -0
  95. package/dist/core/tmux-recovery.d.ts +11 -0
  96. package/dist/core/tmux-recovery.d.ts.map +1 -0
  97. package/dist/core/tmux-recovery.js +19 -0
  98. package/dist/core/tmux-recovery.js.map +1 -0
  99. package/dist/core/types.d.ts +10 -4
  100. package/dist/core/types.d.ts.map +1 -1
  101. package/dist/core/types.js.map +1 -1
  102. package/dist/core/update-check.d.ts +2 -2
  103. package/dist/core/update-check.js +2 -2
  104. package/dist/core/worker-pool.d.ts +3 -0
  105. package/dist/core/worker-pool.d.ts.map +1 -1
  106. package/dist/core/worker-pool.js +70 -3
  107. package/dist/core/worker-pool.js.map +1 -1
  108. package/dist/daemon.d.ts.map +1 -1
  109. package/dist/daemon.js +15 -8
  110. package/dist/daemon.js.map +1 -1
  111. package/dist/dashboard/auth.d.ts +1 -1
  112. package/dist/dashboard/auth.d.ts.map +1 -1
  113. package/dist/dashboard/auth.js +7 -2
  114. package/dist/dashboard/auth.js.map +1 -1
  115. package/dist/dashboard/bot-payload.d.ts +3 -0
  116. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  117. package/dist/dashboard/bot-payload.js +3 -0
  118. package/dist/dashboard/bot-payload.js.map +1 -1
  119. package/dist/dashboard/connector-api.d.ts.map +1 -1
  120. package/dist/dashboard/connector-api.js +39 -13
  121. package/dist/dashboard/connector-api.js.map +1 -1
  122. package/dist/dashboard/history-senders.d.ts +37 -0
  123. package/dist/dashboard/history-senders.d.ts.map +1 -0
  124. package/dist/dashboard/history-senders.js +60 -0
  125. package/dist/dashboard/history-senders.js.map +1 -0
  126. package/dist/dashboard/roles-batch.d.ts +31 -0
  127. package/dist/dashboard/roles-batch.d.ts.map +1 -0
  128. package/dist/dashboard/roles-batch.js +87 -0
  129. package/dist/dashboard/roles-batch.js.map +1 -0
  130. package/dist/dashboard/settings-card-model.d.ts +4 -2
  131. package/dist/dashboard/settings-card-model.d.ts.map +1 -1
  132. package/dist/dashboard/settings-card-model.js +13 -5
  133. package/dist/dashboard/settings-card-model.js.map +1 -1
  134. package/dist/dashboard/settings-write-applier.d.ts +4 -1
  135. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  136. package/dist/dashboard/settings-write-applier.js +6 -1
  137. package/dist/dashboard/settings-write-applier.js.map +1 -1
  138. package/dist/dashboard/skill-install-request.d.ts +4 -0
  139. package/dist/dashboard/skill-install-request.d.ts.map +1 -1
  140. package/dist/dashboard/skill-install-request.js +11 -1
  141. package/dist/dashboard/skill-install-request.js.map +1 -1
  142. package/dist/dashboard/trigger-api.d.ts +9 -1
  143. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  144. package/dist/dashboard/trigger-api.js +48 -10
  145. package/dist/dashboard/trigger-api.js.map +1 -1
  146. package/dist/dashboard/web/app.d.ts.map +1 -1
  147. package/dist/dashboard/web/app.js +286 -271
  148. package/dist/dashboard/web/app.js.map +1 -1
  149. package/dist/dashboard/web/bot-defaults-page.d.ts +26 -0
  150. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  151. package/dist/dashboard/web/bot-defaults-page.js +1408 -12
  152. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  153. package/dist/dashboard/web/bot-defaults.d.ts +91 -4
  154. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  155. package/dist/dashboard/web/bot-defaults.js +43 -2141
  156. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  157. package/dist/dashboard/web/bot-onboarding.d.ts +6 -1
  158. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  159. package/dist/dashboard/web/bot-onboarding.js +261 -306
  160. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  161. package/dist/dashboard/web/connectors-page.d.ts +4 -0
  162. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  163. package/dist/dashboard/web/connectors-page.js +228 -48
  164. package/dist/dashboard/web/connectors-page.js.map +1 -1
  165. package/dist/dashboard/web/dashboard-api.d.ts +9 -0
  166. package/dist/dashboard/web/dashboard-api.d.ts.map +1 -0
  167. package/dist/dashboard/web/dashboard-api.js +18 -0
  168. package/dist/dashboard/web/dashboard-api.js.map +1 -0
  169. package/dist/dashboard/web/dashboard-components.d.ts +92 -0
  170. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -0
  171. package/dist/dashboard/web/dashboard-components.js +250 -0
  172. package/dist/dashboard/web/dashboard-components.js.map +1 -0
  173. package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
  174. package/dist/dashboard/web/dashboard-routes.js +30 -183
  175. package/dist/dashboard/web/dashboard-routes.js.map +1 -1
  176. package/dist/dashboard/web/floating-scrollbars.d.ts +2 -0
  177. package/dist/dashboard/web/floating-scrollbars.d.ts.map +1 -0
  178. package/dist/dashboard/web/floating-scrollbars.js +264 -0
  179. package/dist/dashboard/web/floating-scrollbars.js.map +1 -0
  180. package/dist/dashboard/web/groups-api.d.ts +36 -0
  181. package/dist/dashboard/web/groups-api.d.ts.map +1 -0
  182. package/dist/dashboard/web/groups-api.js +44 -0
  183. package/dist/dashboard/web/groups-api.js.map +1 -0
  184. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  185. package/dist/dashboard/web/groups-page.js +746 -15
  186. package/dist/dashboard/web/groups-page.js.map +1 -1
  187. package/dist/dashboard/web/groups.d.ts +51 -18
  188. package/dist/dashboard/web/groups.d.ts.map +1 -1
  189. package/dist/dashboard/web/groups.js +145 -1096
  190. package/dist/dashboard/web/groups.js.map +1 -1
  191. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  192. package/dist/dashboard/web/i18n.js +545 -255
  193. package/dist/dashboard/web/i18n.js.map +1 -1
  194. package/dist/dashboard/web/insights-page.d.ts.map +1 -1
  195. package/dist/dashboard/web/insights-page.js +1047 -14
  196. package/dist/dashboard/web/insights-page.js.map +1 -1
  197. package/dist/dashboard/web/insights.d.ts +177 -7
  198. package/dist/dashboard/web/insights.d.ts.map +1 -1
  199. package/dist/dashboard/web/insights.js +187 -1926
  200. package/dist/dashboard/web/insights.js.map +1 -1
  201. package/dist/dashboard/web/legacy-workflow-page.d.ts +3 -0
  202. package/dist/dashboard/web/legacy-workflow-page.d.ts.map +1 -0
  203. package/dist/dashboard/web/legacy-workflow-page.js +717 -0
  204. package/dist/dashboard/web/legacy-workflow-page.js.map +1 -0
  205. package/dist/dashboard/web/monitor-room.d.ts.map +1 -1
  206. package/dist/dashboard/web/monitor-room.js +10 -8
  207. package/dist/dashboard/web/monitor-room.js.map +1 -1
  208. package/dist/dashboard/web/monitoring-page.d.ts.map +1 -1
  209. package/dist/dashboard/web/monitoring-page.js +188 -14
  210. package/dist/dashboard/web/monitoring-page.js.map +1 -1
  211. package/dist/dashboard/web/office-page.d.ts.map +1 -1
  212. package/dist/dashboard/web/office-page.js +11 -23
  213. package/dist/dashboard/web/office-page.js.map +1 -1
  214. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  215. package/dist/dashboard/web/overview-page.js +141 -147
  216. package/dist/dashboard/web/overview-page.js.map +1 -1
  217. package/dist/dashboard/web/overview.d.ts +0 -1
  218. package/dist/dashboard/web/overview.d.ts.map +1 -1
  219. package/dist/dashboard/web/overview.js +3 -275
  220. package/dist/dashboard/web/overview.js.map +1 -1
  221. package/dist/dashboard/web/preferences.d.ts +1 -6
  222. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  223. package/dist/dashboard/web/preferences.js +0 -21
  224. package/dist/dashboard/web/preferences.js.map +1 -1
  225. package/dist/dashboard/web/role-batch.d.ts +9 -0
  226. package/dist/dashboard/web/role-batch.d.ts.map +1 -0
  227. package/dist/dashboard/web/role-batch.js +42 -0
  228. package/dist/dashboard/web/role-batch.js.map +1 -0
  229. package/dist/dashboard/web/roles-page.d.ts.map +1 -1
  230. package/dist/dashboard/web/roles-page.js +538 -13
  231. package/dist/dashboard/web/roles-page.js.map +1 -1
  232. package/dist/dashboard/web/roles.d.ts +98 -1
  233. package/dist/dashboard/web/roles.d.ts.map +1 -1
  234. package/dist/dashboard/web/roles.js +117 -903
  235. package/dist/dashboard/web/roles.js.map +1 -1
  236. package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
  237. package/dist/dashboard/web/schedules-page.js +82 -24
  238. package/dist/dashboard/web/schedules-page.js.map +1 -1
  239. package/dist/dashboard/web/sessions-kanban.d.ts +67 -0
  240. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -0
  241. package/dist/dashboard/web/sessions-kanban.js +481 -0
  242. package/dist/dashboard/web/sessions-kanban.js.map +1 -0
  243. package/dist/dashboard/web/sessions-page.d.ts +4 -0
  244. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  245. package/dist/dashboard/web/sessions-page.js +1633 -18
  246. package/dist/dashboard/web/sessions-page.js.map +1 -1
  247. package/dist/dashboard/web/sessions.d.ts +45 -4
  248. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  249. package/dist/dashboard/web/sessions.js +53 -2535
  250. package/dist/dashboard/web/sessions.js.map +1 -1
  251. package/dist/dashboard/web/settings-page.d.ts +0 -2
  252. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  253. package/dist/dashboard/web/settings-page.js +127 -117
  254. package/dist/dashboard/web/settings-page.js.map +1 -1
  255. package/dist/dashboard/web/skills-page.d.ts +10 -1
  256. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  257. package/dist/dashboard/web/skills-page.js +235 -93
  258. package/dist/dashboard/web/skills-page.js.map +1 -1
  259. package/dist/dashboard/web/team-federation-page.d.ts.map +1 -1
  260. package/dist/dashboard/web/team-federation-page.js +446 -28
  261. package/dist/dashboard/web/team-federation-page.js.map +1 -1
  262. package/dist/dashboard/web/team-federation.d.ts +91 -2
  263. package/dist/dashboard/web/team-federation.d.ts.map +1 -1
  264. package/dist/dashboard/web/team-federation.js +87 -474
  265. package/dist/dashboard/web/team-federation.js.map +1 -1
  266. package/dist/dashboard/web/theme-menu.d.ts +1 -0
  267. package/dist/dashboard/web/theme-menu.d.ts.map +1 -1
  268. package/dist/dashboard/web/theme-menu.js +79 -20
  269. package/dist/dashboard/web/theme-menu.js.map +1 -1
  270. package/dist/dashboard/web/ui.d.ts +1 -2
  271. package/dist/dashboard/web/ui.d.ts.map +1 -1
  272. package/dist/dashboard/web/ui.js +8 -20
  273. package/dist/dashboard/web/ui.js.map +1 -1
  274. package/dist/dashboard/web/v3-api.d.ts +16 -0
  275. package/dist/dashboard/web/v3-api.d.ts.map +1 -0
  276. package/dist/dashboard/web/v3-api.js +21 -0
  277. package/dist/dashboard/web/v3-api.js.map +1 -0
  278. package/dist/dashboard/web/v3-components.d.ts +2 -0
  279. package/dist/dashboard/web/v3-components.d.ts.map +1 -0
  280. package/dist/dashboard/web/v3-components.js +290 -0
  281. package/dist/dashboard/web/v3-components.js.map +1 -0
  282. package/dist/dashboard/web/v3-model.d.ts +80 -0
  283. package/dist/dashboard/web/v3-model.d.ts.map +1 -0
  284. package/dist/dashboard/web/v3-model.js +240 -0
  285. package/dist/dashboard/web/v3-model.js.map +1 -0
  286. package/dist/dashboard/web/v3-page.d.ts.map +1 -1
  287. package/dist/dashboard/web/v3-page.js +2 -22
  288. package/dist/dashboard/web/v3-page.js.map +1 -1
  289. package/dist/dashboard/web/v3-terminal.d.ts +14 -0
  290. package/dist/dashboard/web/v3-terminal.d.ts.map +1 -1
  291. package/dist/dashboard/web/v3-terminal.js +31 -19
  292. package/dist/dashboard/web/v3-terminal.js.map +1 -1
  293. package/dist/dashboard/web/webhook-logs-page.d.ts +16 -0
  294. package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -0
  295. package/dist/dashboard/web/webhook-logs-page.js +195 -0
  296. package/dist/dashboard/web/webhook-logs-page.js.map +1 -0
  297. package/dist/dashboard/web/whiteboards-page.d.ts.map +1 -1
  298. package/dist/dashboard/web/whiteboards-page.js +18 -23
  299. package/dist/dashboard/web/whiteboards-page.js.map +1 -1
  300. package/dist/dashboard/web/workflow-version-switch.d.ts +8 -0
  301. package/dist/dashboard/web/workflow-version-switch.d.ts.map +1 -0
  302. package/dist/dashboard/web/workflow-version-switch.js +9 -0
  303. package/dist/dashboard/web/workflow-version-switch.js.map +1 -0
  304. package/dist/dashboard/web/workflows.d.ts +272 -1
  305. package/dist/dashboard/web/workflows.d.ts.map +1 -1
  306. package/dist/dashboard/web/workflows.js +211 -1397
  307. package/dist/dashboard/web/workflows.js.map +1 -1
  308. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  309. package/dist/dashboard/webhook-routes.js +92 -39
  310. package/dist/dashboard/webhook-routes.js.map +1 -1
  311. package/dist/dashboard-web/app.js +1 -65
  312. package/dist/dashboard-web/brand-logo.png +0 -0
  313. package/dist/dashboard-web/chunks/bot-defaults-page-HSQ6HR3R.js +1 -0
  314. package/dist/dashboard-web/chunks/chunk-6TPODDDC.js +1 -0
  315. package/dist/dashboard-web/chunks/chunk-73EJP2MO.js +1 -0
  316. package/dist/dashboard-web/chunks/chunk-BRTZFELS.js +1 -0
  317. package/dist/dashboard-web/chunks/chunk-EVK77ASG.js +1 -0
  318. package/dist/dashboard-web/chunks/chunk-R3FXBFQH.js +1 -0
  319. package/dist/dashboard-web/chunks/chunk-VRLL7SNU.js +1 -0
  320. package/dist/dashboard-web/chunks/chunk-XCPSHD3G.js +37 -0
  321. package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
  322. package/dist/dashboard-web/chunks/chunk-YVRWYGXR.js +1 -0
  323. package/dist/dashboard-web/chunks/chunk-ZJING6FY.js +66 -0
  324. package/dist/dashboard-web/chunks/connectors-page-ODUA5UCW.js +1 -0
  325. package/dist/dashboard-web/chunks/groups-page-OYX5M6NK.js +8 -0
  326. package/dist/dashboard-web/chunks/insights-page-YCQO6OCR.js +22 -0
  327. package/dist/dashboard-web/chunks/legacy-workflow-page-XJAUNONP.js +2 -0
  328. package/dist/dashboard-web/chunks/{monitor-room-RFX4HPS5.js → monitor-room-WTZNYAFV.js} +17 -15
  329. package/dist/dashboard-web/chunks/monitoring-page-R6LJUK7G.js +2 -0
  330. package/dist/dashboard-web/chunks/office-page-4OXK3GNR.js +1 -0
  331. package/dist/dashboard-web/chunks/overview-page-4MVEJY6U.js +1 -0
  332. package/dist/dashboard-web/chunks/roles-page-NQDW27VV.js +1 -0
  333. package/dist/dashboard-web/chunks/schedules-page-JRNKYHOX.js +1 -0
  334. package/dist/dashboard-web/chunks/sessions-page-LF7UVI7B.js +3 -0
  335. package/dist/dashboard-web/chunks/settings-page-WGW7KQQS.js +2 -0
  336. package/dist/dashboard-web/chunks/skills-page-AMD4XP3T.js +3 -0
  337. package/dist/dashboard-web/chunks/team-federation-page-SYG3K4GG.js +1 -0
  338. package/dist/dashboard-web/chunks/v3-page-6NFFWNUK.js +94 -0
  339. package/dist/dashboard-web/chunks/whiteboards-page-FBRUK2AS.js +1 -0
  340. package/dist/dashboard-web/favicon.png +0 -0
  341. package/dist/dashboard-web/index.html +5 -69
  342. package/dist/dashboard-web/style.css +18835 -7211
  343. package/dist/dashboard.js +150 -32
  344. package/dist/dashboard.js.map +1 -1
  345. package/dist/global-config.d.ts +2 -2
  346. package/dist/global-config.d.ts.map +1 -1
  347. package/dist/i18n/en.d.ts.map +1 -1
  348. package/dist/i18n/en.js +3 -0
  349. package/dist/i18n/en.js.map +1 -1
  350. package/dist/i18n/zh.d.ts.map +1 -1
  351. package/dist/i18n/zh.js +3 -0
  352. package/dist/i18n/zh.js.map +1 -1
  353. package/dist/im/lark/card-builder.d.ts +7 -5
  354. package/dist/im/lark/card-builder.d.ts.map +1 -1
  355. package/dist/im/lark/card-builder.js +10 -7
  356. package/dist/im/lark/card-builder.js.map +1 -1
  357. package/dist/im/lark/card-handler.d.ts.map +1 -1
  358. package/dist/im/lark/card-handler.js +25 -1
  359. package/dist/im/lark/card-handler.js.map +1 -1
  360. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  361. package/dist/im/lark/event-dispatcher.js +84 -0
  362. package/dist/im/lark/event-dispatcher.js.map +1 -1
  363. package/dist/platform/binding.d.ts +9 -0
  364. package/dist/platform/binding.d.ts.map +1 -1
  365. package/dist/platform/binding.js +14 -0
  366. package/dist/platform/binding.js.map +1 -1
  367. package/dist/platform/tunnel-client.d.ts.map +1 -1
  368. package/dist/platform/tunnel-client.js +9 -0
  369. package/dist/platform/tunnel-client.js.map +1 -1
  370. package/dist/services/bot-config-store.d.ts +1 -1
  371. package/dist/services/bot-config-store.d.ts.map +1 -1
  372. package/dist/services/bot-config-store.js +11 -5
  373. package/dist/services/bot-config-store.js.map +1 -1
  374. package/dist/services/file-bridge-path.d.ts +8 -0
  375. package/dist/services/file-bridge-path.d.ts.map +1 -0
  376. package/dist/services/file-bridge-path.js +75 -0
  377. package/dist/services/file-bridge-path.js.map +1 -0
  378. package/dist/services/grok-paths.d.ts +27 -0
  379. package/dist/services/grok-paths.d.ts.map +1 -0
  380. package/dist/services/grok-paths.js +106 -0
  381. package/dist/services/grok-paths.js.map +1 -0
  382. package/dist/services/grok-transcript.d.ts +67 -0
  383. package/dist/services/grok-transcript.d.ts.map +1 -0
  384. package/dist/services/grok-transcript.js +630 -0
  385. package/dist/services/grok-transcript.js.map +1 -0
  386. package/dist/services/hermes-session-filter.d.ts +28 -0
  387. package/dist/services/hermes-session-filter.d.ts.map +1 -0
  388. package/dist/services/hermes-session-filter.js +53 -0
  389. package/dist/services/hermes-session-filter.js.map +1 -0
  390. package/dist/services/hermes-transcript.d.ts +3 -0
  391. package/dist/services/hermes-transcript.d.ts.map +1 -1
  392. package/dist/services/hermes-transcript.js +20 -2
  393. package/dist/services/hermes-transcript.js.map +1 -1
  394. package/dist/services/resumable-session-discovery.d.ts +3 -0
  395. package/dist/services/resumable-session-discovery.d.ts.map +1 -1
  396. package/dist/services/resumable-session-discovery.js +6 -1
  397. package/dist/services/resumable-session-discovery.js.map +1 -1
  398. package/dist/services/skill-registry-store.d.ts +9 -0
  399. package/dist/services/skill-registry-store.d.ts.map +1 -1
  400. package/dist/services/skill-registry-store.js +304 -1
  401. package/dist/services/skill-registry-store.js.map +1 -1
  402. package/dist/services/structured-bridge-clis.d.ts +29 -0
  403. package/dist/services/structured-bridge-clis.d.ts.map +1 -0
  404. package/dist/services/structured-bridge-clis.js +76 -0
  405. package/dist/services/structured-bridge-clis.js.map +1 -0
  406. package/dist/services/trigger-log-store.d.ts +59 -0
  407. package/dist/services/trigger-log-store.d.ts.map +1 -1
  408. package/dist/services/trigger-log-store.js +81 -10
  409. package/dist/services/trigger-log-store.js.map +1 -1
  410. package/dist/services/usage-ledger.d.ts +4 -1
  411. package/dist/services/usage-ledger.d.ts.map +1 -1
  412. package/dist/services/usage-ledger.js +48 -10
  413. package/dist/services/usage-ledger.js.map +1 -1
  414. package/dist/services/webhook-audit.d.ts +10 -0
  415. package/dist/services/webhook-audit.d.ts.map +1 -0
  416. package/dist/services/webhook-audit.js +116 -0
  417. package/dist/services/webhook-audit.js.map +1 -0
  418. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  419. package/dist/setup/bot-config-editor.js +2 -0
  420. package/dist/setup/bot-config-editor.js.map +1 -1
  421. package/dist/setup/ensure-tmux.d.ts +26 -12
  422. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  423. package/dist/setup/ensure-tmux.js +85 -18
  424. package/dist/setup/ensure-tmux.js.map +1 -1
  425. package/dist/skills/definitions.d.ts.map +1 -1
  426. package/dist/skills/definitions.js +14 -1
  427. package/dist/skills/definitions.js.map +1 -1
  428. package/dist/skills/injection-mode.d.ts +16 -0
  429. package/dist/skills/injection-mode.d.ts.map +1 -1
  430. package/dist/skills/injection-mode.js +25 -0
  431. package/dist/skills/injection-mode.js.map +1 -1
  432. package/dist/types.d.ts +9 -0
  433. package/dist/types.d.ts.map +1 -1
  434. package/dist/utils/global-install.d.ts +22 -0
  435. package/dist/utils/global-install.d.ts.map +1 -0
  436. package/dist/utils/global-install.js +106 -0
  437. package/dist/utils/global-install.js.map +1 -0
  438. package/dist/utils/install-diagnostics.d.ts +1 -1
  439. package/dist/utils/install-diagnostics.d.ts.map +1 -1
  440. package/dist/utils/install-diagnostics.js +6 -4
  441. package/dist/utils/install-diagnostics.js.map +1 -1
  442. package/dist/utils/install-info.d.ts +2 -0
  443. package/dist/utils/install-info.d.ts.map +1 -1
  444. package/dist/utils/install-info.js +10 -4
  445. package/dist/utils/install-info.js.map +1 -1
  446. package/dist/worker.js +293 -172
  447. package/dist/worker.js.map +1 -1
  448. package/dist/workflows/events/payloads.d.ts +10 -10
  449. package/dist/workflows/events/schema.d.ts +28 -28
  450. package/package.json +8 -7
  451. package/dist/dashboard/web/connectors.d.ts +0 -2
  452. package/dist/dashboard/web/connectors.d.ts.map +0 -1
  453. package/dist/dashboard/web/connectors.js +0 -2
  454. package/dist/dashboard/web/connectors.js.map +0 -1
  455. package/dist/dashboard/web/office.d.ts +0 -2
  456. package/dist/dashboard/web/office.d.ts.map +0 -1
  457. package/dist/dashboard/web/office.js +0 -2
  458. package/dist/dashboard/web/office.js.map +0 -1
  459. package/dist/dashboard/web/schedules.d.ts +0 -2
  460. package/dist/dashboard/web/schedules.d.ts.map +0 -1
  461. package/dist/dashboard/web/schedules.js +0 -2
  462. package/dist/dashboard/web/schedules.js.map +0 -1
  463. package/dist/dashboard/web/settings.d.ts +0 -2
  464. package/dist/dashboard/web/settings.d.ts.map +0 -1
  465. package/dist/dashboard/web/settings.js +0 -2
  466. package/dist/dashboard/web/settings.js.map +0 -1
  467. package/dist/dashboard/web/skills.d.ts +0 -2
  468. package/dist/dashboard/web/skills.d.ts.map +0 -1
  469. package/dist/dashboard/web/skills.js +0 -2
  470. package/dist/dashboard/web/skills.js.map +0 -1
  471. package/dist/dashboard/web/skin-intro.d.ts +0 -4
  472. package/dist/dashboard/web/skin-intro.d.ts.map +0 -1
  473. package/dist/dashboard/web/skin-intro.js +0 -49
  474. package/dist/dashboard/web/skin-intro.js.map +0 -1
  475. package/dist/dashboard/web/v3.d.ts +0 -3
  476. package/dist/dashboard/web/v3.d.ts.map +0 -1
  477. package/dist/dashboard/web/v3.js +0 -539
  478. package/dist/dashboard/web/v3.js.map +0 -1
  479. package/dist/dashboard/web/whiteboards.d.ts +0 -2
  480. package/dist/dashboard/web/whiteboards.d.ts.map +0 -1
  481. package/dist/dashboard/web/whiteboards.js +0 -2
  482. package/dist/dashboard/web/whiteboards.js.map +0 -1
  483. package/dist/dashboard-web/chunks/bot-defaults-page-OCIHPGEU.js +0 -438
  484. package/dist/dashboard-web/chunks/chunk-3N2PPQVD.js +0 -1
  485. package/dist/dashboard-web/chunks/chunk-3UJYRR2Q.js +0 -37
  486. package/dist/dashboard-web/chunks/chunk-5E3MJJL4.js +0 -1
  487. package/dist/dashboard-web/chunks/chunk-7SZRL6FY.js +0 -1
  488. package/dist/dashboard-web/chunks/chunk-JTSE5VP6.js +0 -54
  489. package/dist/dashboard-web/chunks/chunk-LXIK7OUJ.js +0 -1
  490. package/dist/dashboard-web/chunks/chunk-PE6W3C2C.js +0 -1
  491. package/dist/dashboard-web/chunks/chunk-QW54DWU3.js +0 -325
  492. package/dist/dashboard-web/chunks/connectors-IGETYWCP.js +0 -1
  493. package/dist/dashboard-web/chunks/groups-page-MYBS4ZYJ.js +0 -207
  494. package/dist/dashboard-web/chunks/insights-page-UPA32V2S.js +0 -309
  495. package/dist/dashboard-web/chunks/monitoring-page-7T3BESZ2.js +0 -1
  496. package/dist/dashboard-web/chunks/office-page-6DXYOG55.js +0 -1
  497. package/dist/dashboard-web/chunks/overview-page-LJSBCQW4.js +0 -1
  498. package/dist/dashboard-web/chunks/roles-page-ESURTVQH.js +0 -98
  499. package/dist/dashboard-web/chunks/schedules-LML2ISOT.js +0 -1
  500. package/dist/dashboard-web/chunks/sessions-YB73XOTU.js +0 -1
  501. package/dist/dashboard-web/chunks/sessions-page-MCO5QR7Z.js +0 -1
  502. package/dist/dashboard-web/chunks/settings-page-OSDKNVS3.js +0 -2
  503. package/dist/dashboard-web/chunks/skills-page-PXBZJ6P6.js +0 -2
  504. package/dist/dashboard-web/chunks/team-federation-page-TTPVHFPX.js +0 -10
  505. package/dist/dashboard-web/chunks/v3-page-FVPUL665.js +0 -149
  506. package/dist/dashboard-web/chunks/whiteboards-page-HLCTYSSV.js +0 -1
  507. package/dist/dashboard-web/chunks/workflows-FCPM4S4D.js +0 -184
  508. package/dist/dashboard-web/skins/bluearchive-hero.webp +0 -0
  509. package/dist/dashboard-web/skins/dragonball-goku.webp +0 -0
  510. package/dist/dashboard-web/skins/dragonball-wukong.webp +0 -0
  511. package/dist/dashboard-web/skins/genshin-breeze.webp +0 -0
  512. package/dist/dashboard-web/skins/ikun-hero.webp +0 -0
  513. package/dist/dashboard-web/skins/prts-priestess.webp +0 -0
  514. package/dist/dashboard-web/skins/zzz-hero.webp +0 -0
  515. package/dist/dashboard-web/skins/zzz-pattern.svg +0 -25
@@ -1,27 +1,1642 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from 'react';
3
- import { IDLE_CLEANUP_HOUR_OPTIONS } from '../session-cleanup.js';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { IDLE_CLEANUP_HOUR_OPTIONS, parseIdleCleanupHours, selectIdleCleanupCandidates, } from '../session-cleanup.js';
4
5
  import { mountReactPage } from './react-mount.js';
5
- import { renderCliFilterGroup, SESSION_STATUS_OPTIONS, sessionStatusText, wireSessionsPage } from './sessions.js';
6
- import { useT } from './react-hooks.js';
7
- import { readStoredSessionsShowUnknownChats } from './preferences.js';
6
+ import { useStoreSelector, useT } from './react-hooks.js';
7
+ import { KANBAN_TEAM_STORAGE_KEY, normalizeSessionsViewMode, readStoredBoardOrder, readStoredKanbanGroupBy, readStoredSessionsShowUnknownChats, readStoredSessionsViewMode, writeStoredBoardOrder, writeStoredKanbanGroupBy, writeStoredSessionsShowUnknownChats, writeStoredSessionsViewMode, } from './preferences.js';
8
+ import { BOARD_COLUMNS, CLI_FILTER_OPTIONS, ICON, SESSION_STATUS_OPTIONS, canRestartSession, cssToken, deriveSessionBoardColumn, fetchPickerBots, formatTokenCount, historySenderKey, isUnknownChatSession, lockActionLabel, openWriteLink, repoBasename, restartConfirmMessage, sessionLocationText, sessionLocationTitle, sessionRuntimeCounts, sessionSearchText, sessionStatusText, shouldOpenWritableTerminal, terminalHref, tokenCount, } from './sessions.js';
9
+ import { addMonitorRoomSessionIds, monitorRoomUrl } from './monitor-room-store.js';
10
+ import { CreateActionButton, DropdownMenu, LoadingState } from './dashboard-components.js';
11
+ import { store } from './store.js';
12
+ import { attentionWaitSince, botAvatarHtml, botDisplayName, chatDisplayTitle, loadNameMaps, relTime, stripMentionPrefix, t, ui, } from './ui.js';
13
+ import { SessionsKanbanView, } from './sessions-kanban.js';
14
+ function idleCleanupHoursLabel(hours) {
15
+ return hours === 168 ? '7d' : `${hours}H`;
16
+ }
17
+ const idleCleanupThresholdOptions = IDLE_CLEANUP_HOUR_OPTIONS.map(hours => ({
18
+ value: String(hours),
19
+ label: idleCleanupHoursLabel(hours),
20
+ }));
21
+ function rawHtml(html) {
22
+ return { __html: html };
23
+ }
24
+ function windowStorage() {
25
+ return typeof window === 'undefined' ? undefined : window.localStorage;
26
+ }
27
+ function StatusBadge(props) {
28
+ const raw = String(props.status ?? 'unknown');
29
+ return _jsx("span", { className: `status status-${cssToken(raw)}`, children: sessionStatusText(raw) });
30
+ }
31
+ function LockChip(props) {
32
+ if (!props.row.locked)
33
+ return null;
34
+ return _jsx("span", { className: "session-lock-badge", title: t('sessions.locked'), children: t('sessions.locked') });
35
+ }
36
+ function IconActionButton(props) {
37
+ const className = props.className ?? `card-act${props.kind ? ` ${props.kind}` : ''}`;
38
+ return (_jsx("button", { type: "button", id: props.id, className: className, "data-action": props.action, title: props.label, "aria-label": props.label, disabled: props.disabled, onClick: (event) => {
39
+ event.stopPropagation();
40
+ props.onClick(event.currentTarget);
41
+ }, dangerouslySetInnerHTML: rawHtml(props.icon) }));
42
+ }
43
+ function TerminalControls(props) {
44
+ if (!props.url)
45
+ return null;
46
+ const readOnly = !shouldOpenWritableTerminal();
47
+ return (_jsx("span", { className: `term-pill${readOnly ? ' readonly' : ' writable'}`, children: readOnly ? (_jsx("a", { className: "term-btn term-open", href: props.url, target: "_blank", rel: "noopener", title: t('sessions.openTerminal'), "aria-label": t('sessions.openTerminal'), onClick: event => event.stopPropagation(), dangerouslySetInnerHTML: rawHtml(ICON.terminal) })) : (_jsx("button", { type: "button", className: "term-btn term-write", "data-action": "write-link", title: t('sessions.openTerminal'), "aria-label": t('sessions.openTerminal'), onClick: (event) => {
48
+ event.stopPropagation();
49
+ void openWriteLink(props.row, event.currentTarget);
50
+ }, dangerouslySetInnerHTML: rawHtml(ICON.terminal) })) }));
51
+ }
52
+ function ChatScopeLink(props) {
53
+ const row = props.row;
54
+ if (row.scope !== 'chat' || !row.feishuChatLink)
55
+ return null;
56
+ return (_jsx("a", { className: props.className ?? 'card-act', href: row.feishuChatLink, target: "_blank", rel: "noopener", title: t('sessions.openChat'), "aria-label": t('sessions.openChat'), onClick: event => event.stopPropagation(), dangerouslySetInnerHTML: rawHtml(ICON.feishu) }));
57
+ }
8
58
  function SortHeader(props) {
9
- return _jsx("th", { "data-sort": props.sort, "data-label": props.label, children: props.label });
59
+ const active = props.sortKey === props.sort;
60
+ return (_jsx("th", { "data-sort": props.sort, "data-label": props.label, className: active ? 'sorted' : undefined, "aria-sort": active ? (props.sortDir === 'asc' ? 'ascending' : 'descending') : 'none', onClick: () => props.onSort(props.sort), children: active ? `${props.label} ${props.sortDir === 'asc' ? '▲' : '▼'}` : props.label }));
10
61
  }
11
- // Render-once scaffold: after mount, wireSessionsPage owns this subtree with
12
- // delegated events and innerHTML updates. Do not add useState/useStoreSelector
13
- // here until the corresponding controller sections are migrated to React; the
14
- // route intentionally full-remounts on ui changes so first-frame translations
15
- // refresh without a partial React render fighting the imperative controller.
16
- function SessionsPage() {
17
- const tr = useT();
18
- const hostRef = useRef(null);
62
+ function sortValue(s, key) {
63
+ if (key === 'spawnedAt' || key === 'lastMessageAt')
64
+ return Number(s[key] ?? 0);
65
+ if (key === 'tokenIn')
66
+ return tokenCount(s.tokenUsage?.in) ?? -1;
67
+ if (key === 'tokenOut')
68
+ return tokenCount(s.tokenUsage?.out) ?? -1;
69
+ if (key === 'adopt')
70
+ return !!s.adopt;
71
+ if (key === 'chat')
72
+ return sessionLocationText(s).toLowerCase();
73
+ return String(s[key] ?? '').toLowerCase();
74
+ }
75
+ function compareRows(a, b, sortKey, sortDir) {
76
+ const av = sortValue(a, sortKey);
77
+ const bv = sortValue(b, sortKey);
78
+ let cmp = 0;
79
+ if (typeof av === 'number' && typeof bv === 'number')
80
+ cmp = av - bv;
81
+ else if (typeof av === 'boolean' && typeof bv === 'boolean')
82
+ cmp = Number(av) - Number(bv);
83
+ else
84
+ cmp = String(av).localeCompare(String(bv));
85
+ if (cmp === 0)
86
+ cmp = Number(a.lastMessageAt ?? 0) - Number(b.lastMessageAt ?? 0);
87
+ return sortDir === 'asc' ? cmp : -cmp;
88
+ }
89
+ function boardSignalLabel(s) {
90
+ if (s.agentAttention?.reason)
91
+ return s.agentAttention.reason;
92
+ if (s.agentAttention)
93
+ return t('sessions.board.signalAgent');
94
+ if (s.pendingRepo)
95
+ return t('sessions.board.signalRepo');
96
+ if (s.tuiPromptActive)
97
+ return t('sessions.board.signalPrompt');
98
+ if (s.status === 'limited')
99
+ return t('sessions.board.signalLimited');
100
+ return '';
101
+ }
102
+ function compareBoardRows(a, b, column) {
103
+ const av = column === 'needs-you' ? attentionWaitSince(a) : Number(a.lastMessageAt ?? 0);
104
+ const bv = column === 'needs-you' ? attentionWaitSince(b) : Number(b.lastMessageAt ?? 0);
105
+ if (av !== bv)
106
+ return column === 'needs-you' ? av - bv : bv - av;
107
+ return String(a.title ?? a.sessionId).localeCompare(String(b.title ?? b.sessionId));
108
+ }
109
+ function historyTime(v) {
110
+ if (v === undefined || v === null || v === '')
111
+ return '';
112
+ const n = Number(v);
113
+ const d = Number.isFinite(n) && n > 0 ? new Date(n) : new Date(String(v));
114
+ return Number.isNaN(d.getTime()) ? '' : d.toLocaleString();
115
+ }
116
+ function insightDur(ms) {
117
+ if (ms === undefined)
118
+ return '-';
119
+ if (ms < 1000)
120
+ return `${ms}ms`;
121
+ if (ms < 60000)
122
+ return `${(ms / 1000).toFixed(1)}s`;
123
+ return `${Math.floor(ms / 60000)}m${Math.round((ms % 60000) / 1000)}s`;
124
+ }
125
+ function useDialogVisibility(ref, open) {
19
126
  useEffect(() => {
20
- if (!hostRef.current)
127
+ const dialog = ref.current;
128
+ if (!dialog)
129
+ return;
130
+ if (open) {
131
+ if (!dialog.open) {
132
+ try {
133
+ dialog.showModal();
134
+ }
135
+ catch { /* already opening/unsupported */ }
136
+ }
137
+ }
138
+ else if (dialog.open) {
139
+ dialog.close();
140
+ }
141
+ }, [open, ref]);
142
+ }
143
+ function CopyButton(props) {
144
+ const [copied, setCopied] = useState(false);
145
+ return (_jsx("button", { type: "button", "data-copy": props.value, onClick: () => {
146
+ void navigator.clipboard.writeText(props.value);
147
+ setCopied(true);
148
+ window.setTimeout(() => setCopied(false), 800);
149
+ }, children: copied ? t('sessions.copied') : t('sessions.copy') }));
150
+ }
151
+ function LocateButton(props) {
152
+ const [cooldown, setCooldown] = useState(0);
153
+ const [busy, setBusy] = useState(false);
154
+ useEffect(() => {
155
+ if (cooldown <= 0)
156
+ return undefined;
157
+ const timer = window.setTimeout(() => setCooldown(cooldown - 1), 1000);
158
+ return () => window.clearTimeout(timer);
159
+ }, [cooldown]);
160
+ return (_jsx("button", { id: "locate-btn", type: "button", disabled: busy || cooldown > 0, onClick: async () => {
161
+ setBusy(true);
162
+ const ok = await props.locateSession(props.row);
163
+ setBusy(false);
164
+ if (ok)
165
+ setCooldown(30);
166
+ }, children: cooldown > 0 ? t('sessions.cooldown', { seconds: cooldown }) : busy ? t('sessions.locating') : t('sessions.locate') }));
167
+ }
168
+ // Icon variant of LocateButton for board/list cards: same React-owned busy+30s
169
+ // cooldown, but renders the pin icon via IconActionButton (no imperative DOM writes).
170
+ function LocateIconButton(props) {
171
+ const [cooldown, setCooldown] = useState(0);
172
+ const [busy, setBusy] = useState(false);
173
+ useEffect(() => {
174
+ if (cooldown <= 0)
175
+ return undefined;
176
+ const timer = window.setTimeout(() => setCooldown(cooldown - 1), 1000);
177
+ return () => window.clearTimeout(timer);
178
+ }, [cooldown]);
179
+ return (_jsx(IconActionButton, { action: "locate", icon: ICON.pin, label: cooldown > 0 ? t('sessions.cooldown', { seconds: cooldown }) : t('sessions.locate'), disabled: busy || cooldown > 0, onClick: async () => {
180
+ setBusy(true);
181
+ const ok = await props.onLocate(props.row);
182
+ setBusy(false);
183
+ if (ok)
184
+ setCooldown(30);
185
+ } }));
186
+ }
187
+ export function CliFilterGroup(props) {
188
+ const checked = CLI_FILTER_OPTIONS.filter(cli => props.selected.has(cli)).length;
189
+ const detailsRef = useRef(null);
190
+ useEffect(() => {
191
+ const close = () => {
192
+ if (detailsRef.current?.open)
193
+ detailsRef.current.open = false;
194
+ };
195
+ const onPointerDown = (event) => {
196
+ const details = detailsRef.current;
197
+ if (details?.open && !details.contains(event.target))
198
+ close();
199
+ };
200
+ const onKeyDown = (event) => {
201
+ if (event.key === 'Escape')
202
+ close();
203
+ };
204
+ document.addEventListener('pointerdown', onPointerDown, true);
205
+ document.addEventListener('keydown', onKeyDown);
206
+ return () => {
207
+ document.removeEventListener('pointerdown', onPointerDown, true);
208
+ document.removeEventListener('keydown', onKeyDown);
209
+ };
210
+ }, []);
211
+ return (_jsxs("details", { className: "filter-cli", ref: detailsRef, children: [_jsxs("summary", { children: [t('sessions.cli'), " \u00B7 ", _jsx("b", { id: "cli-filter-count", className: checked === CLI_FILTER_OPTIONS.length ? undefined : 'cli-filter-active', children: checked === CLI_FILTER_OPTIONS.length ? t('common.all') : `${checked}/${CLI_FILTER_OPTIONS.length}` })] }), _jsx("div", { className: "filter-cli-pop", role: "group", "aria-label": t('sessions.cli'), children: CLI_FILTER_OPTIONS.map(cli => (_jsxs("label", { className: "filter-check", children: [_jsx("input", { type: "checkbox", name: "cli", value: cli, checked: props.selected.has(cli), onChange: event => props.onToggle(cli, event.currentTarget.checked) }), _jsx("span", { children: cli })] }, cli))) })] }));
212
+ }
213
+ function SessionsFilters(props) {
214
+ const statusOptions = [
215
+ { value: '', label: t('sessions.anyStatus') },
216
+ ...SESSION_STATUS_OPTIONS.map(status => ({ value: status, label: sessionStatusText(status) })),
217
+ ];
218
+ const adoptOptions = [
219
+ { value: '', label: t('sessions.adoptAny') },
220
+ { value: 'yes', label: t('sessions.adoptYes') },
221
+ { value: 'no', label: t('sessions.adoptNo') },
222
+ ];
223
+ const statusLabel = statusOptions.find(option => option.value === props.filters.status)?.label ?? t('sessions.anyStatus');
224
+ const adoptLabel = adoptOptions.find(option => option.value === props.filters.adopt)?.label ?? t('sessions.adoptAny');
225
+ const chatOptions = [
226
+ { value: '', label: t('sessions.chatAny') },
227
+ ...props.chatOptions,
228
+ ];
229
+ const chatLabel = chatOptions.find(option => option.value === props.filters.chat)?.label ?? t('sessions.chatAny');
230
+ return (_jsxs("form", { id: "filters", className: "filters dashboard-toolbar sessions-filters", onSubmit: event => event.preventDefault(), children: [_jsx("input", { type: "search", name: "q", placeholder: t('sessions.search'), value: props.filters.q, onChange: event => props.setFilters(prev => ({ ...prev, q: event.currentTarget.value })) }), _jsx(DropdownMenu, { label: statusLabel, value: props.filters.status, options: statusOptions, onChange: value => props.setFilters(prev => ({ ...prev, status: value })) }), _jsx(DropdownMenu, { label: adoptLabel, value: props.filters.adopt, options: adoptOptions, onChange: value => props.setFilters(prev => ({ ...prev, adopt: value })) }), _jsx(DropdownMenu, { ariaLabel: t('sessions.location'), label: chatLabel, value: props.filters.chat, options: chatOptions, onChange: value => props.setFilters(prev => ({ ...prev, chat: value })) }), _jsx(CliFilterGroup, { selected: props.filters.cli, onToggle: (cli, checked) => {
231
+ props.setFilters(prev => {
232
+ const next = new Set(prev.cli);
233
+ if (checked)
234
+ next.add(cli);
235
+ else
236
+ next.delete(cli);
237
+ return { ...prev, cli: next };
238
+ });
239
+ } }), _jsxs("label", { className: "filter-toggle", children: [_jsx("input", { type: "checkbox", name: "showUnknownChats", checked: props.filters.showUnknownChats, onChange: event => {
240
+ const checked = event.currentTarget.checked;
241
+ writeStoredSessionsShowUnknownChats(windowStorage(), checked);
242
+ props.setFilters(prev => ({ ...prev, showUnknownChats: checked }));
243
+ } }), _jsx("span", { className: "filter-toggle-label", children: t('sessions.showUnknownChats') }), _jsx("span", { className: "filter-toggle-switch", "aria-hidden": "true" })] }), _jsxs("label", { className: "filter-toggle", children: [_jsx("input", { type: "checkbox", name: "active", checked: props.filters.active, onChange: event => props.setFilters(prev => ({ ...prev, active: event.currentTarget.checked })) }), _jsx("span", { className: "filter-toggle-label", children: t('sessions.activeOnly') }), _jsx("span", { className: "filter-toggle-switch", "aria-hidden": "true" })] }), _jsx(IdleCleanupBar, { ...props.idleCleanup })] }));
244
+ }
245
+ function BulkBar(props) {
246
+ const busy = !!props.closeProgress || !!props.lockProgress;
247
+ const lockText = props.lockProgress?.locked ? `${props.lockProgress.done}/${props.lockProgress.total}` : t('sessions.lockSelected');
248
+ const unlockText = props.lockProgress && !props.lockProgress.locked ? `${props.lockProgress.done}/${props.lockProgress.total}` : t('sessions.unlockSelected');
249
+ return (_jsxs("div", { id: "bulk-bar", className: "bulk-bar", hidden: props.selectedCount === 0, children: [_jsx("span", { id: "bulk-count", children: t('sessions.selectedCount', { count: props.selectedCount }) }), _jsx("button", { type: "button", id: "bulk-monitor-room", disabled: busy || props.selectedCount === 0, onClick: props.onAddToMonitorRoom, children: props.monitorRoomText ?? t('sessions.addToMonitorRoom') }), _jsx("button", { type: "button", id: "bulk-lock", disabled: busy || props.lockDisabled, onClick: () => props.onLock(true), children: lockText }), _jsx("button", { type: "button", id: "bulk-unlock", disabled: busy || props.unlockDisabled, onClick: () => props.onLock(false), children: unlockText }), _jsx("button", { type: "button", id: "bulk-close", className: "contrast", disabled: busy, onClick: props.onClose, children: props.closeProgress ? `${props.closeProgress.done}/${props.closeProgress.total}` : t('sessions.closeSelected') }), _jsx("button", { type: "button", id: "bulk-clear", disabled: busy, onClick: props.onClear, children: t('sessions.clearSelection') })] }));
250
+ }
251
+ function IdleCleanupBar(props) {
252
+ const [open, setOpen] = useState(false);
253
+ const [draftHours, setDraftHours] = useState(props.hours);
254
+ const [popStyle, setPopStyle] = useState();
255
+ const rootRef = useRef(null);
256
+ const buttonRef = useRef(null);
257
+ const popRef = useRef(null);
258
+ const count = props.countForHours(draftHours);
259
+ useEffect(() => {
260
+ if (open)
261
+ setDraftHours(props.hours);
262
+ else
263
+ setPopStyle(undefined);
264
+ }, [open, props.hours]);
265
+ useLayoutEffect(() => {
266
+ if (!open)
21
267
  return undefined;
22
- return wireSessionsPage(hostRef.current);
268
+ const place = () => {
269
+ const button = buttonRef.current;
270
+ const pop = popRef.current;
271
+ if (!button || !pop)
272
+ return;
273
+ const margin = 12;
274
+ const viewportWidth = window.innerWidth;
275
+ const viewportHeight = window.innerHeight;
276
+ const width = Math.max(0, Math.min(292, viewportWidth - margin * 2));
277
+ const buttonRect = button.getBoundingClientRect();
278
+ const height = pop.offsetHeight;
279
+ const maxLeft = Math.max(margin, viewportWidth - width - margin);
280
+ const centeredLeft = buttonRect.left + buttonRect.width / 2 - width / 2;
281
+ const left = Math.min(Math.max(centeredLeft, margin), maxLeft);
282
+ const belowTop = buttonRect.bottom + 8;
283
+ const aboveTop = buttonRect.top - height - 8;
284
+ const top = belowTop + height <= viewportHeight - margin || aboveTop < margin
285
+ ? Math.min(Math.max(belowTop, margin), Math.max(margin, viewportHeight - height - margin))
286
+ : Math.max(margin, aboveTop);
287
+ setPopStyle({ left, top, width });
288
+ };
289
+ const frame = window.requestAnimationFrame(place);
290
+ window.addEventListener('resize', place);
291
+ window.addEventListener('scroll', place, true);
292
+ return () => {
293
+ window.cancelAnimationFrame(frame);
294
+ window.removeEventListener('resize', place);
295
+ window.removeEventListener('scroll', place, true);
296
+ };
297
+ }, [open, props.busy, props.status]);
298
+ useEffect(() => {
299
+ if (!open)
300
+ return undefined;
301
+ const onPointerDown = (event) => {
302
+ const target = event.target;
303
+ if (target instanceof Node && rootRef.current?.contains(target))
304
+ return;
305
+ if (target instanceof Node && popRef.current?.contains(target))
306
+ return;
307
+ setOpen(false);
308
+ };
309
+ const onKeyDown = (event) => {
310
+ if (event.key === 'Escape')
311
+ setOpen(false);
312
+ };
313
+ document.addEventListener('pointerdown', onPointerDown, true);
314
+ document.addEventListener('keydown', onKeyDown);
315
+ return () => {
316
+ document.removeEventListener('pointerdown', onPointerDown, true);
317
+ document.removeEventListener('keydown', onKeyDown);
318
+ };
319
+ }, [open]);
320
+ return (_jsxs("div", { id: "idle-cleanup-bar", className: `idle-cleanup-bar${count === 0 ? ' is-empty' : ''}${open ? ' is-open' : ''}`, ref: rootRef, children: [_jsxs("button", { ref: buttonRef, type: "button", id: "idle-cleanup-run", className: "contrast idle-cleanup-run", "aria-haspopup": "dialog", "aria-expanded": open, disabled: props.busy, onClick: () => setOpen(value => !value), children: [_jsxs("svg", { className: "idle-cleanup-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M3.5 6.5h17" }), _jsx("path", { d: "M9 3.5h6" }), _jsx("path", { d: "m6.5 6.5.9 13h9.2l.9-13" }), _jsx("path", { d: "M10 10.5v5M14 10.5v5" })] }), _jsx("span", { children: t('sessions.idleCleanupRun') })] }), open && typeof document !== 'undefined' ? createPortal((_jsxs("div", { ref: popRef, className: "idle-cleanup-pop", role: "dialog", "aria-label": t('sessions.idleCleanupRun'), style: popStyle ?? { visibility: 'hidden' }, children: [_jsxs("div", { className: "idle-cleanup-pop-head", children: [_jsx("span", { className: "idle-cleanup-pop-title", children: t('sessions.idleCleanupRun') }), _jsxs("span", { id: "idle-cleanup-count", className: "idle-cleanup-count", children: [_jsx("span", { className: "idle-cleanup-dot", "aria-hidden": "true" }), t('sessions.idleCleanupCount', { count })] })] }), _jsxs("div", { className: "idle-cleanup-pop-field", children: [_jsx("span", { className: "idle-cleanup-label", children: t('sessions.idleCleanupOlderThan') }), _jsx("div", { id: "idle-cleanup-threshold", className: "idle-cleanup-threshold-options", role: "radiogroup", "aria-label": t('sessions.idleCleanupThreshold'), children: idleCleanupThresholdOptions.map(option => {
321
+ const hours = parseIdleCleanupHours(option.value);
322
+ const active = hours === draftHours;
323
+ return (_jsx("button", { type: "button", className: active ? 'active' : undefined, "aria-pressed": active ? 'true' : 'false', disabled: props.busy, onClick: () => setDraftHours(hours), children: option.label }, option.value));
324
+ }) })] }), props.busy && props.status ? (_jsx("span", { id: "idle-cleanup-status", className: "idle-cleanup-status", "aria-live": "polite", children: props.status })) : null, _jsxs("div", { className: "idle-cleanup-pop-actions", children: [_jsx("button", { type: "button", className: "idle-cleanup-cancel", disabled: props.busy, onClick: () => setOpen(false), children: t('sessions.idleCleanupCancel') }), _jsx("button", { type: "button", className: "idle-cleanup-confirm", disabled: props.busy || count === 0, onClick: () => {
325
+ void props.onRun(draftHours).then(() => setOpen(false));
326
+ }, children: t('sessions.idleCleanupApply') })] })] })), document.body) : null] }));
327
+ }
328
+ function SessionsTable(props) {
329
+ const selectAllRef = useRef(null);
330
+ useLayoutEffect(() => {
331
+ if (selectAllRef.current)
332
+ selectAllRef.current.indeterminate = props.selectAllIndeterminate;
333
+ }, [props.selectAllIndeterminate]);
334
+ const headers = [
335
+ ['botName', t('sessions.bot')],
336
+ ['cliId', t('sessions.cli')],
337
+ ['status', t('sessions.status')],
338
+ ['chat', t('sessions.location')],
339
+ ['tokenIn', t('sessions.tokenIn')],
340
+ ['tokenOut', t('sessions.tokenOut')],
341
+ ['title', t('sessions.titleCol')],
342
+ ['workingDir', t('sessions.workingDir')],
343
+ ['spawnedAt', t('sessions.created')],
344
+ ['lastMessageAt', t('sessions.last')],
345
+ ['adopt', t('sessions.adopt')],
346
+ ];
347
+ const labels = {
348
+ select: t('sessions.selectSession'),
349
+ botName: t('sessions.bot'),
350
+ cliId: t('sessions.cli'),
351
+ status: t('sessions.status'),
352
+ chat: t('sessions.location'),
353
+ tokenIn: t('sessions.tokenIn'),
354
+ tokenOut: t('sessions.tokenOut'),
355
+ title: t('sessions.titleCol'),
356
+ workingDir: t('sessions.workingDir'),
357
+ spawnedAt: t('sessions.created'),
358
+ lastMessageAt: t('sessions.last'),
359
+ adopt: t('sessions.adopt'),
360
+ actions: t('sessions.actions'),
361
+ };
362
+ return (_jsxs("table", { id: "sessions-table", hidden: props.hidden, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: _jsx("input", { ref: selectAllRef, type: "checkbox", id: "select-all", title: t('sessions.activeOnly'), checked: props.selectAllChecked, disabled: props.selectAllDisabled, onChange: event => props.onSelectAll(event.currentTarget.checked) }) }), headers.map(([sort, label]) => (_jsx(SortHeader, { sort: sort, label: label, sortKey: props.sortKey, sortDir: props.sortDir, onSort: props.onSort }, sort))), _jsx("th", { children: t('sessions.actions') })] }) }), _jsx("tbody", { children: props.rows.length ? props.rows.map(row => {
363
+ const closed = row.status === 'closed';
364
+ const id = String(row.sessionId);
365
+ return (_jsxs("tr", { "data-id": id, onClick: () => props.onOpen(row), children: [_jsx("td", { className: "sessions-table-select-cell", "data-label": labels.select, onClick: event => event.stopPropagation(), children: _jsx("input", { type: "checkbox", className: "row-select", checked: props.selected.has(id), disabled: closed, onChange: event => props.onSelect(id, event.currentTarget.checked) }) }), _jsx("td", { "data-label": labels.botName, children: botDisplayName(row) }), _jsx("td", { "data-label": labels.cliId, children: _jsx("span", { className: `badge cli-${cssToken(row.cliId)}`, children: row.cliId ?? 'unknown' }) }), _jsxs("td", { "data-label": labels.status, children: [_jsx(StatusBadge, { status: row.status }), _jsx(LockChip, { row: row })] }), _jsx("td", { className: "session-location-cell", "data-label": labels.chat, title: sessionLocationTitle(row), children: sessionLocationText(row) }), _jsx("td", { className: "token-cell", "data-label": labels.tokenIn, children: formatTokenCount(row.tokenUsage?.in) }), _jsx("td", { className: "token-cell", "data-label": labels.tokenOut, children: formatTokenCount(row.tokenUsage?.out) }), _jsx("td", { className: "sessions-table-text-cell", "data-label": labels.title, title: String(row.title ?? ''), children: stripMentionPrefix(row.title ?? '').slice(0, 48) }), _jsx("td", { className: "sessions-table-path-cell", "data-label": labels.workingDir, title: row.workingDir ?? '', children: String(row.workingDir ?? '').slice(-34) }), _jsx("td", { "data-label": labels.spawnedAt, children: relTime(row.spawnedAt) }), _jsx("td", { "data-label": labels.lastMessageAt, children: relTime(row.lastMessageAt) }), _jsx("td", { "data-label": labels.adopt, children: row.adopt ? _jsx("span", { className: "badge", children: "adopt" }) : null }), _jsx("td", { className: "sessions-table-action-cell", "data-label": labels.actions, children: _jsx("button", { className: "open", type: "button", children: t('sessions.details') }) })] }, id));
366
+ }) : (_jsx("tr", { children: _jsx("td", { colSpan: 13, className: "empty", children: t('sessions.empty') }) })) })] }));
367
+ }
368
+ function BoardCard(props) {
369
+ const row = props.row;
370
+ const title = stripMentionPrefix(row.title) || row.sessionId;
371
+ const botName = botDisplayName(row);
372
+ const chatTitle = chatDisplayTitle(row);
373
+ const term = terminalHref(row);
374
+ const signal = boardSignalLabel(row);
375
+ const repo = repoBasename(row.workingDir);
376
+ const onCardClick = (event) => {
377
+ const target = event.target;
378
+ if (target.closest('a, button, input, label'))
379
+ return;
380
+ props.onToggleSelect(row);
381
+ };
382
+ return (_jsxs("article", { className: `session-card${props.selected ? ' selected' : ''}${row.locked ? ' locked' : ''}`, "data-id": row.sessionId, "aria-pressed": props.selected, onClick: onCardClick, children: [_jsxs("div", { className: "session-card-top", children: [_jsx("span", { dangerouslySetInnerHTML: rawHtml(botAvatarHtml({ name: botName, larkAppId: row.larkAppId, size: 'sm' })) }), _jsxs("div", { className: "session-card-title", children: [_jsx("strong", { title: String(row.title ?? title), children: String(title).slice(0, 72) }), _jsxs("span", { children: [botName, " \u00B7 ", chatTitle ?? row.cliId ?? 'unknown'] })] }), _jsxs("span", { className: "session-card-status-group", children: [_jsx(StatusBadge, { status: row.status }), _jsx(LockChip, { row: row })] })] }), repo !== '-' || row.adopt || signal ? (_jsxs("div", { className: "session-card-meta", children: [repo !== '-' ? _jsx("span", { title: row.workingDir ?? '', children: repo }) : null, row.adopt ? _jsx("span", { className: "badge", children: "adopt" }) : null, signal ? _jsx("span", { className: "session-signal", title: signal, children: signal }) : null] })) : null, _jsx("div", { className: "session-card-time", children: _jsx("span", { children: row.agentAttention?.at
383
+ ? `${t('sessions.board.waiting')} ${relTime(attentionWaitSince(row))}`
384
+ : `${t('sessions.last')}: ${relTime(row.lastMessageAt)}` }) }), _jsxs("div", { className: "session-card-actions", children: [_jsx(ChatScopeLink, { row: row }), !row.feishuChatLink || row.scope !== 'chat' ? (_jsx(LocateIconButton, { row: row, onLocate: props.onLocate })) : null, _jsx(IconActionButton, { action: "details", icon: ICON.details, label: t('sessions.details'), onClick: () => props.onOpen(row) }), canRestartSession(row) ? (_jsx(IconActionButton, { action: "restart", icon: ICON.restart, label: t('sessions.restart'), onClick: button => props.onRestart(row, button) })) : null, _jsx(TerminalControls, { row: row, url: term }), _jsx(IconActionButton, { action: "lock", icon: row.locked ? ICON.unlock : ICON.lock, label: lockActionLabel(row), kind: row.locked ? 'locked' : '', onClick: button => props.onLock(row, !row.locked, button) }), _jsx(IconActionButton, { action: "close", icon: ICON.close, label: t('sessions.close'), kind: "danger", onClick: button => props.onClose(row, button) })] })] }));
385
+ }
386
+ function BoardView(props) {
387
+ useEffect(() => {
388
+ if (!props.hidden && !props.animated)
389
+ props.onAnimated();
390
+ }, [props.animated, props.hidden, props.onAnimated]);
391
+ const groups = new Map(BOARD_COLUMNS.map(column => [column.id, []]));
392
+ for (const row of props.rows) {
393
+ const column = deriveSessionBoardColumn(row);
394
+ if (column)
395
+ groups.get(column).push(row);
396
+ }
397
+ const columns = props.order
398
+ .map(id => BOARD_COLUMNS.find(column => column.id === id))
399
+ .filter((column) => !!column);
400
+ return (_jsx("div", { id: "sessions-board", className: `sessions-board${props.animated || props.hidden ? '' : ' board-enter'}`, hidden: props.hidden, children: columns.map((column, idx) => {
401
+ const columnRows = (groups.get(column.id) ?? []).sort((a, b) => compareBoardRows(a, b, column.id));
402
+ return (_jsxs("section", { className: `session-board-column session-board-${column.id}${props.dragColId === column.id ? ' dragging' : ''}${props.dragOverCol === column.id ? ' drag-over' : ''}`, "data-col": column.id, onDragOver: (event) => {
403
+ if (!props.dragColId)
404
+ return;
405
+ event.preventDefault();
406
+ if (event.dataTransfer)
407
+ event.dataTransfer.dropEffect = 'move';
408
+ if (props.dragColId !== column.id)
409
+ props.onDragOverCol(column.id);
410
+ }, onDrop: (event) => {
411
+ if (!props.dragColId)
412
+ return;
413
+ event.preventDefault();
414
+ props.onMoveColumnTo(props.dragColId, column.id);
415
+ props.onDragCol(null);
416
+ props.onDragOverCol(null);
417
+ }, children: [_jsxs("header", { draggable: true, title: t('sessions.board.dragHint'), onDragStart: (event) => {
418
+ props.onDragCol(column.id);
419
+ if (event.dataTransfer) {
420
+ event.dataTransfer.effectAllowed = 'move';
421
+ event.dataTransfer.setData('text/plain', column.id);
422
+ }
423
+ }, onDragEnd: () => {
424
+ props.onDragCol(null);
425
+ props.onDragOverCol(null);
426
+ }, children: [_jsxs("div", { children: [_jsx("h2", { children: t(column.labelKey) }), _jsx("p", { children: t(column.hintKey) })] }), _jsxs("span", { className: "session-board-head-right", children: [_jsxs("span", { className: "session-board-move", children: [_jsx("button", { type: "button", "data-move-col": column.id, "data-dir": "-1", "aria-label": t('sessions.board.moveLeft'), disabled: idx === 0, onClick: () => props.onMoveColumn(column.id, -1), children: "\u2039" }), _jsx("button", { type: "button", "data-move-col": column.id, "data-dir": "1", "aria-label": t('sessions.board.moveRight'), disabled: idx === columns.length - 1, onClick: () => props.onMoveColumn(column.id, 1), children: "\u203A" })] }), _jsx("span", { className: "session-board-count", children: columnRows.length })] })] }), _jsx("div", { className: "session-board-list", children: columnRows.length ? columnRows.map(row => (_jsx(BoardCard, { row: row, selected: props.selected.has(row.sessionId), onToggleSelect: props.onToggleSelect, onOpen: props.onOpen, onHistory: props.onHistory, onLocate: props.onLocate, onRestart: props.onRestart, onLock: props.onLock, onClose: props.onClose }, row.sessionId))) : _jsx("div", { className: "session-board-empty", children: t('sessions.board.emptyColumn') }) })] }, column.id));
427
+ }) }));
428
+ }
429
+ function HistoryBubble(props) {
430
+ const m = props.message;
431
+ const human = m.senderType === 'user';
432
+ const botSender = m.senderType === 'app' || m.senderType === 'bot';
433
+ const name = human
434
+ ? (m.senderName || (props.ownerOpenId && m.senderId === props.ownerOpenId ? t('sessions.history.owner') : t('sessions.history.user')))
435
+ : (m.senderName || String(m.senderId ?? '').slice(0, 16) || t(botSender ? 'sessions.history.bot' : 'sessions.history.system'));
436
+ const content = String(m.content ?? '').trim() || `[${m.msgType ?? 'message'}]`;
437
+ return (_jsxs("div", { className: `history-msg${props.groupStart ? ' group-start' : ' continuation'}`, children: [props.groupStart ? (human ? (m.senderAvatar ? (_jsx("img", { className: "history-avatar-img", src: String(m.senderAvatar), alt: "", decoding: "async", referrerPolicy: "no-referrer" })) : _jsx("span", { className: "history-avatar-user", "aria-hidden": "true", children: String(name).slice(0, 1) })) : _jsx("span", { className: "history-avatar-bot", dangerouslySetInnerHTML: rawHtml(botAvatarHtml({ name, larkAppId: m.senderBotAppId, avatarUrl: m.senderAvatar, size: 'sm' })) })) : _jsx("span", { className: "history-avatar-spacer", "aria-hidden": "true" }), _jsxs("div", { className: "history-msg-main", children: [props.groupStart ? _jsxs("div", { className: "history-msg-meta", children: [_jsx("span", { children: name }), _jsx("time", { children: historyTime(m.createTime) })] }) : null, _jsx("div", { className: "history-bubble", children: content })] })] }));
438
+ }
439
+ function HistoryModal(props) {
440
+ const dialogRef = useRef(null);
441
+ useDialogVisibility(dialogRef, !!props.state);
442
+ const row = props.state ? store.sessions.get(props.state.sessionId) : null;
443
+ const bodyRef = useRef(null);
444
+ const loaded = !!props.state && !props.state.loading && !props.state.error && props.state.messages.length > 0;
445
+ // Open pinned to the newest message (old imperative code did scrollTop = scrollHeight).
446
+ useEffect(() => {
447
+ if (!loaded)
448
+ return;
449
+ const el = bodyRef.current;
450
+ if (el)
451
+ el.scrollTop = el.scrollHeight;
452
+ }, [loaded, props.state?.sessionId, props.state?.messages.length]);
453
+ return (_jsx("dialog", { id: "history-modal", className: "history-modal", ref: dialogRef, onClose: props.onClose, onClick: event => { if (event.target === event.currentTarget)
454
+ props.onClose(); }, children: props.state && row ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "term-modal-head", children: [_jsxs("span", { className: "term-modal-title", children: [_jsx("span", { dangerouslySetInnerHTML: rawHtml(botAvatarHtml({ name: botDisplayName(row), larkAppId: row.larkAppId, size: 'sm' })) }), _jsx("strong", { title: String(row.title ?? ''), children: (stripMentionPrefix(row.title) || row.sessionId).slice(0, 60) }), _jsx("span", { className: "history-scope-tag", children: t('sessions.history.title') })] }), _jsx("span", { className: "term-modal-actions", children: _jsx(IconActionButton, { id: "history-close", icon: ICON.close, label: t('sessions.dismiss'), onClick: props.onClose }) })] }), _jsxs("div", { className: "history-body", ref: bodyRef, children: [props.state.loading ? _jsx(LoadingState, { label: t('sessions.history.loading'), className: "term-modal-loading", compact: true }) : null, !props.state.loading && props.state.error ? (_jsxs("div", { className: "history-error", children: [t('sessions.history.fail'), ": ", props.state.error, props.state.stale ? _jsxs(_Fragment, { children: [_jsx("br", {}), _jsx("span", { children: t('sessions.history.staleHint') })] }) : null] })) : null, !props.state.loading && !props.state.error && props.state.messages.length === 0 ? (_jsx("div", { className: "history-error", children: t('sessions.history.empty') })) : null, !props.state.loading && !props.state.error && props.state.messages.length > 0 ? (_jsx("div", { className: "history-list", children: props.state.messages.map((message, index, messages) => (_jsx(HistoryBubble, { message: message, ownerOpenId: props.state?.ownerOpenId, groupStart: index === 0 || historySenderKey(messages[index - 1]) !== historySenderKey(message) }, message.messageId ?? index))) })) : null] })] })) : null }));
455
+ }
456
+ function TerminalNameEditor(props) {
457
+ const [editing, setEditing] = useState(false);
458
+ const [value, setValue] = useState('');
459
+ const inputRef = useRef(null);
460
+ const title = stripMentionPrefix(props.row.title) || props.row.sessionId;
461
+ useLayoutEffect(() => {
462
+ if (!editing || !inputRef.current)
463
+ return;
464
+ const input = inputRef.current;
465
+ input.focus();
466
+ input.select();
467
+ const fit = () => {
468
+ const cs = getComputedStyle(input);
469
+ const span = document.createElement('span');
470
+ span.style.cssText = 'position:absolute;visibility:hidden;white-space:pre';
471
+ span.style.fontSize = cs.fontSize;
472
+ span.style.fontFamily = cs.fontFamily;
473
+ span.style.fontWeight = cs.fontWeight;
474
+ span.style.letterSpacing = cs.letterSpacing;
475
+ span.textContent = input.value || ' ';
476
+ document.body.appendChild(span);
477
+ const max = Math.round(window.innerWidth * 0.6);
478
+ input.style.width = `${Math.min(Math.max(span.offsetWidth + 22, 80), max)}px`;
479
+ span.remove();
480
+ };
481
+ fit();
482
+ }, [editing, value]);
483
+ const finish = (commit) => {
484
+ const next = value.trim();
485
+ setEditing(false);
486
+ if (commit && next && next !== title)
487
+ props.onRename(props.row, next);
488
+ };
489
+ if (editing) {
490
+ return (_jsx("input", { ref: inputRef, type: "text", className: "term-modal-name-input", maxLength: 200, value: value, onChange: event => setValue(event.currentTarget.value), onBlur: () => finish(true), onKeyDown: event => {
491
+ event.stopPropagation();
492
+ if (event.key === 'Enter') {
493
+ event.preventDefault();
494
+ finish(true);
495
+ }
496
+ else if (event.key === 'Escape') {
497
+ event.preventDefault();
498
+ finish(false);
499
+ }
500
+ } }));
501
+ }
502
+ return (_jsxs(_Fragment, { children: [_jsx("strong", { className: "term-modal-name", title: String(props.row.title ?? title), children: String(title).slice(0, 60) }), _jsx(IconActionButton, { id: "term-modal-edit", icon: ICON.edit, label: t('sessions.kanban.rename'), onClick: () => {
503
+ setValue(stripMentionPrefix(props.row.title) || '');
504
+ setEditing(true);
505
+ } })] }));
506
+ }
507
+ function TerminalModal(props) {
508
+ const dialogRef = useRef(null);
509
+ useDialogVisibility(dialogRef, !!props.state);
510
+ const row = props.state ? store.sessions.get(props.state.sessionId) : null;
511
+ const readonlyUrl = row ? terminalHref(row) : null;
512
+ const url = props.state?.url ?? readonlyUrl ?? '';
513
+ const rowCli = row ? String(row.cliId ?? 'unknown') : '';
514
+ const rowRepo = row ? repoBasename(row.workingDir) : '';
515
+ return (_jsx("dialog", { id: "term-modal", className: "term-modal", ref: dialogRef, onClose: props.onClose, onClick: event => { if (event.target === event.currentTarget)
516
+ props.onClose(); }, children: props.state && row ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "term-modal-head", children: [_jsxs("span", { className: "term-modal-title", children: [_jsx("span", { dangerouslySetInnerHTML: rawHtml(botAvatarHtml({ name: botDisplayName(row), larkAppId: row.larkAppId, size: 'sm' })) }), _jsxs("span", { className: "term-modal-title-copy", children: [_jsxs("span", { className: "term-modal-title-main", children: [_jsx(TerminalNameEditor, { row: row, onRename: props.onRename }), _jsx(StatusBadge, { status: row.status })] }), _jsxs("span", { className: "term-modal-subtitle", children: [_jsx("span", { children: botDisplayName(row) }), _jsx("span", { children: rowCli }), rowRepo !== '-' ? _jsx("span", { title: row.workingDir ?? '', children: rowRepo }) : null] })] })] }), _jsxs("span", { className: "term-modal-actions", children: [row.feishuChatLink ? (_jsx("a", { className: "card-act", href: row.feishuChatLink, target: "_blank", rel: "noopener", title: t('sessions.kanban.openFeishu'), "aria-label": t('sessions.kanban.openFeishu'), dangerouslySetInnerHTML: rawHtml(ICON.feishu) })) : null, _jsx("a", { id: "term-modal-tab", className: "card-act", href: url, target: "_blank", rel: "noopener", title: t('sessions.kanban.openTab'), "aria-label": t('sessions.kanban.openTab'), dangerouslySetInnerHTML: rawHtml(ICON.terminal) }), _jsx(IconActionButton, { id: "term-modal-close", icon: ICON.close, label: t('sessions.dismiss'), onClick: props.onClose })] })] }), _jsx("div", { className: "term-modal-body", children: _jsx("div", { className: "term-modal-frame-shell", children: props.state.loading ? _jsx(LoadingState, { label: t('sessions.kanban.terminalLoading'), className: "term-modal-loading", compact: true }) : (_jsx("iframe", { className: "term-modal-frame", src: url, allow: "clipboard-read; clipboard-write" })) }) })] })) : null }));
517
+ }
518
+ function LandPanel(props) {
519
+ const [state, setState] = useState(null);
520
+ useEffect(() => setState(null), [props.row.sessionId]);
521
+ const load = async () => {
522
+ setState({ loading: true });
523
+ try {
524
+ const r = await fetch(`/api/sessions/${encodeURIComponent(props.row.sessionId)}/sandbox-diff`);
525
+ const d = await r.json().catch(() => ({}));
526
+ if (!d.ok) {
527
+ setState({ loading: false, message: _jsxs("p", { children: [t('sessions.landUnavailable'), ": ", String(d.error ?? r.status)] }) });
528
+ return;
529
+ }
530
+ if (d.empty) {
531
+ setState({ loading: false, message: _jsx("p", { children: t('sessions.landEmpty') }) });
532
+ return;
533
+ }
534
+ const full = String(d.patch ?? '');
535
+ setState({
536
+ loading: false,
537
+ diff: d,
538
+ patch: full.slice(0, 20000) + (full.length > 20000 ? '\n...(truncated)' : ''),
539
+ });
540
+ }
541
+ catch (e) {
542
+ setState({ loading: false, message: _jsxs("p", { children: [t('sessions.landUnavailable'), ": ", String(e)] }) });
543
+ }
544
+ };
545
+ const apply = async () => {
546
+ const rr = await fetch(`/api/sessions/${encodeURIComponent(props.row.sessionId)}/sandbox-land/apply`, { method: 'POST' });
547
+ const res = await rr.json().catch(() => ({}));
548
+ setState({
549
+ loading: false,
550
+ message: res.ok
551
+ ? _jsxs("p", { children: [t('sessions.landApplied'), ": ", res.files, " files (+", res.insertions, "/-", res.deletions, ") \u2192 ", _jsx("code", { children: String(res.workingDir ?? '') })] })
552
+ : _jsxs("p", { children: [t('sessions.landFailed'), ": ", String(res.error ?? rr.status)] }),
553
+ });
554
+ };
555
+ const discard = async () => {
556
+ await fetch(`/api/sessions/${encodeURIComponent(props.row.sessionId)}/sandbox-land/discard`, { method: 'POST' });
557
+ setState({ loading: false, message: _jsx("p", { children: t('sessions.landDiscarded') }) });
558
+ };
559
+ return (_jsxs(_Fragment, { children: [_jsx("button", { id: "land-btn", type: "button", disabled: state?.loading, onClick: () => void load(), children: t('sessions.land') }), _jsxs("div", { id: "land-area", children: [state?.loading ? _jsx(LoadingState, { label: t('sessions.landLoading'), compact: true }) : null, state?.message, state?.diff && state.patch !== undefined ? (_jsxs(_Fragment, { children: [_jsxs("p", { children: [_jsx("b", { children: state.diff.files }), " files (+", state.diff.insertions, "/-", state.diff.deletions, ") \u2192 ", _jsx("code", { children: String(state.diff.workingDir ?? '') })] }), _jsx("pre", { style: { maxHeight: 320, overflow: 'auto', whiteSpace: 'pre-wrap' }, children: state.patch }), _jsxs("div", { className: "actions", children: [_jsx("button", { id: "land-apply", type: "button", className: "primary", onClick: () => void apply(), children: t('sessions.landApply') }), _jsx("button", { id: "land-discard", type: "button", className: "contrast", onClick: () => void discard(), children: t('sessions.landDiscard') })] })] })) : null] })] }));
560
+ }
561
+ function InsightReport(props) {
562
+ const rep = props.report;
563
+ if (!rep || rep.status !== 'ok') {
564
+ const msg = rep?.error?.message ? String(rep.error.message) : String(rep?.status ?? 'error');
565
+ return _jsxs("p", { children: [t('sessions.insightUnavailable'), ": ", msg] });
566
+ }
567
+ const a = rep.agg ?? {};
568
+ if (!a.totalSpans)
569
+ return _jsx("p", { children: t('sessions.insightEmpty') });
570
+ const metaBits = [
571
+ rep.meta?.asOf ? t('sessions.insightAsOf', { asOf: String(rep.meta.asOf) }) : null,
572
+ rep.meta?.partial ? t('sessions.insightPartial') : null,
573
+ ].filter(Boolean);
574
+ const suggestions = Array.isArray(rep.suggestions) ? rep.suggestions : [];
575
+ const spans = Array.isArray(rep.spans) ? rep.spans : [];
576
+ const suggestionIcon = (sev) => (sev === 'bad' ? '!' : sev === 'warn' ? '!' : 'i');
577
+ const spanIcon = (status) => (status === 'error' ? '!' : status === 'running' ? '...' : 'ok');
578
+ return (_jsxs(_Fragment, { children: [metaBits.length ? _jsx("p", { style: { fontSize: 12, color: 'var(--muted,#8f959e)' }, children: metaBits.join(' · ') }) : null, _jsx("p", { children: t('sessions.insightMetrics', {
579
+ total: String(a.totalSpans ?? 0),
580
+ failed: String(a.failedSpans ?? 0),
581
+ slow: String(a.slowSpans ?? 0),
582
+ rw: (a.readWriteRatio === null || a.readWriteRatio === undefined) ? '-' : String(a.readWriteRatio),
583
+ compactions: String(a.compactions ?? 0),
584
+ }) }), suggestions.length ? (_jsxs("details", { open: true, children: [_jsx("summary", { children: t('sessions.insightSuggestions') }), _jsx("ul", { style: { paddingLeft: 18, margin: '6px 0' }, children: suggestions.map((sg, index) => (_jsxs("li", { children: [suggestionIcon(String(sg.severity ?? '')), " ", _jsx("b", { children: String(sg.title ?? '') }), " - ", String(sg.action ?? ''), Array.isArray(sg.evidence) && sg.evidence.length ? (_jsxs(_Fragment, { children: [_jsx("br", {}), _jsx("small", { style: { color: 'var(--muted,#8f959e)' }, children: sg.evidence.join('; ') })] })) : null] }, index))) })] })) : null, spans.length ? (_jsxs("details", { children: [_jsxs("summary", { children: [t('sessions.insightSpans'), " (", spans.length, ")"] }), rep.meta?.capped ? (_jsx("p", { style: { fontSize: 12, color: 'var(--muted,#8f959e)' }, children: t('sessions.insightCapped', { shown: String(rep.meta.spansReturned ?? spans.length), total: String(rep.meta.spansTotal ?? spans.length) }) })) : null, _jsx("div", { style: { maxHeight: 320, overflow: 'auto' }, children: _jsx("table", { style: { width: '100%', fontSize: 12 }, children: _jsx("tbody", { children: [...spans].sort((x, y) => (x.relStartMs ?? 0) - (y.relStartMs ?? 0)).map((sp, index) => {
585
+ const io = [sp.inputSummary, sp.outputSummary].filter(Boolean).map(String).join(' -> ');
586
+ return (_jsxs("tr", { style: sp.status === 'error' ? { color: 'var(--danger,#d33)' } : undefined, children: [_jsx("td", { children: spanIcon(String(sp.status ?? '')) }), _jsx("td", { children: _jsx("code", { children: String(sp.tool ?? '') }) }), _jsx("td", { children: String(sp.phase ?? '') }), _jsx("td", { children: insightDur(sp.durationMs) }), _jsxs("td", { children: ["#", String(sp.turnIndex ?? 0)] }), _jsx("td", { children: io })] }, index));
587
+ }) }) }) })] })) : null] }));
588
+ }
589
+ function InsightPanel(props) {
590
+ const [state, setState] = useState(null);
591
+ useEffect(() => setState(null), [props.row.sessionId]);
592
+ if (!ui.authed)
593
+ return null;
594
+ const load = async () => {
595
+ setState({ loading: true });
596
+ try {
597
+ const r = await fetch(`/api/sessions/${encodeURIComponent(props.row.sessionId)}/insight?detail=spans`);
598
+ const d = await r.json().catch(() => ({}));
599
+ if (!d.ok || !d.report)
600
+ setState({ loading: false, error: String(d.error ?? r.status) });
601
+ else
602
+ setState({ loading: false, report: d.report });
603
+ }
604
+ catch (e) {
605
+ setState({ loading: false, error: String(e) });
606
+ }
607
+ };
608
+ return (_jsxs(_Fragment, { children: [_jsx("button", { id: "insight-btn", type: "button", disabled: state?.loading, onClick: () => void load(), children: t('sessions.insight') }), _jsxs("div", { id: "insight-area", children: [state?.loading ? _jsx(LoadingState, { label: t('sessions.insightLoading'), compact: true }) : null, state?.error ? _jsxs("p", { children: [t('sessions.insightUnavailable'), ": ", state.error] }) : null, state?.report ? _jsx(InsightReport, { report: state.report }) : null] })] }));
609
+ }
610
+ function Drawer(props) {
611
+ const dialogRef = useRef(null);
612
+ useDialogVisibility(dialogRef, !!props.row);
613
+ const row = props.row;
614
+ const terminal = row ? terminalHref(row) : null;
615
+ return (_jsx("dialog", { id: "drawer", ref: dialogRef, onClose: props.onClose, onClick: event => { if (event.target === event.currentTarget)
616
+ props.onClose(); }, children: row ? (_jsxs("article", { children: [_jsxs("header", { children: [_jsxs("div", { className: "drawer-title-row", children: [_jsx("h3", { children: stripMentionPrefix(row.title) || row.sessionId }), _jsx(IconActionButton, { id: "drawer-close", className: "card-act drawer-close-btn", icon: ICON.close, label: t('sessions.dismiss'), onClick: props.onClose })] }), _jsxs("span", { className: "drawer-status-line", children: [_jsx(StatusBadge, { status: row.status }), _jsx(LockChip, { row: row })] }), _jsxs("p", { children: [_jsx("code", { children: row.sessionId }), " ", _jsx(CopyButton, { value: row.sessionId })] })] }), _jsxs("p", { children: [_jsxs("b", { children: [t('sessions.bot'), ":"] }), " ", botDisplayName(row), " \u00B7 ", _jsxs("b", { children: [t('sessions.cli'), ":"] }), " ", row.cliId ?? '?'] }), _jsxs("p", { children: [_jsxs("b", { children: [t('sessions.location'), ":"] }), " ", sessionLocationText(row)] }), _jsxs("p", { children: [_jsx("b", { children: "chatId:" }), " ", _jsx("code", { children: row.chatId ?? '' }), " ", _jsx(CopyButton, { value: row.chatId ?? '' })] }), _jsxs("p", { children: [_jsx("b", { children: "rootMessageId:" }), " ", _jsx("code", { children: row.rootMessageId ?? '' }), " ", _jsx(CopyButton, { value: row.rootMessageId ?? '' })] }), row.threadId ? _jsxs("p", { children: [_jsx("b", { children: "threadId:" }), " ", _jsx("code", { children: row.threadId })] }) : null, _jsxs("p", { children: [_jsxs("b", { children: [t('sessions.workingDir'), ":"] }), " ", row.workingDir ?? '-'] }), _jsxs("div", { className: "actions", children: [_jsx(ChatScopeLink, { row: row }), !row.feishuChatLink || row.scope !== 'chat' ? _jsx(LocateButton, { row: row, locateSession: props.locateSession }) : null, _jsx("button", { id: "history-drawer-btn", type: "button", onClick: () => props.openHistory(row), children: t('sessions.history.title') }), _jsx(TerminalControls, { row: row, url: terminal }), canRestartSession(row) ? (_jsx("button", { id: "restart-btn", type: "button", onClick: async (event) => { if (await props.restartSession(row, event.currentTarget))
617
+ props.onClose(); }, children: t('sessions.restart') })) : null, row.status !== 'closed' ? (_jsx("button", { id: "lock-btn", type: "button", onClick: event => void props.setSessionLocked(row, !row.locked, event.currentTarget), children: lockActionLabel(row) })) : null, row.queued && row.status !== 'closed' ? (_jsx("button", { id: "start-btn", type: "button", className: "primary", onClick: async (event) => { if (await props.startSession(row, event.currentTarget))
618
+ props.onClose(); }, children: t('sessions.create.start') })) : null, row.status === 'closed' ? (_jsx("button", { id: "resume-btn", type: "button", className: "primary", onClick: async (event) => { if (await props.resumeSession(row, event.currentTarget))
619
+ props.onClose(); }, children: t('sessions.resume') })) : null, row.status !== 'closed' ? (_jsx("button", { id: "close-btn", type: "button", className: "contrast", onClick: async (event) => { if (await props.closeSession(row, event.currentTarget))
620
+ props.onClose(); }, children: t('sessions.close') })) : null, _jsx(LandPanel, { row: row }), _jsx(InsightPanel, { row: row })] })] })) : null }));
621
+ }
622
+ function CreateSessionDialog(props) {
623
+ const state = props.state;
624
+ useEffect(() => {
625
+ const dialog = props.dialog;
626
+ return () => {
627
+ if (dialog.open)
628
+ dialog.close();
629
+ };
630
+ }, [props.dialog]);
631
+ useEffect(() => {
632
+ const dialog = props.dialog;
633
+ if (state) {
634
+ if (!dialog.open) {
635
+ try {
636
+ dialog.showModal();
637
+ }
638
+ catch { /* already open */ }
639
+ }
640
+ }
641
+ else if (dialog.open) {
642
+ dialog.close();
643
+ }
644
+ }, [props.dialog, state]);
645
+ useEffect(() => {
646
+ const dialog = props.dialog;
647
+ const handleClose = () => props.onClose();
648
+ const handleClick = (event) => {
649
+ if (event.target === dialog)
650
+ props.onClose();
651
+ };
652
+ dialog.addEventListener('close', handleClose);
653
+ dialog.addEventListener('click', handleClick);
654
+ return () => {
655
+ dialog.removeEventListener('close', handleClose);
656
+ dialog.removeEventListener('click', handleClick);
657
+ };
658
+ }, [props]);
659
+ const [content, setContent] = useState('');
660
+ const [selectedBots, setSelectedBots] = useState(new Set());
661
+ const [mode, setMode] = useState('lead');
662
+ const [lead, setLead] = useState('');
663
+ const [column, setColumn] = useState('in_progress');
664
+ const [name, setName] = useState('');
665
+ const [bindWorkingDir, setBindWorkingDir] = useState('');
666
+ const [advancedOpen, setAdvancedOpen] = useState(false);
667
+ const [submitting, setSubmitting] = useState(false);
668
+ useEffect(() => {
669
+ if (!state)
670
+ return;
671
+ setContent('');
672
+ setSelectedBots(new Set());
673
+ setMode('lead');
674
+ setLead('');
675
+ setColumn('in_progress');
676
+ setName('');
677
+ setBindWorkingDir('');
678
+ setAdvancedOpen(false);
679
+ setSubmitting(false);
680
+ }, [state]);
681
+ if (!state)
682
+ return null;
683
+ if (state.success) {
684
+ const body = state.success;
685
+ const link = typeof body.shareLink === 'string' && body.shareLink ? body.shareLink : '';
686
+ const failedN = Array.isArray(body.failed) ? body.failed.length : 0;
687
+ const spawnedN = Array.isArray(body.spawned) ? body.spawned.length : 0;
688
+ const colNote = body.column === 'backlog' ? t('sessions.create.doneBacklog') : t('sessions.create.doneInProgress');
689
+ return (_jsxs("article", { className: "cs-card", children: [_jsx("header", { className: "cs-header", children: _jsx("h3", { children: t('sessions.create.doneTitle') }) }), _jsxs("p", { children: [colNote, "\uFF08", spawnedN, "\uFF09"] }), failedN > 0 ? _jsx("p", { className: "cs-warn", children: t('sessions.create.partialFail', { n: String(failedN) }) }) : null, link ? _jsx("p", { children: _jsx("a", { href: link, target: "_blank", rel: "noopener", children: t('sessions.create.openChat') }) }) : null, _jsx("div", { className: "actions", children: _jsx("button", { type: "button", id: "cs-done", className: "primary", onClick: props.onClose, children: t('sessions.create.close') }) })] }));
690
+ }
691
+ if (state.loading) {
692
+ return (_jsxs("article", { className: "cs-card", children: [_jsx("header", { className: "cs-header", children: _jsx("h3", { children: t('sessions.create.title') }) }), _jsx(LoadingState, { label: t('common.loading'), className: "cs-loading", compact: true }), _jsx("div", { className: "actions cs-actions", children: _jsx("button", { type: "button", id: "cs-cancel", onClick: props.onClose, children: t('sessions.create.cancel') }) })] }));
693
+ }
694
+ const bots = state.bots;
695
+ const checkedIds = [...selectedBots];
696
+ const leadOptions = checkedIds;
697
+ const nameOf = (id) => bots.find(bot => bot.larkAppId === id)?.botName ?? id;
698
+ const submit = async (event) => {
699
+ event.preventDefault();
700
+ const text = content.trim();
701
+ if (!text) {
702
+ alert(t('sessions.create.errContent'));
703
+ return;
704
+ }
705
+ if (checkedIds.length === 0) {
706
+ alert(t('sessions.create.errNoBot'));
707
+ return;
708
+ }
709
+ const leadLarkAppId = lead || checkedIds[0] || '';
710
+ if (mode === 'lead' && (!leadLarkAppId || !checkedIds.includes(leadLarkAppId))) {
711
+ alert(t('sessions.create.errLead'));
712
+ return;
713
+ }
714
+ setSubmitting(true);
715
+ try {
716
+ const r = await fetch('/api/sessions/create', {
717
+ method: 'POST',
718
+ headers: { 'content-type': 'application/json' },
719
+ body: JSON.stringify({
720
+ content: text,
721
+ larkAppIds: checkedIds,
722
+ mode,
723
+ column,
724
+ leadLarkAppId: mode === 'lead' ? leadLarkAppId : undefined,
725
+ name: name.trim() || undefined,
726
+ bindWorkingDir: bindWorkingDir.trim() || undefined,
727
+ }),
728
+ });
729
+ const body = await r.json().catch(() => null);
730
+ if (r.ok && body?.ok)
731
+ props.onSuccess(body);
732
+ else if (r.status !== 401)
733
+ alert(`${t('sessions.create.failed')}: ${body?.error ?? r.status}`);
734
+ }
735
+ catch (e) {
736
+ alert(`${t('sessions.create.failed')}: ${e}`);
737
+ }
738
+ finally {
739
+ setSubmitting(false);
740
+ }
741
+ };
742
+ return (_jsxs("article", { className: "cs-card", children: [_jsx("header", { className: "cs-header", children: _jsx("h3", { children: t('sessions.create.title') }) }), _jsxs("form", { id: "cs-form", onSubmit: submit, children: [_jsxs("fieldset", { className: "cs-content", children: [_jsx("legend", { children: t('sessions.create.content') }), _jsx("textarea", { name: "content", rows: 5, placeholder: t('sessions.create.contentPlaceholder'), required: true, value: content, onChange: event => setContent(event.currentTarget.value) })] }), _jsxs("fieldset", { className: "cs-bots", children: [_jsx("legend", { children: t('sessions.create.bots') }), bots.length ? (_jsx("div", { className: "cs-bot-list", children: bots.map(bot => (_jsxs("label", { className: "cs-bot", children: [_jsx("input", { type: "checkbox", name: "bot", value: bot.larkAppId, checked: selectedBots.has(bot.larkAppId), onChange: event => {
743
+ const checked = event.currentTarget.checked;
744
+ setSelectedBots(prev => {
745
+ const next = new Set(prev);
746
+ if (checked)
747
+ next.add(bot.larkAppId);
748
+ else
749
+ next.delete(bot.larkAppId);
750
+ if (!next.has(lead))
751
+ setLead(next.values().next().value ?? '');
752
+ return next;
753
+ });
754
+ } }), " ", _jsx("span", { children: bot.botName })] }, bot.larkAppId))) })) : _jsx("p", { className: "cs-empty", children: t('sessions.create.noBots') })] }), _jsxs("fieldset", { className: "cs-mode", children: [_jsx("legend", { children: t('sessions.create.mode') }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "mode", value: "lead", checked: mode === 'lead', onChange: () => setMode('lead') }), " ", t('sessions.create.modeLead')] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "mode", value: "all", checked: mode === 'all', onChange: () => setMode('all') }), " ", t('sessions.create.modeAll')] }), _jsx("small", { children: t('sessions.create.modeHelp') })] }), _jsxs("fieldset", { className: "cs-lead-row", hidden: mode !== 'lead', children: [_jsx("legend", { children: t('sessions.create.lead') }), _jsx("select", { name: "lead", disabled: leadOptions.length === 0, value: leadOptions.includes(lead) ? lead : '', onChange: event => setLead(event.currentTarget.value), children: leadOptions.length ? leadOptions.map(id => _jsx("option", { value: id, children: nameOf(id) }, id)) : (_jsx("option", { value: "", disabled: true, children: t('sessions.create.leadPickFirst') })) }), _jsx("small", { children: t('sessions.create.leadHelp') })] }), _jsxs("fieldset", { className: "cs-column", children: [_jsx("legend", { children: t('sessions.create.column') }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "column", value: "in_progress", checked: column === 'in_progress', onChange: () => setColumn('in_progress') }), " ", t('sessions.create.columnInProgress')] }), _jsxs("label", { children: [_jsx("input", { type: "radio", name: "column", value: "backlog", checked: column === 'backlog', onChange: () => setColumn('backlog') }), " ", t('sessions.create.columnBacklog')] }), _jsx("small", { children: t('sessions.create.columnHelp') })] }), _jsxs("fieldset", { className: `cs-advanced${advancedOpen ? ' open' : ''}`, children: [_jsx("legend", { children: _jsx("button", { type: "button", id: "cs-advanced-title", className: "cs-advanced-title", "aria-expanded": advancedOpen, "aria-controls": "cs-advanced-fields", onClick: () => setAdvancedOpen(open => !open), children: t('sessions.create.optionalConfig') }) }), advancedOpen ? (_jsxs("div", { id: "cs-advanced-fields", className: "cs-advanced-fields", children: [_jsxs("label", { className: "cs-advanced-field", children: [_jsx("span", { children: t('sessions.create.groupName') }), _jsx("input", { className: "cs-pill-input", type: "text", name: "name", maxLength: 60, placeholder: t('sessions.create.groupNamePlaceholder'), value: name, onChange: event => setName(event.currentTarget.value) })] }), _jsxs("label", { className: "cs-advanced-field", children: [_jsx("span", { children: t('sessions.create.workingDir') }), _jsx("input", { className: "cs-pill-input", type: "text", name: "bindWorkingDir", placeholder: "e.g. ~/projects/foo", value: bindWorkingDir, onChange: event => setBindWorkingDir(event.currentTarget.value) }), _jsx("small", { children: t('sessions.create.workingDirHelp') })] })] })) : null] }), _jsxs("div", { className: "actions cs-actions", children: [_jsx("button", { type: "button", id: "cs-cancel", onClick: props.onClose, children: t('sessions.create.cancel') }), _jsx("button", { type: "submit", className: "cs-submit", disabled: submitting || bots.length === 0, children: submitting ? t('sessions.create.submitting') : t('sessions.create.submit') })] })] })] }));
755
+ }
756
+ function SessionsPage() {
757
+ useT();
758
+ const storeRows = useStoreSelector(snapshot => [...snapshot.sessions.values()]);
759
+ const [revision, setRevision] = useState(0);
760
+ const refresh = useCallback(() => setRevision(v => v + 1), []);
761
+ const [filters, setFilters] = useState({
762
+ q: '',
763
+ status: '',
764
+ adopt: '',
765
+ chat: '',
766
+ showUnknownChats: readStoredSessionsShowUnknownChats(windowStorage()),
767
+ active: true,
768
+ cli: new Set(CLI_FILTER_OPTIONS),
769
+ });
770
+ const [selected, setSelected] = useState(new Set());
771
+ const [sortKey, setSortKey] = useState('lastMessageAt');
772
+ const [sortDir, setSortDir] = useState('desc');
773
+ const [viewMode, setViewMode] = useState(() => readStoredSessionsViewMode(windowStorage()));
774
+ const [boardOrder, setBoardOrder] = useState(() => readStoredBoardOrder(windowStorage()));
775
+ const [boardAnimated, setBoardAnimated] = useState(false);
776
+ const [dragColId, setDragColId] = useState(null);
777
+ const [dragOverCol, setDragOverCol] = useState(null);
778
+ const [kanbanGroupBy, setKanbanGroupBy] = useState(() => readStoredKanbanGroupBy(windowStorage()));
779
+ const viewStageSignature = `${viewMode}:${viewMode === 'kanban' ? kanbanGroupBy : '-'}`;
780
+ const viewStageInitialRef = useRef(true);
781
+ const [viewStageAnimKey, setViewStageAnimKey] = useState(0);
782
+ const [kanbanTeams, setKanbanTeams] = useState([]);
783
+ const [kanbanChatBots, setKanbanChatBots] = useState(null);
784
+ const [kanbanTeamsLoaded, setKanbanTeamsLoaded] = useState(false);
785
+ const kanbanTeamsLoadingRef = useRef(false);
786
+ const [kanbanTeamKey, setKanbanTeamKey] = useState(() => {
787
+ try {
788
+ return window.localStorage.getItem(KANBAN_TEAM_STORAGE_KEY) ?? '';
789
+ }
790
+ catch {
791
+ return '';
792
+ }
793
+ });
794
+ const [teamBoard, setTeamBoard] = useState({ data: null, key: '', fetchedAt: 0 });
795
+ const teamBoardLoadingRef = useRef(false);
796
+ const restartCooldownIds = useRef(new Set());
797
+ const [teamScopeText, setTeamScopeText] = useState('');
798
+ const [bulkCloseProgress, setBulkCloseProgress] = useState(null);
799
+ const [bulkLockProgress, setBulkLockProgress] = useState(null);
800
+ const [monitorRoomFeedback, setMonitorRoomFeedback] = useState(null);
801
+ const [idleCleanupBusy, setIdleCleanupBusy] = useState(false);
802
+ const [idleCleanupHours, setIdleCleanupHours] = useState(24);
803
+ const [idleCleanupStatus, setIdleCleanupStatus] = useState('');
804
+ const [drawerSessionId, setDrawerSessionId] = useState(null);
805
+ const [historyState, setHistoryState] = useState(null);
806
+ const [termState, setTermState] = useState(null);
807
+ const [kanbanHost, setKanbanHost] = useState(null);
808
+ const [createDialogEl, setCreateDialogEl] = useState(null);
809
+ const [createState, setCreateState] = useState(null);
810
+ const [createLoading, setCreateLoading] = useState(false);
811
+ const createRequestRef = useRef(0);
812
+ useLayoutEffect(() => {
813
+ if (viewStageInitialRef.current) {
814
+ viewStageInitialRef.current = false;
815
+ return;
816
+ }
817
+ setViewStageAnimKey(value => value + 1);
818
+ }, [viewStageSignature]);
819
+ useEffect(() => {
820
+ setCreateDialogEl(document.getElementById('create-session-modal'));
821
+ }, []);
822
+ useEffect(() => {
823
+ void loadNameMaps().then(refresh);
824
+ }, [refresh]);
825
+ const chatOptions = useMemo(() => {
826
+ const options = new Map();
827
+ for (const row of storeRows) {
828
+ const chatId = String(row.chatId ?? '').trim();
829
+ if (!chatId)
830
+ continue;
831
+ if (!filters.showUnknownChats && isUnknownChatSession(row))
832
+ continue;
833
+ const label = sessionLocationText(row);
834
+ const existing = options.get(chatId);
835
+ if (!existing || label < existing)
836
+ options.set(chatId, label);
837
+ }
838
+ return [...options.entries()]
839
+ .sort((a, b) => a[1].localeCompare(b[1]))
840
+ .map(([value, label]) => ({ value, label }));
841
+ }, [filters.showUnknownChats, revision, storeRows]);
842
+ useEffect(() => {
843
+ if (!filters.chat)
844
+ return;
845
+ if (chatOptions.some(option => option.value === filters.chat))
846
+ return;
847
+ setFilters(prev => ({ ...prev, chat: '' }));
848
+ }, [chatOptions, filters.chat]);
849
+ const rows = useMemo(() => {
850
+ const q = filters.q.toLowerCase();
851
+ const cli = [...filters.cli];
852
+ const cliFilterActive = cli.length > 0 && cli.length < CLI_FILTER_OPTIONS.length;
853
+ const keepClosed = viewMode === 'kanban';
854
+ return storeRows
855
+ .filter(s => !cliFilterActive || cli.includes(s.cliId ?? 'unknown'))
856
+ .filter(s => filters.showUnknownChats || !isUnknownChatSession(s))
857
+ .filter(s => !filters.status || s.status === filters.status)
858
+ .filter(s => !filters.adopt || (filters.adopt === 'yes') === !!s.adopt)
859
+ .filter(s => !filters.chat || String(s.chatId ?? '') === filters.chat)
860
+ .filter(s => !filters.active || keepClosed || s.status !== 'closed')
861
+ .filter(s => !q || sessionSearchText(s).includes(q))
862
+ .sort((a, b) => compareRows(a, b, sortKey, sortDir));
863
+ }, [filters, revision, sortDir, sortKey, storeRows, viewMode]);
864
+ const runtimeCounts = useMemo(() => sessionRuntimeCounts(storeRows), [storeRows]);
865
+ const rowsById = useMemo(() => new Map(storeRows.map(row => [row.sessionId, row])), [storeRows, revision]);
866
+ const boardRows = useMemo(() => rows.filter(row => row.status !== 'closed'), [rows]);
867
+ const visibleRows = viewMode === 'table' ? rows : boardRows;
868
+ const selectableRows = visibleRows.filter(row => row.status !== 'closed');
869
+ const selectedRows = [...selected]
870
+ .map(id => rowsById.get(id))
871
+ .filter((row) => !!row && row.status !== 'closed');
872
+ const selectAllChecked = selectableRows.length > 0 && selectableRows.every(row => selected.has(row.sessionId));
873
+ const selectAllIndeterminate = selectableRows.some(row => selected.has(row.sessionId)) && !selectAllChecked;
874
+ useEffect(() => {
875
+ setSelected(prev => {
876
+ let changed = false;
877
+ const next = new Set();
878
+ for (const id of prev) {
879
+ const row = rowsById.get(id);
880
+ if (row && row.status !== 'closed')
881
+ next.add(id);
882
+ else
883
+ changed = true;
884
+ }
885
+ return changed ? next : prev;
886
+ });
887
+ }, [rowsById]);
888
+ const teamChatIdsFor = useCallback((team) => {
889
+ const teamChats = new Set();
890
+ if (!team)
891
+ return teamChats;
892
+ for (const chatId of team.groupChats)
893
+ teamChats.add(chatId);
894
+ if (kanbanChatBots) {
895
+ for (const [chatId, c] of kanbanChatBots) {
896
+ if (teamChats.has(chatId))
897
+ continue;
898
+ let hasTeamBot = false;
899
+ for (const id of team.botIds) {
900
+ if (c.botIds.has(id)) {
901
+ hasTeamBot = true;
902
+ break;
903
+ }
904
+ }
905
+ if (!hasTeamBot)
906
+ continue;
907
+ for (const n of c.observedNames) {
908
+ if (team.botNames.has(n)) {
909
+ teamChats.add(chatId);
910
+ break;
911
+ }
912
+ }
913
+ }
914
+ }
915
+ return teamChats;
916
+ }, [kanbanChatBots]);
917
+ const currentCleanupVisibleRows = useMemo(() => {
918
+ if (viewMode === 'kanban' && kanbanGroupBy === 'team') {
919
+ const team = kanbanTeams.find(tm => tm.key === kanbanTeamKey) ?? kanbanTeams[0];
920
+ const teamChats = teamChatIdsFor(team);
921
+ return rows.filter(row => teamChats.has(String(row.chatId)));
922
+ }
923
+ return rows;
924
+ }, [kanbanGroupBy, kanbanTeamKey, kanbanTeams, rows, teamChatIdsFor, viewMode]);
925
+ const idleCleanupCandidatesFor = useCallback((hours) => selectIdleCleanupCandidates(currentCleanupVisibleRows, hours), [currentCleanupVisibleRows]);
926
+ const loadKanbanTeams = useCallback(async () => {
927
+ if (kanbanTeamsLoadingRef.current || kanbanTeamsLoaded)
928
+ return;
929
+ kanbanTeamsLoadingRef.current = true;
930
+ try {
931
+ const [hosted, remote, groups] = await Promise.all([
932
+ fetch('/api/team/hosted').then(r => r.json()).catch(() => null),
933
+ fetch('/api/team/remote-roster').then(r => r.json()).catch(() => null),
934
+ fetch('/api/groups').then(r => r.json()).catch(() => null),
935
+ ]);
936
+ if (Array.isArray(groups?.chats)) {
937
+ setKanbanChatBots(new Map(groups.chats.map((c) => [
938
+ String(c.chatId),
939
+ {
940
+ botIds: new Set((c.memberBots ?? []).filter((mb) => mb.inChat).map((mb) => String(mb.larkAppId))),
941
+ observedNames: new Set((c.observedBotNames ?? []).map((n) => String(n))),
942
+ },
943
+ ])));
944
+ }
945
+ const rosterBots = (bots) => ({
946
+ ids: new Set(bots.map((b) => String(b.larkAppId))),
947
+ names: new Set(bots.map((b) => String(b.name ?? '')).filter(Boolean)),
948
+ });
949
+ const teams = [];
950
+ for (const tm of hosted?.teams ?? []) {
951
+ const { ids, names } = rosterBots(tm.bots ?? []);
952
+ teams.push({
953
+ key: `local:${tm.teamId}`,
954
+ label: tm.isDefault ? t('team.myHostedTeam') : String(tm.name ?? tm.teamId),
955
+ botIds: ids,
956
+ botNames: names,
957
+ groupChats: new Set((tm.groupChatIds ?? []).map((c) => String(c))),
958
+ });
959
+ }
960
+ for (const m of remote?.memberships ?? []) {
961
+ const { ids, names } = rosterBots(m.roster?.bots ?? []);
962
+ teams.push({
963
+ key: `${m.hubUrl}::${m.teamId}`,
964
+ label: String(m.teamName ?? m.teamId ?? m.hubUrl),
965
+ botIds: ids,
966
+ botNames: names,
967
+ groupChats: new Set(),
968
+ });
969
+ }
970
+ setKanbanTeams(teams);
971
+ setKanbanTeamKey(prev => (teams.length && !teams.some(tm => tm.key === prev)) ? teams[0].key : prev);
972
+ }
973
+ finally {
974
+ setKanbanTeamsLoaded(true);
975
+ kanbanTeamsLoadingRef.current = false;
976
+ }
977
+ }, [kanbanTeamsLoaded]);
978
+ const persistTeamBoardMove = useCallback(async (teamKey, sessionId, column, position, prevEntry) => {
979
+ try {
980
+ const isLocal = teamKey.startsWith('local:');
981
+ const r = isLocal
982
+ ? await fetch(`/api/team/board/local/${encodeURIComponent(teamKey.slice('local:'.length))}/move`, {
983
+ method: 'POST',
984
+ headers: { 'content-type': 'application/json' },
985
+ body: JSON.stringify({ sessionId, column, position }),
986
+ })
987
+ : await fetch('/api/team/remote-board-move', {
988
+ method: 'POST',
989
+ headers: { 'content-type': 'application/json' },
990
+ body: JSON.stringify({ key: teamKey, sessionId, column, position }),
991
+ });
992
+ const body = await r.json().catch(() => ({}));
993
+ if (!r.ok || body?.ok === false) {
994
+ setTeamBoard(prev => {
995
+ if (!prev.data || prev.key !== teamKey)
996
+ return prev;
997
+ const board = { ...prev.data.board };
998
+ if (prevEntry)
999
+ board[sessionId] = prevEntry;
1000
+ else
1001
+ delete board[sessionId];
1002
+ return { ...prev, data: { ...prev.data, board } };
1003
+ });
1004
+ if (r.status !== 401)
1005
+ alert(`${t('sessions.kanban.moveFail')}: ${body?.error ?? r.status}`);
1006
+ }
1007
+ }
1008
+ catch (e) {
1009
+ setTeamBoard(prev => {
1010
+ if (!prev.data || prev.key !== teamKey)
1011
+ return prev;
1012
+ const board = { ...prev.data.board };
1013
+ if (prevEntry)
1014
+ board[sessionId] = prevEntry;
1015
+ else
1016
+ delete board[sessionId];
1017
+ return { ...prev, data: { ...prev.data, board } };
1018
+ });
1019
+ alert(`${t('sessions.kanban.moveFail')}: ${e}`);
1020
+ }
1021
+ }, []);
1022
+ const ensureTeamBoard = useCallback(async (team) => {
1023
+ const fresh = teamBoard.key === team.key && Date.now() - teamBoard.fetchedAt < 30_000;
1024
+ if (teamBoardLoadingRef.current || fresh)
1025
+ return;
1026
+ teamBoardLoadingRef.current = true;
1027
+ try {
1028
+ const isLocal = team.key.startsWith('local:');
1029
+ const u = isLocal
1030
+ ? `/api/team/board/local/${encodeURIComponent(team.key.slice('local:'.length))}`
1031
+ : `/api/team/remote-board?key=${encodeURIComponent(team.key)}`;
1032
+ const r = await fetch(u);
1033
+ const body = await r.json().catch(() => ({}));
1034
+ if (!r.ok || body?.ok === false)
1035
+ return;
1036
+ const myDeploymentId = typeof body.deploymentId === 'string' ? body.deploymentId : null;
1037
+ const remoteRows = [];
1038
+ for (const rep of Array.isArray(body.reports) ? body.reports : []) {
1039
+ if (myDeploymentId && rep.deploymentId === myDeploymentId)
1040
+ continue;
1041
+ for (const s of Array.isArray(rep.sessions) ? rep.sessions : []) {
1042
+ remoteRows.push({ ...s, remoteDeployment: rep.deploymentName || rep.deploymentId });
1043
+ }
1044
+ }
1045
+ setTeamBoard({
1046
+ key: team.key,
1047
+ fetchedAt: Date.now(),
1048
+ data: {
1049
+ board: body.board && typeof body.board === 'object' ? body.board : {},
1050
+ remoteRows,
1051
+ },
1052
+ });
1053
+ }
1054
+ finally {
1055
+ teamBoardLoadingRef.current = false;
1056
+ }
1057
+ }, [teamBoard.fetchedAt, teamBoard.key]);
1058
+ useEffect(() => {
1059
+ if (viewMode === 'kanban' && kanbanGroupBy === 'team' && !kanbanTeamsLoaded)
1060
+ void loadKanbanTeams();
1061
+ }, [kanbanGroupBy, kanbanTeamsLoaded, loadKanbanTeams, viewMode]);
1062
+ useEffect(() => {
1063
+ const timer = window.setInterval(() => {
1064
+ if (viewMode === 'kanban' && kanbanGroupBy === 'team') {
1065
+ setTeamBoard(prev => ({ ...prev, fetchedAt: 0 }));
1066
+ refresh();
1067
+ }
1068
+ }, 30_000);
1069
+ return () => window.clearInterval(timer);
1070
+ }, [kanbanGroupBy, refresh, viewMode]);
1071
+ const persistBoardMove = useCallback(async (row, column, position, prev) => {
1072
+ try {
1073
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/board`, {
1074
+ method: 'POST',
1075
+ headers: { 'content-type': 'application/json' },
1076
+ body: JSON.stringify({ column, position }),
1077
+ });
1078
+ const body = await r.json().catch(() => ({}));
1079
+ if (!r.ok || body?.ok === false) {
1080
+ row.kanbanColumn = prev.column;
1081
+ row.kanbanPosition = prev.position;
1082
+ refresh();
1083
+ if (r.status !== 401)
1084
+ alert(`${t('sessions.kanban.moveFail')}: ${body?.error ?? r.status}`);
1085
+ }
1086
+ }
1087
+ catch (e) {
1088
+ row.kanbanColumn = prev.column;
1089
+ row.kanbanPosition = prev.position;
1090
+ refresh();
1091
+ alert(`${t('sessions.kanban.moveFail')}: ${e}`);
1092
+ }
1093
+ }, [refresh]);
1094
+ const handleKanbanMoves = useCallback((moves) => {
1095
+ let changed = false;
1096
+ for (const move of moves) {
1097
+ const sessionId = String(move.row.sessionId);
1098
+ if (!sessionId)
1099
+ continue;
1100
+ if (move.row.status === 'closed' && move.column !== 'done')
1101
+ continue;
1102
+ if (kanbanGroupBy === 'team') {
1103
+ const team = kanbanTeams.find(tm => tm.key === kanbanTeamKey) ?? kanbanTeams[0];
1104
+ if (!team)
1105
+ continue;
1106
+ // Compute the prior slot from committed state and persist OUTSIDE the updater —
1107
+ // updaters must be pure (a POST inside would double-fire under StrictMode).
1108
+ const priorBoard = (teamBoard.key === team.key && teamBoard.data) ? teamBoard.data.board : {};
1109
+ const previous = priorBoard[sessionId];
1110
+ setTeamBoard(prev => {
1111
+ const base = (prev.key === team.key && prev.data) ? prev.data : { board: {}, remoteRows: prev.data?.remoteRows ?? [] };
1112
+ const board = { ...base.board, [sessionId]: { column: move.column, position: move.position } };
1113
+ return { ...prev, key: team.key, data: { ...base, board } };
1114
+ });
1115
+ void persistTeamBoardMove(team.key, sessionId, move.column, move.position, previous);
1116
+ changed = true;
1117
+ continue;
1118
+ }
1119
+ const row = store.sessions.get(sessionId);
1120
+ if (!row)
1121
+ continue;
1122
+ const prev = { column: row.kanbanColumn, position: row.kanbanPosition };
1123
+ row.kanbanColumn = move.column;
1124
+ row.kanbanPosition = move.position;
1125
+ void persistBoardMove(row, move.column, move.position, prev);
1126
+ changed = true;
1127
+ }
1128
+ if (changed)
1129
+ refresh();
1130
+ }, [kanbanGroupBy, kanbanTeamKey, kanbanTeams, teamBoard, persistBoardMove, persistTeamBoardMove, refresh]);
1131
+ const persistRename = useCallback(async (row, title) => {
1132
+ const prevTitle = row.title;
1133
+ row.title = title;
1134
+ refresh();
1135
+ try {
1136
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/rename`, {
1137
+ method: 'POST',
1138
+ headers: { 'content-type': 'application/json' },
1139
+ body: JSON.stringify({ title }),
1140
+ });
1141
+ const body = await r.json().catch(() => ({}));
1142
+ if (!r.ok || body?.ok === false) {
1143
+ row.title = prevTitle;
1144
+ refresh();
1145
+ if (r.status !== 401)
1146
+ alert(`${t('sessions.kanban.renameFail')}: ${body?.error ?? r.status}`);
1147
+ }
1148
+ }
1149
+ catch (e) {
1150
+ row.title = prevTitle;
1151
+ refresh();
1152
+ alert(`${t('sessions.kanban.renameFail')}: ${e}`);
1153
+ }
1154
+ }, [refresh]);
1155
+ const locateSession = useCallback(async (row) => {
1156
+ // Busy/cooldown UI is owned by the React-state LocateButton / LocateIconButton;
1157
+ // do NOT imperatively mutate the button here — the board's locate button renders
1158
+ // its icon via dangerouslySetInnerHTML and a textContent write permanently wipes it.
1159
+ try {
1160
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/locate`, { method: 'POST' });
1161
+ const body = await r.json();
1162
+ if (body.ok)
1163
+ return true;
1164
+ alert(`Locate failed: ${body.error ?? r.status}`);
1165
+ return false;
1166
+ }
1167
+ catch (e) {
1168
+ alert(`Locate error: ${e}`);
1169
+ return false;
1170
+ }
1171
+ }, []);
1172
+ const closeSession = useCallback(async (row, closeBtn) => {
1173
+ if (!confirm(t('sessions.closeConfirm')))
1174
+ return false;
1175
+ if (closeBtn)
1176
+ closeBtn.disabled = true;
1177
+ try {
1178
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/close`, { method: 'POST' });
1179
+ const body = await r.json().catch(() => ({}));
1180
+ if (!r.ok || body?.ok === false) {
1181
+ if (r.status !== 401)
1182
+ alert(`Close failed: ${body?.error ?? r.status}`);
1183
+ return false;
1184
+ }
1185
+ setSelected(prev => {
1186
+ const next = new Set(prev);
1187
+ next.delete(row.sessionId);
1188
+ return next;
1189
+ });
1190
+ refresh();
1191
+ return true;
1192
+ }
1193
+ catch (e) {
1194
+ alert(`Close error: ${e}`);
1195
+ return false;
1196
+ }
1197
+ finally {
1198
+ if (closeBtn)
1199
+ closeBtn.disabled = false;
1200
+ }
1201
+ }, [refresh]);
1202
+ const setSessionLocked = useCallback(async (row, locked, btn) => {
1203
+ const prev = !!row.locked;
1204
+ if (prev === locked)
1205
+ return true;
1206
+ row.locked = locked;
1207
+ refresh();
1208
+ if (btn)
1209
+ btn.disabled = true;
1210
+ try {
1211
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/lock`, {
1212
+ method: 'POST',
1213
+ headers: { 'content-type': 'application/json' },
1214
+ body: JSON.stringify({ locked }),
1215
+ });
1216
+ const body = await r.json().catch(() => ({}));
1217
+ if (!r.ok || body?.ok === false) {
1218
+ row.locked = prev;
1219
+ refresh();
1220
+ if (r.status !== 401)
1221
+ alert(`${t('sessions.lockFailed')}: ${body?.error ?? r.status}`);
1222
+ return false;
1223
+ }
1224
+ row.locked = !!body.locked;
1225
+ refresh();
1226
+ return true;
1227
+ }
1228
+ catch (e) {
1229
+ row.locked = prev;
1230
+ refresh();
1231
+ alert(`${t('sessions.lockFailed')}: ${e}`);
1232
+ return false;
1233
+ }
1234
+ finally {
1235
+ if (btn)
1236
+ btn.disabled = false;
1237
+ }
1238
+ }, [refresh]);
1239
+ const restartSession = useCallback(async (row, restartBtn) => {
1240
+ if (restartCooldownIds.current.has(row.sessionId))
1241
+ return false;
1242
+ if (!confirm(restartConfirmMessage(row)))
1243
+ return false;
1244
+ if (restartBtn)
1245
+ restartBtn.disabled = true;
1246
+ try {
1247
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/restart`, { method: 'POST' });
1248
+ const body = await r.json().catch(() => ({}));
1249
+ if (!r.ok || body?.ok === false) {
1250
+ if (r.status !== 401)
1251
+ alert(`${t('sessions.restartFailed')}: ${body?.error ?? r.status}`);
1252
+ return false;
1253
+ }
1254
+ restartCooldownIds.current.add(row.sessionId);
1255
+ window.setTimeout(() => restartCooldownIds.current.delete(row.sessionId), 5000);
1256
+ return true;
1257
+ }
1258
+ catch (e) {
1259
+ alert(`${t('sessions.restartFailed')}: ${e}`);
1260
+ return false;
1261
+ }
1262
+ finally {
1263
+ if (restartBtn)
1264
+ restartBtn.disabled = false;
1265
+ }
1266
+ }, []);
1267
+ const resumeSession = useCallback(async (row, button) => {
1268
+ if (button)
1269
+ button.disabled = true;
1270
+ try {
1271
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/resume`, { method: 'POST' });
1272
+ const body = await r.json().catch(() => ({}));
1273
+ if (!r.ok || body.ok === false) {
1274
+ alert(`${t('sessions.resumeFailed')}: ${body?.error ?? r.status}`);
1275
+ return false;
1276
+ }
1277
+ return true;
1278
+ }
1279
+ catch (e) {
1280
+ alert(`${t('sessions.resumeFailed')}: ${e}`);
1281
+ return false;
1282
+ }
1283
+ finally {
1284
+ if (button)
1285
+ button.disabled = false;
1286
+ }
1287
+ }, []);
1288
+ const startSession = useCallback(async (row, button) => {
1289
+ if (button)
1290
+ button.disabled = true;
1291
+ try {
1292
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/start`, { method: 'POST' });
1293
+ const body = await r.json().catch(() => ({}));
1294
+ if (!r.ok || body.ok === false) {
1295
+ if (r.status !== 401)
1296
+ alert(`${t('sessions.create.startFailed')}: ${body?.error ?? r.status}`);
1297
+ return false;
1298
+ }
1299
+ return true;
1300
+ }
1301
+ catch (e) {
1302
+ alert(`${t('sessions.create.startFailed')}: ${e}`);
1303
+ return false;
1304
+ }
1305
+ finally {
1306
+ if (button)
1307
+ button.disabled = false;
1308
+ }
1309
+ }, []);
1310
+ const openHistoryModal = useCallback((row) => {
1311
+ setHistoryState({ sessionId: row.sessionId, loading: true, messages: [] });
1312
+ void (async () => {
1313
+ try {
1314
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/history?limit=80`);
1315
+ const body = await r.json().catch(() => ({}));
1316
+ if (!r.ok || body?.ok === false) {
1317
+ const errCode = String(body?.error ?? r.status);
1318
+ const stale = errCode === 'not_found_yet' || errCode === 'not_found';
1319
+ setHistoryState(prev => prev?.sessionId === row.sessionId ? { sessionId: row.sessionId, loading: false, messages: [], error: errCode, stale } : prev);
1320
+ return;
1321
+ }
1322
+ const messages = Array.isArray(body.messages) ? body.messages : [];
1323
+ setHistoryState(prev => prev?.sessionId === row.sessionId ? { sessionId: row.sessionId, loading: false, messages, ownerOpenId: body.ownerOpenId } : prev);
1324
+ }
1325
+ catch (e) {
1326
+ setHistoryState(prev => prev?.sessionId === row.sessionId ? { sessionId: row.sessionId, loading: false, messages: [], error: String(e) } : prev);
1327
+ }
1328
+ })();
1329
+ }, []);
1330
+ const openTerminalModal = useCallback((row) => {
1331
+ const readonlyUrl = terminalHref(row);
1332
+ if (!readonlyUrl) {
1333
+ setDrawerSessionId(row.sessionId);
1334
+ return;
1335
+ }
1336
+ setTermState({ sessionId: row.sessionId, url: readonlyUrl, loading: true });
1337
+ void (async () => {
1338
+ let url = readonlyUrl;
1339
+ if (shouldOpenWritableTerminal()) {
1340
+ try {
1341
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/write-link`);
1342
+ const body = await r.json().catch(() => ({}));
1343
+ if (r.ok && body?.ok !== false && body?.url)
1344
+ url = body.url;
1345
+ }
1346
+ catch {
1347
+ // fallback to read-only URL
1348
+ }
1349
+ }
1350
+ setTermState(prev => prev?.sessionId === row.sessionId ? { sessionId: row.sessionId, url, loading: false } : prev);
1351
+ })();
1352
+ }, []);
1353
+ const runBulkClose = useCallback(async () => {
1354
+ const ids = [...selected];
1355
+ if (ids.length === 0)
1356
+ return;
1357
+ if (!confirm(t('sessions.closeBulkConfirm', { count: ids.length })))
1358
+ return;
1359
+ setBulkCloseProgress({ done: 0, total: ids.length });
1360
+ let done = 0;
1361
+ let failed = 0;
1362
+ const queue = [...ids];
1363
+ async function worker() {
1364
+ while (queue.length) {
1365
+ const sid = queue.shift();
1366
+ try {
1367
+ const r = await fetch(`/api/sessions/${encodeURIComponent(sid)}/close`, { method: 'POST' });
1368
+ const body = await r.json().catch(() => ({}));
1369
+ if (!r.ok || body?.ok === false)
1370
+ failed += 1;
1371
+ }
1372
+ catch {
1373
+ failed += 1;
1374
+ }
1375
+ finally {
1376
+ done += 1;
1377
+ setBulkCloseProgress({ done, total: ids.length });
1378
+ }
1379
+ }
1380
+ }
1381
+ await Promise.all(Array.from({ length: Math.min(6, ids.length) }, () => worker()));
1382
+ setBulkCloseProgress(null);
1383
+ setSelected(new Set());
1384
+ refresh();
1385
+ if (failed > 0)
1386
+ alert(`Failed: ${failed}/${ids.length}`);
1387
+ }, [refresh, selected]);
1388
+ const runBulkLock = useCallback(async (locked) => {
1389
+ const targetRows = [...selected]
1390
+ .map(id => rowsById.get(id))
1391
+ .filter((row) => !!row && row.status !== 'closed' && !!row.locked !== locked);
1392
+ if (targetRows.length === 0)
1393
+ return;
1394
+ setBulkLockProgress({ locked, done: 0, total: targetRows.length });
1395
+ let done = 0;
1396
+ let failed = 0;
1397
+ const queue = [...targetRows];
1398
+ async function worker() {
1399
+ while (queue.length) {
1400
+ const row = queue.shift();
1401
+ const prev = !!row.locked;
1402
+ try {
1403
+ const r = await fetch(`/api/sessions/${encodeURIComponent(row.sessionId)}/lock`, {
1404
+ method: 'POST',
1405
+ headers: { 'content-type': 'application/json' },
1406
+ body: JSON.stringify({ locked }),
1407
+ });
1408
+ const body = await r.json().catch(() => ({}));
1409
+ if (!r.ok || body?.ok === false) {
1410
+ failed += 1;
1411
+ row.locked = prev;
1412
+ }
1413
+ else {
1414
+ row.locked = !!body.locked;
1415
+ }
1416
+ }
1417
+ catch {
1418
+ failed += 1;
1419
+ row.locked = prev;
1420
+ }
1421
+ finally {
1422
+ done += 1;
1423
+ setBulkLockProgress({ locked, done, total: targetRows.length });
1424
+ }
1425
+ }
1426
+ }
1427
+ await Promise.all(Array.from({ length: Math.min(6, targetRows.length) }, () => worker()));
1428
+ setBulkLockProgress(null);
1429
+ refresh();
1430
+ if (failed > 0)
1431
+ alert(`${t('sessions.lockFailed')}: ${failed}/${targetRows.length}`);
1432
+ }, [refresh, rowsById, selected]);
1433
+ const addSelectedToMonitorRoom = useCallback(() => {
1434
+ const ids = [...selected].filter(id => !!rowsById.get(id));
1435
+ if (ids.length === 0)
1436
+ return;
1437
+ const result = addMonitorRoomSessionIds(ids);
1438
+ setMonitorRoomFeedback(t('sessions.monitorRoomAdded', { added: result.added, total: result.total }));
1439
+ window.setTimeout(() => setMonitorRoomFeedback(null), 1800);
1440
+ }, [rowsById, selected]);
1441
+ const runIdleCleanup = useCallback(async (hours) => {
1442
+ const nextHours = parseIdleCleanupHours(hours);
1443
+ if (!nextHours)
1444
+ return;
1445
+ const candidates = idleCleanupCandidatesFor(nextHours);
1446
+ if (candidates.length === 0)
1447
+ return;
1448
+ setIdleCleanupHours(nextHours);
1449
+ setIdleCleanupBusy(true);
1450
+ setIdleCleanupStatus(t('sessions.idleCleanupRunning'));
1451
+ try {
1452
+ const r = await fetch('/api/sessions/cleanup-idle', {
1453
+ method: 'POST',
1454
+ headers: { 'content-type': 'application/json' },
1455
+ body: JSON.stringify({ olderThanHours: nextHours, sessionIds: candidates.map(c => c.sessionId) }),
1456
+ });
1457
+ const body = await r.json().catch(() => ({}));
1458
+ if (!r.ok) {
1459
+ if (r.status !== 401)
1460
+ alert(`${t('sessions.idleCleanupFailed')}: ${body?.error ?? r.status}`);
1461
+ setIdleCleanupStatus('');
1462
+ return;
1463
+ }
1464
+ setSelected(prev => {
1465
+ const next = new Set(prev);
1466
+ for (const item of body?.results ?? []) {
1467
+ if (item?.ok && item?.sessionId)
1468
+ next.delete(String(item.sessionId));
1469
+ }
1470
+ return next;
1471
+ });
1472
+ setIdleCleanupStatus(t('sessions.idleCleanupDone', {
1473
+ closed: Number(body?.closed ?? 0),
1474
+ failed: Number(body?.failed ?? 0),
1475
+ }));
1476
+ refresh();
1477
+ }
1478
+ catch (e) {
1479
+ alert(`${t('sessions.idleCleanupFailed')}: ${e}`);
1480
+ setIdleCleanupStatus('');
1481
+ }
1482
+ finally {
1483
+ setIdleCleanupBusy(false);
1484
+ }
1485
+ }, [idleCleanupCandidatesFor, refresh]);
1486
+ const setView = (nextRaw) => {
1487
+ const next = normalizeSessionsViewMode(nextRaw) ?? 'board';
1488
+ if (next === viewMode)
1489
+ return;
1490
+ setViewMode(next);
1491
+ writeStoredSessionsViewMode(windowStorage(), next);
1492
+ };
1493
+ const moveColumn = (id, delta) => {
1494
+ setBoardOrder(prev => {
1495
+ const from = prev.indexOf(id);
1496
+ const to = from + delta;
1497
+ if (from < 0 || to < 0 || to >= prev.length)
1498
+ return prev;
1499
+ const next = [...prev];
1500
+ next.splice(from, 1);
1501
+ next.splice(to, 0, id);
1502
+ writeStoredBoardOrder(windowStorage(), next);
1503
+ return next;
1504
+ });
1505
+ };
1506
+ const moveColumnTo = (id, targetId) => {
1507
+ if (id === targetId)
1508
+ return;
1509
+ setBoardOrder(prev => {
1510
+ const from = prev.indexOf(id);
1511
+ const to = prev.indexOf(targetId);
1512
+ if (from < 0 || to < 0)
1513
+ return prev;
1514
+ const next = [...prev];
1515
+ next.splice(from, 1);
1516
+ next.splice(to, 0, id);
1517
+ writeStoredBoardOrder(windowStorage(), next);
1518
+ return next;
1519
+ });
1520
+ };
1521
+ const kanbanState = useMemo(() => ({
1522
+ rows,
1523
+ groupBy: kanbanGroupBy,
1524
+ teams: kanbanTeams,
1525
+ teamsLoaded: kanbanTeamsLoaded,
1526
+ teamKey: kanbanTeamKey,
1527
+ teamBoardData: teamBoard.data,
1528
+ teamBoardKey: teamBoard.key,
1529
+ }), [kanbanGroupBy, kanbanTeamKey, kanbanTeams, kanbanTeamsLoaded, rows, teamBoard.data, teamBoard.key]);
1530
+ const drawerRow = drawerSessionId ? rowsById.get(drawerSessionId) ?? null : null;
1531
+ const kanbanTeamOptions = useMemo(() => {
1532
+ if (!kanbanTeamsLoaded)
1533
+ return [{ value: '__loading', label: t('sessions.kanban.teamLoading') }];
1534
+ if (!kanbanTeams.length)
1535
+ return [{ value: '', label: t('sessions.kanban.noTeam') }];
1536
+ return kanbanTeams.map(team => ({ value: team.key, label: team.label }));
1537
+ }, [kanbanTeams, kanbanTeamsLoaded]);
1538
+ const kanbanTeamDisabled = !kanbanTeamsLoaded || kanbanTeams.length === 0;
1539
+ const kanbanTeamValue = kanbanTeams.some(team => team.key === kanbanTeamKey)
1540
+ ? kanbanTeamKey
1541
+ : (kanbanTeamOptions[0]?.value ?? '');
1542
+ const kanbanTeamLabel = kanbanTeamOptions.find(option => option.value === kanbanTeamValue)?.label
1543
+ ?? t('sessions.kanban.groupTeam');
1544
+ const closeCreateSession = useCallback(() => {
1545
+ createRequestRef.current += 1;
1546
+ setCreateLoading(false);
1547
+ setCreateState(null);
23
1548
  }, []);
24
- return (_jsxs("section", { className: "page", ref: hostRef, children: [_jsxs("div", { className: "page-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "eyebrow", children: tr('nav.sessions') }), _jsx("h1", { children: tr('sessions.title') }), _jsx("p", { children: tr('sessions.subtitle') })] }), _jsxs("div", { className: "sessions-view-controls", children: [_jsx("span", { id: "kanban-team-stats", className: "kanban-team-stats", hidden: true }), _jsx("select", { id: "kanban-team", className: "kanban-team-select", "aria-label": tr('sessions.kanban.groupTeam'), hidden: true }), _jsxs("div", { className: "segmented kanban-groupby", id: "kanban-groupby", role: "group", "aria-label": tr('sessions.kanban.groupBy'), hidden: true, children: [_jsx("button", { type: "button", "data-groupby": "flow", children: tr('sessions.kanban.groupFlow') }), _jsx("button", { type: "button", "data-groupby": "team", children: tr('sessions.kanban.groupTeam') }), _jsx("button", { type: "button", "data-groupby": "bot", children: tr('sessions.kanban.groupBot') })] }), _jsxs("div", { className: "segmented sessions-view-toggle", role: "group", "aria-label": tr('sessions.viewMode'), children: [_jsx("button", { type: "button", "data-view": "kanban", children: tr('sessions.viewKanban') }), _jsx("button", { type: "button", "data-view": "board", children: tr('sessions.viewBoard') }), _jsx("button", { type: "button", "data-view": "table", children: tr('sessions.viewTable') })] }), _jsx("button", { type: "button", id: "monitor-room-open", className: "monitor-room-open", children: tr('sessions.monitorRoom') })] })] }), _jsxs("form", { id: "filters", className: "filters sessions-filters", children: [_jsx("input", { type: "search", name: "q", placeholder: tr('sessions.search') }), _jsxs("select", { name: "status", children: [_jsx("option", { value: "", children: tr('sessions.anyStatus') }), SESSION_STATUS_OPTIONS.map(status => (_jsx("option", { value: status, children: sessionStatusText(status) }, status)))] }), _jsxs("select", { name: "adopt", children: [_jsx("option", { value: "", children: tr('sessions.adoptAny') }), _jsx("option", { value: "yes", children: tr('sessions.adoptYes') }), _jsx("option", { value: "no", children: tr('sessions.adoptNo') })] }), _jsx("select", { name: "chat", "aria-label": tr('sessions.location'), children: _jsx("option", { value: "", children: tr('sessions.chatAny') }) }), _jsx("span", { style: { display: 'contents' }, dangerouslySetInnerHTML: { __html: renderCliFilterGroup() } }), _jsxs("label", { className: "filter-toggle", children: [_jsx("input", { type: "checkbox", name: "showUnknownChats", defaultChecked: readStoredSessionsShowUnknownChats(typeof window === 'undefined' ? undefined : window.localStorage) }), " ", _jsx("span", { children: tr('sessions.showUnknownChats') })] }), _jsxs("label", { className: "filter-toggle", children: [_jsx("input", { type: "checkbox", name: "active", defaultChecked: true }), " ", _jsx("span", { children: tr('sessions.activeOnly') })] })] }), _jsxs("div", { id: "idle-cleanup-bar", className: "idle-cleanup-bar", children: [_jsxs("div", { className: "idle-cleanup-summary", children: [_jsx("span", { className: "idle-cleanup-dot", "aria-hidden": "true" }), _jsx("span", { id: "idle-cleanup-count", className: "idle-cleanup-count" })] }), _jsxs("div", { className: "idle-cleanup-controls", children: [_jsx("span", { className: "idle-cleanup-label", children: tr('sessions.idleCleanupOlderThan') }), _jsx("div", { id: "idle-cleanup-threshold", className: "idle-cleanup-thresholds", role: "group", "aria-label": tr('sessions.idleCleanupThreshold'), children: IDLE_CLEANUP_HOUR_OPTIONS.map(hours => (_jsx("button", { type: "button", "data-hours": hours, "aria-pressed": hours === 24 ? 'true' : 'false', children: hours === 168 ? '7d' : `${hours}H` }, hours))) }), _jsx("button", { type: "button", id: "idle-cleanup-run", className: "contrast idle-cleanup-run", children: tr('sessions.idleCleanupRun') })] }), _jsx("span", { id: "idle-cleanup-status", className: "idle-cleanup-status", "aria-live": "polite" })] }), _jsxs("div", { id: "bulk-bar", className: "bulk-bar", hidden: true, children: [_jsx("span", { id: "bulk-count" }), _jsx("button", { type: "button", id: "bulk-monitor-room", children: tr('sessions.addToMonitorRoom') }), _jsx("button", { type: "button", id: "bulk-lock", children: tr('sessions.lockSelected') }), _jsx("button", { type: "button", id: "bulk-unlock", children: tr('sessions.unlockSelected') }), _jsx("button", { type: "button", id: "bulk-close", className: "contrast", children: tr('sessions.closeSelected') }), _jsx("button", { type: "button", id: "bulk-clear", children: tr('sessions.clearSelection') })] }), _jsxs("table", { id: "sessions-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: _jsx("input", { type: "checkbox", id: "select-all", title: tr('sessions.activeOnly') }) }), _jsx(SortHeader, { sort: "botName", label: tr('sessions.bot') }), _jsx(SortHeader, { sort: "cliId", label: tr('sessions.cli') }), _jsx(SortHeader, { sort: "status", label: tr('sessions.status') }), _jsx(SortHeader, { sort: "chat", label: tr('sessions.location') }), _jsx(SortHeader, { sort: "tokenIn", label: tr('sessions.tokenIn') }), _jsx(SortHeader, { sort: "tokenOut", label: tr('sessions.tokenOut') }), _jsx(SortHeader, { sort: "title", label: tr('sessions.titleCol') }), _jsx(SortHeader, { sort: "workingDir", label: tr('sessions.workingDir') }), _jsx(SortHeader, { sort: "spawnedAt", label: tr('sessions.created') }), _jsx(SortHeader, { sort: "lastMessageAt", label: tr('sessions.last') }), _jsx(SortHeader, { sort: "adopt", label: tr('sessions.adopt') }), _jsx("th", { children: tr('sessions.actions') })] }) }), _jsx("tbody", {})] }), _jsx("div", { id: "sessions-board", className: "sessions-board", hidden: true }), _jsx("div", { id: "sessions-kanban", className: "sessions-kanban", hidden: true }), _jsx("dialog", { id: "drawer" }), _jsx("dialog", { id: "term-modal", className: "term-modal" }), _jsx("dialog", { id: "history-modal", className: "history-modal" })] }));
1549
+ const openCreateSession = async () => {
1550
+ const requestId = createRequestRef.current + 1;
1551
+ createRequestRef.current = requestId;
1552
+ setCreateState({ bots: [], loading: true });
1553
+ setCreateLoading(true);
1554
+ try {
1555
+ const bots = await fetchPickerBots();
1556
+ if (createRequestRef.current !== requestId)
1557
+ return;
1558
+ setCreateState({ bots });
1559
+ }
1560
+ finally {
1561
+ if (createRequestRef.current === requestId)
1562
+ setCreateLoading(false);
1563
+ }
1564
+ };
1565
+ return (_jsxs("section", { className: "page sessions-page", children: [_jsxs("div", { className: "page-heading", children: [_jsxs("div", { className: "sessions-heading-main", children: [_jsx("p", { className: "eyebrow", children: t('nav.sessions') }), _jsx("h1", { children: t('sessions.title') }), _jsxs("div", { className: "sessions-view-controls", children: [_jsx("div", { className: "segmented sessions-view-toggle", role: "group", "aria-label": t('sessions.viewMode'), children: [
1566
+ ['kanban', t('sessions.viewKanban')],
1567
+ ['board', t('sessions.viewBoard')],
1568
+ ['table', t('sessions.viewTable')],
1569
+ ].map(([value, label]) => (_jsx("button", { type: "button", "data-view": value, className: viewMode === value ? 'active' : undefined, "aria-pressed": viewMode === value, onClick: () => setView(value), children: label }, value))) }), _jsx("div", { className: `segmented kanban-groupby${viewMode === 'kanban' ? ' is-visible' : ' is-collapsed'}`, id: "kanban-groupby", role: "group", "aria-label": t('sessions.kanban.groupBy'), "aria-hidden": viewMode !== 'kanban', children: [
1570
+ ['flow', t('sessions.kanban.groupFlow')],
1571
+ ['team', t('sessions.kanban.groupTeam')],
1572
+ ['bot', t('sessions.kanban.groupBot')],
1573
+ ].map(([value, label]) => (_jsx("button", { type: "button", "data-groupby": value, className: kanbanGroupBy === value ? 'active' : undefined, "aria-pressed": kanbanGroupBy === value, onClick: () => {
1574
+ setKanbanGroupBy(value);
1575
+ writeStoredKanbanGroupBy(windowStorage(), value);
1576
+ }, children: label }, value))) }), _jsx("span", { id: "kanban-team-stats", className: "kanban-team-stats", hidden: !(viewMode === 'kanban' && kanbanGroupBy === 'team' && kanbanTeamsLoaded), children: teamScopeText }), _jsx(DropdownMenu, { id: "kanban-team", className: "kanban-team-menu", ariaLabel: t('sessions.kanban.groupTeam'), hidden: !(viewMode === 'kanban' && kanbanGroupBy === 'team'), disabled: kanbanTeamDisabled, label: kanbanTeamLabel, value: kanbanTeamValue, options: kanbanTeamOptions, onChange: next => {
1577
+ if (!kanbanTeams.some(team => team.key === next))
1578
+ return;
1579
+ setKanbanTeamKey(next);
1580
+ try {
1581
+ window.localStorage.setItem(KANBAN_TEAM_STORAGE_KEY, next);
1582
+ }
1583
+ catch { /* current page only */ }
1584
+ } })] })] }), _jsxs("div", { className: "page-heading-actions sessions-page-actions", children: [_jsx("button", { type: "button", id: "monitor-room-open", className: "monitor-room-open", onClick: () => { window.location.href = monitorRoomUrl(); }, children: t('sessions.monitorRoom') }), ui.authed ? (_jsx(CreateActionButton, { className: "page-primary-action create-session-btn", disabled: createLoading, onClick: () => void openCreateSession(), children: t('sessions.create.button') })) : null] })] }), _jsxs("div", { className: "session-runtime-stats", "aria-live": "polite", children: [_jsxs("span", { children: [_jsx("b", { children: runtimeCounts.logical }), t('sessions.runtime.logical')] }), _jsxs("span", { children: [_jsx("b", { children: runtimeCounts.resident }), t('sessions.runtime.resident')] }), _jsxs("span", { children: [_jsx("b", { children: runtimeCounts.dormant }), t('sessions.runtime.dormant')] })] }), _jsx(SessionsFilters, { chatOptions: chatOptions, filters: filters, setFilters: setFilters, idleCleanup: {
1585
+ busy: idleCleanupBusy,
1586
+ hours: idleCleanupHours,
1587
+ status: idleCleanupStatus,
1588
+ countForHours: hours => idleCleanupCandidatesFor(hours).length,
1589
+ onRun: runIdleCleanup,
1590
+ } }), _jsx(BulkBar, { selectedCount: selected.size, lockDisabled: !selectedRows.some(row => !row.locked), unlockDisabled: !selectedRows.some(row => !!row.locked), closeProgress: bulkCloseProgress, lockProgress: bulkLockProgress, monitorRoomText: monitorRoomFeedback, onClear: () => setSelected(new Set()), onClose: () => void runBulkClose(), onAddToMonitorRoom: addSelectedToMonitorRoom, onLock: locked => void runBulkLock(locked) }), _jsxs("div", { className: `sessions-view-stage${viewStageAnimKey > 0 ? ' sessions-view-stage-enter' : ''}`, "data-view": viewMode, "data-kanban-group": kanbanGroupBy, children: [_jsx(SessionsTable, { rows: rows, selected: selected, hidden: viewMode !== 'table', sortKey: sortKey, sortDir: sortDir, selectAllChecked: selectAllChecked, selectAllIndeterminate: selectAllIndeterminate, selectAllDisabled: selectableRows.length === 0, onOpen: row => setDrawerSessionId(row.sessionId), onSelect: (id, checked) => setSelected(prev => {
1591
+ const next = new Set(prev);
1592
+ if (checked)
1593
+ next.add(id);
1594
+ else
1595
+ next.delete(id);
1596
+ return next;
1597
+ }), onSelectAll: checked => setSelected(prev => {
1598
+ const next = new Set(prev);
1599
+ for (const row of selectableRows) {
1600
+ if (checked)
1601
+ next.add(row.sessionId);
1602
+ else
1603
+ next.delete(row.sessionId);
1604
+ }
1605
+ return next;
1606
+ }), onSort: (key) => {
1607
+ if (sortKey === key)
1608
+ setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
1609
+ else {
1610
+ setSortKey(key);
1611
+ setSortDir(key === 'spawnedAt' || key === 'lastMessageAt' ? 'desc' : 'asc');
1612
+ }
1613
+ } }), _jsx(BoardView, { rows: boardRows, selected: selected, hidden: viewMode !== 'board', order: boardOrder, animated: boardAnimated, dragColId: dragColId, dragOverCol: dragOverCol, onAnimated: () => setBoardAnimated(true), onMoveColumn: moveColumn, onMoveColumnTo: moveColumnTo, onDragCol: setDragColId, onDragOverCol: setDragOverCol, onToggleSelect: row => setSelected(prev => {
1614
+ const next = new Set(prev);
1615
+ if (next.has(row.sessionId))
1616
+ next.delete(row.sessionId);
1617
+ else
1618
+ next.add(row.sessionId);
1619
+ return next;
1620
+ }), onOpen: row => setDrawerSessionId(row.sessionId), onHistory: openHistoryModal, onLocate: row => locateSession(row), onRestart: (row, button) => void restartSession(row, button), onLock: (row, locked, button) => void setSessionLocked(row, locked, button), onClose: (row, button) => void closeSession(row, button) }), _jsx("div", { id: "sessions-kanban", ref: setKanbanHost, className: `sessions-kanban${kanbanGroupBy === 'bot' ? ' kanban-mode-bot' : ''}`, hidden: viewMode !== 'kanban', children: viewMode === 'kanban' ? (_jsx(SessionsKanbanView, { host: kanbanHost, ...kanbanState, canRestartSession: canRestartSession, getTeamChatIds: teamChatIdsFor, icons: {
1621
+ details: ICON.details,
1622
+ feishu: ICON.feishu,
1623
+ history: ICON.history,
1624
+ lock: ICON.lock,
1625
+ restart: ICON.restart,
1626
+ terminal: ICON.terminal,
1627
+ unlock: ICON.unlock,
1628
+ }, lockActionLabel: lockActionLabel, sessionStatusText: sessionStatusText, onDetails: row => setDrawerSessionId(String(row.sessionId)), onHistory: openHistoryModal, onMoveRows: handleKanbanMoves, onNeedTeamBoard: team => { void ensureTeamBoard(team); }, onNeedTeams: () => { void loadKanbanTeams(); }, onOpenTerminal: openTerminalModal, onRename: (row, title) => { const s = store.sessions.get(String(row.sessionId)); if (s)
1629
+ void persistRename(s, title); }, onRestart: (row, button) => { const s = store.sessions.get(String(row.sessionId)); if (s)
1630
+ void restartSession(s, button); }, onTeamScope: scope => setTeamScopeText(scope ? t('sessions.kanban.teamScope', { chats: scope.chats, sessions: scope.sessions }) : ''), onToggleLock: (row, button) => { const s = store.sessions.get(String(row.sessionId)); if (s)
1631
+ void setSessionLocked(s, !s.locked, button); }, onToggleSelect: row => setSelected(prev => {
1632
+ const id = String(row.sessionId);
1633
+ const next = new Set(prev);
1634
+ if (next.has(id))
1635
+ next.delete(id);
1636
+ else
1637
+ next.add(id);
1638
+ return next;
1639
+ }), selectedSessionIds: selected })) : null })] }, viewStageAnimKey), _jsx(Drawer, { row: drawerRow, onClose: () => setDrawerSessionId(null), locateSession: locateSession, openHistory: openHistoryModal, resumeSession: resumeSession, restartSession: restartSession, closeSession: closeSession, setSessionLocked: setSessionLocked, startSession: startSession }), _jsx(TerminalModal, { state: termState, onClose: () => setTermState(null), onRename: persistRename }), _jsx(HistoryModal, { state: historyState, onClose: () => setHistoryState(null) }), createDialogEl ? createPortal(_jsx(CreateSessionDialog, { dialog: createDialogEl, state: createState, onClose: closeCreateSession, onSuccess: body => setCreateState(prev => prev ? { ...prev, success: body } : prev) }), createDialogEl) : null] }));
25
1640
  }
26
1641
  export function renderSessionsPage(root) {
27
1642
  return mountReactPage(root, _jsx(SessionsPage, {}));