@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
@@ -3,16 +3,31 @@ import fsp from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import readline from 'node:readline';
5
5
  import { parseFilesInputTag } from '../../../../shared/image-attachments.js';
6
- import { prepareTranscriptMessages } from '../../../../shared/message-unification.js';
7
- import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage, truncateSubagentActivity, } from '../../../../shared/utils.js';
6
+ import { prepareTranscriptMessages, truncateNestedOutput } from '../../../../shared/message-unification.js';
7
+ import { createNormalizedMessage, generateMessageId, isPlaceholderProviderModel, readObjectRecord, sliceTailPage, stripAnsiSequences, truncateSubagentActivity, } from '../../../../shared/utils.js';
8
8
  import { sessionsDb } from '../../../../modules/database/index.js';
9
+ import { getClaudeSDKSessionStartTime } from '../../../../modules/providers/list/claude/claude-runtime.provider.js';
9
10
  import { summarizeClaudeTokenUsage } from '../../../../modules/providers/services/provider-token-usage.service.js';
10
11
  const PROVIDER = 'claude';
12
+ /** Tokens the way the CLI writes them: 725k rather than 724,871. */
13
+ const formatTokenCount = (tokens) => {
14
+ if (tokens >= 1_000_000) {
15
+ return `${(tokens / 1_000_000).toFixed(1)}M`;
16
+ }
17
+ return tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : `${tokens}`;
18
+ };
19
+ /** A duration the way the CLI writes one: `2m 22s`. */
20
+ const formatDuration = (milliseconds) => {
21
+ const seconds = Math.round(milliseconds / 1_000);
22
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
23
+ };
11
24
  /**
12
25
  * Upper bound on how much of a subagent's timeline is sent to the client. A
13
26
  * long-running agent can record hundreds of tool calls, and the transcript only
14
27
  * ever shows them behind a collapsed header, so shipping the whole history on
15
- * every load costs far more than it shows.
28
+ * every load costs far more than it shows. The newest steps are the ones
29
+ * kept: a running agent's timeline is polled for what it is doing now, and
30
+ * the card says how many earlier ones were left out.
16
31
  */
17
32
  const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
18
33
  /**
@@ -24,7 +39,7 @@ const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
24
39
  */
