cli-jaw 2.2.16 → 2.2.17

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 (411) hide show
  1. package/README.md +10 -1
  2. package/dist/bin/commands/_shared/dashboard-client.js +42 -0
  3. package/dist/bin/commands/_shared/dashboard-client.js.map +1 -0
  4. package/dist/bin/commands/_shared/search-format.js +40 -0
  5. package/dist/bin/commands/_shared/search-format.js.map +1 -0
  6. package/dist/bin/commands/chat-search.js +5 -3
  7. package/dist/bin/commands/chat-search.js.map +1 -1
  8. package/dist/bin/commands/dashboard-chat.js +14 -31
  9. package/dist/bin/commands/dashboard-chat.js.map +1 -1
  10. package/dist/bin/commands/dashboard-memory.js +12 -49
  11. package/dist/bin/commands/dashboard-memory.js.map +1 -1
  12. package/dist/bin/commands/doctor.js +33 -10
  13. package/dist/bin/commands/doctor.js.map +1 -1
  14. package/dist/bin/commands/init.js +13 -1
  15. package/dist/bin/commands/init.js.map +1 -1
  16. package/dist/bin/commands/memory.js +5 -1
  17. package/dist/bin/commands/memory.js.map +1 -1
  18. package/dist/bin/commands/slack.js +57 -3
  19. package/dist/bin/commands/slack.js.map +1 -1
  20. package/dist/bin/commands/tui/input-handler.js +1 -1
  21. package/dist/bin/commands/tui/input-handler.js.map +1 -1
  22. package/dist/bin/commands/tui/slash-command-runner.js +4 -5
  23. package/dist/bin/commands/tui/slash-command-runner.js.map +1 -1
  24. package/dist/bin/postinstall.js +16 -18
  25. package/dist/bin/postinstall.js.map +1 -1
  26. package/dist/lib/media-kind.js +28 -0
  27. package/dist/lib/media-kind.js.map +1 -0
  28. package/dist/lib/upload.js +27 -10
  29. package/dist/lib/upload.js.map +1 -1
  30. package/dist/server.js +35 -51
  31. package/dist/server.js.map +1 -1
  32. package/dist/src/agent/args.js +34 -0
  33. package/dist/src/agent/args.js.map +1 -1
  34. package/dist/src/agent/codex-app-client.js +1016 -135
  35. package/dist/src/agent/codex-app-client.js.map +1 -1
  36. package/dist/src/agent/codex-app-events.js +72 -1
  37. package/dist/src/agent/codex-app-events.js.map +1 -1
  38. package/dist/src/agent/codex-host-pool.js +617 -0
  39. package/dist/src/agent/codex-host-pool.js.map +1 -0
  40. package/dist/src/agent/jwc-runtime.js +12 -4
  41. package/dist/src/agent/jwc-runtime.js.map +1 -1
  42. package/dist/src/agent/kiro-auth.js +10 -4
  43. package/dist/src/agent/kiro-auth.js.map +1 -1
  44. package/dist/src/agent/kiro-runtime.js +45 -8
  45. package/dist/src/agent/kiro-runtime.js.map +1 -1
  46. package/dist/src/agent/lifecycle-handler.js +75 -52
  47. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  48. package/dist/src/agent/memory-flush-controller.js +241 -33
  49. package/dist/src/agent/memory-flush-controller.js.map +1 -1
  50. package/dist/src/agent/prompt-context.js +4 -0
  51. package/dist/src/agent/prompt-context.js.map +1 -1
  52. package/dist/src/agent/runtime-pool.js +64 -34
  53. package/dist/src/agent/runtime-pool.js.map +1 -1
  54. package/dist/src/agent/session-persistence.js +71 -15
  55. package/dist/src/agent/session-persistence.js.map +1 -1
  56. package/dist/src/agent/spawn/queue.js +319 -162
  57. package/dist/src/agent/spawn/queue.js.map +1 -1
  58. package/dist/src/agent/spawn.js +581 -268
  59. package/dist/src/agent/spawn.js.map +1 -1
  60. package/dist/src/bgtask/types.js +1 -1
  61. package/dist/src/bgtask/types.js.map +1 -1
  62. package/dist/src/browser/adaptive-fetch/safety.js +25 -0
  63. package/dist/src/browser/adaptive-fetch/safety.js.map +1 -1
  64. package/dist/src/browser/connection.js +2 -8
  65. package/dist/src/browser/connection.js.map +1 -1
  66. package/dist/src/cli/capability-probe-worker.js +172 -0
  67. package/dist/src/cli/capability-probe-worker.js.map +1 -0
  68. package/dist/src/cli/capability-probe.js +83 -0
  69. package/dist/src/cli/capability-probe.js.map +1 -0
  70. package/dist/src/cli/claude-models.js +2 -2
  71. package/dist/src/cli/claude-models.js.map +1 -1
  72. package/dist/src/cli/cli-status-worker.js +296 -0
  73. package/dist/src/cli/cli-status-worker.js.map +1 -0
  74. package/dist/src/cli/cli-status.js +81 -0
  75. package/dist/src/cli/cli-status.js.map +1 -0
  76. package/dist/src/cli/command-context.js +17 -5
  77. package/dist/src/cli/command-context.js.map +1 -1
  78. package/dist/src/cli/compact.js +56 -19
  79. package/dist/src/cli/compact.js.map +1 -1
  80. package/dist/src/cli/handlers-runtime.js +6 -4
  81. package/dist/src/cli/handlers-runtime.js.map +1 -1
  82. package/dist/src/cli/handlers.js +7 -2
  83. package/dist/src/cli/handlers.js.map +1 -1
  84. package/dist/src/cli/opencodex-models.js +5 -32
  85. package/dist/src/cli/opencodex-models.js.map +1 -1
  86. package/dist/src/cli/opencodex-runtime.js +82 -0
  87. package/dist/src/cli/opencodex-runtime.js.map +1 -0
  88. package/dist/src/cli/readiness.js +35 -18
  89. package/dist/src/cli/readiness.js.map +1 -1
  90. package/dist/src/cli/registry-live.js +8 -2
  91. package/dist/src/cli/registry-live.js.map +1 -1
  92. package/dist/src/cli/registry.js +1 -1
  93. package/dist/src/cli/registry.js.map +1 -1
  94. package/dist/src/core/browser-open-default.js +6 -4
  95. package/dist/src/core/browser-open-default.js.map +1 -1
  96. package/dist/src/core/browser-open.js +11 -15
  97. package/dist/src/core/browser-open.js.map +1 -1
  98. package/dist/src/core/bus.js +7 -1
  99. package/dist/src/core/bus.js.map +1 -1
  100. package/dist/src/core/chat-sessions.js +121 -12
  101. package/dist/src/core/chat-sessions.js.map +1 -1
  102. package/dist/src/core/cli-detect.js +148 -4
  103. package/dist/src/core/cli-detect.js.map +1 -1
  104. package/dist/src/core/codex-config.js +21 -0
  105. package/dist/src/core/codex-config.js.map +1 -1
  106. package/dist/src/core/compact.js +73 -21
  107. package/dist/src/core/compact.js.map +1 -1
  108. package/dist/src/core/config.js +449 -20
  109. package/dist/src/core/config.js.map +1 -1
  110. package/dist/src/core/db.js +225 -0
  111. package/dist/src/core/db.js.map +1 -1
  112. package/dist/src/core/event-bus.js +49 -4
  113. package/dist/src/core/event-bus.js.map +1 -1
  114. package/dist/src/core/event-scope.js +100 -0
  115. package/dist/src/core/event-scope.js.map +1 -0
  116. package/dist/src/core/main-session.js +64 -21
  117. package/dist/src/core/main-session.js.map +1 -1
  118. package/dist/src/core/message-summary.js +12 -2
  119. package/dist/src/core/message-summary.js.map +1 -1
  120. package/dist/src/core/platform-kind.js +139 -0
  121. package/dist/src/core/platform-kind.js.map +1 -0
  122. package/dist/src/core/rate-limit.js +156 -0
  123. package/dist/src/core/rate-limit.js.map +1 -0
  124. package/dist/src/core/runtime-settings.js +165 -17
  125. package/dist/src/core/runtime-settings.js.map +1 -1
  126. package/dist/src/core/session-context.js +7 -0
  127. package/dist/src/core/session-context.js.map +1 -0
  128. package/dist/src/core/session-ops.js +53 -9
  129. package/dist/src/core/session-ops.js.map +1 -1
  130. package/dist/src/core/settings-merge.js +105 -3
  131. package/dist/src/core/settings-merge.js.map +1 -1
  132. package/dist/src/core/settings-watch.js +29 -5
  133. package/dist/src/core/settings-watch.js.map +1 -1
  134. package/dist/src/core/widget-watcher.js +15 -2
  135. package/dist/src/core/widget-watcher.js.map +1 -1
  136. package/dist/src/discord/bot.js +5 -2
  137. package/dist/src/discord/bot.js.map +1 -1
  138. package/dist/src/discord/commands.js +4 -4
  139. package/dist/src/discord/commands.js.map +1 -1
  140. package/dist/src/goal/heartbeat.js +1 -1
  141. package/dist/src/goal/heartbeat.js.map +1 -1
  142. package/dist/src/goal/runtime.js +1 -1
  143. package/dist/src/goal/runtime.js.map +1 -1
  144. package/dist/src/goal-run/controller.js +2 -2
  145. package/dist/src/goal-run/controller.js.map +1 -1
  146. package/dist/src/manager/internal-fetch.js +15 -0
  147. package/dist/src/manager/internal-fetch.js.map +1 -0
  148. package/dist/src/manager/logs.js +2 -1
  149. package/dist/src/manager/logs.js.map +1 -1
  150. package/dist/src/manager/memory/chat-federation.js +104 -11
  151. package/dist/src/manager/memory/chat-federation.js.map +1 -1
  152. package/dist/src/manager/memory/federation.js +100 -0
  153. package/dist/src/manager/memory/federation.js.map +1 -1
  154. package/dist/src/manager/metadata.js +4 -1
  155. package/dist/src/manager/metadata.js.map +1 -1
  156. package/dist/src/manager/notes/assets.js +1 -1
  157. package/dist/src/manager/notes/assets.js.map +1 -1
  158. package/dist/src/manager/notes/frontmatter.js +5 -49
  159. package/dist/src/manager/notes/frontmatter.js.map +1 -1
  160. package/dist/src/manager/notes/link-resolver.js +2 -2
  161. package/dist/src/manager/notes/link-resolver.js.map +1 -1
  162. package/dist/src/manager/notes/remote-assets.js +1 -1
  163. package/dist/src/manager/notes/remote-assets.js.map +1 -1
  164. package/dist/src/manager/notes/routes.js +2 -2
  165. package/dist/src/manager/notes/routes.js.map +1 -1
  166. package/dist/src/manager/notes/store.js +2 -2
  167. package/dist/src/manager/notes/store.js.map +1 -1
  168. package/dist/src/manager/notes/trash.js +1 -1
  169. package/dist/src/manager/notes/trash.js.map +1 -1
  170. package/dist/src/manager/notes/vault-index.js +2 -2
  171. package/dist/src/manager/notes/vault-index.js.map +1 -1
  172. package/dist/src/manager/notes/wiki-links.js +1 -1
  173. package/dist/src/manager/notes/wiki-links.js.map +1 -1
  174. package/dist/src/manager/notes/wiki-routes.js +84 -0
  175. package/dist/src/manager/notes/wiki-routes.js.map +1 -0
  176. package/dist/src/manager/proxy.js +1 -0
  177. package/dist/src/manager/proxy.js.map +1 -1
  178. package/dist/src/manager/routes/dashboard-memory.js +24 -1
  179. package/dist/src/manager/routes/dashboard-memory.js.map +1 -1
  180. package/dist/src/manager/routes/runtime-monitor.js +1 -1
  181. package/dist/src/manager/routes/runtime-monitor.js.map +1 -1
  182. package/dist/src/manager/scan.js +5 -3
  183. package/dist/src/manager/scan.js.map +1 -1
  184. package/dist/src/manager/server.js +20 -6
  185. package/dist/src/manager/server.js.map +1 -1
  186. package/dist/src/manager/telegram-hub/hub-bot.js +4 -3
  187. package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
  188. package/dist/src/manager/worker-events.js +2 -1
  189. package/dist/src/manager/worker-events.js.map +1 -1
  190. package/dist/src/memory/heartbeat.js +6 -5
  191. package/dist/src/memory/heartbeat.js.map +1 -1
  192. package/dist/src/memory/indexing.js +162 -45
  193. package/dist/src/memory/indexing.js.map +1 -1
  194. package/dist/src/messaging/channel-validate.js +14 -1
  195. package/dist/src/messaging/channel-validate.js.map +1 -1
  196. package/dist/src/messaging/redact.js +2 -1
  197. package/dist/src/messaging/redact.js.map +1 -1
  198. package/dist/src/messaging/session-key.js +21 -2
  199. package/dist/src/messaging/session-key.js.map +1 -1
  200. package/dist/src/notes/constants.js.map +1 -0
  201. package/dist/src/notes/frontmatter.js +59 -0
  202. package/dist/src/notes/frontmatter.js.map +1 -0
  203. package/dist/src/notes/path-guards.js.map +1 -0
  204. package/dist/src/notes/search.js.map +1 -0
  205. package/dist/src/orchestrator/attestation.js +1 -1
  206. package/dist/src/orchestrator/attestation.js.map +1 -1
  207. package/dist/src/orchestrator/gateway.js +135 -31
  208. package/dist/src/orchestrator/gateway.js.map +1 -1
  209. package/dist/src/orchestrator/pipeline.js +53 -21
  210. package/dist/src/orchestrator/pipeline.js.map +1 -1
  211. package/dist/src/orchestrator/scope.js +36 -2
  212. package/dist/src/orchestrator/scope.js.map +1 -1
  213. package/dist/src/orchestrator/session-lanes.js +87 -0
  214. package/dist/src/orchestrator/session-lanes.js.map +1 -0
  215. package/dist/src/orchestrator/session-work.js +25 -0
  216. package/dist/src/orchestrator/session-work.js.map +1 -0
  217. package/dist/src/orchestrator/state-machine.js +1 -5
  218. package/dist/src/orchestrator/state-machine.js.map +1 -1
  219. package/dist/src/orchestrator/worker-registry.js +20 -10
  220. package/dist/src/orchestrator/worker-registry.js.map +1 -1
  221. package/dist/src/prompt/builder.js +40 -2
  222. package/dist/src/prompt/builder.js.map +1 -1
  223. package/dist/src/prompt/templates/a1-system.md +8 -1
  224. package/dist/src/routes/agent-control.js +44 -8
  225. package/dist/src/routes/agent-control.js.map +1 -1
  226. package/dist/src/routes/chat-sessions.js +34 -6
  227. package/dist/src/routes/chat-sessions.js.map +1 -1
  228. package/dist/src/routes/command.js +63 -8
  229. package/dist/src/routes/command.js.map +1 -1
  230. package/dist/src/routes/events.js +11 -2
  231. package/dist/src/routes/events.js.map +1 -1
  232. package/dist/src/routes/link-preview.js +1 -27
  233. package/dist/src/routes/link-preview.js.map +1 -1
  234. package/dist/src/routes/messages.js +6 -5
  235. package/dist/src/routes/messages.js.map +1 -1
  236. package/dist/src/routes/messaging.js +45 -1
  237. package/dist/src/routes/messaging.js.map +1 -1
  238. package/dist/src/routes/orchestrate.js +53 -33
  239. package/dist/src/routes/orchestrate.js.map +1 -1
  240. package/dist/src/routes/search.js +84 -0
  241. package/dist/src/routes/search.js.map +1 -0
  242. package/dist/src/routes/session-request.js +29 -0
  243. package/dist/src/routes/session-request.js.map +1 -0
  244. package/dist/src/routes/settings.js +110 -3
  245. package/dist/src/routes/settings.js.map +1 -1
  246. package/dist/src/routes/static.js +30 -8
  247. package/dist/src/routes/static.js.map +1 -1
  248. package/dist/src/routes/system.js +1 -1
  249. package/dist/src/routes/system.js.map +1 -1
  250. package/dist/src/routes/wiki.js +122 -0
  251. package/dist/src/routes/wiki.js.map +1 -0
  252. package/dist/src/search/contract.js +2 -0
  253. package/dist/src/search/contract.js.map +1 -0
  254. package/dist/src/search/coordinator.js +153 -0
  255. package/dist/src/search/coordinator.js.map +1 -0
  256. package/dist/src/search/provider.js +40 -0
  257. package/dist/src/search/provider.js.map +1 -0
  258. package/dist/src/search/providers/chat.js +115 -0
  259. package/dist/src/search/providers/chat.js.map +1 -0
  260. package/dist/src/search/providers/memory.js +53 -0
  261. package/dist/src/search/providers/memory.js.map +1 -0
  262. package/dist/src/search/providers/wiki.js +80 -0
  263. package/dist/src/search/providers/wiki.js.map +1 -0
  264. package/dist/src/slack/api.js +10 -1
  265. package/dist/src/slack/api.js.map +1 -1
  266. package/dist/src/slack/attachment-recovery.js +42 -0
  267. package/dist/src/slack/attachment-recovery.js.map +1 -0
  268. package/dist/src/slack/bot.js +120 -41
  269. package/dist/src/slack/bot.js.map +1 -1
  270. package/dist/src/slack/commands.js +24 -8
  271. package/dist/src/slack/commands.js.map +1 -1
  272. package/dist/src/slack/events.js +12 -1
  273. package/dist/src/slack/events.js.map +1 -1
  274. package/dist/src/slack/history.js +108 -0
  275. package/dist/src/slack/history.js.map +1 -0
  276. package/dist/src/slack/inbound-file.js +242 -0
  277. package/dist/src/slack/inbound-file.js.map +1 -0
  278. package/dist/src/slack/inbound-url.js +39 -0
  279. package/dist/src/slack/inbound-url.js.map +1 -0
  280. package/dist/src/slack/ingress.js +140 -0
  281. package/dist/src/slack/ingress.js.map +1 -0
  282. package/dist/src/slack/manifest.js +3 -0
  283. package/dist/src/slack/manifest.js.map +1 -1
  284. package/dist/src/slack/socket.js +1 -1
  285. package/dist/src/slack/socket.js.map +1 -1
  286. package/dist/src/slack/thread-tracker.js +86 -0
  287. package/dist/src/slack/thread-tracker.js.map +1 -0
  288. package/dist/src/telegram/bot.js +21 -11
  289. package/dist/src/telegram/bot.js.map +1 -1
  290. package/dist/src/wiki/config.js +193 -0
  291. package/dist/src/wiki/config.js.map +1 -0
  292. package/dist/src/wiki/entities.js +61 -0
  293. package/dist/src/wiki/entities.js.map +1 -0
  294. package/dist/src/wiki/ontology.js +72 -0
  295. package/dist/src/wiki/ontology.js.map +1 -0
  296. package/dist/src/wiki/prompt.js +172 -0
  297. package/dist/src/wiki/prompt.js.map +1 -0
  298. package/dist/src/wiki/safe-traversal.js +198 -0
  299. package/dist/src/wiki/safe-traversal.js.map +1 -0
  300. package/dist/src/wiki/scaffold.js +77 -0
  301. package/dist/src/wiki/scaffold.js.map +1 -0
  302. package/package.json +1 -1
  303. package/public/css/session-hub.css +266 -0
  304. package/public/dist/assets/AdvancedExport-DYGcCAUq.js +1 -0
  305. package/public/dist/assets/Agent-CNNQ7KkJ.js +1 -0
  306. package/public/dist/assets/{DesignPanel-BGMPhZSx.js → DesignPanel-DcL369nB.js} +1 -1
  307. package/public/dist/assets/{DocPanel-Dz-J_l5M.js → DocPanel-K8A2pm_1.js} +1 -1
  308. package/public/dist/assets/{FolderPanel-DbPhzzvi.js → FolderPanel-CV52EzwG.js} +1 -1
  309. package/public/dist/assets/{MilkdownWysiwygEditor-Em7iC6y9.js → MilkdownWysiwygEditor-BKOCDI4Z.js} +1 -1
  310. package/public/dist/assets/api-B25zNNFJ.js +1 -0
  311. package/public/dist/assets/{app-DX4e6isX.css → app-DjgGeXhJ.css} +1 -1
  312. package/public/dist/assets/app-Dsgp4xpU.js +55 -0
  313. package/public/dist/assets/bgtask-badge-C6h5C-o4.js +1 -0
  314. package/public/dist/assets/{bgtask-badge-qMkKOZx0.js → bgtask-badge-CpavV0Jc.js} +4 -4
  315. package/public/dist/assets/{employees-BnobWM_2.js → employees-BJyEgim7.js} +4 -4
  316. package/public/dist/assets/idb-cache-B5PX63p7.js +1 -0
  317. package/public/dist/assets/{idb-cache-BzzftUET.js → idb-cache-lOntwU3S.js} +1 -1
  318. package/public/dist/assets/{iframe-renderer-C5vRbbl9.js → iframe-renderer-B8k3AvTv.js} +12 -12
  319. package/public/dist/assets/iframe-renderer-BO_GgCTR.js +1 -0
  320. package/public/dist/assets/manager-LkGs6qEJ.css +1 -0
  321. package/public/dist/assets/manager-kXYQyMhR.js +13 -0
  322. package/public/dist/assets/{memory-DF4kPwwC.js → memory-D2RbBVO-.js} +11 -11
  323. package/public/dist/assets/memory-amsvg4Ey.js +1 -0
  324. package/public/dist/assets/message-history-BGHZxvgV.js +1 -0
  325. package/public/dist/assets/message-history-Bw2E_b8L.js +1 -0
  326. package/public/dist/assets/pi-settings-5HVg1sXJ.js +1 -0
  327. package/public/dist/assets/render-BkGcpH7z.js +157 -0
  328. package/public/dist/assets/settings-B1FPORO2.js +56 -0
  329. package/public/dist/assets/settings-CMN-zN4Q.js +1 -0
  330. package/public/dist/assets/settings-core-B_OjGYVN.js +1 -0
  331. package/public/dist/assets/settings-core-CxRwqx86.js +144 -0
  332. package/public/dist/assets/sidebar-DW78DANx.js +49 -0
  333. package/public/dist/assets/skills-Dz-9Yrxr.js +1 -0
  334. package/public/dist/assets/{skills-IGquen_j.js → skills-lWi0ZFiV.js} +6 -6
  335. package/public/dist/assets/{slash-commands-ZRDOL99E.js → slash-commands-DYOI6nKj.js} +1 -1
  336. package/public/dist/assets/slash-commands-xJFCdTTM.js +1 -0
  337. package/public/dist/assets/{trace-drawer-D8To8fYt.js → trace-drawer-DBX9xzmt.js} +1 -1
  338. package/public/dist/assets/{ui-Hx_mNoDf.js → ui-Bx7eqnXt.js} +1 -1
  339. package/public/dist/assets/ui-JOcrYeG-.js +1 -0
  340. package/public/dist/assets/{wiki-link-suggestions-DZQc78gj.js → wiki-link-suggestions-Dp6kBQVu.js} +1 -1
  341. package/public/dist/index.html +4 -4
  342. package/public/dist/manager/index.html +2 -2
  343. package/public/index.html +3 -2
  344. package/public/js/event-channel.ts +25 -0
  345. package/public/js/features/channel-onboarding-flow.ts +16 -1
  346. package/public/js/features/channel-onboarding.ts +20 -2
  347. package/public/js/features/chat-messages.ts +3 -1
  348. package/public/js/features/chat-search.ts +2 -1
  349. package/public/js/features/chat.ts +56 -18
  350. package/public/js/features/elicitation.ts +15 -0
  351. package/public/js/features/idb-cache.ts +34 -0
  352. package/public/js/features/message-history.ts +2 -1
  353. package/public/js/features/session-hub.ts +334 -0
  354. package/public/js/features/settings-cli-status-render.ts +1 -1
  355. package/public/js/features/settings-cli-status.ts +25 -11
  356. package/public/js/features/settings-core.ts +72 -3
  357. package/public/js/features/settings-types.ts +32 -0
  358. package/public/js/features/ui-status.ts +2 -1
  359. package/public/js/main.ts +20 -0
  360. package/public/js/render/compose-block.ts +7 -0
  361. package/public/js/ws.ts +22 -2
  362. package/public/locales/en.json +46 -4
  363. package/public/locales/ja.json +46 -4
  364. package/public/locales/ko.json +46 -4
  365. package/public/locales/zh.json +46 -4
  366. package/public/manager/src/App.tsx +1 -1
  367. package/public/manager/src/InstancePreview.tsx +5 -1
  368. package/public/manager/src/api.ts +2 -1
  369. package/public/manager/src/components/InstanceGroups.tsx +10 -0
  370. package/public/manager/src/components/InstanceListContent.tsx +24 -1
  371. package/public/manager/src/components/InstanceRow.tsx +33 -3
  372. package/public/manager/src/components/InstanceSessionList.tsx +132 -0
  373. package/public/manager/src/hooks/useActiveSessionDisclosure.ts +45 -0
  374. package/public/manager/src/lib/resilient-fetch.ts +93 -0
  375. package/public/manager/src/lib/session-store.ts +216 -0
  376. package/public/manager/src/manager-components.css +97 -0
  377. package/public/manager/src/settings/pages/AdvancedExport.tsx +12 -2
  378. package/public/manager/src/settings/pages/Agent.tsx +113 -1
  379. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +1 -1
  380. package/scripts/burst-scenario.mjs +72 -0
  381. package/scripts/check-redaction-sinks.mjs +1 -0
  382. package/scripts/check-strict-baseline.mjs +1 -1
  383. package/dist/src/manager/notes/constants.js.map +0 -1
  384. package/dist/src/manager/notes/path-guards.js.map +0 -1
  385. package/dist/src/manager/notes/search.js.map +0 -1
  386. package/public/dist/assets/AdvancedExport-DsDpqXoH.js +0 -1
  387. package/public/dist/assets/Agent-BjbZVZ9p.js +0 -1
  388. package/public/dist/assets/api-C9dOxby7.js +0 -1
  389. package/public/dist/assets/app-BzD_ZLvt.js +0 -55
  390. package/public/dist/assets/bgtask-badge-Dqbp-Y4e.js +0 -1
  391. package/public/dist/assets/idb-cache-ZH6lOebk.js +0 -1
  392. package/public/dist/assets/iframe-renderer-Df4KIVyx.js +0 -1
  393. package/public/dist/assets/manager-C4eNDNFt.css +0 -1
  394. package/public/dist/assets/manager-DaN201mN.js +0 -13
  395. package/public/dist/assets/memory-D-bULTHl.js +0 -1
  396. package/public/dist/assets/message-history-C4WOisGx.js +0 -1
  397. package/public/dist/assets/message-history-ZBoza6ew.js +0 -1
  398. package/public/dist/assets/pi-settings-CBUGDAuT.js +0 -1
  399. package/public/dist/assets/render-DYX6GRQ0.js +0 -122
  400. package/public/dist/assets/settings-DbJpGoaX.js +0 -55
  401. package/public/dist/assets/settings-SreihUje.js +0 -1
  402. package/public/dist/assets/settings-core-B1Y5Nb4Z.js +0 -1
  403. package/public/dist/assets/settings-core-DobZWIZ7.js +0 -140
  404. package/public/dist/assets/sidebar-Cp22g0Gv.js +0 -49
  405. package/public/dist/assets/skills-DwPIP08e.js +0 -1
  406. package/public/dist/assets/slash-commands-BBVfwcnW.js +0 -1
  407. package/public/dist/assets/ui-CAicpUZc.js +0 -1
  408. /package/dist/src/{manager/notes → notes}/constants.js +0 -0
  409. /package/dist/src/{manager/notes → notes}/path-guards.js +0 -0
  410. /package/dist/src/{manager/notes → notes}/search.js +0 -0
  411. /package/public/dist/assets/{pi-settings-Dh5lgb87.js → pi-settings-DEuKgvIj.js} +0 -0
