@cloudcli-ai/cloudcli 1.37.3 → 1.37.4

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 (295) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-N423BO3Z-CJntskwA.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-CIfH4uMN.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-_IpEkukg.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-BttxrpgZ.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-g51dSRr1.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-D_-LbQUa.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-BtlHzwaO.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-UzkpvpAT.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-BKwx5Gag.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-D_KEBH5f.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rK2DGyto.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CeBl_N9i.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-BiQLWVIr.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-C1knVGV5.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-CtKbJkvF.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-DjX8y40Z.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-DM6YeRba.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-wZJ-vy5B.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-BzP0ejkH.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BTL_j23J.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-Cf1Norgu.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DPiUEKr-.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-CTcTpiz5.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-DPMSdYm0.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-7yvD6uiF.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-CfgeDoV6.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-B27ng5zu.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-CcC6b_7t.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-BX5kPGrO.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
  34. package/dist/assets/index-CdjIFxM2.js +5004 -0
  35. package/dist/assets/index-DgafhGTF.css +32 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-CUP-SlN4.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-BTTDhOpX.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-ARXcc61m.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-BieMpqMA.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-61vxjRnP.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-nhnwkfQx.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-Cm6x5vT1.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-CcYBq_9H.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CfvIjfzo.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-SgYoWPNb.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-D2Mu5E_f.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-Azwe_k4Q.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-DN1Ju3PA.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-Bj4qC34S.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-Co4PdklG.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  51. package/dist/assets/{stateDiagram-OKZ733FA-nOvL9dRC.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-UEYNNEHI-DNv8ppw7.js → stateDiagram-v2-UEYNNEHI-vAaE4zNo.js} +1 -1
  53. package/dist/assets/{swimlanes-SLNWSIFB-CCK3wMid.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  55. package/dist/assets/{timeline-definition-Z64GVDOM-BC29RNFm.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  56. package/dist/assets/{vennDiagram-T6HMQDX7-pZlZCtEo.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-T6FBY63Y-De8k2-zM.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  58. package/dist/assets/{xychartDiagram-ELKLHX3M-Bk8ykH0w.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/server/index.js +4 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/modules/agent/agent.module.js +9 -2
  63. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.routes.js +175 -128
  65. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  66. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  67. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  68. package/dist-server/server/modules/agent/tests/agent.routes.test.js +18 -1
  69. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  70. package/dist-server/server/modules/database/migrations.js +13 -0
  71. package/dist-server/server/modules/database/migrations.js.map +1 -1
  72. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  73. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  74. package/dist-server/server/modules/database/repositories/sessions.db.js +7 -0
  75. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  76. package/dist-server/server/modules/database/schema.js +7 -0
  77. package/dist-server/server/modules/database/schema.js.map +1 -1
  78. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  79. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  80. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  81. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  82. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  83. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  84. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  85. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  86. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  87. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  88. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  89. package/dist-server/server/modules/git/git.routes.js +92 -5
  90. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  91. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  92. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  93. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  94. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  95. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  96. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  97. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  98. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  99. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  100. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  101. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  102. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  103. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  104. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  105. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +42 -8
  106. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  107. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +6 -16
  108. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  109. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +305 -16
  110. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  111. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  112. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  113. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +686 -77
  114. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  115. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +31 -0
  116. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  117. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +11 -4
  118. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  119. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +57 -0
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  121. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  122. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  123. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -2
  124. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  125. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +16 -3
  126. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  127. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  128. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  130. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/provider.routes.js +110 -2
  132. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  133. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  134. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  135. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  136. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +9 -1
  138. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  139. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  140. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  141. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  142. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  143. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  144. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  145. package/dist-server/server/modules/providers/services/session-history-cache.service.js +4 -0
  146. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -1
  147. package/dist-server/server/modules/providers/services/sessions.service.js +73 -3
  148. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  149. package/dist-server/server/modules/providers/tests/claude-auth.test.js +131 -0
  150. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  151. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  152. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  153. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  154. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  155. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  156. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  157. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  158. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  159. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1368 -12
  160. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  161. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  162. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  163. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  164. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  165. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +32 -0
  166. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -1
  167. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +50 -0
  168. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  169. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  170. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  171. package/dist-server/server/modules/providers/tests/opencode-models.test.js +79 -0
  172. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  173. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  174. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  175. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  176. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  177. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  178. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  179. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  180. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  181. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +19 -0
  182. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -1
  183. package/dist-server/server/modules/providers/tests/sessions.service.test.js +175 -1
  184. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  185. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +13 -1
  186. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -1
  187. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +36 -1
  188. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -1
  189. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +27 -5
  190. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  191. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +38 -1
  192. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  193. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +230 -20
  194. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  195. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  196. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  197. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  198. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  199. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +420 -0
  200. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  201. package/dist-server/server/shared/message-unification.js +1 -1
  202. package/dist-server/server/shared/message-unification.js.map +1 -1
  203. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  204. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  205. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  206. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  207. package/dist-server/server/shared/utils.js +126 -0
  208. package/dist-server/server/shared/utils.js.map +1 -1
  209. package/electron/desktopWindow.js +6 -0
  210. package/electron/viewHost.js +25 -2
  211. package/package.json +4 -4
  212. package/server/index.ts +5 -1
  213. package/server/modules/agent/agent.module.ts +11 -1
  214. package/server/modules/agent/agent.routes.ts +198 -140
  215. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  216. package/server/modules/agent/tests/agent.routes.test.ts +18 -1
  217. package/server/modules/database/migrations.ts +14 -0
  218. package/server/modules/database/repositories/provider-models.ts +79 -6
  219. package/server/modules/database/repositories/sessions.db.ts +8 -0
  220. package/server/modules/database/schema.ts +7 -0
  221. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  222. package/server/modules/file-tree/file-tree.module.ts +2 -1
  223. package/server/modules/file-tree/file-tree.service.ts +49 -7
  224. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  225. package/server/modules/git/git-branch-diff.service.ts +204 -0
  226. package/server/modules/git/git-branch.service.ts +2 -12
  227. package/server/modules/git/git.routes.ts +100 -6
  228. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  229. package/server/modules/projects/project-clone.routes.ts +142 -0
  230. package/server/modules/projects/projects.routes.ts +16 -81
  231. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  232. package/server/modules/projects/services/project-clone.service.ts +102 -34
  233. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  234. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  235. package/server/modules/providers/list/claude/claude-auth.provider.ts +60 -10
  236. package/server/modules/providers/list/claude/claude-models.provider.ts +10 -22
  237. package/server/modules/providers/list/claude/claude-runtime.provider.js +322 -15
  238. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  239. package/server/modules/providers/list/claude/claude-sessions.provider.ts +796 -88
  240. package/server/modules/providers/list/codex/codex-models.provider.ts +31 -0
  241. package/server/modules/providers/list/codex/codex-runtime.provider.ts +12 -4
  242. package/server/modules/providers/list/codex/codex-sessions.provider.ts +63 -0
  243. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  244. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +9 -2
  245. package/server/modules/providers/list/opencode/opencode-models.provider.ts +18 -3
  246. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  247. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  248. package/server/modules/providers/provider.routes.ts +131 -2
  249. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  250. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  251. package/server/modules/providers/services/provider-capabilities.service.ts +17 -1
  252. package/server/modules/providers/services/provider-models.service.ts +71 -4
  253. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  254. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  255. package/server/modules/providers/services/session-history-cache.service.ts +13 -2
  256. package/server/modules/providers/services/sessions.service.ts +108 -8
  257. package/server/modules/providers/tests/claude-auth.test.ts +150 -0
  258. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  259. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  260. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  261. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  262. package/server/modules/providers/tests/claude-sessions.test.ts +1630 -12
  263. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  264. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  265. package/server/modules/providers/tests/codex-runtime.test.ts +36 -0
  266. package/server/modules/providers/tests/codex-sessions.test.ts +59 -0
  267. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  268. package/server/modules/providers/tests/opencode-models.test.ts +100 -0
  269. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  270. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  271. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  272. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  273. package/server/modules/providers/tests/session-history-cache.service.test.ts +22 -0
  274. package/server/modules/providers/tests/sessions.service.test.ts +237 -1
  275. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +14 -1
  276. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +45 -1
  277. package/server/modules/websocket/README.md +4 -2
  278. package/server/modules/websocket/services/chat-run-registry.service.ts +29 -5
  279. package/server/modules/websocket/services/chat-websocket.service.ts +58 -1
  280. package/server/modules/websocket/services/shell-websocket.service.ts +269 -22
  281. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  282. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  283. package/server/modules/websocket/tests/shell-websocket.service.test.ts +521 -0
  284. package/server/shared/interfaces.ts +29 -0
  285. package/server/shared/message-unification.ts +1 -1
  286. package/server/shared/tests/read-only-roots.test.ts +139 -0
  287. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  288. package/server/shared/types.ts +261 -7
  289. package/server/shared/utils.ts +136 -0
  290. package/dist/assets/channel-C5xU9jMw.js +0 -1
  291. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +0 -1
  292. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +0 -1
  293. package/dist/assets/index-BTDHQ6Id.css +0 -32
  294. package/dist/assets/index-sFfz8iBR.js +0 -4931
  295. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +0 -8
@@ -36,9 +36,14 @@ import {
36
36
  notifyRunStopped,
37
37
  notifyUserIfEnabled
38
38
  } from '@/modules/notifications/index.js';
39
+ import { sessionHistoryCache } from '@/modules/providers/services/session-history-cache.service.js';
39
40
  import { createCompleteMessage, createNormalizedMessage } from '@/shared/utils.js';
40
41
 
41
42
  const activeSessions = new Map();
43
+ // Outstanding background tasks per live session, keyed like activeSessions. An
44
+ // entry lives exactly as long as the map entry it shadows: cleared when the
45
+ // session is removed, and reset when a newer run takes the key over.
46
+ const backgroundWork = createBackgroundWorkTracker();
42
47
  const pendingToolApprovals = new Map();
43
48
  // Sessions cancelled via abort-session. The abort handler already sent the
44
49
  // terminal `complete` (aborted: true) to the client, so the run loop must not
@@ -332,6 +337,9 @@ function addSession(sessionId, queryInstance, writer = null, releaseInput = null
332
337
  console.error(`Error interrupting superseded run for session ${sessionId}:`, error?.message || error);
333
338
  });
334
339
  existing.releaseInput?.();
340
+ // Whatever the superseded process had outstanding dies with it and will
341
+ // never report, so the new run starts from an empty task set.
342
+ backgroundWork.clear(sessionId);
335
343
  }
336
344
  const carried = superseding ? null : existing;
337
345
  activeSessions.set(sessionId, {
@@ -342,6 +350,44 @@ function addSession(sessionId, queryInstance, writer = null, releaseInput = null
342
350
  // Re-registered mid-run once the provider session id lands; keep the closer.
343
351
  releaseInput: releaseInput || carried?.releaseInput || null
344
352
  });
353
+ // The history reader reports a background agent as running or stopped by
354
+ // whether this entry exists, and the cached history does not see this map.
355
+ sessionHistoryCache.invalidate(sessionId);
356
+ }
357
+
358
+ /**
359
+ * Lets go of the process an earlier run left holding a session open, so the
360
+ * next turn can take the session over.
361
+ *
362
+ * Closing stdin is not enough on its own: the CLI's wind-down waits for the
363
+ * background agents the process still has going — for up to the ceiling it
364
+ * was started with (BG_WAIT_CEILING_MS) — so an abandoned process with an
365
+ * agent outstanding would run next to the process that replaced it for as
366
+ * long as the agent does, both appending to the same session file. Stop the
367
+ * tasks first, while stdin is still open: the SDK drops control requests
368
+ * written after stdin has ended, which is also why the interrupt a later
369
+ * supersede sends to this process cannot do the job.
370
+ *
371
+ * The stops are not awaited — each request is written to the CLI before this
372
+ * returns, and a reply is only missing when the process has already gone.
373
+ * @param {string} sessionId - Session identifier
374
+ */
375
+ function releaseHeldSession(sessionId) {
376
+ const held = getSession(sessionId);
377
+ if (!held) {
378
+ return;
379
+ }
380
+ for (const task of backgroundWork.outstanding(sessionId)) {
381
+ // A stop that cannot be delivered changes nothing about what happens
382
+ // next: the process is on its way out either way. Guard the synchronous
383
+ // call too, so a failing stop can never skip the release below.
384
+ try {
385
+ Promise.resolve(held.instance?.stopTask?.(task.taskId)).catch(() => {});
386
+ } catch {
387
+ // Same as a rejected stop.
388
+ }
389
+ }
390
+ held.releaseInput?.();
345
391
  }
346
392
 
347
393
  /**
@@ -350,6 +396,11 @@ function addSession(sessionId, queryInstance, writer = null, releaseInput = null
350
396
  */
351
397
  function removeSession(sessionId) {
352
398
  activeSessions.delete(sessionId);
399
+ // No process, no background work: anything still tracked was killed with it.
400
+ backgroundWork.clear(sessionId);
401
+ // See addSession: a page cached while the process was up still says
402
+ // `running` for any agent that never reported back.
403
+ sessionHistoryCache.invalidate(sessionId);
353
404
  }
354
405
 
355
406
  /**
@@ -539,9 +590,12 @@ function extractCumulativeTokenBudget(sdkMessage) {
539
590
  };
540
591
  }
541
592
 
542
- // Tool calls that leave work running past the end of a turn. Bash only counts
543
- // when it is explicitly backgrounded; the rest defer or watch work by nature.
544
- const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate', 'TaskCreate']);
593
+ // Tool calls that leave work running past the end of a turn. Bash and Agent only
594
+ // count when they are backgrounded; the rest defer or watch work by nature.
595
+ // Workflow belongs here rather than in a branch of its own: its input schema has
596
+ // no foreground option at all, so every call returns a task id immediately and
597
+ // reports back in a later turn.
598
+ const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate', 'TaskCreate', 'Workflow']);
545
599
 
546
600
  /**
547
601
  * Detects tool calls that keep working after the turn's `result` arrives.
@@ -549,10 +603,14 @@ const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate',
549
603
  * Only turns that start background work need their CLI process held open; every
550
604
  * other turn can let it exit immediately, as it did before the hold existed.
551
605
  *
606
+ * Used by the providers module's tests, which pin the tool matching directly:
607
+ * the alternative is driving a whole SDK run to observe whether stdin was held,
608
+ * and the cost of getting this wrong is silently killed background work.
609
+ *
552
610
  * @param {Object} sdkMessage - SDK stream message
553
611
  * @returns {boolean} True when the message launches work that outlives the turn
554
612
  */
555
- function startsBackgroundWork(sdkMessage) {
613
+ export function startsBackgroundWork(sdkMessage) {
556
614
  const content = sdkMessage?.message?.content;
557
615
  if (!Array.isArray(content)) {
558
616
  return false;
@@ -565,10 +623,162 @@ function startsBackgroundWork(sdkMessage) {
565
623
  if (block.name === 'Bash') {
566
624
  return block.input?.run_in_background === true;
567
625
  }
626
+ // A backgrounded subagent outlives the turn exactly like a backgrounded
627
+ // Bash does, so the process has to be held open for it to report back.
628
+ // Agents background by default — `run_in_background` is optional and only
629
+ // an explicit `false` opts out — hence `!== false` rather than `=== true`.
630
+ // A foreground agent must stay out of DEFERRED_WORK_TOOLS: it never pushes
631
+ // a follow-up turn, so it would pin the process for the full ceiling.
632
+ if (block.name === 'Agent') {
633
+ return block.input?.run_in_background !== false;
634
+ }
568
635
  return DEFERRED_WORK_TOOLS.has(block.name);
569
636
  });
570
637
  }
571
638
 
639
+ // `task_updated` patch statuses after which a task is gone for good. `pending`,
640
+ // `running` and `paused` are still outstanding; `killed` is what the CLI
641
+ // writes when it stops a task itself (the task notification spells it
642
+ // `stopped`).
643
+ const TERMINAL_TASK_STATUSES = new Set(['completed', 'failed', 'killed']);
644
+
645
+ /**
646
+ * Tracks the background tasks each live session still has outstanding, folded
647
+ * from the `system` task events the SDK stream already carries.
648
+ *
649
+ * `startsBackgroundWork` above only knows that a turn *launched* something
650
+ * lasting; this knows what is still running and which task ids it answers to,
651
+ * which is what the running-sessions list and a stop request need once the
652
+ * turn's `result` has gone out and nothing else remembers the session is busy.
653
+ *
654
+ * Verified against a real query (SDK 0.3.165): `task_started` carries
655
+ * `task_id`, `tool_use_id`, `task_type` and `description` for every agent,
656
+ * workflow and backgrounded command (a foreground Bash emits nothing);
657
+ * `task_notification` settles a task with any status; `task_updated` carries
658
+ * only `task_id` and a patch, and is terminal when the patch's `status` is.
659
+ * Housekeeping tasks the CLI starts on its own have no `tool_use_id` and are
660
+ * not tracked — nothing in the transcript could show them.
661
+ *
662
+ * Exported so the folding can be driven with the four event shapes directly;
663
+ * the runtime keeps one instance keyed like `activeSessions`.
664
+ *
665
+ * @returns {{
666
+ * apply: (sessionKey: string, message: Object) => void,
667
+ * hasOutstanding: (sessionKey: string) => boolean,
668
+ * has: (sessionKey: string, taskId: string) => boolean,
669
+ * outstanding: (sessionKey: string) => Array<import('@/shared/types.js').BackgroundTaskSummary>,
670
+ * clear: (sessionKey: string) => void,
671
+ * list: () => Array<{ sessionId: string, tasks: Array<import('@/shared/types.js').BackgroundTaskSummary> }>
672
+ * }}
673
+ */
674
+ export function createBackgroundWorkTracker() {
675
+ /** @type {Map<string, Map<string, import('@/shared/types.js').BackgroundTaskSummary>>} */
676
+ const sessions = new Map();
677
+ /**
678
+ * Tool-use ids the session's own turns issued. A task started for a call an
679
+ * agent made inside its own transcript — a workflow agent's backgrounded
680
+ * command, say — reaches this stream too, and nothing in the parent
681
+ * transcript could show it; it is kept for stopping but flagged `nested`.
682
+ * @type {Map<string, Set<string>>}
683
+ */
684
+ const ownToolUseIds = new Map();
685
+
686
+ const remove = (sessionKey, taskId) => {
687
+ const tasks = sessions.get(sessionKey);
688
+ if (!tasks) {
689
+ return;
690
+ }
691
+ tasks.delete(taskId);
692
+ if (tasks.size === 0) {
693
+ sessions.delete(sessionKey);
694
+ }
695
+ };
696
+
697
+ return {
698
+ apply(sessionKey, message) {
699
+ if (message?.type === 'assistant' && !message.parent_tool_use_id) {
700
+ const content = message.message?.content;
701
+ if (Array.isArray(content)) {
702
+ for (const block of content) {
703
+ if (block?.type === 'tool_use' && typeof block.id === 'string') {
704
+ let ids = ownToolUseIds.get(sessionKey);
705
+ if (!ids) {
706
+ ids = new Set();
707
+ ownToolUseIds.set(sessionKey, ids);
708
+ }
709
+ ids.add(block.id);
710
+ }
711
+ }
712
+ }
713
+ return;
714
+ }
715
+ if (message?.type !== 'system' || typeof message.task_id !== 'string') {
716
+ return;
717
+ }
718
+ switch (message.subtype) {
719
+ case 'task_started': {
720
+ if (typeof message.tool_use_id !== 'string') {
721
+ return;
722
+ }
723
+ const task = {
724
+ taskId: message.task_id,
725
+ toolUseId: message.tool_use_id,
726
+ taskType: message.task_type,
727
+ description: message.description,
728
+ startedAt: Date.now()
729
+ };
730
+ if (typeof message.workflow_name === 'string') {
731
+ task.workflowName = message.workflow_name;
732
+ }
733
+ if (!ownToolUseIds.get(sessionKey)?.has(message.tool_use_id)) {
734
+ task.nested = true;
735
+ }
736
+ let tasks = sessions.get(sessionKey);
737
+ if (!tasks) {
738
+ tasks = new Map();
739
+ sessions.set(sessionKey, tasks);
740
+ }
741
+ tasks.set(task.taskId, task);
742
+ return;
743
+ }
744
+ case 'task_notification':
745
+ remove(sessionKey, message.task_id);
746
+ return;
747
+ case 'task_updated':
748
+ if (TERMINAL_TASK_STATUSES.has(message.patch?.status)) {
749
+ remove(sessionKey, message.task_id);
750
+ }
751
+ return;
752
+ default:
753
+ }
754
+ },
755
+
756
+ hasOutstanding(sessionKey) {
757
+ return sessions.has(sessionKey);
758
+ },
759
+
760
+ has(sessionKey, taskId) {
761
+ return Boolean(sessions.get(sessionKey)?.has(taskId));
762
+ },
763
+
764
+ outstanding(sessionKey) {
765
+ return Array.from(sessions.get(sessionKey)?.values() ?? []);
766
+ },
767
+
768
+ clear(sessionKey) {
769
+ sessions.delete(sessionKey);
770
+ ownToolUseIds.delete(sessionKey);
771
+ },
772
+
773
+ list() {
774
+ return Array.from(sessions, ([sessionId, tasks]) => ({
775
+ sessionId,
776
+ tasks: Array.from(tasks.values())
777
+ }));
778
+ }
779
+ };
780
+ }
781
+
572
782
  /**
573
783
  * Builds the SDK user messages for one turn.
574
784
  *
@@ -724,6 +934,12 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
724
934
  // Set when a turn starts background work, cleared when the next `result`
725
935
  // arrives — only turns with work still outstanding hold their process open.
726
936
  let backgroundWorkPending = false;
937
+ // Set when the stream reports a task starting during this turn. Task events
938
+ // are the exact word on what is still running, so when the turn produced
939
+ // any, the tracker decides the hold; `startsBackgroundWork` is the fallback
940
+ // for tools that emit none (Monitor, ScheduleWakeup, CronCreate, TaskCreate)
941
+ // and for an SDK that does not report tasks at all.
942
+ let sawTaskEventThisTurn = false;
727
943
  // True while the process is being held open for background work, so a later
728
944
  // `result` can be recognised as that work reporting back.
729
945
  let heldForBackgroundWork = false;
@@ -734,7 +950,7 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
734
950
  // A new turn supersedes any earlier one still holding this session's process
735
951
  // open, so held runs cannot stack up across a conversation.
736
952
  if (sessionKey()) {
737
- getSession(sessionKey())?.releaseInput?.();
953
+ releaseHeldSession(sessionKey());
738
954
  }
739
955
 
740
956
  // Arms (or re-arms) the idle countdown that eventually closes stdin.
@@ -889,10 +1105,13 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
889
1105
  return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
890
1106
  };
891
1107
 
1108
+ // The SDK's own `query`, unless the caller supplies one (tests script the
1109
+ // stream to drive the hold logic below without a CLI process).
1110
+ const createQuery = context.createQuery ?? query;
892
1111
  let heldPrompt = createHeldPromptStream(promptMessages);
893
1112
  releasePromptStream = heldPrompt.release;
894
1113
  try {
895
- queryInstance = query({
1114
+ queryInstance = createQuery({
896
1115
  prompt: heldPrompt.stream,
897
1116
  options: sdkOptions
898
1117
  });
@@ -905,7 +1124,7 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
905
1124
  heldPrompt.release();
906
1125
  heldPrompt = createHeldPromptStream(promptMessages);
907
1126
  releasePromptStream = heldPrompt.release;
908
- queryInstance = query({
1127
+ queryInstance = createQuery({
909
1128
  prompt: heldPrompt.stream,
910
1129
  options: sdkOptions
911
1130
  });
@@ -971,10 +1190,32 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
971
1190
  if (startsBackgroundWork(message)) {
972
1191
  backgroundWorkPending = true;
973
1192
  }
1193
+ if (message.type === 'system' && message.subtype === 'task_started') {
1194
+ sawTaskEventThisTurn = true;
1195
+ }
1196
+ backgroundWork.apply(sessionKey(), message);
1197
+
1198
+ // A task the user stopped gets no follow-up turn from the CLI — only its
1199
+ // `stopped` notification — so when that was the last outstanding task
1200
+ // nothing will ever push the `result` the release below waits for, and
1201
+ // the process would sit until the idle ceiling. Release it here. A
1202
+ // completed task is different: the CLI relays its result in a turn of
1203
+ // its own, which closing stdin now would cut short.
1204
+ if (
1205
+ heldForBackgroundWork
1206
+ && message.type === 'system'
1207
+ && message.subtype === 'task_notification'
1208
+ && message.status === 'stopped'
1209
+ && !backgroundWork.hasOutstanding(sessionKey())
1210
+ ) {
1211
+ heldForBackgroundWork = false;
1212
+ releasePromptStream();
1213
+ }
974
1214
 
975
1215
  if (message.type === 'result') {
976
1216
  // The turn is done as far as the client is concerned.
977
1217
  const abortPending = sessionKey() ? abortedSessionIds.has(sessionKey()) : false;
1218
+ const stillOutstanding = backgroundWork.hasOutstanding(sessionKey());
978
1219
  if (!turnCompleteSent && !abortPending) {
979
1220
  turnCompleteSent = true;
980
1221
  ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
@@ -985,9 +1226,10 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
985
1226
  sessionName: sessionSummary,
986
1227
  stopReason: 'completed'
987
1228
  });
988
- } else if (heldForBackgroundWork && !abortPending) {
1229
+ } else if (heldForBackgroundWork && !abortPending && !stillOutstanding) {
989
1230
  // A result after the turn already reported complete means the work we
990
- // held the process open for has finished and pushed a follow-up turn.
1231
+ // held the process open for has finished and pushed a follow-up turn
1232
+ // — the last of it, when nothing else is still running.
991
1233
  notifyBackgroundWorkCompleted({
992
1234
  userId: ws?.userId || null,
993
1235
  provider: 'claude',
@@ -995,11 +1237,26 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
995
1237
  sessionName: sessionSummary
996
1238
  });
997
1239
  }
998
- if (backgroundWorkPending) {
999
- // Work started during this turn is still running. Hold the process
1000
- // open so it can finish and report back in a follow-up turn; the
1001
- // ceiling is only a backstop for work that never reports.
1002
- backgroundWorkPending = false;
1240
+ // Work started during this turn, or work from an earlier turn that
1241
+ // has not settled yet (a follow-up turn reports one task in while
1242
+ // another is still going), is still running. Hold the process open
1243
+ // so it can finish and report back in a follow-up turn; the ceiling
1244
+ // is only a backstop for work that never reports.
1245
+ //
1246
+ // The release when the last task settles is this same branch on the
1247
+ // follow-up turn the CLI pushes for it, not the settling event
1248
+ // itself: closing stdin at that moment would cut the turn that
1249
+ // relays the task's result.
1250
+ //
1251
+ // When the turn reported its tasks, the tracker is the whole truth: an
1252
+ // Agent call without `run_in_background` is scored as background by
1253
+ // `startsBackgroundWork`, but the CLI runs it in the foreground and
1254
+ // it has settled before this `result` — holding for it kept a process
1255
+ // alive for the full ceiling with nothing outstanding.
1256
+ const holdForTurn = sawTaskEventThisTurn ? stillOutstanding : backgroundWorkPending || stillOutstanding;
1257
+ backgroundWorkPending = false;
1258
+ sawTaskEventThisTurn = false;
1259
+ if (holdForTurn) {
1003
1260
  heldForBackgroundWork = true;
1004
1261
  scheduleRelease();
1005
1262
  } else {
@@ -1139,6 +1396,36 @@ async function abortClaudeSDKSession(sessionId) {
1139
1396
  }
1140
1397
  }
1141
1398
 
1399
+ /**
1400
+ * Sessions whose background tasks are still outstanding, with the tasks.
1401
+ *
1402
+ * A session stays here after its turn's `result` for as long as the process
1403
+ * is held open for the work — which is exactly the window in which nothing
1404
+ * else (the chat run registry marks the run completed at `result`) knows the
1405
+ * session is still busy.
1406
+ * @returns {Array<{ sessionId: string, tasks: Array<import('@/shared/types.js').BackgroundTaskSummary> }>}
1407
+ */
1408
+ function listClaudeSDKBackgroundWork() {
1409
+ return backgroundWork.list();
1410
+ }
1411
+
1412
+ /**
1413
+ * Stops one outstanding background task through the SDK, which then emits a
1414
+ * `task_notification` with status `stopped` — the same event that drops the
1415
+ * task from the tracker and settles its card.
1416
+ * @param {string} sessionId - Session identifier
1417
+ * @param {string} taskId - The task's `task_id` as reported on `task_started`
1418
+ * @returns {Promise<boolean>} False when no live process is tracking the task
1419
+ */
1420
+ async function stopClaudeSDKTask(sessionId, taskId) {
1421
+ const session = getSession(sessionId);
1422
+ if (!session || !backgroundWork.has(sessionId, taskId)) {
1423
+ return false;
1424
+ }
1425
+ await session.instance.stopTask(taskId);
1426
+ return true;
1427
+ }
1428
+
1142
1429
  /**
1143
1430
  * Checks if an SDK session is currently active
1144
1431
  * @param {string} sessionId - Session identifier
@@ -1146,7 +1433,22 @@ async function abortClaudeSDKSession(sessionId) {
1146
1433
  */
1147
1434
  function isClaudeSDKSessionActive(sessionId) {
1148
1435
  const session = getSession(sessionId);
1149
- return session && session.status === 'active';
1436
+ return Boolean(session && session.status === 'active');
1437
+ }
1438
+
1439
+ /**
1440
+ * When the run behind a session started, or null when no run is up.
1441
+ *
1442
+ * The history reader uses this to tell a background agent launched by the
1443
+ * live process (still able to report back) from one launched by an earlier
1444
+ * process that has since exited (never will): a launch row older than the
1445
+ * live run cannot belong to it.
1446
+ * @param {string} sessionId - Session identifier
1447
+ * @returns {number|null} Epoch milliseconds the live run started, or null
1448
+ */
1449
+ function getClaudeSDKSessionStartTime(sessionId) {
1450
+ const session = getSession(sessionId);
1451
+ return session && session.status === 'active' ? session.startTime : null;
1150
1452
  }
1151
1453
 
1152
1454
  /**
@@ -1201,13 +1503,18 @@ export const claudeRuntime = {
1201
1503
  resolve: resolveToolApproval,
1202
1504
  listPending: getPendingApprovalsForSession,
1203
1505
  },
1506
+ listBackgroundWork: listClaudeSDKBackgroundWork,
1507
+ stopBackgroundTask: stopClaudeSDKTask,
1204
1508
  };
1205
1509
 
1206
1510
  // Export public API
1207
1511
  export {
1208
1512
  queryClaudeSDK,
1209
1513
  abortClaudeSDKSession,
1514
+ listClaudeSDKBackgroundWork,
1515
+ stopClaudeSDKTask,
1210
1516
  isClaudeSDKSessionActive,
1517
+ getClaudeSDKSessionStartTime,
1211
1518
  getActiveClaudeSDKSessions,
1212
1519
  resolveToolApproval,
1213
1520
  getPendingApprovalsForSession,
@@ -147,9 +147,9 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
147
147
  };
148
148
  }
149
149
 
150
- let sessionName = nameMap.get(parsed.sessionId);
150
+ let sessionName = await this.extractSessionTitle(filePath, parsed.sessionId);
151
151
  if (!sessionName) {
152
- sessionName = await this.extractSessionAiTitleFromEnd(filePath, parsed.sessionId);
152
+ sessionName = nameMap.get(parsed.sessionId);
153
153
  }
154
154
 
155
155
  return {
@@ -158,7 +158,17 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
158
158
  };
159
159
  }
160
160
 
161
- private async extractSessionAiTitleFromEnd(
161
+ /**
162
+ * Returns the best available title for one session from its transcript.
163
+ *
164
+ * Scans forward keeping the last match of each event type, then prefers
165
+ * `custom-title` (a manual `/rename`) over `ai-title` over `last-prompt`.
166
+ * Claude writes `custom-title` immediately before `ai-title`, so a reverse
167
+ * scan that returns its first hit would always lose the manual rename.
168
+ *
169
+ * Returns undefined on a missing or unreadable file so sync can continue.
170
+ */
171
+ private async extractSessionTitle(
162
172
  filePath: string,
163
173
  sessionId: string
164
174
  ): Promise<string | undefined> {
@@ -166,7 +176,11 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
166
176
  const content = await readFile(filePath, 'utf8');
167
177
  const lines = content.split(/\r?\n/);
168
178
 
169
- for (let index = lines.length - 1; index >= 0; index -= 1) {
179
+ let foundCustomTitle: string | undefined;
180
+ let foundAiTitle: string | undefined;
181
+ let foundLastPrompt: string | undefined;
182
+
183
+ for (let index = 0; index < lines.length; index += 1) {
170
184
  const line = lines[index]?.trim();
171
185
  if (!line) {
172
186
  continue;
@@ -182,18 +196,30 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
182
196
  const data = parsed as Record<string, unknown>;
183
197
  const eventType = typeof data.type === 'string' ? data.type : undefined;
184
198
  const eventSessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined;
185
- const aiTitle = typeof data.aiTitle === 'string' ? data.aiTitle : undefined;
186
- const lastPrompt = typeof data.lastPrompt === 'string' ? data.lastPrompt : undefined;
187
- const claudeRenamedTitle = typeof data.customTitle === 'string' ? data.customTitle : undefined;
188
-
189
- if (
190
- (eventType === 'ai-title' && eventSessionId === sessionId && aiTitle?.trim()) ||
191
- (eventType === 'last-prompt' && eventSessionId === sessionId && lastPrompt?.trim()) ||
192
- (eventType === "custom-title" && eventSessionId === sessionId && claudeRenamedTitle?.trim())
193
- ) {
194
- return aiTitle || lastPrompt || claudeRenamedTitle;
199
+
200
+ if (eventSessionId !== sessionId) {
201
+ continue;
202
+ }
203
+
204
+ if (eventType === 'custom-title') {
205
+ const title = typeof data.customTitle === 'string' ? data.customTitle : undefined;
206
+ if (title?.trim()) {
207
+ foundCustomTitle = title;
208
+ }
209
+ } else if (eventType === 'ai-title') {
210
+ const title = typeof data.aiTitle === 'string' ? data.aiTitle : undefined;
211
+ if (title?.trim()) {
212
+ foundAiTitle = title;
213
+ }
214
+ } else if (eventType === 'last-prompt') {
215
+ const prompt = typeof data.lastPrompt === 'string' ? data.lastPrompt : undefined;
216
+ if (prompt?.trim()) {
217
+ foundLastPrompt = prompt;
218
+ }
195
219
  }
196
220
  }
221
+
222
+ return foundCustomTitle || foundAiTitle || foundLastPrompt;
197
223
  } catch {
198
224
  // Ignore missing/unreadable files so sync can continue.
199
225
  }