@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
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import pty, { type IPty } from 'node-pty';
6
6
  import { WebSocket, type RawData } from 'ws';
7
7
 
8
- import { parseIncomingJsonObject } from '@/shared/utils.js';
8
+ import { parseIncomingJsonObject, stripAnsiSequences } from '@/shared/utils.js';
9
9
 
10
10
  type ShellIncomingMessage = {
11
11
  type?: string;
@@ -20,8 +20,11 @@ type ShellIncomingMessage = {
20
20
  isPlainShell?: boolean;
21
21
  forceRestart?: boolean;
22
22
  bypassPermissions?: boolean;
23
+ colorScheme?: string;
23
24
  };
24
25
 
26
+ type ShellColorScheme = 'light' | 'dark';
27
+
25
28
  type PtySessionEntry = {
26
29
  pty: IPty;
27
30
  ws: WebSocket | null;
@@ -29,18 +32,17 @@ type PtySessionEntry = {
29
32
  timeoutId: NodeJS.Timeout | null;
30
33
  projectPath: string;
31
34
  sessionId: string | null;
35
+ // The app theme a Claude CLI in this pty was launched for; null for any other
36
+ // program. A running CLI keeps its launch-time colours, so a reattaching
37
+ // client needs this, not its own current theme, to know if they differ.
38
+ claudeColorScheme: ShellColorScheme | null;
32
39
  };
33
40
 
34
41
  const ptySessionsMap = new Map<string, PtySessionEntry>();
35
42
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
36
43
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
37
- const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
38
44
  const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
39
45
 
40
- function stripAnsiSequences(value: string): string {
41
- return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
42
- }
43
-
44
46
  function normalizeDetectedUrl(url: string): string | null {
45
47
  const cleanedUrl = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
46
48
  if (!cleanedUrl) {
@@ -90,6 +92,17 @@ function extractUrlsFromText(value: string): string[] {
90
92
  return Array.from(new Set([...directMatches, ...wrappedMatches]));
91
93
  }
92
94
 
95
+ /**
96
+ * Tells the client which app theme the Claude CLI in its pty was launched for,
97
+ * so the Shell can suggest a restart once the app theme no longer matches it.
98
+ * Clients that predate this frame ignore unknown frame types.
99
+ */
100
+ function sendClaudeColorScheme(ws: WebSocket, colorScheme: ShellColorScheme | null): void {
101
+ if (colorScheme && ws.readyState === WebSocket.OPEN) {
102
+ ws.send(JSON.stringify({ type: 'claude_theme', colorScheme }));
103
+ }
104
+ }
105
+
93
106
  function shouldAutoOpenUrlFromOutput(value: string): boolean {
94
107
  const normalizedOutput = value.toLowerCase();
95
108
  return (
@@ -130,6 +143,14 @@ function readNumber(value: unknown, fallback: number): number {
130
143
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
131
144
  }
132
145
 
146
+ /**
147
+ * Reads the app theme the Shell tab is painted in. Older clients send none and
148
+ * anything unexpected is ignored, which keeps the launch exactly as before.
149
+ */
150
+ function readColorScheme(value: unknown): ShellColorScheme | null {
151
+ return value === 'light' || value === 'dark' ? value : null;
152
+ }
153
+
133
154
  /**
134
155
  * Parses incoming websocket shell messages and keeps processing safe when
135
156
  * malformed payloads are received.
@@ -173,13 +194,23 @@ function resolveResumeSessionId(
173
194
  return resolvedSessionId;
174
195
  }
175
196
 
197
+ type ShellLaunch = {
198
+ command: string;
199
+ /**
200
+ * Whether the command starts a Claude CLI that paints in the app's colour
201
+ * scheme, so a later theme toggle can be applied by restarting it.
202
+ */
203
+ claudeFollowsAppTheme: boolean;
204
+ };
205
+
176
206
  /**
177
207
  * Resolves provider command line for plain shell and agent-backed shell modes.
178
208
  */
179
209
  function buildShellCommand(
180
210
  message: ShellIncomingMessage,
181
- dependencies: ShellWebSocketDependencies
182
- ): string {
211
+ dependencies: ShellWebSocketDependencies,
212
+ projectPath: string
213
+ ): ShellLaunch {
183
214
  const hasSession = readBoolean(message.hasSession);
184
215
  const initialCommand = readString(message.initialCommand);
185
216
  const provider = readString(message.provider, 'claude');
@@ -189,32 +220,34 @@ function buildShellCommand(
189
220
  (!!initialCommand && !hasSession) ||
190
221
  provider === 'plain-shell';
191
222
 
223
+ const otherProgram = (command: string): ShellLaunch => ({ command, claudeFollowsAppTheme: false });
224
+
192
225
  if (isPlainShell) {
193
- return initialCommand;
226
+ return otherProgram(initialCommand);
194
227
  }
195
228
 
196
229
  if (provider === 'cursor') {
197
230
  if (resumeSessionId) {
198
- return `cursor-agent --resume="${resumeSessionId}"`;
231
+ return otherProgram(`cursor-agent --resume="${resumeSessionId}"`);
199
232
  }
200
- return 'cursor-agent';
233
+ return otherProgram('cursor-agent');
201
234
  }
202
235
 
203
236
  if (provider === 'codex') {
204
237
  if (resumeSessionId) {
205
238
  if (os.platform() === 'win32') {
206
- return `codex resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
239
+ return otherProgram(`codex resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { codex }`);
207
240
  }
208
- return `codex resume "${resumeSessionId}" || codex`;
241
+ return otherProgram(`codex resume "${resumeSessionId}" || codex`);
209
242
  }
210
- return 'codex';
243
+ return otherProgram('codex');
211
244
  }
212
245
 
213
246
  if (provider === 'opencode') {
214
247
  if (resumeSessionId) {
215
- return `opencode --session "${resumeSessionId}"`;
248
+ return otherProgram(`opencode --session "${resumeSessionId}"`);
216
249
  }
217
- return initialCommand || 'opencode';
250
+ return otherProgram(initialCommand || 'opencode');
218
251
  }
219
252
 
220
253
  // Launching with the flag is what unlocks "bypass permissions" in the CLI's
@@ -223,14 +256,215 @@ function buildShellCommand(
223
256
  const bypassFlag = readBoolean(message.bypassPermissions)
224
257
  ? ' --dangerously-skip-permissions'
225
258
  : '';
226
- const command = initialCommand || `claude${bypassFlag}`;
259
+ const theme = buildClaudeThemeLaunch(readColorScheme(message.colorScheme), projectPath);
260
+ const launchFlags = `${bypassFlag}${theme.flag}`;
227
261
  if (resumeSessionId) {
228
- if (os.platform() === 'win32') {
229
- return `claude --resume "${resumeSessionId}"${bypassFlag}; if ($LASTEXITCODE -ne 0) { claude${bypassFlag} }`;
262
+ const command = os.platform() === 'win32'
263
+ ? `claude --resume "${resumeSessionId}"${launchFlags}; if ($LASTEXITCODE -ne 0) { claude${launchFlags} }`
264
+ : `claude --resume "${resumeSessionId}"${launchFlags} || claude${launchFlags}`;
265
+ return { command, claudeFollowsAppTheme: theme.followsAppTheme };
266
+ }
267
+ if (initialCommand) {
268
+ // A caller's own command (e.g. `claude /login`) is launched as given.
269
+ return otherProgram(initialCommand);
270
+ }
271
+ return { command: `claude${launchFlags}`, claudeFollowsAppTheme: theme.followsAppTheme };
272
+ }
273
+
274
+ // Claude Code's built-in theme ids (the `/theme` picker of CLI 2.1.280). A
275
+ // `custom:<slug>` theme is valid too; any other value is ignored by the CLI.
276
+ const CLAUDE_THEME_IDS = new Set([
277
+ 'auto',
278
+ 'dark',
279
+ 'light',
280
+ 'dark-daltonized',
281
+ 'light-daltonized',
282
+ 'dark-ansi',
283
+ 'light-ansi',
284
+ ]);
285
+
286
+ // Dark themes and their light counterparts. A light theme, `auto` (it asks the
287
+ // terminal for its background, which xterm answers from the live theme) or a
288
+ // custom theme is the user's own choice and is left alone.
289
+ const LIGHT_CLAUDE_THEME_BY_DARK_THEME = new Map([
290
+ ['dark', 'light'],
291
+ ['dark-daltonized', 'light-daltonized'],
292
+ ['dark-ansi', 'light-ansi'],
293
+ ]);
294
+
295
+ // The CLI refuses settings files over 2 MiB, so a bigger one holds no theme it
296
+ // would use. The legacy global config (`~/.claude.json`) is not a settings file
297
+ // and can grow to several MB; its cap is only there so a huge file is never
298
+ // pulled into the server's memory.
299
+ const MAX_CLAUDE_SETTINGS_FILE_BYTES = 2 * 1024 * 1024;
300
+ const MAX_CLAUDE_GLOBAL_CONFIG_FILE_BYTES = 64 * 1024 * 1024;
301
+
302
+ /**
303
+ * Reads a config file only when it is a regular, non-empty file of sane size,
304
+ * else returns null. The project can be a cloned repository whose
305
+ * `.claude/settings.local.json` is a symlink to a FIFO or to a device such as
306
+ * /dev/zero: reading one would stall the server's event loop or exhaust its
307
+ * memory. `statSync` follows the symlink without opening the target. Empty
308
+ * files are skipped too: they hold no theme, and kernel pseudo-files (procfs,
309
+ * tracefs) report a size of 0 even when a read would block.
310
+ */
311
+ function readRegularConfigFile(filePath: string, maxBytes = MAX_CLAUDE_SETTINGS_FILE_BYTES): string | null {
312
+ try {
313
+ const stats = fs.statSync(filePath);
314
+ if (!stats.isFile() || stats.size === 0 || stats.size > maxBytes) {
315
+ return null;
230
316
  }
231
- return `claude --resume "${resumeSessionId}"${bypassFlag} || claude${bypassFlag}`;
317
+ return fs.readFileSync(filePath, 'utf8');
318
+ } catch {
319
+ return null;
232
320
  }
233
- return command;
321
+ }
322
+
323
+ /**
324
+ * Reads the `theme` key of one Claude config file. A missing, unreadable,
325
+ * special (FIFO, device) or unparsable file, or a value the CLI would reject,
326
+ * counts as unset.
327
+ */
328
+ function readClaudeThemeFile(filePath: string, maxBytes: number): string | null {
329
+ const text = readRegularConfigFile(filePath, maxBytes);
330
+ if (text === null) {
331
+ return null;
332
+ }
333
+ try {
334
+ const config: unknown = JSON.parse(text);
335
+ const theme = config && typeof config === 'object' ? (config as Record<string, unknown>).theme : undefined;
336
+ if (typeof theme !== 'string') {
337
+ return null;
338
+ }
339
+ return CLAUDE_THEME_IDS.has(theme) || theme.startsWith('custom:') ? theme : null;
340
+ } catch {
341
+ return null;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Finds the theme Claude Code would start with in `projectPath`, read-only and
347
+ * in the CLI's own order: local, project and user settings, then the legacy
348
+ * global config key, then its "dark" default. Managed settings are not read:
349
+ * they outrank `--settings`, so no flag could change their theme anyway.
350
+ */
351
+ function resolveClaudeTheme(projectPath: string): string {
352
+ const configDirOverride = process.env.CLAUDE_CONFIG_DIR;
353
+ const configHome = configDirOverride || path.join(os.homedir(), '.claude');
354
+ const legacyGlobalConfigPath = path.join(configHome, '.config.json');
355
+ const configFiles: Array<[string, number]> = [
356
+ [path.join(projectPath, '.claude', 'settings.local.json'), MAX_CLAUDE_SETTINGS_FILE_BYTES],
357
+ [path.join(projectPath, '.claude', 'settings.json'), MAX_CLAUDE_SETTINGS_FILE_BYTES],
358
+ [path.join(configHome, 'settings.json'), MAX_CLAUDE_SETTINGS_FILE_BYTES],
359
+ [
360
+ fs.existsSync(legacyGlobalConfigPath)
361
+ ? legacyGlobalConfigPath
362
+ : path.join(configDirOverride || os.homedir(), '.claude.json'),
363
+ MAX_CLAUDE_GLOBAL_CONFIG_FILE_BYTES,
364
+ ],
365
+ ];
366
+
367
+ for (const [filePath, maxBytes] of configFiles) {
368
+ const theme = readClaudeThemeFile(filePath, maxBytes);
369
+ if (theme) {
370
+ return theme;
371
+ }
372
+ }
373
+ return 'dark';
374
+ }
375
+
376
+ let hasLoggedThemeSettingsFileError = false;
377
+
378
+ /**
379
+ * Returns a settings file that holds only `{"theme": theme}`, writing it when
380
+ * it is missing or differs. It lives in the app's own data folder
381
+ * (`~/.cloudcli`, like the chat assets); the user's Claude config is never
382
+ * written. The write goes through a temp file and a rename, so a CLI starting
383
+ * at the same time never reads half a file. Returns null, and logs once, when
384
+ * the file cannot be written (a read-only home, say): the launch then simply
385
+ * goes ahead without a theme.
386
+ */
387
+ function ensureClaudeThemeSettingsFile(theme: string): string | null {
388
+ const contents = `${JSON.stringify({ theme })}\n`;
389
+ let tempPath: string | null = null;
390
+ try {
391
+ const filePath = path.join(os.homedir(), '.cloudcli', `claude-shell-theme-${theme}.json`);
392
+ // Anything but the expected regular file is replaced; the rename never
393
+ // opens what it replaces.
394
+ if (readRegularConfigFile(filePath) !== contents) {
395
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
396
+ tempPath = `${filePath}.${process.pid}.tmp`;
397
+ fs.writeFileSync(tempPath, contents);
398
+ fs.renameSync(tempPath, filePath);
399
+ }
400
+ return filePath;
401
+ } catch (error) {
402
+ if (tempPath) {
403
+ // Asynchronous and unchecked: cleaning up must not throw into the launch.
404
+ fs.rm(tempPath, { force: true }, () => undefined);
405
+ }
406
+ if (!hasLoggedThemeSettingsFileError) {
407
+ hasLoggedThemeSettingsFileError = true;
408
+ const message = error instanceof Error ? error.message : String(error);
409
+ console.warn(`[WARN] Shell: cannot write the Claude theme settings file, launching without it: ${message}`);
410
+ }
411
+ return null;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Quotes one argument for the shell that runs the launch command. bash takes
417
+ * everything between single quotes literally, so a `'` closes the quote, adds
418
+ * an escaped one and reopens it. PowerShell (win32) single quotes are literal
419
+ * too, backslashes included; a quote is doubled, and PowerShell also treats
420
+ * the typographic single quotes as quote characters.
421
+ */
422
+ function quoteShellArgument(value: string): string {
423
+ if (os.platform() === 'win32') {
424
+ return `'${value.replace(/['\u2018\u2019\u201A\u201B]/g, '$&$&')}'`;
425
+ }
426
+ return `'${value.replace(/'/g, `'\\''`)}'`;
427
+ }
428
+
429
+ type ClaudeThemeLaunch = {
430
+ flag: string;
431
+ /** Whether the CLI will paint in the app's colour scheme. */
432
+ followsAppTheme: boolean;
433
+ };
434
+
435
+ /**
436
+ * Claude Code paints its own colours in its own theme, dark by default, so in
437
+ * a light Shell tab its prompt rows became dark bands. Only a light tab gets a
438
+ * flag, and only when the user's theme is a dark one: it is swapped for the
439
+ * matching light variant. `--settings` applies it to this launch only. It is
440
+ * given a file path rather than inline JSON because PowerShell strips the
441
+ * JSON's double quotes when it passes an argument to a native program.
442
+ *
443
+ * The CLI follows the app theme when it gets that flag, when a dark tab runs a
444
+ * dark theme, and with `auto` (it asks the terminal, which xterm answers from
445
+ * the live theme). A light or custom theme the user chose, or a flag that
446
+ * could not be written, paints the same in either app theme, so restarting
447
+ * after a toggle would change nothing.
448
+ */
449
+ function buildClaudeThemeLaunch(colorScheme: ShellColorScheme | null, projectPath: string): ClaudeThemeLaunch {
450
+ if (!colorScheme) {
451
+ return { flag: '', followsAppTheme: false };
452
+ }
453
+ const userTheme = resolveClaudeTheme(projectPath);
454
+ if (userTheme === 'auto') {
455
+ return { flag: '', followsAppTheme: true };
456
+ }
457
+ const lightTheme = LIGHT_CLAUDE_THEME_BY_DARK_THEME.get(userTheme);
458
+ if (!lightTheme) {
459
+ return { flag: '', followsAppTheme: false };
460
+ }
461
+ if (colorScheme === 'dark') {
462
+ return { flag: '', followsAppTheme: true };
463
+ }
464
+ const settingsPath = ensureClaudeThemeSettingsFile(lightTheme);
465
+ return settingsPath
466
+ ? { flag: ` --settings ${quoteShellArgument(settingsPath)}`, followsAppTheme: true }
467
+ : { flag: '', followsAppTheme: false };
234
468
  }
235
469
 
236
470
  function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
@@ -318,6 +552,7 @@ export function handleShellConnection(
318
552
  const provider = readString(data.provider, 'claude');
319
553
  const initialCommand = readString(data.initialCommand);
320
554
  const forceRestart = readBoolean(data.forceRestart);
555
+ const colorScheme = readColorScheme(data.colorScheme);
321
556
  const isPlainShell =
322
557
  readBoolean(data.isPlainShell) ||
323
558
  (!!initialCommand && !hasSession) ||
@@ -377,6 +612,7 @@ export function handleShellConnection(
377
612
  }
378
613
 
379
614
  existingSession.ws = ws;
615
+ sendClaudeColorScheme(ws, existingSession.claudeColorScheme);
380
616
  return;
381
617
  }
382
618
 
@@ -397,8 +633,13 @@ export function handleShellConnection(
397
633
  return;
398
634
  }
399
635
 
400
- const shellCommand = buildShellCommand(data, dependencies);
636
+ const { command: shellCommand, claudeFollowsAppTheme } = buildShellCommand(
637
+ data,
638
+ dependencies,
639
+ resolvedProjectPath,
640
+ );
401
641
  const resumeSessionId = resolveResumeSessionId(data, dependencies);
642
+ const claudeColorScheme = claudeFollowsAppTheme ? colorScheme : null;
402
643
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
403
644
  const shellArgs =
404
645
  os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
@@ -417,6 +658,10 @@ export function handleShellConnection(
417
658
  TERM: 'xterm-256color',
418
659
  COLORTERM: 'truecolor',
419
660
  FORCE_COLOR: '3',
661
+ // rxvt's "foreground;background" palette indices (15 = white, 0 = black).
662
+ // Programs that cannot query the terminal (vim, Claude Code's "auto"
663
+ // theme before its OSC 11 reply arrives) read it to pick light or dark.
664
+ ...(colorScheme ? { COLORFGBG: colorScheme === 'light' ? '0;15' : '15;0' } : {}),
420
665
  },
421
666
  });
422
667
 
@@ -427,6 +672,7 @@ export function handleShellConnection(
427
672
  timeoutId: null,
428
673
  projectPath,
429
674
  sessionId,
675
+ claudeColorScheme,
430
676
  });
431
677
 
432
678
  shellProcess.onData((chunk) => {
@@ -555,6 +801,7 @@ export function handleShellConnection(
555
801
  data: welcomeMsg,
556
802
  })
557
803
  );
804
+ sendClaudeColorScheme(ws, claudeColorScheme);
558
805
  return;
559
806
  }
560
807
 
@@ -0,0 +1,167 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import test, { mock } from 'node:test';
7
+
8
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
9
+ import { handleChatConnection } from '@/modules/websocket/services/chat-websocket.service.js';
10
+ import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
11
+ import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
12
+
13
+ /**
14
+ * A session's turn ends while its agents, workflow or backgrounded command are
15
+ * still going. The run registry marks the run completed at the turn's
16
+ * `complete`, but the runtime keeps producing events through that run's
17
+ * writer — task progress, and the turn the CLI pushes when the task reports.
18
+ * A tab opened during that work has to attach to the run to see them, and the
19
+ * run has to still be there to attach to.
20
+ */
21
+
22
+ const SESSION_ID = 'held-session';
23
+ const FIVE_MINUTES = 5 * 60 * 1000;
24
+
25
+ function createFakeSocket() {
26
+ const socket = new EventEmitter() as EventEmitter & {
27
+ readyState: number;
28
+ frames: Array<Record<string, unknown>>;
29
+ send: (data: string) => void;
30
+ };
31
+ socket.readyState = 1;
32
+ socket.frames = [];
33
+ socket.send = (data: string) => socket.frames.push(JSON.parse(data) as Record<string, unknown>);
34
+ return socket;
35
+ }
36
+
37
+ async function withIsolatedDatabase(runTest: () => Promise<void>): Promise<void> {
38
+ const previousDatabasePath = process.env.DATABASE_PATH;
39
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'chat-background-subscribe-'));
40
+ closeConnection();
41
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
42
+ await initializeDatabase();
43
+ try {
44
+ sessionsDb.createAppSession(SESSION_ID, 'claude', tempDirectory);
45
+ await runTest();
46
+ } finally {
47
+ connectedClients.clear();
48
+ chatRunRegistry.clearAll();
49
+ chatRunRegistry.setRetentionGuard(() => false);
50
+ closeConnection();
51
+ if (previousDatabasePath === undefined) {
52
+ delete process.env.DATABASE_PATH;
53
+ } else {
54
+ process.env.DATABASE_PATH = previousDatabasePath;
55
+ }
56
+ await rm(tempDirectory, { recursive: true, force: true });
57
+ }
58
+ }
59
+
60
+ /** Starts a run for the session and ends its turn, leaving the run completed but registered. */
61
+ function startAndCompleteRun(connection: ReturnType<typeof createFakeSocket>) {
62
+ const run = chatRunRegistry.startRun({
63
+ appSessionId: SESSION_ID,
64
+ provider: 'claude',
65
+ providerSessionId: 'native-1',
66
+ connection: connection as never,
67
+ userId: 1,
68
+ });
69
+ assert.ok(run);
70
+ run.writer.send({ kind: 'complete', provider: 'claude', sessionId: 'native-1', exitCode: 0 });
71
+ assert.equal(chatRunRegistry.isProcessing(SESSION_ID), false);
72
+ return run;
73
+ }
74
+
75
+ const settle = () => new Promise((resolve) => { setTimeout(resolve, 30); });
76
+
77
+ test('a tab that subscribes during background work receives the run\'s later events', async () => {
78
+ await withIsolatedDatabase(async () => {
79
+ const run = startAndCompleteRun(createFakeSocket());
80
+
81
+ const newTab = createFakeSocket();
82
+ handleChatConnection(
83
+ newTab as never,
84
+ { user: { id: 1 } } as never,
85
+ { runtime: { hasBackgroundWork: (sessionId: string) => sessionId === SESSION_ID, getPendingApprovalsForSession: () => [] } as never },
86
+ );
87
+ newTab.emit('message', JSON.stringify({ type: 'chat.subscribe', sessions: [{ sessionId: SESSION_ID, lastSeq: 0 }] }));
88
+ await settle();
89
+
90
+ // Not processing — the composer stays usable — but attached.
91
+ assert.equal(newTab.frames[0]?.kind, 'chat_subscribed');
92
+ assert.equal(newTab.frames[0]?.isProcessing, false);
93
+
94
+ run.writer.send({ kind: 'task_status', provider: 'claude', sessionId: 'native-1', event: 'notification', taskId: 't1', status: 'completed' } as never);
95
+ assert.ok(
96
+ newTab.frames.some((frame) => frame.kind === 'task_status'),
97
+ 'the task event must reach the tab that subscribed after the turn ended',
98
+ );
99
+ });
100
+ });
101
+
102
+ test('a tab that subscribes to an idle session is not attached to its old run', async () => {
103
+ await withIsolatedDatabase(async () => {
104
+ const run = startAndCompleteRun(createFakeSocket());
105
+
106
+ const newTab = createFakeSocket();
107
+ handleChatConnection(
108
+ newTab as never,
109
+ { user: { id: 1 } } as never,
110
+ { runtime: { hasBackgroundWork: () => false, getPendingApprovalsForSession: () => [] } as never },
111
+ );
112
+ newTab.emit('message', JSON.stringify({ type: 'chat.subscribe', sessions: [{ sessionId: SESSION_ID, lastSeq: 0 }] }));
113
+ await settle();
114
+
115
+ run.writer.send({ kind: 'task_status', provider: 'claude', sessionId: 'native-1', event: 'notification', taskId: 't1', status: 'completed' } as never);
116
+ assert.ok(!newTab.frames.some((frame) => frame.kind === 'task_status'));
117
+ });
118
+ });
119
+
120
+ test('a completed run stays registered while its session still has background work', async () => {
121
+ mock.timers.enable({ apis: ['setTimeout'] });
122
+ try {
123
+ await withIsolatedDatabase(async () => {
124
+ let working = true;
125
+ chatRunRegistry.setRetentionGuard((sessionId) => sessionId === SESSION_ID && working);
126
+ startAndCompleteRun(createFakeSocket());
127
+
128
+ mock.timers.tick(FIVE_MINUTES);
129
+ assert.ok(chatRunRegistry.getRun(SESSION_ID), 'the run must outlive the retention window while work is outstanding');
130
+
131
+ working = false;
132
+ mock.timers.tick(FIVE_MINUTES);
133
+ assert.equal(chatRunRegistry.getRun(SESSION_ID), undefined, 'once the work is gone the run is evicted as before');
134
+ });
135
+ } finally {
136
+ mock.timers.reset();
137
+ }
138
+ });
139
+
140
+ test('a retained run\'s eviction timer cannot evict the session\'s next run early', async () => {
141
+ // Run one completes with work outstanding and is re-armed at five minutes;
142
+ // run two takes the slot and completes with two minutes of retention to
143
+ // go. Run one's timer firing on the slot alone would evict run two.
144
+ mock.timers.enable({ apis: ['setTimeout'] });
145
+ try {
146
+ await withIsolatedDatabase(async () => {
147
+ let working = true;
148
+ chatRunRegistry.setRetentionGuard((sessionId) => sessionId === SESSION_ID && working);
149
+ const first = startAndCompleteRun(createFakeSocket());
150
+ mock.timers.tick(FIVE_MINUTES);
151
+ assert.equal(chatRunRegistry.getRun(SESSION_ID), first);
152
+
153
+ working = false;
154
+ mock.timers.tick(FIVE_MINUTES - 2 * 60 * 1000);
155
+ const second = startAndCompleteRun(createFakeSocket());
156
+ assert.equal(chatRunRegistry.getRun(SESSION_ID), second);
157
+
158
+ // Run one's re-armed timer fires now; run two's has two minutes left.
159
+ mock.timers.tick(2 * 60 * 1000);
160
+ assert.equal(chatRunRegistry.getRun(SESSION_ID), second, 'run two keeps its own retention window');
161
+ mock.timers.tick(FIVE_MINUTES);
162
+ assert.equal(chatRunRegistry.getRun(SESSION_ID), undefined);
163
+ });
164
+ } finally {
165
+ mock.timers.reset();
166
+ }
167
+ });