@@ -0,0 +1,334 @@
1
+ import { API_BASE, api, getAuthToken } from '../api.js';
2
+ import { escapeHtml } from '../render/html.js';
3
+ import { getLang, t } from './i18n.js';
4
+
5
+ export type SessionSource = 'local' | 'slack' | 'telegram' | 'discord';
6
+
7
+ export interface SessionListItem {
8
+ id: string;
9
+ seq: number;
10
+ label: string | null;
11
+ message_count: number;
12
+ source?: SessionSource;
13
+ remoteKey?: string | null;
14
+ lastActivityAt?: string | null;
15
+ }
16
+
17
+ export interface SessionListResponse {
18
+ sessions: SessionListItem[];
19
+ active: string;
20
+ }
21
+
22
+ type SessionViewMode = 'off' | 'hub' | 'session' | 'redirect';
23
+
24
+ interface SessionViewState {
25
+ enabled: boolean;
26
+ mode: SessionViewMode;
27
+ activeId: string | null;
28
+ viewed: SessionListItem | null;
29
+ }
30
+
31
+ const viewState: SessionViewState = {
32
+ enabled: false,
33
+ mode: 'off',
34
+ activeId: null,
35
+ viewed: null,
36
+ };
37
+
38
+ let cancelRecordingOnReadonly: (() => void) | null = null;
39
+ let latestList: SessionListResponse | null = null;
40
+
41
+ const ALLOWED_READONLY_COMMAND = /^\s*(?:\/switch(?:\s|$)|\/sessions(?:\s|$)|\/fork(?:\s|$)|\/\d+(?:\s|$))/i;
42
+
43
+ function routePath(pathname: string): string {
44
+ const withoutProxy = API_BASE && pathname.startsWith(API_BASE)
45
+ ? pathname.slice(API_BASE.length)
46
+ : pathname;
47
+ return withoutProxy || '/';
48
+ }
49
+
50
+ function parsedSeq(pathname: string): number | null {
51
+ const match = routePath(pathname).match(/^\/(\d+)\/?$/);
52
+ return match ? Number(match[1]) : null;
53
+ }
54
+
55
+ function navigationFieldsPresent(response: SessionListResponse): boolean {
56
+ return response.sessions.some(session => Object.prototype.hasOwnProperty.call(session, 'source'));
57
+ }
58
+
59
+ export function configureSessionView(response: SessionListResponse, pathname = window.location.pathname): SessionViewMode {
60
+ latestList = response;
61
+ if (!navigationFieldsPresent(response)) {
62
+ Object.assign(viewState, { enabled: false, mode: 'off', activeId: null, viewed: null });
63
+ return 'off';
64
+ }
65
+
66
+ const path = routePath(pathname);
67
+ if (path === '/' || path === '') {
68
+ Object.assign(viewState, { enabled: true, mode: 'hub', activeId: response.active, viewed: null });
69
+ return 'hub';
70
+ }
71
+
72
+ const seq = parsedSeq(pathname);
73
+ const viewed = seq === null ? null : response.sessions.find(session => session.seq === seq) || null;
74
+ if (!viewed) {
75
+ Object.assign(viewState, { enabled: true, mode: 'redirect', activeId: response.active, viewed: null });
76
+ return 'redirect';
77
+ }
78
+
79
+ Object.assign(viewState, { enabled: true, mode: 'session', activeId: response.active, viewed });
80
+ if (!canSendFromCurrentView()) cancelRecordingOnReadonly?.();
81
+ return 'session';
82
+ }
83
+
84
+ export function canSendFromCurrentView(commandText = ''): boolean {
85
+ // Fail CLOSED on numeric routes before initialization. Write listeners are
86
+ // installed at module load (main.ts:120) but initializeSessionView() only
87
+ // runs later in bootstrap, so an early click on /:seq would otherwise pass
88
+ // the guard and write to the GLOBAL active session — the exact
89
+ // cross-session write this guard exists to prevent.
90
+ if (!viewState.enabled) {
91
+ if (parsedSeq(window.location.pathname) === null) return true;
92
+ return commandText ? ALLOWED_READONLY_COMMAND.test(commandText) : false;
93
+ }
94
+ if (commandText && ALLOWED_READONLY_COMMAND.test(commandText)) return true;
95
+ return viewState.mode === 'session'
96
+ && viewState.viewed?.id === viewState.activeId
97
+ && viewState.viewed.remoteKey === null;
98
+ }
99
+
100
+ export function currentSessionId(): string | null {
101
+ return viewState.enabled && viewState.mode === 'session' ? viewState.viewed?.id || null : null;
102
+ }
103
+
104
+ // Mirrors scopeForChatSession() on the server (src/orchestrator/scope.ts). A tab on the
105
+ // hub or on a pre-navigation build returns null, which means an unfiltered connection
106
+ // that receives everything — the behaviour before per-tab scoping existed.
107
+ //
108
+ // Remotely bound sessions run under their remote key, so a tab viewing one must
109
+ // subscribe to that key rather than a local scope it would never see events on.
110
+ export function currentEventScope(): string | null {
111
+ if (!viewState.enabled || viewState.mode !== 'session') return null;
112
+ const viewed = viewState.viewed;
113
+ if (!viewed) return null;
114
+ if (viewed.id === 'default') return 'default';
115
+ if (viewed.remoteKey) return viewed.remoteKey;
116
+ return `local:${viewed.id}`;
117
+ }
118
+
119
+ export function withCurrentSessionQuery(path: string): string {
120
+ const sessionId = currentSessionId();
121
+ if (!sessionId) return path;
122
+ if (path.startsWith('?')) {
123
+ const params = new URLSearchParams(path.slice(1));
124
+ params.set('session', sessionId);
125
+ return `?${params.toString()}`;
126
+ }
127
+ const url = new URL(path, 'http://session.local');
128
+ url.searchParams.set('session', sessionId);
129
+ return `${url.pathname}${url.search}${url.hash}`;
130
+ }
131
+
132
+ // Writes carry the session id, not a scope: the scope rule belongs to the server
133
+ // (src/orchestrator/scope.ts). A tab on the hub or a pre-navigation build sends
134
+ // nothing extra and the server keeps using the globally active session, which is
135
+ // exactly the behaviour before per-tab routing existed.
136
+ export function withCurrentSessionBody<T extends Record<string, unknown>>(body: T): T & { sessionId?: string } {
137
+ const sessionId = currentSessionId();
138
+ return sessionId ? { ...body, sessionId } : body;
139
+ }
140
+
141
+ function sessionHref(seq: number): string {
142
+ return `${API_BASE}/${seq}`;
143
+ }
144
+
145
+ function hubHref(): string {
146
+ return API_BASE || '/';
147
+ }
148
+
149
+ function displayLabel(session: SessionListItem): string {
150
+ if (session.seq === 0) return t('sessionHub.defaultSession');
151
+ return session.label?.trim() || t('sessionHub.sessionLabel', { seq: session.seq });
152
+ }
153
+
154
+ function formatLastActivity(value: string | null | undefined): string {
155
+ if (value == null) return t('sessionHub.noActivity');
156
+ const date = new Date(value);
157
+ if (Number.isNaN(date.getTime())) return t('sessionHub.noActivity');
158
+ return new Intl.DateTimeFormat(getLang(), { dateStyle: 'medium', timeStyle: 'short' }).format(date);
159
+ }
160
+
161
+ function sourceLabel(source: SessionSource | undefined): string {
162
+ return t(`sessionHub.source.${source || 'local'}`);
163
+ }
164
+
165
+ function renderSessionCard(session: SessionListItem, activeId: string): string {
166
+ const remoteBound = session.remoteKey != null;
167
+ const canDelete = session.id !== 'default' && !remoteBound;
168
+ const deleteReason = remoteBound ? t('sessionHub.deleteRemoteDisabled') : '';
169
+ return `<article class="session-card${session.id === activeId ? ' is-active' : ''}" data-session-id="${escapeHtml(session.id)}">
170
+ <a class="session-card-link" href="${escapeHtml(sessionHref(session.seq))}">
171
+ <div class="session-card-heading">
172
+ <span class="session-seq">/${session.seq}</span>
173
+ <span class="session-source">${escapeHtml(sourceLabel(session.source))}</span>
174
+ </div>
175
+ <h2>${escapeHtml(displayLabel(session))}</h2>
176
+ <dl class="session-card-meta">
177
+ <div><dt>${escapeHtml(t('sessionHub.messages'))}</dt><dd>${session.message_count}</dd></div>
178
+ <div><dt>${escapeHtml(t('sessionHub.lastActivity'))}</dt><dd>${escapeHtml(formatLastActivity(session.lastActivityAt))}</dd></div>
179
+ </dl>
180
+ </a>
181
+ ${session.id === 'default' ? '' : (canDelete
182
+ // A disabled button is not focusable and its title is unreliable on
183
+ // touch and assistive tech, so the remote-bound reason is rendered as
184
+ // visible text and wired with aria-describedby instead.
185
+ ? `<button type="button" class="session-delete" data-delete-session="${escapeHtml(session.id)}">${escapeHtml(t('sessionHub.delete'))}</button>`
186
+ : `<button type="button" class="session-delete" data-delete-session="${escapeHtml(session.id)}" aria-disabled="true" aria-describedby="delete-reason-${escapeHtml(session.id)}">${escapeHtml(t('sessionHub.delete'))}</button>
187
+ <p class="session-delete-reason" id="delete-reason-${escapeHtml(session.id)}">${escapeHtml(deleteReason)}</p>`)}
188
+ </article>`;
189
+ }
190
+
191
+ function renderHubSkeleton(): void {
192
+ document.body.classList.add('session-hub-mode');
193
+ let hub = document.getElementById('sessionHub');
194
+ if (!hub) {
195
+ hub = document.createElement('main');
196
+ hub.id = 'sessionHub';
197
+ hub.className = 'session-hub';
198
+ document.body.prepend(hub);
199
+ }
200
+ hub.setAttribute('aria-busy', 'true');
201
+ hub.innerHTML = `<div class="session-hub-shell">
202
+ <header class="session-hub-header"><div><p class="session-hub-kicker">CLI-JAW</p><h1>${escapeHtml(t('sessionHub.title'))}</h1></div></header>
203
+ <div class="session-grid session-grid-loading" aria-hidden="true">
204
+ <div class="session-card-skeleton"><span></span><span></span><span></span></div>
205
+ <div class="session-card-skeleton"><span></span><span></span><span></span></div>
206
+ </div>
207
+ </div>`;
208
+ }
209
+
210
+ function renderHub(response: SessionListResponse): void {
211
+ document.body.classList.add('session-hub-mode');
212
+ let hub = document.getElementById('sessionHub');
213
+ if (!hub) {
214
+ hub = document.createElement('main');
215
+ hub.id = 'sessionHub';
216
+ hub.className = 'session-hub';
217
+ document.body.prepend(hub);
218
+ }
219
+ hub.removeAttribute('aria-busy');
220
+ const onlyDefault = response.sessions.length === 1 && response.sessions[0]?.id === 'default';
221
+ hub.innerHTML = `<div class="session-hub-shell">
222
+ <header class="session-hub-header">
223
+ <div><p class="session-hub-kicker">CLI-JAW</p><h1>${escapeHtml(t('sessionHub.title'))}</h1></div>
224
+ <p>${escapeHtml(t('sessionHub.description'))}</p>
225
+ </header>
226
+ ${onlyDefault ? `<section class="session-hub-empty" aria-labelledby="sessionHubEmptyTitle">
227
+ <h2 id="sessionHubEmptyTitle">${escapeHtml(t('sessionHub.empty.title'))}</h2>
228
+ <p>${escapeHtml(t('sessionHub.empty.description'))}</p>
229
+ <a href="${escapeHtml(sessionHref(0))}#settings">${escapeHtml(t('sessionHub.empty.action'))}</a>
230
+ </section>` : ''}
231
+ <section class="session-grid" aria-label="${escapeHtml(t('sessionHub.listAria'))}">
232
+ ${response.sessions.map(session => renderSessionCard(session, response.active)).join('')}
233
+ </section>
234
+ <p class="session-hub-error" id="sessionHubError" role="status" aria-live="polite"></p>
235
+ </div>`;
236
+ if (!hub.dataset['eventsBound']) {
237
+ hub.addEventListener('click', handleHubClick);
238
+ hub.dataset['eventsBound'] = 'true';
239
+ }
240
+ }
241
+
242
+ async function handleHubClick(event: Event): Promise<void> {
243
+ const button = (event.target as HTMLElement | null)?.closest<HTMLButtonElement>('[data-delete-session]');
244
+ // aria-disabled keeps the control focusable so its reason can be announced,
245
+ // so activation has to be suppressed here rather than by the DOM.
246
+ if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return;
247
+ event.preventDefault();
248
+ const sessionId = button.dataset['deleteSession'];
249
+ if (!sessionId || !window.confirm(t('sessionHub.deleteConfirm'))) return;
250
+ button.disabled = true;
251
+ const token = await getAuthToken();
252
+ try {
253
+ const response = await fetch(`${API_BASE}/api/chat-sessions/${encodeURIComponent(sessionId)}`, {
254
+ method: 'DELETE',
255
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
256
+ });
257
+ if (!response.ok) throw new Error(String(response.status));
258
+ const refreshed = await api<SessionListResponse>('/api/chat-sessions');
259
+ if (refreshed) {
260
+ latestList = refreshed;
261
+ renderHub(refreshed);
262
+ }
263
+ } catch {
264
+ const error = document.getElementById('sessionHubError');
265
+ if (error) error.textContent = t('sessionHub.deleteFailed');
266
+ button.disabled = false;
267
+ }
268
+ }
269
+
270
+ function renderSessionViewChrome(): void {
271
+ const viewed = viewState.viewed;
272
+ if (!viewed) return;
273
+ document.title = `${displayLabel(viewed)} · CLI-JAW`;
274
+ const header = document.querySelector<HTMLElement>('.chat-header > span');
275
+ if (header) header.insertAdjacentHTML('beforeend', `<span class="session-view-label">/${viewed.seq} · ${escapeHtml(displayLabel(viewed))}</span>`);
276
+ if (!canSendFromCurrentView()) showReadOnlySwitchAffordance();
277
+ }
278
+
279
+ export function showReadOnlySwitchAffordance(): void {
280
+ if (!viewState.enabled || !viewState.viewed) return;
281
+ let notice = document.getElementById('sessionReadonlyNotice');
282
+ if (!notice) {
283
+ notice = document.createElement('div');
284
+ notice.id = 'sessionReadonlyNotice';
285
+ notice.className = 'session-readonly-notice';
286
+ notice.setAttribute('role', 'status');
287
+ document.querySelector('.chat-input-area')?.before(notice);
288
+ }
289
+ const remote = viewState.viewed.remoteKey != null;
290
+ const command = remote ? '/sessions' : `/switch ${viewState.viewed.seq}`;
291
+ const actionLabel = remote
292
+ ? t('sessionReadonly.openSessions')
293
+ : t('sessionReadonly.switch', { seq: viewState.viewed.seq });
294
+ notice.innerHTML = `<span>${escapeHtml(t(remote ? 'sessionReadonly.remote' : 'sessionReadonly.message'))}</span>
295
+ <button type="button">${escapeHtml(actionLabel)}</button>`;
296
+ notice.querySelector('button')?.addEventListener('click', () => {
297
+ const input = document.getElementById('chatInput') as HTMLTextAreaElement | null;
298
+ if (!input) return;
299
+ input.value = command;
300
+ input.dispatchEvent(new Event('input', { bubbles: true }));
301
+ input.dispatchEvent(new Event('cmd-execute', { bubbles: true }));
302
+ });
303
+ }
304
+
305
+ export function handleSessionListBroadcast(message: { deleted?: { id?: unknown; seq?: unknown } }): boolean {
306
+ if (!viewState.enabled || viewState.mode !== 'session' || !viewState.viewed) return false;
307
+ const deleted = message.deleted;
308
+ if (!deleted) return false;
309
+ if (deleted.id !== viewState.viewed.id && deleted.seq !== viewState.viewed.seq) return false;
310
+ window.location.assign(hubHref());
311
+ return true;
312
+ }
313
+
314
+ export async function initializeSessionView(options: { cancelRecording?: () => void } = {}): Promise<SessionViewMode> {
315
+ cancelRecordingOnReadonly = options.cancelRecording || null;
316
+ const response = await api<SessionListResponse>('/api/chat-sessions');
317
+ if (!response) {
318
+ if (parsedSeq(window.location.pathname) !== null) {
319
+ Object.assign(viewState, { enabled: true, mode: 'redirect', activeId: null, viewed: null });
320
+ window.location.replace(hubHref());
321
+ return 'redirect';
322
+ }
323
+ return 'off';
324
+ }
325
+ const mode = configureSessionView(response);
326
+ if (mode === 'hub') {
327
+ renderHubSkeleton();
328
+ const refreshed = await api<SessionListResponse>('/api/chat-sessions');
329
+ renderHub(refreshed || response);
330
+ }
331
+ if (mode === 'session') renderSessionViewChrome();
332
+ if (mode === 'redirect') window.location.replace(hubHref());
333
+ return mode;
334
+ }
@@ -12,7 +12,7 @@ type QuotaSetupHint = {
12
12
  };