25
40
  async function readClaudeSubagentTranscript(filePath) {
26
41
  const activity = [];
27
- const transcript = { activity, endedMidToolCall: false };
42
+ const transcript = { activity };
28
43
  const toolsById = new Map();
29
44
  try {
30
45
  const fileStream = fs.createReadStream(filePath);
@@ -39,6 +54,9 @@ async function readClaudeSubagentTranscript(filePath) {
39
54
  try {
40
55
  const entry = JSON.parse(line);
41
56
  const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
57
+ if (timestamp && !transcript.startedAt) {
58
+ transcript.startedAt = timestamp;
59
+ }
42
60
  if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
43
61
  if (typeof entry.message.model === 'string') {
44
62
  transcript.model = entry.message.model;
@@ -98,8 +116,6 @@ async function readClaudeSubagentTranscript(filePath) {
98
116
  const message = error instanceof Error ? error.message : String(error);
99
117
  console.warn(`Error parsing agent file ${filePath}:`, message);
100
118
  }
101
- const lastActivity = activity[activity.length - 1];
102
- transcript.endedMidToolCall = lastActivity?.kind === 'tool' && !lastActivity.toolResult;
103
119
  return transcript;
104
120
  }
105
121
  /** Reads the sidecar `.meta.json` Claude writes next to a subagent transcript. */
@@ -140,13 +156,122 @@ async function findClaudeSubagentTranscript(projectDirectory, providerSessionId,
140
156
  }
141
157
  return null;
142
158
  }
159
+ /**
160
+ * Reads which agents a workflow run spawned and how far each got.
161
+ *
162
+ * The run appends one record per event to `<transcriptDir>/journal.jsonl`:
163
+ * `started` names the agent (and the label and phase the script gave it, when
164
+ * it gave any), `result` and `failed` settle it. The journal is what the
165
+ * workflow itself resumes from, so it is the only record of an agent that is
166
+ * still running — its transcript file is written, but nothing in it says the
167
+ * agent is not done. A missing directory or journal means the run left no
168
+ * record here (a fork copies only the parent's transcript), which reads as no
169
+ * agents rather than an error.
170
+ */
171
+ async function readWorkflowJournal(transcriptDir) {
172
+ const agentsById = new Map();
173
+ let journal;
174
+ try {
175
+ journal = await fsp.readFile(path.join(transcriptDir, 'journal.jsonl'), 'utf8');
176
+ }
177
+ catch {
178
+ return [];
179
+ }
180
+ for (const line of journal.split('\n')) {
181
+ if (!line.trim()) {
182
+ continue;
183
+ }
184
+ let entry;
185
+ try {
186
+ entry = JSON.parse(line);
187
+ }
188
+ catch {
189
+ // The workflow appends while this reads, so the last line can be partial.
190
+ continue;
191
+ }
192
+ const agentId = typeof entry.agentId === 'string' ? entry.agentId : '';
193
+ if (!agentId) {
194
+ continue;
195
+ }
196
+ if (entry.type === 'started') {
197
+ agentsById.set(agentId, {
198
+ id: agentId,
199
+ label: typeof entry.label === 'string' ? entry.label : undefined,
200
+ phase: typeof entry.phase === 'string' ? entry.phase : undefined,
201
+ status: 'running',
202
+ });
203
+ continue;
204
+ }
205
+ const agent = agentsById.get(agentId);
206
+ if (!agent) {
207
+ continue;
208
+ }
209
+ if (entry.type === 'result') {
210
+ agent.status = 'completed';
211
+ }
212
+ else if (entry.type === 'failed') {
213
+ agent.status = 'failed';
214
+ }
215
+ }
216
+ return [...agentsById.values()];
217
+ }
218
+ function readBackgroundLaunch(toolUseResult) {
219
+ const result = readObjectRecord(toolUseResult);
220
+ if (!result) {
221
+ return null;
222
+ }
223
+ if (result.agentId) {
224
+ return 'agent';
225
+ }
226
+ if (result.status === 'async_launched') {
227
+ return 'workflow';
228
+ }
229
+ if (typeof result.backgroundTaskId === 'string' && result.backgroundTaskId) {
230
+ return 'bash';
231
+ }
232
+ return null;
233
+ }
143
234
  function readTaggedValue(content, tagName) {
144
235
  const match = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`).exec(content);
145
236
  return match ? match[1].trim() : '';
146
237
  }
147
238
  /**
148
- * Collects every `<task-notification>` turn keyed by the tool call that
149
- * spawned the agent it reports on.
239
+ * Reads every `<task-notification>` payload one transcript row carries.
240
+ *
241
+ * Claude writes the same completion report in two shapes, and which one you get
242
+ * depends only on when the agent happened to finish:
243
+ *
244
+ * - as an ordinary user-role turn, when the queued notification is delivered
245
+ * between turns (the harness also logs a contentless `dequeue` for it);
246
+ * - as a top-level `queue-operation` record whose `content` holds the payload,
247
+ * when the harness consumes it inline while the parent turn is still running
248
+ * (`remove`), or when the session ends before it is ever delivered
249
+ * (`enqueue`).
250
+ *
251
+ * The operation names the queue's bookkeeping, not whether the agent finished,
252
+ * so every queue-operation payload counts. Reading only the user-role shape
253
+ * loses the majority of them: across the 919 transcripts this was measured on,
254
+ * 31 of 56 background agent launches report *only* through a queue-operation
255
+ * record, and their result and status were invisible to every consumer.
256
+ */
257
+ function readTaskNotificationTexts(message) {
258
+ if (message.type === 'queue-operation') {
259
+ return typeof message.content === 'string' ? [message.content] : [];
260
+ }
261
+ if (message.message?.role !== 'user') {
262
+ return [];
263
+ }
264
+ const content = message.message.content;
265
+ if (typeof content === 'string') {
266
+ return [content];
267
+ }
268
+ return Array.isArray(content)
269
+ ? content.filter((part) => part?.type === 'text').map((part) => String(part.text ?? ''))
270
+ : [];
271
+ }
272
+ /**
273
+ * Collects every `<task-notification>` keyed by the tool call that spawned the
274
+ * agent it reports on.
150
275
  *
151
276
  * Only notifications that name a tool-use id are collected: without one there
152
277
  * is no card to fold them into, and they must keep rendering on their own.
@@ -154,16 +279,7 @@ function readTaggedValue(content, tagName) {
154
279
  function collectTaskNotifications(messages) {
155
280
  const notifications = new Map();
156
281
  for (const message of messages) {
157
- if (message.message?.role !== 'user') {
158
- continue;
159
- }
160
- const content = message.message.content;
161
- const texts = typeof content === 'string'
162
- ? [content]
163
- : Array.isArray(content)
164
- ? content.filter((part) => part?.type === 'text').map((part) => String(part.text ?? ''))
165
- : [];
166
- for (const text of texts) {
282
+ for (const text of readTaskNotificationTexts(message)) {
167
283
  if (!text.trimStart().startsWith('<task-notification>')) {
168
284
  continue;
169
285
  }
@@ -171,9 +287,14 @@ function collectTaskNotifications(messages) {
171
287
  if (!toolUseId) {
172
288
  continue;
173
289
  }
174
- // A resumed agent notifies more than once; the last word wins.
290
+ // A resumed agent notifies more than once; the last word wins, and the
291
+ // rows that carried the earlier words go with it.
292
+ const sourceUuids = notifications.get(toolUseId)?.sourceUuids ?? [];
293
+ if (typeof message.uuid === 'string' && message.uuid) {
294
+ sourceUuids.push(message.uuid);
295
+ }
175
296
  notifications.set(toolUseId, {
176
- sourceUuid: String(message.uuid ?? ''),
297
+ sourceUuids,
177
298
  toolUseId,
178
299
  status: readTaggedValue(text, 'status') || 'completed',
179
300
  summary: readTaggedValue(text, 'summary'),
@@ -183,8 +304,8 @@ function collectTaskNotifications(messages) {
183
304
  }
184
305
  return notifications;
185
306
  }
186
- /** Reads the `tool_use_id` off the tool-result row that launched an agent. */
187
- function readAgentToolUseId(message) {
307
+ /** Reads the `tool_use_id` off the tool-result row that launched background work. */
308
+ function readLaunchToolUseId(message) {
188
309
  const content = message.message?.content;
189
310
  if (!Array.isArray(content)) {
190
311
  return null;
@@ -196,8 +317,8 @@ function readAgentToolUseId(message) {
196
317
  }
197
318
  return null;
198
319
  }
199
- /** Swaps an agent launch acknowledgement for the agent's actual answer. */
200
- function replaceAgentToolResultContent(message, replacement) {
320
+ /** Swaps a launch acknowledgement for the work's actual answer. */
321
+ function replaceLaunchToolResultContent(message, replacement) {
201
322
  const content = message.message?.content;
202
323
  if (!Array.isArray(content)) {
203
324
  return;
@@ -247,12 +368,111 @@ function isUserPromptRow(row) {
247
368
  return typeof content === 'string' && content.length > 0;
248
369
  }
249
370
  /**
250
- * Drops the rows belonging to prompts that were replaced by an edit.
371
+ * Returns the uuids on the chain the next `claude --resume` replays, from the
372
+ * leaf it continues from back to the root.
373
+ *
374
+ * Mirrors the transcript loader of the Claude CLI, which the Agent SDK's
375
+ * `resume` spawns as well. Every CLI process stamps a `last-prompt` row naming
376
+ * the leaf it was on when it exits, so with two writers on one session it is
377
+ * that stamp — not the newest row — that decides which branch is replayed. The
378
+ * newest conversation row only takes over when it descends from the stamped
379
+ * leaf (the stamping process kept going), or when the file carries no stamp.
380
+ * A compact boundary clears the stamp, and `progress` rows are skipped over
381
+ * when linking a row to its parent, as the CLI does.
382
+ */
383
+ function resolveResumePath(rows) {
384
+ const conversation = new Map();
385
+ const progressParents = new Map();
386
+ const parentOf = (row) => {
387
+ const parentUuid = typeof row.parentUuid === 'string' ? row.parentUuid : undefined;
388
+ if (parentUuid && progressParents.has(parentUuid)) {
389
+ return progressParents.get(parentUuid) ?? undefined;
390
+ }
391
+ return parentUuid;
392
+ };
393
+ let newestRow;
394
+ let stampedLeaf;
395
+ let stampIsExplicit = false;
396
+ for (const row of rows) {
397
+ if (row.type === 'progress' && typeof row.uuid === 'string') {
398
+ progressParents.set(row.uuid, parentOf(row) ?? null);
399
+ continue;
400
+ }
401
+ if (typeof row.uuid === 'string'
402
+ && (row.type === 'user' || row.type === 'assistant' || row.type === 'attachment' || row.type === 'system')) {
403
+ conversation.set(row.uuid, row);
404
+ if (row.isSidechain !== true) {
405
+ newestRow = row.uuid;
406
+ stampIsExplicit = false;
407
+ }
408
+ if (row.type === 'system' && row.subtype === 'compact_boundary') {
409
+ stampedLeaf = undefined;
410
+ stampIsExplicit = false;
411
+ }
412
+ }
413
+ else if (row.type === 'last-prompt' && typeof row.leafUuid === 'string' && row.leafUuid) {
414
+ stampIsExplicit = row.explicit === true || (stampIsExplicit && row.leafUuid === stampedLeaf);
415
+ stampedLeaf = row.leafUuid;
416
+ }
417
+ }
418
+ const chainFrom = (leaf) => {
419
+ const chain = [];
420
+ const seen = new Set();
421
+ let cursor = leaf;
422
+ while (cursor && conversation.has(cursor) && !seen.has(cursor)) {
423
+ seen.add(cursor);
424
+ chain.push(cursor);
425
+ cursor = parentOf(conversation.get(cursor));
426
+ }
427
+ return chain;
428
+ };
429
+ let leaf = stampedLeaf && conversation.has(stampedLeaf) ? stampedLeaf : undefined;
430
+ if (leaf && !stampIsExplicit && newestRow && newestRow !== leaf && chainFrom(newestRow).includes(leaf)) {
431
+ leaf = newestRow;
432
+ }
433
+ return new Set(chainFrom(leaf ?? newestRow));
434
+ }
435
+ /**
436
+ * Maps each row's uuid to the file position of the newest row in its subtree.
437
+ *
438
+ * The transcript is append-only and a child is always written after its
439
+ * parent, so one backward pass hands every row's position to its parent and
440
+ * each uuid ends up holding the position of the last line its branch grew.
441
+ * Rows after a compact boundary do not link back across it, so this only
442
+ * orders branches within one compaction segment.
443
+ */
444
+ function indexNewestRowPerBranch(rows) {
445
+ const newestByUuid = new Map();
446
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
447
+ const row = rows[index];
448
+ if (typeof row.uuid !== 'string') {
449
+ continue;
450
+ }
451
+ const newest = newestByUuid.get(row.uuid) ?? index;
452
+ newestByUuid.set(row.uuid, newest);
453
+ if (typeof row.parentUuid === 'string' && (newestByUuid.get(row.parentUuid) ?? -1) < newest) {
454
+ newestByUuid.set(row.parentUuid, newest);
455
+ }
456
+ }
457
+ return newestByUuid;
458
+ }
459
+ /**
460
+ * Drops the rows belonging to prompts the next resume will not replay.
251
461
  *
252
462
  * When a message is edited, Claude resumes the conversation partway and appends
253
463
  * the replacement, so two prompts end up sharing one parent and the file holds
254
464
  * both the abandoned attempt and the live one. A flat read would show them
255
- * stacked, which reads as the app having sent the message twice.
465
+ * stacked, which reads as the app having sent the message twice. The same shape
466
+ * appears without an edit when a session is driven from Chat and from the
467
+ * native `claude` CLI at once: both writers branch off the row each of them saw
468
+ * last.
469
+ *
470
+ * Which sibling survives is decided by the branch the runtime will actually
471
+ * resume: the sibling on the chain `resolveResumePath` returns. Going by the
472
+ * newest prompt, or by the newest row, would show a branch the model was never
473
+ * sent whenever the two writers exit in a different order than they wrote. A
474
+ * fork that is not on that chain at all (one made before a compaction) keeps
475
+ * the branch that was appended to last.
256
476
  *
257
477
  * Only sibling *prompts* are treated as a fork. Branch points made by parallel
258
478
  * tool calls are extremely common — one assistant turn writes several chained
@@ -273,15 +493,29 @@ function dropSupersededPromptBranches(rows) {
273
493
  promptSiblings.set(row.parentUuid, [row]);
274
494
  }
275
495
  }
496
+ const forks = [...promptSiblings.values()].filter((siblings) => siblings.length > 1);
497
+ if (forks.length === 0) {
498
+ return rows;
499
+ }
500
+ const resumePath = resolveResumePath(rows);
501
+ let newestByUuid;
502
+ const newestRowOf = (row) => {
503
+ newestByUuid ??= indexNewestRowPerBranch(rows);
504
+ return typeof row.uuid === 'string' ? newestByUuid.get(row.uuid) ?? -1 : -1;
505
+ };
276
506
  const supersededRoots = new Set();
277
- for (const siblings of promptSiblings.values()) {
278
- if (siblings.length < 2) {
279
- continue;
507
+ for (const siblings of forks) {
508
+ let live = siblings.find((row) => typeof row.uuid === 'string' && resumePath.has(row.uuid));
509
+ if (!live) {
510
+ live = siblings[0];
511
+ for (const row of siblings) {
512
+ if (newestRowOf(row) > newestRowOf(live)) {
513
+ live = row;
514
+ }
515
+ }
280
516
  }
281
- // The transcript is append-only, so the last prompt written under a parent
282
- // is the one that replaced the others.
283
- for (const row of siblings.slice(0, -1)) {
284
- if (typeof row.uuid === 'string') {
517
+ for (const row of siblings) {
518
+ if (row !== live && typeof row.uuid === 'string') {
285
519
  supersededRoots.add(row.uuid);
286
520
  }
287
521
  }
@@ -299,7 +533,7 @@ function dropSupersededPromptBranches(rows) {
299
533
  }
300
534
  return rows.filter((row) => typeof row.uuid !== 'string' || !abandoned.has(row.uuid));
301
535
  }
302
- async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
536
+ async function getSessionMessages(sessionId, providerSessionId, limit, offset, getLiveRunStartTime) {
303
537
  try {
304
538
  // The DB row is keyed by the app-facing session id, while the JSONL rows
305
539
  // on disk carry the provider-native id — both ids are needed here.
@@ -317,7 +551,8 @@ async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
317
551
  }
318
552
  }
319
553
  // Read each spawned agent's own transcript once, then hang it off every
320
- // row that references it.
554
+ // row that references it. Status is not part of this: the transcript
555
+ // cannot say whether the agent finished (see the loop below).
321
556
  const subagentsById = new Map();
322
557
  for (const agentId of agentIds) {
323
558
  const located = await findClaudeSubagentTranscript(projectDir, providerSessionId, agentId);
@@ -329,9 +564,8 @@ async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
329
564
  readClaudeSubagentMeta(located.metaPath),
330
565
  ]);
331
566
  subagentsById.set(agentId, {
332
- endedMidToolCall: transcript.endedMidToolCall,
333
567
  activity: transcript.activity
334
- .slice(0, MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
568
+ .slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
335
569
  .map(truncateSubagentActivity),
336
570
  info: {
337
571
  id: agentId,
@@ -339,58 +573,146 @@ async function getSessionMessages(sessionId, providerSessionId, limit, offset) {
339
573
  type: meta.agentType,
340
574
  description: meta.description,
341
575
  model: transcript.model,
342
- status: 'completed',
343
576
  activityCount: transcript.activity.length,
344
577
  },
345
578
  });
346
579
  }
347
- // An async agent's launch result is internal bookkeeping ("Async agent
348
- // launched successfully…"); its real answer arrives later as a separate
349
- // `<task-notification>` turn. Folding the notification back onto the tool
350
- // call that started the agent keeps one card per agent instead of a card,
351
- // an unrelated status line, and a stray markdown reply.
580
+ // Read each workflow run's journal once, keyed by the directory the launch
581
+ // row names. The journal is the only thing that says which of a run's
582
+ // agents are still going — see `readWorkflowJournal`.
583
+ const workflowAgentsByDir = new Map();
584
+ for (const message of messages) {
585
+ const transcriptDir = message.toolUseResult?.transcriptDir;
586
+ if (readBackgroundLaunch(message.toolUseResult) === 'workflow'
587
+ && typeof transcriptDir === 'string'
588
+ && transcriptDir
589
+ && !workflowAgentsByDir.has(transcriptDir)) {
590
+ workflowAgentsByDir.set(transcriptDir, await readWorkflowJournal(transcriptDir));
591
+ }
592
+ }
593
+ // A background launch's tool result is internal bookkeeping ("Async agent
594
+ // launched successfully…", "Workflow launched in background. Task ID: …",
595
+ // "Command running in background with ID: …"); its real answer arrives
596
+ // later as a separate `<task-notification>` turn. Folding the notification
597
+ // back onto the tool call that started the work keeps one card per launch
598
+ // instead of a card, an unrelated status line, and a stray markdown reply.
599
+ // Reading only rows with an `agentId` left a workflow's card on its launch
600
+ // acknowledgement forever, with its report rendering as a raw user bubble
601
+ // further down or, for a queue-operation record, nowhere at all.
352
602
  const notificationsByToolUseId = collectTaskNotifications(messages);
353
603
  const foldedNotificationUuids = new Set();
604
+ // A background agent runs inside the CLI process that launched it, so once
605
+ // that process is gone an agent that has not reported back never will. A
606
+ // later run of the same session is a new process, so a launch row older
607
+ // than the live run belongs to a process that has already exited. The
608
+ // runtime keys its process map by the app session id when the chat gateway
609
+ // starts a run and by the provider-native id when the agent API does, so
610
+ // both are asked.
611
+ const liveRunStartedAt = getLiveRunStartTime(sessionId) ?? getLiveRunStartTime(providerSessionId);
612
+ const launchedByLiveRun = (message) => liveRunStartedAt !== null && Date.parse(String(message.timestamp ?? '')) >= liveRunStartedAt;
354
613
  for (const message of messages) {
355
- const agentId = message.toolUseResult?.agentId;
356
- if (!agentId) {
614
+ const launch = readBackgroundLaunch(message.toolUseResult);
615
+ if (!launch) {
357
616
  continue;
358
617
  }
359
- const subagent = subagentsById.get(String(agentId));
360
- const toolUseId = readAgentToolUseId(message);
618
+ const toolUseId = readLaunchToolUseId(message);
361
619
  const notification = toolUseId ? notificationsByToolUseId.get(toolUseId) : undefined;
362
- // An async agent's launch row never tells you it finished — only the
363
- // later notification does. When that notification is missing (a live run,
364
- // or one compacted out of the transcript), the agent's own transcript is
365
- // the evidence: a timeline that does not stop mid-tool-call is done.
366
- const isAwaitingAsyncAgent = message.toolUseResult?.isAsync === true
367
- && !notification
368
- && (!subagent || subagent.endedMidToolCall);
369
- if (subagent) {
370
- if (subagent.activity.length > 0) {
620
+ // A workflow and a backgrounded shell command have no foreground form;
621
+ // only an agent can hand its answer back inline.
622
+ const isAsyncLaunch = launch !== 'agent' || message.toolUseResult?.isAsync === true;
623
+ // An async launch row never tells you it finished — only the later
624
+ // notification does, so a missing notification means the outcome is
625
+ // still unknown.
626
+ //
627
+ // The agent's own transcript cannot stand in for that. A background agent
628
+ // routinely ends its turn cleanly while its work is still outstanding: it
629
+ // replies "I've started that, waiting for it to finish" and stops, or it
630
+ // has live background children of its own, which is precisely when the
631
+ // harness holds the notification back. Treating a cleanly-ended timeline
632
+ // as proof of completion therefore marked every background agent
633
+ // `completed` the moment it launched, collapsing its card and dropping
634
+ // its spinner while it was still working.
635
+ //
636
+ // "Unknown" only means "still outstanding" while the process the agent
637
+ // runs in is alive. Once it is gone — the run was stopped or crashed, or
638
+ // this transcript is a fork, which copies the parent's conversation rows
639
+ // but not its agent files or queue-operation records — nothing can still
640
+ // report, and reading that as `running` pinned a spinner on the card
641
+ // forever.
642
+ //
643
+ // A synchronous agent hands its answer back inline on its own tool
644
+ // result, so its status never depends on a notification or the process.
645
+ const unreportedStatus = isAsyncLaunch
646
+ ? launchedByLiveRun(message) ? 'running' : 'stopped'
647
+ : 'completed';
648
+ if (launch === 'agent') {
649
+ const agentId = String(message.toolUseResult.agentId);
650
+ const subagent = subagentsById.get(agentId);
651
+ const status = notification
652
+ ? notification.status === 'completed' ? 'completed' : 'failed'
653
+ : unreportedStatus;
654
+ if (subagent && subagent.activity.length > 0) {
371
655
  message.subagentTools = subagent.activity;
372
656
  }
657
+ // The agent's own transcript names its type, model and timeline, but
658
+ // it is not what says whether the agent finished, and it can be
659
+ // missing — a fork copies only the parent's `.jsonl`. The card reads
660
+ // its status off `subagent`, so the launch row publishes one with or
661
+ // without it; otherwise a notification saying "completed" left the
662
+ // card `running`.
373
663
  message.subagent = {
374
- ...subagent.info,
375
- description: subagent.info.description
664
+ id: agentId,
665
+ ...subagent?.info,
666
+ description: subagent?.info.description
376
667
  ?? (typeof message.toolUseResult?.description === 'string' ? message.toolUseResult.description : undefined),
377
- model: subagent.info.model
668
+ model: subagent?.info.model
378
669
  ?? (typeof message.toolUseResult?.resolvedModel === 'string' ? message.toolUseResult.resolvedModel : undefined),
379
- status: isAwaitingAsyncAgent
380
- ? 'running'
381
- : notification && notification.status !== 'completed'
382
- ? 'failed'
383
- : 'completed',
670
+ status,
671
+ };
672
+ }
673
+ else if (launch === 'workflow') {
674
+ // The notification's own word is kept here, `stopped` included: a
675
+ // workflow the user stopped is not one that failed.
676
+ const status = notification
677
+ ? notification.status === 'completed' || notification.status === 'stopped' ? notification.status : 'failed'
678
+ : unreportedStatus;
679
+ // The journal marks an agent `started` and later `result`/`failed`.
680
+ // Once the run itself has settled, a `started` with no second record
681
+ // is not still going: the run was stopped under it, or a resume re-ran
682
+ // the step under a new id. Drawing those as running put a pulsing dot
683
+ // on a finished card.
684
+ const agents = (workflowAgentsByDir.get(String(message.toolUseResult.transcriptDir ?? '')) ?? [])
685
+ .map((agent) => (status !== 'running' && agent.status === 'running' ? { ...agent, status: 'stopped' } : agent));
686
+ const countWith = (agentStatus) => agents.filter((agent) => agent.status === agentStatus).length;
687
+ message.workflow = {
688
+ runId: String(message.toolUseResult.runId ?? ''),
689
+ name: String(message.toolUseResult.workflowName ?? ''),
690
+ description: typeof message.toolUseResult.summary === 'string' ? message.toolUseResult.summary : undefined,
691
+ status,
692
+ agents,
693
+ agentCounts: {
694
+ total: agents.length,
695
+ completed: countWith('completed'),
696
+ failed: countWith('failed'),
697
+ running: countWith('running'),
698
+ stopped: countWith('stopped'),
699
+ },
700
+ scriptPath: typeof message.toolUseResult.scriptPath === 'string' ? message.toolUseResult.scriptPath : undefined,
384
701
  };
385
702
  }
386
703
  if (notification) {
387
- replaceAgentToolResultContent(message, notification.result || notification.summary);
388
- foldedNotificationUuids.add(notification.sourceUuid);
704
+ replaceLaunchToolResultContent(message, notification.result || notification.summary);
705
+ for (const sourceUuid of notification.sourceUuids) {
706
+ foldedNotificationUuids.add(sourceUuid);
707
+ }
389
708
  }
390
- else if (message.toolUseResult?.isAsync === true) {
391
- // Without a notification there is no answer to show, and the launch
392
- // acknowledgement is internal bookkeeping the user must never read.
393
- replaceAgentToolResultContent(message, '');
709
+ else if (isAsyncLaunch && launch !== 'bash') {
710
+ // Without a notification there is no answer to show, and an agent's
711
+ // or workflow's launch acknowledgement is internal bookkeeping the
712
+ // user must never read. A backgrounded command's acknowledgement is
713
+ // different: it names the output file, which is the only handle on
714
+ // the command's output until it reports, so it stays.
715
+ replaceLaunchToolResultContent(message, '');
394
716
  }
395
717
  }
396
718
  const sortedMessages = messages
@@ -489,14 +811,156 @@ function buildLocalCommandDisplayText(payload) {
489
811
  }
490
812
  return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
491
813
  }
814
+ const readOptionalString = (value) => typeof value === 'string' && value ? value : undefined;
815
+ /**
816
+ * The model an assistant row reports, or undefined when there is nothing
817
+ * honest to report.
818
+ *
819
+ * Rows Claude Code fabricated locally — a usage-limit notice, an API-error
820
+ * placeholder — are stamped `<synthetic>`; labelling one with a model would
821
+ * claim a request that never ran, so those are dropped here rather than
822
+ * guessed at further down.
823
+ */
824
+ const readAssistantModel = (value) => {
825
+ const model = readOptionalString(value)?.trim();
826
+ return model && !isPlaceholderProviderModel(model) ? model : undefined;
827
+ };
828
+ /** The CLI's `{total_tokens, tool_uses, duration_ms}` in the app's spelling. */
829
+ function readTaskUsage(value) {
830
+ const usage = readObjectRecord(value);
831
+ if (!usage) {
832
+ return undefined;
833
+ }
834
+ return {
835
+ totalTokens: Number(usage.total_tokens) || 0,
836
+ toolUses: Number(usage.tool_uses) || 0,
837
+ durationMs: Number(usage.duration_ms) || 0,
838
+ };
839
+ }
840
+ const readOptionalNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
841
+ /**
842
+ * The SDK's word on one workflow agent, in the app's spelling.
843
+ *
844
+ * The SDK reports `start` for an agent the script has queued as well as one
845
+ * that is running; only an `agentId` — the transcript the agent writes — tells
846
+ * the two apart. `progress` is a running agent that has reported, `done` one
847
+ * that finished, and anything else (`error`, `failed`) one that did not.
848
+ */
849
+ function readWorkflowAgentState(state, agentId) {
850
+ switch (state) {
851
+ case 'start':
852
+ return agentId ? 'running' : 'queued';
853
+ case 'progress':
854
+ return 'running';
855
+ case 'done':
856
+ return 'done';
857
+ default:
858
+ return 'failed';
859
+ }
860
+ }
492
861
  /**
493
- * Claude local-command stdout may contain ANSI styling codes because it was
494
- * captured from the terminal. The web chat should receive readable plain text.
862
+ * The per-agent progress a workflow's `task_progress` carries in its
863
+ * undocumented `workflow_progress`, or undefined when the event has none (an
864
+ * agent's or shell command's progress, or a workflow that has not spawned).
495
865
  */
496
- function stripAnsiFormatting(text) {
497
- return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
866
+ function readWorkflowAgentsProgress(value) {
867
+ if (!Array.isArray(value)) {
868
+ return undefined;
869
+ }
870
+ const agents = [];
871
+ for (const raw of value) {
872
+ const entry = readObjectRecord(raw);
873
+ const index = readOptionalNumber(entry?.index);
874
+ if (!entry || entry.type !== 'workflow_agent' || index === undefined) {
875
+ continue;
876
+ }
877
+ const agentId = readOptionalString(entry.agentId);
878
+ agents.push({
879
+ index,
880
+ label: readOptionalString(entry.label),
881
+ phase: readOptionalString(entry.phaseTitle),
882
+ agentId,
883
+ model: readOptionalString(entry.model),
884
+ state: readWorkflowAgentState(entry.state, agentId),
885
+ startedAt: readOptionalNumber(entry.startedAt),
886
+ lastToolName: readOptionalString(entry.lastToolName),
887
+ lastToolSummary: readOptionalString(entry.lastToolSummary),
888
+ promptPreview: readOptionalString(entry.promptPreview),
889
+ tokens: readOptionalNumber(entry.tokens),
890
+ toolCalls: readOptionalNumber(entry.toolCalls),
891
+ durationMs: readOptionalNumber(entry.durationMs),
892
+ resultPreview: readOptionalString(entry.resultPreview),
893
+ });
894
+ }
895
+ return agents;
896
+ }
897
+ /**
898
+ * Maps one of the SDK's four background-task `system` events onto the
899
+ * `task_status` fields, or returns null for any other `system` subtype.
900
+ *
901
+ * `task_started` and `task_progress` name the tool call that launched the
902
+ * task; `task_updated` carries only the task id and a patch of what changed,
903
+ * so the client has to remember the id from the start event. The patch's
904
+ * `killed` is spelled `stopped` here, the word the task notification and the
905
+ * history reader already use for a task that ended without an outcome.
906
+ */
907
+ function readTaskStatusEvent(raw) {
908
+ const taskId = readOptionalString(raw.task_id);
909
+ if (!taskId) {
910
+ return null;
911
+ }
912
+ const shared = {
913
+ taskId,
914
+ toolUseId: readOptionalString(raw.tool_use_id),
915
+ };
916
+ switch (raw.subtype) {
917
+ case 'task_started':
918
+ return {
919
+ ...shared,
920
+ event: 'started',
921
+ taskType: readOptionalString(raw.task_type),
922
+ workflowName: readOptionalString(raw.workflow_name),
923
+ description: readOptionalString(raw.description),
924
+ };
925
+ case 'task_progress':
926
+ // A workflow's progress reports on each agent it spawned. Its
927
+ // task-level `last_tool_name` is the current agent's label rather than
928
+ // a tool — and nothing draws a task's, so it is not forwarded; the
929
+ // client reads the current agent from `agents`.
930
+ return {
931
+ ...shared,
932
+ event: 'progress',
933
+ description: readOptionalString(raw.description),
934
+ summary: readOptionalString(raw.summary),
935
+ usage: readTaskUsage(raw.usage),
936
+ agents: readWorkflowAgentsProgress(raw.workflow_progress),
937
+ };
938
+ case 'task_updated': {
939
+ const patched = readOptionalString(readObjectRecord(raw.patch)?.status);
940
+ return {
941
+ ...shared,
942
+ event: 'updated',
943
+ status: patched === 'killed' ? 'stopped' : patched,
944
+ };
945
+ }
946
+ case 'task_notification':
947
+ return {
948
+ ...shared,
949
+ event: 'notification',
950
+ status: readOptionalString(raw.status),
951
+ summary: readOptionalString(raw.summary),
952
+ usage: readTaskUsage(raw.usage),
953
+ outputFile: readOptionalString(raw.output_file),
954
+ };
955
+ default:
956
+ return null;
957
+ }
498
958
  }
499
959
  export class ClaudeSessionsProvider {
960
+ getLiveRunStartTime;
961
+ constructor({ getLiveRunStartTime = getClaudeSDKSessionStartTime } = {}) {
962
+ this.getLiveRunStartTime = getLiveRunStartTime;
963
+ }
500
964
  /**
501
965
  * Normalizes one Claude JSONL entry or live SDK stream event into the shared
502
966
  * message shape consumed by REST and WebSocket clients.
@@ -536,6 +1000,80 @@ export class ClaudeSessionsProvider {
536
1000
  const messages = [];
537
1001
  const ts = raw.timestamp || new Date().toISOString();
538
1002
  const baseId = raw.uuid || generateMessageId('claude');
1003
+ // The live stream reports on background work — a launched workflow, agent
1004
+ // or shell command — through four `system` subtypes that the transcript
1005
+ // never records. They normalize to one `task_status` kind so the client
1006
+ // can keep a workflow's card current while it runs; before this they fell
1007
+ // through every branch below and were dropped.
1008
+ if (raw.type === 'system') {
1009
+ const taskStatus = readTaskStatusEvent(raw);
1010
+ if (taskStatus) {
1011
+ return [createNormalizedMessage({
1012
+ id: baseId,
1013
+ sessionId,
1014
+ timestamp: ts,
1015
+ provider: PROVIDER,
1016
+ kind: 'task_status',
1017
+ ...taskStatus,
1018
+ })];
1019
+ }
1020
+ }
1021
+ // A compaction is the most expensive thing a long session does without
1022
+ // being asked, and neither record that describes it survives the branches
1023
+ // below: `system` events normalize to nothing. Both become one ordinary
1024
+ // assistant row, so a client that knows nothing of `compact` still reads
1025
+ // the sentence, while one that does can draw it as a row of its own.
1026
+ if (raw.type === 'system' && (raw.subtype === 'compact_boundary' || raw.subtype === 'status')) {
1027
+ const compactionRow = (content, compact) => [
1028
+ createNormalizedMessage({
1029
+ id: `${baseId}_compact`,
1030
+ sessionId,
1031
+ timestamp: ts,
1032
+ provider: PROVIDER,
1033
+ kind: 'text',
1034
+ role: 'assistant',
1035
+ content,
1036
+ compact,
1037
+ }),
1038
+ ];
1039
+ if (raw.subtype === 'compact_boundary') {
1040
+ // `compact_metadata` live, `compactMetadata` on disk.
1041
+ const metadata = readObjectRecord(raw.compact_metadata ?? raw.compactMetadata) ?? {};
1042
+ const trigger = metadata.trigger === 'manual' ? 'manual' : 'auto';
1043
+ const preTokens = Number(metadata.pre_tokens ?? metadata.preTokens) || 0;
1044
+ const postTokens = Number(metadata.post_tokens ?? metadata.postTokens) || 0;
1045
+ const durationMs = Number(metadata.duration_ms ?? metadata.durationMs) || 0;
1046
+ const said = ['Compacted', trigger];
1047
+ if (preTokens) {
1048
+ said.push(postTokens
1049
+ ? `${formatTokenCount(preTokens)} → ${formatTokenCount(postTokens)} tokens`
1050
+ : `${formatTokenCount(preTokens)} tokens`);
1051
+ }
1052
+ if (durationMs) {
1053
+ said.push(formatDuration(durationMs));
1054
+ }
1055
+ return compactionRow(said.join(' · '), {
1056
+ phase: 'done',
1057
+ trigger,
1058
+ preTokens,
1059
+ postTokens,
1060
+ durationMs,
1061
+ });
1062
+ }
1063
+ // A status message reports several things, and compaction is the only one
1064
+ // with anything to say: every other turn sends `requesting` or nothing.
1065
+ if (raw.status === 'compacting') {
1066
+ return compactionRow('Compacting conversation…', { phase: 'running' });
1067
+ }
1068
+ if (raw.compact_result === 'failed') {
1069
+ const error = typeof raw.compact_error === 'string' ? raw.compact_error : null;
1070
+ return compactionRow(`Compaction failed${error ? `: ${error}` : ''}`, {
1071
+ phase: 'failed',
1072
+ error,
1073
+ });
1074
+ }
1075
+ return [];
1076
+ }
539
1077
  if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
540
1078
  if (Array.isArray(raw.message.content)) {
541
1079
  // Image attachments sent through the SDK are persisted as base64
@@ -562,7 +1100,14 @@ export class ClaudeSessionsProvider {
562
1100
  toolId: part.tool_use_id,
563
1101
  content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
564
1102
  isError: Boolean(part.is_error),
565
- toolUseResult: raw.toolUseResult,
1103
+ // `toolUseResult` on disk, `tool_use_result` on the live SDK
1104
+ // stream. Reading only the transcript key meant a live agent
1105
+ // launch reached the client with no `isAsync`, so the card could
1106
+ // not tell the launch acknowledgement from an answer. The stream
1107
+ // value is the tool's whole structured output — an Edit carries
1108
+ // the file it edited — so it gets the same per-string cap the
1109
+ // transcript path applies before anything is sent or buffered.
1110
+ toolUseResult: truncateNestedOutput(raw.toolUseResult ?? raw.tool_use_result),
566
1111
  }));
567
1112
  }
568
1113
  else if (part.type === 'text') {
@@ -679,7 +1224,7 @@ export class ClaudeSessionsProvider {
679
1224
  */
680
1225
  const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
681
1226
  if (localCommandStdout !== null) {
682
- const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
1227
+ const stdoutText = stripAnsiSequences(localCommandStdout).trim();
683
1228
  if (stdoutText) {
684
1229
  messages.push(createNormalizedMessage({
685
1230
  id: baseId,
@@ -749,6 +1294,12 @@ export class ClaudeSessionsProvider {
749
1294
  return messages;
750
1295
  }
751
1296
  if (raw.message?.role === 'assistant' && raw.message?.content) {
1297
+ // Which model actually answered. It rides on the assistant row itself in
1298
+ // both the transcript and the live SDK stream, so history renders it
1299
+ // retroactively with no migration. Only prose carries it: a tool call has
1300
+ // no footer to show it in, and a locally-fabricated row (`<synthetic>`,
1301
+ // e.g. the usage-limit notice) gets no label rather than a guessed one.
1302
+ const answeredByModel = readAssistantModel(raw.message.model);
752
1303
  if (Array.isArray(raw.message.content)) {
753
1304
  let partIndex = 0;
754
1305
  for (const part of raw.message.content) {
@@ -761,6 +1312,7 @@ export class ClaudeSessionsProvider {
761
1312
  kind: 'text',
762
1313
  role: 'assistant',
763
1314
  content: part.text,
1315
+ model: answeredByModel,
764
1316
  }));
765
1317
  }
766
1318
  else if (part.type === 'tool_use') {
@@ -797,6 +1349,7 @@ export class ClaudeSessionsProvider {
797
1349
  kind: 'text',
798
1350
  role: 'assistant',
799
1351
  content: raw.message.content,
1352
+ model: answeredByModel,
800
1353
  }));
801
1354
  }
802
1355
  return messages;
@@ -845,6 +1398,60 @@ export class ClaudeSessionsProvider {
845
1398
  }
846
1399
  return { found: true, resumeThroughId: null };
847
1400
  }
1401
+ /**
1402
+ * Reads one workflow agent's timeline from the transcript the run wrote for
1403
+ * it, under `<projectDir>/<providerSessionId>/subagents/workflows/<runId>/`
1404
+ * — the only place it exists, since the SDK never streams a workflow agent's
1405
+ * rows to the parent session.
1406
+ *
1407
+ * The journal beside it settles the agent when it has; short of that the
1408
+ * agent is running only while the process that spawned it is — the same
1409
+ * rule history applies to a launch row, keyed here on the agent's first row
1410
+ * rather than the launch's.
1411
+ */
1412
+ async readWorkflowAgentActivity(sessionId, runId, agentId) {
1413
+ const session = sessionsDb.getSessionById(sessionId);
1414
+ const jsonlPath = session?.jsonl_path;
1415
+ const providerSessionId = session?.provider_session_id;
1416
+ if (!jsonlPath || !providerSessionId) {
1417
+ return null;
1418
+ }
1419
+ const transcriptDir = path.join(path.dirname(jsonlPath), providerSessionId, 'subagents', 'workflows', runId);
1420
+ const transcriptPath = path.join(transcriptDir, `agent-${agentId}.jsonl`);
1421
+ try {
1422
+ await fsp.access(transcriptPath);
1423
+ }
1424
+ catch {
1425
+ return null;
1426
+ }
1427
+ const [transcript, journalAgents] = await Promise.all([
1428
+ readClaudeSubagentTranscript(transcriptPath),
1429
+ readWorkflowJournal(transcriptDir),
1430
+ ]);
1431
+ const journalAgent = journalAgents.find((agent) => agent.id === agentId);
1432
+ let status;
1433
+ if (journalAgent && journalAgent.status !== 'running') {
1434
+ status = journalAgent.status;
1435
+ }
1436
+ else {
1437
+ const liveRunStartedAt = this.getLiveRunStartTime(sessionId) ?? this.getLiveRunStartTime(providerSessionId);
1438
+ status = liveRunStartedAt !== null && Date.parse(transcript.startedAt ?? '') >= liveRunStartedAt
1439
+ ? 'running'
1440
+ : 'stopped';
1441
+ }
1442
+ return {
1443
+ agent: {
1444
+ id: agentId,
1445
+ label: journalAgent?.label,
1446
+ model: transcript.model,
1447
+ status,
1448
+ },
1449
+ activity: transcript.activity
1450
+ .slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
1451
+ .map(truncateSubagentActivity),
1452
+ activityCount: transcript.activity.length,
1453
+ };
1454
+ }
848
1455
  /**
849
1456
  * Loads Claude JSONL history for a project/session and returns normalized
850
1457
  * messages, preserving the existing pagination behavior from projects.js.
@@ -856,7 +1463,7 @@ export class ClaudeSessionsProvider {
856
1463
  try {
857
1464
  // Load full history first so `total` reflects frontend-normalized messages,
858
1465
  // not raw JSONL records.
859
- result = await getSessionMessages(sessionId, providerSessionId, null, 0);
1466
+ result = await getSessionMessages(sessionId, providerSessionId, null, 0, this.getLiveRunStartTime);
860
1467
  }
861
1468
  catch (error) {
862
1469
  const message = error instanceof Error ? error.message : String(error);
@@ -874,6 +1481,7 @@ export class ClaudeSessionsProvider {
874
1481
  isError: Boolean(part.is_error),
875
1482
  subagentTools: raw.subagentTools,
876
1483
  subagent: raw.subagent,
1484
+ workflow: raw.workflow,
877
1485
  toolUseResult: raw.toolUseResult,
878
1486
  });
879
1487
  }
@@ -899,6 +1507,7 @@ export class ClaudeSessionsProvider {
899
1507
  };
900
1508
  msg.subagentTools = toolResult.subagentTools;
901
1509
  msg.subagent = toolResult.subagent;
1510
+ msg.workflow = toolResult.workflow;
902
1511
  }
903
1512
  }
904
1513
  // Everything the transcript draws, and nothing else — so a page of N rows