13
13
 
14
14
  export const QUOTA_HIDDEN_CLIS = new Set(['ai-e', 'codex-app']);
15
- export const SIDEBAR_HIDDEN_CLIS = new Set(['ai-e', 'claude-e', 'jwc', 'pi', 'codex-app', 'gemini']);
15
+ export const SIDEBAR_HIDDEN_CLIS = new Set(['ai-e', 'claude-e', 'jwc', 'pi', 'gemini']);
16
16
  export const QUOTA_CUSTOM_MSG: Record<string, string> = {};
17
17
 
18
18
  export const QUOTA_SETUP_HINTS: Record<string, QuotaSetupHint> = {
@@ -5,7 +5,7 @@ import { t } from './i18n.js';
5
5
  import { state } from '../state.js';
6
6
  import { ICONS } from '../icons.js';
7
7
  import { providerIcon, providerLabel } from '../provider-icons.js';
8
- import { resolveQuotaWindowDisplay, type QuotaEntry } from './settings-types.js';
8
+ import { describeCliProbe, resolveQuotaWindowDisplay, type CliStatusInfo, type QuotaEntry } from './settings-types.js';
9
9
  import {
10
10
  buildAccountParts,
11
11
  normalizeQuotaWindowLabel,
@@ -184,7 +184,7 @@ export function setCliStatusInterval(value: string): void {
184
184
 
185
185
  function scheduleEmbeddedQuotaRetry(
186
186
  seq: number,
187
- cliStatus: Record<string, { available: boolean }>,
187
+ cliStatus: Record<string, CliStatusInfo>,
188
188
  cachedQuota: Record<string, QuotaEntry> | null | undefined,
189
189
  ): void {
190
190
  if (!isEmbeddedPreviewFrame()) return;
@@ -201,7 +201,7 @@ function scheduleEmbeddedQuotaRetry(
201
201
 
202
202
  async function fetchAndRenderQuota(
203
203
  seq: number,
204
- cliStatus: Record<string, { available: boolean }>,
204
+ cliStatus: Record<string, CliStatusInfo>,
205
205
  cachedQuota: Record<string, QuotaEntry> | null | undefined,
206
206
  ): Promise<boolean> {
207
207
  const quota = await api<Record<string, QuotaEntry>>('/api/quota');
@@ -219,7 +219,7 @@ async function fetchAndRenderQuota(
219
219
  function scheduleQuotaFetch(
220
220
  force: boolean,
221
221
  seq: number,
222
- cliStatus: Record<string, { available: boolean }>,
222
+ cliStatus: Record<string, CliStatusInfo>,
223
223
  cachedQuota: Record<string, QuotaEntry> | null | undefined,
224
224
  ): Promise<boolean> | void {
225
225
  const run = () => { void fetchAndRenderQuota(seq, cliStatus, cachedQuota).catch(() => {}); };
@@ -243,7 +243,7 @@ export async function loadCliStatus(force = false): Promise<void> {
243
243
  const interval = readCliStatusInterval();
244
244
  if (!force && state.cliStatusCache && interval > 0 && (Date.now() - state.cliStatusTs) < interval * 1000) {
245
245
  renderCliStatus({
246
- cliStatus: (state.cliStatusCache as Record<string, unknown>)?.['cliStatus'] as Record<string, { available: boolean }> | null,
246
+ cliStatus: (state.cliStatusCache as Record<string, unknown>)?.['cliStatus'] as Record<string, CliStatusInfo> | null,
247
247
  quota: (state.cliStatusCache as Record<string, unknown>)?.['quota'] as Record<string, QuotaEntry> | null,
248
248
  });
249
249
  return;
@@ -252,7 +252,7 @@ export async function loadCliStatus(force = false): Promise<void> {
252
252
  const cachedQuota = (state.cliStatusCache as Record<string, unknown> | null)?.['quota'] as Record<string, QuotaEntry> | null | undefined;
253
253
  if (el && !cachedQuota) el.innerHTML = '<div style="color:var(--text-dim);font-size:11px">Loading...</div>';
254
254
 
255
- const cliStatus = await api<Record<string, { available: boolean }>>('/api/cli-status');
255
+ const cliStatus = await api<Record<string, CliStatusInfo>>('/api/cli-status');
256
256
  if (seq !== cliStatusLoadSeq) return;
257
257
  if (!cliStatus || typeof cliStatus !== 'object') {
258
258
  if (el) el.innerHTML = '<div style="color:var(--text-dim);font-size:11px">Failed to load CLI status</div>';
@@ -278,7 +278,7 @@ export async function loadCliStatus(force = false): Promise<void> {
278
278
  await cliStatusLoadInFlight;
279
279
  }
280
280
 
281
- function renderCliStatus(data: { cliStatus: Record<string, { available: boolean }> | null; quota: Record<string, QuotaEntry> | null }): void {
281
+ function renderCliStatus(data: { cliStatus: Record<string, CliStatusInfo> | null; quota: Record<string, QuotaEntry> | null }): void {
282
282
  const { cliStatus, quota } = data;
283
283
  const el = document.getElementById('cliStatusList');
284
284
 
@@ -306,9 +306,14 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
306
306
 
307
307
  for (const [name, info] of Object.entries(cliStatus)) {
308
308
  if (SIDEBAR_HIDDEN_CLIS.has(name)) continue;
309
- const q = quota?.[name];
309
+ const probeDescription = describeCliProbe(info);
310
+ const checking = probeDescription === 'checking';
311
+ const capabilityFailed = probeDescription === 'capability-failed';
312
+ const q = checking ? undefined : quota?.[name];
310
313
  let dotClass: string;
311
- if (!info.available) {
314
+ if (checking) {
315
+ dotClass = 'checking';
316
+ } else if (!info.available || capabilityFailed) {
312
317
  dotClass = 'missing';
313
318
  } else if (!q || q.error) {
314
319
  dotClass = 'ok';
@@ -330,7 +335,7 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
330
335
  }
331
336
 
332
337
  let authHint = '';
333
- if (!info.available || dotClass === 'warn') {
338
+ if (!checking && (!info.available || dotClass === 'warn')) {
334
339
  const hint = AUTH_HINTS[name];
335
340
  if (hint) {
336
341
  const isNotInstalled = !info.available;
@@ -398,6 +403,14 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
398
403
  ? `<span style="margin-left:auto;font-size:10px;color:var(--text-dim);white-space:nowrap">$${q.billing.usedUsd.toFixed(1)}/$${q.billing.limitUsd}</span>`
399
404
  : '';
400
405
 
406
+ const probeLine = checking
407
+ ? '<div role="status" aria-live="polite" style="font-size:10px;color:var(--text-dim);margin:2px 0 0 16px">상태 확인 중</div>'
408
+ : capabilityFailed
409
+ ? `<div role="status" style="font-size:10px;color:var(--error);margin:2px 0 0 16px">설치됨, app-server 사용 불가${info.reason ? ` · ${escapeHtml(info.reason)}` : ''}</div>`
410
+ : probeDescription === 'stale'
411
+ ? '<div role="status" style="font-size:10px;color:var(--text-dim);margin:2px 0 0 16px">이전 상태 표시 중 · 새로 확인 중</div>'
412
+ : '';
413
+
401
414
  html += `
402
415
  <div class="settings-group" style="margin-bottom:6px;padding:8px 10px">
403
416
  <div class="cli-status-row" style="display:flex;align-items:center">
@@ -406,6 +419,7 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
406
419
  <span class="cli-name" style="font-weight:600">${escapeHtml(providerLabel(name))}${quotaHelpMark}</span>${name === 'copilot' ? `<button id="copilotKeychainBtn" style="font-size:9px;margin-left:6px;padding:1px 5px;background:var(--border);color:var(--text-dim);border:1px solid var(--text-dim);border-radius:3px;cursor:pointer;vertical-align:middle;line-height:1" title="${t('copilot.keychainHint')}">${ICONS.key}</button>` : ''}${billingLabel}
407
420
  </div>
408
421
  ${accountLine}
422
+ ${probeLine}
409
423
  ${authHint}
410
424
  ${windowsHtml}
411
425
  </div>
@@ -416,7 +430,7 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
416
430
 
417
431
  const allEntries = Object.entries(cliStatus);
418
432
  const hasReadyCli = allEntries.some(([name, info]) => {
419
- if (!info.available) return false;
433
+ if (describeCliProbe(info) !== 'ready') return false;
420
434
  const q = quota?.[name];
421
435
  return !q || q.authenticated !== false;
422
436
  });
@@ -4,8 +4,8 @@ import type { CliEntry } from '../constants.js';
4
4
  import { escapeHtml } from '../render.js';
5
5
  import { syncStoredLocale } from '../locale.js';
6
6
  import { t } from './i18n.js';
7
- import { api, apiJson, apiFire } from '../api.js';
8
- import type { PerCliConfig, SettingsData } from './settings-types.js';
7
+ import { API_BASE, api, apiJson, apiFire, getAuthToken } from '../api.js';
8
+ import { shouldHydrateRuntimeMigrationResponse, type PerCliConfig, type SettingsData } from './settings-types.js';
9
9
  import { setCachedPi, syncPiProviderDropdown, syncPiModelDropdown, piDiscoveredModels } from './pi-settings.js';
10
10
  import { initSttSettings } from './settings-stt.js';
11
11
  import { loadTelegramSettings } from './settings-telegram.js';
@@ -20,6 +20,70 @@ import { loadHeaderGitStatus, refreshHeaderGitStatusFromSettingsChange } from '.
20
20
 
21
21
  let activeSettingsSave: Promise<void> | null = null;
22
22
 
23
+ type MigrationResponse = SettingsData | { ok?: boolean; data?: SettingsData; settings?: SettingsData };
24
+
25
+ function unwrapMigrationSettings(payload: MigrationResponse | null): SettingsData | null {
26
+ if (!payload || typeof payload !== 'object') return null;
27
+ if ('settings' in payload && payload.settings) return payload.settings;
28
+ if ('data' in payload && payload.data) return payload.data;
29
+ return payload as SettingsData;
30
+ }
31
+
32
+ async function resolvePendingRuntimeMigration(snapshot: SettingsData): Promise<SettingsData> {
33
+ if (snapshot.runtimeDefaultMigration?.state !== 'pending') return snapshot;
34
+ const action = window.confirm('신규 설치의 기본 런타임이 Codex App으로 변경되었습니다. 지금 Codex App을 사용하시겠습니까?')
35
+ ? 'accept'
36
+ : 'keep';
37
+ try {
38
+ const token = await getAuthToken();
39
+ const response = await fetch(`${API_BASE}/api/settings/runtime-default-migration`, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
44
+ },
45
+ body: JSON.stringify({ action }),
46
+ });
47
+ const payload = await response.json().catch(() => null) as MigrationResponse | null;
48
+ if (shouldHydrateRuntimeMigrationResponse(response.status)) {
49
+ return unwrapMigrationSettings(payload) ?? snapshot;
50
+ }
51
+ window.alert(`런타임 선택을 저장하지 못했습니다 (${response.status}). 다음 설정 진입 때 다시 안내합니다.`);
52
+ } catch {
53
+ window.alert('런타임 선택을 저장하지 못했습니다. 다음 설정 진입 때 다시 안내합니다.');
54
+ }
55
+ return snapshot;
56
+ }
57
+
58
+ async function resolvePendingMultiSessionMigration(snapshot: SettingsData): Promise<SettingsData> {
59
+ if (snapshot.multiSessionDefaultMigration?.state !== 'pending') return snapshot;
60
+ // The prompt names both halves because accepting applies both: sessions on, and a
61
+ // second lane so a second tab does not queue behind the first. Turning it on without
62
+ // the lane would change what the screen shows and nothing about how it runs.
63
+ const action = window.confirm(
64
+ '이제 대화 세션을 여러 개 열 수 있습니다. 켜면 동시 실행도 2로 올라가서, 두 번째 세션이 첫 번째가 끝나기를 기다리지 않습니다. 지금 켤까요?',
65
+ ) ? 'accept' : 'keep';
66
+ try {
67
+ const token = await getAuthToken();
68
+ const response = await fetch(`${API_BASE}/api/settings/multi-session-default-migration`, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
73
+ },
74
+ body: JSON.stringify({ action }),
75
+ });
76
+ const payload = await response.json().catch(() => null) as MigrationResponse | null;
77
+ if (shouldHydrateRuntimeMigrationResponse(response.status)) {
78
+ return unwrapMigrationSettings(payload) ?? snapshot;
79
+ }
80
+ window.alert(`세션 선택을 저장하지 못했습니다 (${response.status}). 다음 설정 진입 때 다시 안내합니다.`);
81
+ } catch {
82
+ window.alert('세션 선택을 저장하지 못했습니다. 다음 설정 진입 때 다시 안내합니다.');
83
+ }
84
+ return snapshot;
85
+ }
86
+
23
87
  function setHeaderCli(cli: string): void {
24
88
  const hdr = document.getElementById('headerCli');
25
89
  if (!hdr) return;
@@ -362,8 +426,13 @@ function syncCliProviderControl(settings: SettingsData | null, cli: string): str
362
426
 
363
427
  export async function loadSettings(): Promise<void> {
364
428
  await loadCliRegistry();
365
- const s = await api<SettingsData>('/api/settings');
429
+ let s = await api<SettingsData>('/api/settings');
366
430
  if (!s) return;
431
+ s = await resolvePendingRuntimeMigration(s);
432
+ // Runtime first, then sessions: which CLI runs is the earlier decision, and a v1
433
+ // install has both pending at once. The second call takes the snapshot the first
434
+ // returned — passing the pre-call one would write back over the answer just given.
435
+ s = await resolvePendingMultiSessionMigration(s);
367
436
  syncStoredLocale(s.locale ?? '');
368
437
  syncCliOptionSelects(s);
369
438
  setCachedPi(s.pi);
@@ -46,6 +46,35 @@ export interface QuotaEntry {
46
46
  }
47
47
  export interface PiProfileView { id: string; label: string; mode: string; endpoint: string; apiKind?: string; apiKeySet?: boolean; apiKeyLast4?: string; model: string; }
48
48
  export interface PiSettingsView { defaultProfileId: string; profiles: PiProfileView[]; discoveredModels?: Record<string, string[]>; }
49
+ export interface RuntimeDefaultMigration {
50
+ id: 'codex-app-default-v2';
51
+ state: 'pending' | 'accepted' | 'kept' | 'already-codex-app';
52
+ fromCli: string;
53
+ toCli: 'codex-app';
54
+ }
55
+ export interface MultiSessionDefaultMigration {
56
+ id: 'multi-session-default-v3';
57
+ state: 'pending' | 'accepted' | 'kept' | 'already-enabled';
58
+ }
59
+ export interface CliStatusInfo {
60
+ available: boolean | null;
61
+ binaryInstalled: boolean | null;
62
+ capabilityReady: boolean | null;
63
+ authenticated: boolean | null;
64
+ path: string | null;
65
+ source: string;
66
+ probeState: 'checking' | 'fresh' | 'stale';
67
+ reason?: string;
68
+ }
69
+ export function describeCliProbe(info: CliStatusInfo): 'checking' | 'capability-failed' | 'stale' | 'ready' | 'unavailable' {
70
+ if (info.probeState === 'checking') return 'checking';
71
+ if (info.binaryInstalled === true && info.capabilityReady === false) return 'capability-failed';
72
+ if (info.probeState === 'stale') return 'stale';
73
+ return info.available === true && info.capabilityReady !== false ? 'ready' : 'unavailable';
74
+ }
75
+ export function shouldHydrateRuntimeMigrationResponse(status: number): boolean {
76
+ return status >= 200 && status < 300 || status === 409;
77
+ }
49
78
  export interface SettingsData {
50
79
  cli: string; workingDir: string; permissions: string; locale?: string; showReasoning?: boolean;
51
80
  perCli?: Record<string, PerCliConfig>;
@@ -59,4 +88,7 @@ export interface SettingsData {
59
88
  projectDirs?: string[] | null;
60
89
  stt?: { engine?: string; geminiKeySet?: boolean; geminiKeyLast4?: string; geminiModel?: string; whisperModel?: string; openaiKeySet?: boolean; openaiKeyLast4?: string };
61
90
  pi?: PiSettingsView;
91
+ settingsSchemaVersion?: number;
92
+ runtimeDefaultMigration?: RuntimeDefaultMigration | null;
93
+ multiSessionDefaultMigration?: MultiSessionDefaultMigration | null;
62
94
  }
@@ -3,6 +3,7 @@ import { api } from '../api.js';
3
3
  import { ICONS } from '../icons.js';
4
4
  import { t } from './i18n.js';
5
5
  import { showSkeleton, removeSkeleton } from './chat-messages.js';
6
+ import { withCurrentSessionQuery } from './session-hub.js';
6
7
 
7
8
  export function setStatus(s: string): void {
8
9
  const badge = document.getElementById('statusBadge');
@@ -47,7 +48,7 @@ export function updateStatMsgs(count: number): void {
47
48
  }
48
49
 
49
50
  export async function loadStats(): Promise<void> {
50
- const result = await api<{ count: number }>('/api/messages/count');
51
+ const result = await api<{ count: number }>(withCurrentSessionQuery('/api/messages/count'));
51
52
  if (!result) return;
52
53
  updateStatMsgs(result.count);
53
54
  }
package/public/js/main.ts CHANGED
@@ -95,6 +95,8 @@ import { initAttentionBadge } from './features/attention-badge.js';
95
95
  import { initHelpDialog } from './features/help-dialog.js';
96
96
  import { initChatSearch, toggleChatSearch, closeChatSearch } from './features/chat-search.js';
97
97
  import { initMediaLightbox } from './features/media-lightbox.js';
98
+ import { currentEventScope, initializeSessionView } from './features/session-hub.js';
99
+ import { setEventChannelScopeProvider } from './event-channel.js';
98
100
 
99
101
  function isLocalPreviewOrigin(origin: string): boolean {
100
102
  if (origin === window.location.origin) return true;
@@ -559,9 +561,23 @@ async function bootstrap(): Promise<void> {
559
561
  ensurePreviewCapabilityListener();
560
562
  ensurePreviewInsertTextListener();
561
563
  await initI18n();
564
+ const sessionMode = await initializeSessionView({ cancelRecording });
565
+ if (sessionMode === 'redirect') return;
566
+ // Must run before either connect() below: the channel reads this on every
567
+ // (re)connect, and an unregistered provider opens an unfiltered stream that
568
+ // would carry other sessions' output into this tab.
569
+ setEventChannelScopeProvider(currentEventScope);
562
570
  const langSel = document.getElementById('langSelect') as HTMLSelectElement | null;
563
571
  if (langSel) langSel.value = getLang();
564
572
  initHelpDialog();
573
+ if (sessionMode === 'hub') {
574
+ // The hub has no chat surface, but the badge/help listeners are part of
575
+ // the documented boot order (AB-003 / HD-002): they must be installed
576
+ // before connect() regardless of which view is rendering.
577
+ initAttentionBadge();
578
+ connect();
579
+ return;
580
+ }
565
581
  initChatSearch();
566
582
  initMediaLightbox();
567
583
  document.getElementById('chatSearchTrigger')?.addEventListener('click', toggleChatSearch);
@@ -590,6 +606,10 @@ async function bootstrap(): Promise<void> {
590
606
  await initAvatar();
591
607
  initMsgCopy();
592
608
  initGestures();
609
+ if (window.location.hash === '#settings') {
610
+ const settingsTab = document.getElementById('tabBtnSettings');
611
+ if (settingsTab) switchTab('settings', settingsTab);
612
+ }
593
613
  try { sessionStorage.removeItem(STALE_BUNDLE_RELOAD_KEY); } catch {}
594
614
 
595
615
  // Phase 127-F2: prewarm Mermaid at idle so first diagram renders fast.