@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
@@ -6,31 +6,56 @@ import readline from 'node:readline';
6
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
7
7
  import type {
8
8
  AnyRecord,
9
+ CompactionInfo,
9
10
  FetchHistoryOptions,
10
11
  FetchHistoryResult,
11
12
  NormalizedMessage,
12
13
  SubagentActivity,
13
14
  SubagentInfo,
15
+ TaskUsage,
16
+ WorkflowAgentActivity,
17
+ WorkflowAgentInfo,
18
+ WorkflowAgentProgress,
19
+ WorkflowInfo,
14
20
  } from '@/shared/types.js';
15
21
  import { parseFilesInputTag } from '@/shared/image-attachments.js';
16
- import { prepareTranscriptMessages } from '@/shared/message-unification.js';
22
+ import { prepareTranscriptMessages, truncateNestedOutput } from '@/shared/message-unification.js';
17
23
  import {
18
24
  createNormalizedMessage,
19
25
  generateMessageId,
26
+ isPlaceholderProviderModel,
20
27
  readObjectRecord,
21
28
  sliceTailPage,
29
+ stripAnsiSequences,
22
30
  truncateSubagentActivity,
23
31
  } from '@/shared/utils.js';
24
32
  import { sessionsDb } from '@/modules/database/index.js';
33
+ import { getClaudeSDKSessionStartTime } from '@/modules/providers/list/claude/claude-runtime.provider.js';
25
34
  import { summarizeClaudeTokenUsage } from '@/modules/providers/services/provider-token-usage.service.js';
26
35
 
27
36
  const PROVIDER = 'claude';
28
37
 
38
+ /** Tokens the way the CLI writes them: 725k rather than 724,871. */
39
+ const formatTokenCount = (tokens: number): string => {
40
+ if (tokens >= 1_000_000) {
41
+ return `${(tokens / 1_000_000).toFixed(1)}M`;
42
+ }
43
+ return tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : `${tokens}`;
44
+ };
45
+
46
+ /** A duration the way the CLI writes one: `2m 22s`. */
47
+ const formatDuration = (milliseconds: number): string => {
48
+ const seconds = Math.round(milliseconds / 1_000);
49
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
50
+ };
51
+
29
52
  /**
30
53
  * Upper bound on how much of a subagent's timeline is sent to the client. A
31
54
  * long-running agent can record hundreds of tool calls, and the transcript only
32
55
  * ever shows them behind a collapsed header, so shipping the whole history on
33
- * every load costs far more than it shows.
56
+ * every load costs far more than it shows. The newest steps are the ones
57
+ * kept: a running agent's timeline is polled for what it is doing now, and
58
+ * the card says how many earlier ones were left out.
34
59
  */
35
60
  const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
36
61
 
@@ -39,6 +64,7 @@ type ClaudeToolResult = {
39
64
  isError: boolean;
40
65
  subagentTools?: SubagentActivity[];
41
66
  subagent?: SubagentInfo;
67
+ workflow?: WorkflowInfo;
42
68
  toolUseResult?: unknown;
43
69
  };
44
70
 
@@ -63,11 +89,8 @@ type ClaudeHistoryMessagesResult =
63
89
  type ClaudeSubagentTranscript = {
64
90
  activity: SubagentActivity[];
65
91
  model?: string;
66
- /**
67
- * True when the transcript's last tool call never received a result, which
68
- * is the only evidence in the file that the agent stopped mid-flight.
69
- */
70
- endedMidToolCall: boolean;
92
+ /** When the agent's first row was written: the moment it was spawned. */
93
+ startedAt?: string;
71
94
  };
72
95
 
73
96
  /**
@@ -79,7 +102,7 @@ type ClaudeSubagentTranscript = {
79
102
  */
80
103
  async function readClaudeSubagentTranscript(filePath: string): Promise<ClaudeSubagentTranscript> {
81
104
  const activity: SubagentActivity[] = [];
82
- const transcript: ClaudeSubagentTranscript = { activity, endedMidToolCall: false };
105
+ const transcript: ClaudeSubagentTranscript = { activity };
83
106
  const toolsById = new Map<string, SubagentActivity>();
84
107
 
85
108
  try {
@@ -97,6 +120,9 @@ async function readClaudeSubagentTranscript(filePath: string): Promise<ClaudeSub
97
120
  try {
98
121
  const entry = JSON.parse(line) as AnyRecord;
99
122
  const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
123
+ if (timestamp && !transcript.startedAt) {
124
+ transcript.startedAt = timestamp;
125
+ }
100
126
 
101
127
  if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
102
128
  if (typeof entry.message.model === 'string') {
@@ -160,9 +186,6 @@ async function readClaudeSubagentTranscript(filePath: string): Promise<ClaudeSub
160
186
  console.warn(`Error parsing agent file ${filePath}:`, message);
161
187
  }
162
188
 
163
- const lastActivity = activity[activity.length - 1];
164
- transcript.endedMidToolCall = lastActivity?.kind === 'tool' && !lastActivity.toolResult;
165
-
166
189
  return transcript;
167
190
  }
168
191
 
@@ -215,9 +238,111 @@ async function findClaudeSubagentTranscript(
215
238
  return null;
216
239
  }
217
240
 
241
+ /**
242
+ * Reads which agents a workflow run spawned and how far each got.
243
+ *
244
+ * The run appends one record per event to `<transcriptDir>/journal.jsonl`:
245
+ * `started` names the agent (and the label and phase the script gave it, when
246
+ * it gave any), `result` and `failed` settle it. The journal is what the
247
+ * workflow itself resumes from, so it is the only record of an agent that is
248
+ * still running — its transcript file is written, but nothing in it says the
249
+ * agent is not done. A missing directory or journal means the run left no
250
+ * record here (a fork copies only the parent's transcript), which reads as no
251
+ * agents rather than an error.
252
+ */
253
+ async function readWorkflowJournal(transcriptDir: string): Promise<WorkflowAgentInfo[]> {
254
+ const agentsById = new Map<string, WorkflowAgentInfo>();
255
+
256
+ let journal: string;
257
+ try {
258
+ journal = await fsp.readFile(path.join(transcriptDir, 'journal.jsonl'), 'utf8');
259
+ } catch {
260
+ return [];
261
+ }
262
+
263
+ for (const line of journal.split('\n')) {
264
+ if (!line.trim()) {
265
+ continue;
266
+ }
267
+
268
+ let entry: AnyRecord;
269
+ try {
270
+ entry = JSON.parse(line) as AnyRecord;
271
+ } catch {
272
+ // The workflow appends while this reads, so the last line can be partial.
273
+ continue;
274
+ }
275
+
276
+ const agentId = typeof entry.agentId === 'string' ? entry.agentId : '';
277
+ if (!agentId) {
278
+ continue;
279
+ }
280
+
281
+ if (entry.type === 'started') {
282
+ agentsById.set(agentId, {
283
+ id: agentId,
284
+ label: typeof entry.label === 'string' ? entry.label : undefined,
285
+ phase: typeof entry.phase === 'string' ? entry.phase : undefined,
286
+ status: 'running',
287
+ });
288
+ continue;
289
+ }
290
+
291
+ const agent = agentsById.get(agentId);
292
+ if (!agent) {
293
+ continue;
294
+ }
295
+ if (entry.type === 'result') {
296
+ agent.status = 'completed';
297
+ } else if (entry.type === 'failed') {
298
+ agent.status = 'failed';
299
+ }
300
+ }
301
+
302
+ return [...agentsById.values()];
303
+ }
304
+
305
+ /**
306
+ * Which kind of background work a tool-result row launched, read off the
307
+ * structured `toolUseResult` the CLI stores beside it.
308
+ *
309
+ * All three answer the same way — a launch acknowledgement now, a
310
+ * `<task-notification>` later — but each is recognised by its own key: an
311
+ * `Agent` by the `agentId` of the transcript it writes, a `Workflow` by the
312
+ * `async_launched` status it shares with an agent launch (it has no agent of
313
+ * its own), and a backgrounded `Bash` by the `backgroundTaskId` the shell
314
+ * reports. A synchronous `Agent` also carries `agentId` and is included here
315
+ * because its card is built the same way; only `isAsync` says it launched.
316
+ */
317
+ type ClaudeBackgroundLaunch = 'agent' | 'workflow' | 'bash';
318
+
319
+ function readBackgroundLaunch(toolUseResult: unknown): ClaudeBackgroundLaunch | null {
320
+ const result = readObjectRecord(toolUseResult);
321
+ if (!result) {
322
+ return null;
323
+ }
324
+ if (result.agentId) {
325
+ return 'agent';
326
+ }
327
+ if (result.status === 'async_launched') {
328
+ return 'workflow';
329
+ }
330
+ if (typeof result.backgroundTaskId === 'string' && result.backgroundTaskId) {
331
+ return 'bash';
332
+ }
333
+ return null;
334
+ }
335
+
218
336
  type ClaudeTaskNotification = {
219
- /** `uuid` of the transcript row the notification came from, so it can be dropped. */
220
- sourceUuid: string;
337
+ /**
338
+ * `uuid`s of every transcript row that notified for this call, so all of
339
+ * them can be dropped once the latest one is folded. A task that resumes or
340
+ * is first reported as stopped and later as completed notifies more than
341
+ * once, and every earlier row is superseded bookkeeping. Queue-operation
342
+ * records carry no `uuid` and never render on their own, so they add
343
+ * nothing here.
344
+ */
345
+ sourceUuids: string[];
221
346
  toolUseId: string;
222
347
  status: string;
223
348
  summary: string;
@@ -230,8 +355,46 @@ function readTaggedValue(content: string, tagName: string): string {
230
355
  }
231
356
 
232
357
  /**
233
- * Collects every `<task-notification>` turn keyed by the tool call that
234
- * spawned the agent it reports on.
358
+ * Reads every `<task-notification>` payload one transcript row carries.
359
+ *
360
+ * Claude writes the same completion report in two shapes, and which one you get
361
+ * depends only on when the agent happened to finish:
362
+ *
363
+ * - as an ordinary user-role turn, when the queued notification is delivered
364
+ * between turns (the harness also logs a contentless `dequeue` for it);
365
+ * - as a top-level `queue-operation` record whose `content` holds the payload,
366
+ * when the harness consumes it inline while the parent turn is still running
367
+ * (`remove`), or when the session ends before it is ever delivered
368
+ * (`enqueue`).
369
+ *
370
+ * The operation names the queue's bookkeeping, not whether the agent finished,
371
+ * so every queue-operation payload counts. Reading only the user-role shape
372
+ * loses the majority of them: across the 919 transcripts this was measured on,
373
+ * 31 of 56 background agent launches report *only* through a queue-operation
374
+ * record, and their result and status were invisible to every consumer.
375
+ */
376
+ function readTaskNotificationTexts(message: AnyRecord): string[] {
377
+ if (message.type === 'queue-operation') {
378
+ return typeof message.content === 'string' ? [message.content] : [];
379
+ }
380
+
381
+ if (message.message?.role !== 'user') {
382
+ return [];
383
+ }
384
+
385
+ const content = message.message.content;
386
+ if (typeof content === 'string') {
387
+ return [content];
388
+ }
389
+
390
+ return Array.isArray(content)
391
+ ? content.filter((part: AnyRecord) => part?.type === 'text').map((part: AnyRecord) => String(part.text ?? ''))
392
+ : [];
393
+ }
394
+
395
+ /**
396
+ * Collects every `<task-notification>` keyed by the tool call that spawned the
397
+ * agent it reports on.
235
398
  *
236
399
  * Only notifications that name a tool-use id are collected: without one there
237
400
  * is no card to fold them into, and they must keep rendering on their own.
@@ -240,18 +403,7 @@ function collectTaskNotifications(messages: AnyRecord[]): Map<string, ClaudeTask
240
403
  const notifications = new Map<string, ClaudeTaskNotification>();
241
404
 
242
405
  for (const message of messages) {
243
- if (message.message?.role !== 'user') {
244
- continue;
245
- }
246
-
247
- const content = message.message.content;
248
- const texts: string[] = typeof content === 'string'
249
- ? [content]
250
- : Array.isArray(content)
251
- ? content.filter((part: AnyRecord) => part?.type === 'text').map((part: AnyRecord) => String(part.text ?? ''))
252
- : [];
253
-
254
- for (const text of texts) {
406
+ for (const text of readTaskNotificationTexts(message)) {
255
407
  if (!text.trimStart().startsWith('<task-notification>')) {
256
408
  continue;
257
409
  }
@@ -261,9 +413,14 @@ function collectTaskNotifications(messages: AnyRecord[]): Map<string, ClaudeTask
261
413
  continue;
262
414
  }
263
415
 
264
- // A resumed agent notifies more than once; the last word wins.
416
+ // A resumed agent notifies more than once; the last word wins, and the
417
+ // rows that carried the earlier words go with it.
418
+ const sourceUuids = notifications.get(toolUseId)?.sourceUuids ?? [];
419
+ if (typeof message.uuid === 'string' && message.uuid) {
420
+ sourceUuids.push(message.uuid);
421
+ }
265
422
  notifications.set(toolUseId, {
266
- sourceUuid: String(message.uuid ?? ''),
423
+ sourceUuids,
267
424
  toolUseId,
268
425
  status: readTaggedValue(text, 'status') || 'completed',
269
426
  summary: readTaggedValue(text, 'summary'),
@@ -275,8 +432,8 @@ function collectTaskNotifications(messages: AnyRecord[]): Map<string, ClaudeTask
275
432
  return notifications;
276
433
  }
277
434
 
278
- /** Reads the `tool_use_id` off the tool-result row that launched an agent. */
279
- function readAgentToolUseId(message: AnyRecord): string | null {
435
+ /** Reads the `tool_use_id` off the tool-result row that launched background work. */
436
+ function readLaunchToolUseId(message: AnyRecord): string | null {
280
437
  const content = message.message?.content;
281
438
  if (!Array.isArray(content)) {
282
439
  return null;
@@ -291,8 +448,8 @@ function readAgentToolUseId(message: AnyRecord): string | null {
291
448
  return null;
292
449
  }
293
450
 
294
- /** Swaps an agent launch acknowledgement for the agent's actual answer. */
295
- function replaceAgentToolResultContent(message: AnyRecord, replacement: string): void {
451
+ /** Swaps a launch acknowledgement for the work's actual answer. */
452
+ function replaceLaunchToolResultContent(message: AnyRecord, replacement: string): void {
296
453
  const content = message.message?.content;
297
454
  if (!Array.isArray(content)) {
298
455
  return;
@@ -349,12 +506,119 @@ function isUserPromptRow(row: AnyRecord): boolean {
349
506
  }
350
507
 
351
508
  /**
352
- * Drops the rows belonging to prompts that were replaced by an edit.
509
+ * Returns the uuids on the chain the next `claude --resume` replays, from the
510
+ * leaf it continues from back to the root.
511
+ *
512
+ * Mirrors the transcript loader of the Claude CLI, which the Agent SDK's
513
+ * `resume` spawns as well. Every CLI process stamps a `last-prompt` row naming
514
+ * the leaf it was on when it exits, so with two writers on one session it is
515
+ * that stamp — not the newest row — that decides which branch is replayed. The
516
+ * newest conversation row only takes over when it descends from the stamped
517
+ * leaf (the stamping process kept going), or when the file carries no stamp.
518
+ * A compact boundary clears the stamp, and `progress` rows are skipped over
519
+ * when linking a row to its parent, as the CLI does.
520
+ */
521
+ function resolveResumePath(rows: AnyRecord[]): Set<string> {
522
+ const conversation = new Map<string, AnyRecord>();
523
+ const progressParents = new Map<string, string | null>();
524
+ const parentOf = (row: AnyRecord): string | undefined => {
525
+ const parentUuid = typeof row.parentUuid === 'string' ? row.parentUuid : undefined;
526
+ if (parentUuid && progressParents.has(parentUuid)) {
527
+ return progressParents.get(parentUuid) ?? undefined;
528
+ }
529
+ return parentUuid;
530
+ };
531
+
532
+ let newestRow: string | undefined;
533
+ let stampedLeaf: string | undefined;
534
+ let stampIsExplicit = false;
535
+
536
+ for (const row of rows) {
537
+ if (row.type === 'progress' && typeof row.uuid === 'string') {
538
+ progressParents.set(row.uuid, parentOf(row) ?? null);
539
+ continue;
540
+ }
541
+ if (typeof row.uuid === 'string'
542
+ && (row.type === 'user' || row.type === 'assistant' || row.type === 'attachment' || row.type === 'system')) {
543
+ conversation.set(row.uuid, row);
544
+ if (row.isSidechain !== true) {
545
+ newestRow = row.uuid;
546
+ stampIsExplicit = false;
547
+ }
548
+ if (row.type === 'system' && row.subtype === 'compact_boundary') {
549
+ stampedLeaf = undefined;
550
+ stampIsExplicit = false;
551
+ }
552
+ } else if (row.type === 'last-prompt' && typeof row.leafUuid === 'string' && row.leafUuid) {
553
+ stampIsExplicit = row.explicit === true || (stampIsExplicit && row.leafUuid === stampedLeaf);
554
+ stampedLeaf = row.leafUuid;
555
+ }
556
+ }
557
+
558
+ const chainFrom = (leaf: string | undefined): string[] => {
559
+ const chain: string[] = [];
560
+ const seen = new Set<string>();
561
+ let cursor = leaf;
562
+ while (cursor && conversation.has(cursor) && !seen.has(cursor)) {
563
+ seen.add(cursor);
564
+ chain.push(cursor);
565
+ cursor = parentOf(conversation.get(cursor) as AnyRecord);
566
+ }
567
+ return chain;
568
+ };
569
+
570
+ let leaf = stampedLeaf && conversation.has(stampedLeaf) ? stampedLeaf : undefined;
571
+ if (leaf && !stampIsExplicit && newestRow && newestRow !== leaf && chainFrom(newestRow).includes(leaf)) {
572
+ leaf = newestRow;
573
+ }
574
+
575
+ return new Set(chainFrom(leaf ?? newestRow));
576
+ }
577
+
578
+ /**
579
+ * Maps each row's uuid to the file position of the newest row in its subtree.
580
+ *
581
+ * The transcript is append-only and a child is always written after its
582
+ * parent, so one backward pass hands every row's position to its parent and
583
+ * each uuid ends up holding the position of the last line its branch grew.
584
+ * Rows after a compact boundary do not link back across it, so this only
585
+ * orders branches within one compaction segment.
586
+ */
587
+ function indexNewestRowPerBranch(rows: AnyRecord[]): Map<string, number> {
588
+ const newestByUuid = new Map<string, number>();
589
+
590
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
591
+ const row = rows[index];
592
+ if (typeof row.uuid !== 'string') {
593
+ continue;
594
+ }
595
+ const newest = newestByUuid.get(row.uuid) ?? index;
596
+ newestByUuid.set(row.uuid, newest);
597
+ if (typeof row.parentUuid === 'string' && (newestByUuid.get(row.parentUuid) ?? -1) < newest) {
598
+ newestByUuid.set(row.parentUuid, newest);
599
+ }
600
+ }
601
+
602
+ return newestByUuid;
603
+ }
604
+
605
+ /**
606
+ * Drops the rows belonging to prompts the next resume will not replay.
353
607
  *
354
608
  * When a message is edited, Claude resumes the conversation partway and appends
355
609
  * the replacement, so two prompts end up sharing one parent and the file holds
356
610
  * both the abandoned attempt and the live one. A flat read would show them
357
- * stacked, which reads as the app having sent the message twice.
611
+ * stacked, which reads as the app having sent the message twice. The same shape
612
+ * appears without an edit when a session is driven from Chat and from the
613
+ * native `claude` CLI at once: both writers branch off the row each of them saw
614
+ * last.
615
+ *
616
+ * Which sibling survives is decided by the branch the runtime will actually
617
+ * resume: the sibling on the chain `resolveResumePath` returns. Going by the
618
+ * newest prompt, or by the newest row, would show a branch the model was never
619
+ * sent whenever the two writers exit in a different order than they wrote. A
620
+ * fork that is not on that chain at all (one made before a compaction) keeps
621
+ * the branch that was appended to last.
358
622
  *
359
623
  * Only sibling *prompts* are treated as a fork. Branch points made by parallel
360
624
  * tool calls are extremely common — one assistant turn writes several chained
@@ -375,15 +639,31 @@ function dropSupersededPromptBranches(rows: AnyRecord[]): AnyRecord[] {
375
639
  }
376
640
  }
377
641
 
642
+ const forks = [...promptSiblings.values()].filter((siblings) => siblings.length > 1);
643
+ if (forks.length === 0) {
644
+ return rows;
645
+ }
646
+
647
+ const resumePath = resolveResumePath(rows);
648
+ let newestByUuid: Map<string, number> | undefined;
649
+ const newestRowOf = (row: AnyRecord): number => {
650
+ newestByUuid ??= indexNewestRowPerBranch(rows);
651
+ return typeof row.uuid === 'string' ? newestByUuid.get(row.uuid) ?? -1 : -1;
652
+ };
653
+
378
654
  const supersededRoots = new Set<string>();
379
- for (const siblings of promptSiblings.values()) {
380
- if (siblings.length < 2) {
381
- continue;
655
+ for (const siblings of forks) {
656
+ let live = siblings.find((row) => typeof row.uuid === 'string' && resumePath.has(row.uuid));
657
+ if (!live) {
658
+ live = siblings[0];
659
+ for (const row of siblings) {
660
+ if (newestRowOf(row) > newestRowOf(live)) {
661
+ live = row;
662
+ }
663
+ }
382
664
  }
383
- // The transcript is append-only, so the last prompt written under a parent
384
- // is the one that replaced the others.
385
- for (const row of siblings.slice(0, -1)) {
386
- if (typeof row.uuid === 'string') {
665
+ for (const row of siblings) {
666
+ if (row !== live && typeof row.uuid === 'string') {
387
667
  supersededRoots.add(row.uuid);
388
668
  }
389
669
  }
@@ -405,11 +685,15 @@ function dropSupersededPromptBranches(rows: AnyRecord[]): AnyRecord[] {
405
685
  return rows.filter((row) => typeof row.uuid !== 'string' || !abandoned.has(row.uuid));
406
686
  }
407
687
 
688
+ /** When the CLI run behind a session started (epoch ms), or null when none is up. */
689
+ type LiveRunStartTimeProbe = (sessionId: string) => number | null;
690
+
408
691
  async function getSessionMessages(
409
692
  sessionId: string,
410
693
  providerSessionId: string,
411
694
  limit: number | null,
412
695
  offset: number,
696
+ getLiveRunStartTime: LiveRunStartTimeProbe,
413
697
  ): Promise<ClaudeHistoryMessagesResult> {
414
698
  try {
415
699
  // The DB row is keyed by the app-facing session id, while the JSONL rows
@@ -435,11 +719,11 @@ async function getSessionMessages(
435
719
  }
436
720
 
437
721
  // Read each spawned agent's own transcript once, then hang it off every
438
- // row that references it.
722
+ // row that references it. Status is not part of this: the transcript
723
+ // cannot say whether the agent finished (see the loop below).
439
724
  const subagentsById = new Map<string, {
440
725
  activity: SubagentActivity[];
441
- info: SubagentInfo;
442
- endedMidToolCall: boolean;
726
+ info: Omit<SubagentInfo, 'status'>;
443
727
  }>();
444
728
  for (const agentId of agentIds) {
445
729
  const located = await findClaudeSubagentTranscript(projectDir, providerSessionId, agentId);
@@ -453,9 +737,8 @@ async function getSessionMessages(
453
737
  ]);
454
738
 
455
739
  subagentsById.set(agentId, {
456
- endedMidToolCall: transcript.endedMidToolCall,
457
740
  activity: transcript.activity
458
- .slice(0, MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
741
+ .slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
459
742
  .map(truncateSubagentActivity),
460
743
  info: {
461
744
  id: agentId,
@@ -463,62 +746,158 @@ async function getSessionMessages(
463
746
  type: meta.agentType,
464
747
  description: meta.description,
465
748
  model: transcript.model,
466
- status: 'completed',
467
749
  activityCount: transcript.activity.length,
468
750
  },
469
751
  });
470
752
  }
471
753
 
472
- // An async agent's launch result is internal bookkeeping ("Async agent
473
- // launched successfully…"); its real answer arrives later as a separate
474
- // `<task-notification>` turn. Folding the notification back onto the tool
475
- // call that started the agent keeps one card per agent instead of a card,
476
- // an unrelated status line, and a stray markdown reply.
754
+ // Read each workflow run's journal once, keyed by the directory the launch
755
+ // row names. The journal is the only thing that says which of a run's
756
+ // agents are still going — see `readWorkflowJournal`.
757
+ const workflowAgentsByDir = new Map<string, WorkflowAgentInfo[]>();
758
+ for (const message of messages) {
759
+ const transcriptDir = message.toolUseResult?.transcriptDir;
760
+ if (
761
+ readBackgroundLaunch(message.toolUseResult) === 'workflow'
762
+ && typeof transcriptDir === 'string'
763
+ && transcriptDir
764
+ && !workflowAgentsByDir.has(transcriptDir)
765
+ ) {
766
+ workflowAgentsByDir.set(transcriptDir, await readWorkflowJournal(transcriptDir));
767
+ }
768
+ }
769
+
770
+ // A background launch's tool result is internal bookkeeping ("Async agent
771
+ // launched successfully…", "Workflow launched in background. Task ID: …",
772
+ // "Command running in background with ID: …"); its real answer arrives
773
+ // later as a separate `<task-notification>` turn. Folding the notification
774
+ // back onto the tool call that started the work keeps one card per launch
775
+ // instead of a card, an unrelated status line, and a stray markdown reply.
776
+ // Reading only rows with an `agentId` left a workflow's card on its launch
777
+ // acknowledgement forever, with its report rendering as a raw user bubble
778
+ // further down or, for a queue-operation record, nowhere at all.
477
779
  const notificationsByToolUseId = collectTaskNotifications(messages);
478
780
  const foldedNotificationUuids = new Set<string>();
479
781
 
782
+ // A background agent runs inside the CLI process that launched it, so once
783
+ // that process is gone an agent that has not reported back never will. A
784
+ // later run of the same session is a new process, so a launch row older
785
+ // than the live run belongs to a process that has already exited. The
786
+ // runtime keys its process map by the app session id when the chat gateway
787
+ // starts a run and by the provider-native id when the agent API does, so
788
+ // both are asked.
789
+ const liveRunStartedAt = getLiveRunStartTime(sessionId) ?? getLiveRunStartTime(providerSessionId);
790
+ const launchedByLiveRun = (message: AnyRecord): boolean =>
791
+ liveRunStartedAt !== null && Date.parse(String(message.timestamp ?? '')) >= liveRunStartedAt;
792
+
480
793
  for (const message of messages) {
481
- const agentId = message.toolUseResult?.agentId;
482
- if (!agentId) {
794
+ const launch = readBackgroundLaunch(message.toolUseResult);
795
+ if (!launch) {
483
796
  continue;
484
797
  }
485
798
 
486
- const subagent = subagentsById.get(String(agentId));
487
- const toolUseId = readAgentToolUseId(message);
799
+ const toolUseId = readLaunchToolUseId(message);
488
800
  const notification = toolUseId ? notificationsByToolUseId.get(toolUseId) : undefined;
489
- // An async agent's launch row never tells you it finished — only the
490
- // later notification does. When that notification is missing (a live run,
491
- // or one compacted out of the transcript), the agent's own transcript is
492
- // the evidence: a timeline that does not stop mid-tool-call is done.
493
- const isAwaitingAsyncAgent = message.toolUseResult?.isAsync === true
494
- && !notification
495
- && (!subagent || subagent.endedMidToolCall);
496
-
497
- if (subagent) {
498
- if (subagent.activity.length > 0) {
801
+ // A workflow and a backgrounded shell command have no foreground form;
802
+ // only an agent can hand its answer back inline.
803
+ const isAsyncLaunch = launch !== 'agent' || message.toolUseResult?.isAsync === true;
804
+
805
+ // An async launch row never tells you it finished — only the later
806
+ // notification does, so a missing notification means the outcome is
807
+ // still unknown.
808
+ //
809
+ // The agent's own transcript cannot stand in for that. A background agent
810
+ // routinely ends its turn cleanly while its work is still outstanding: it
811
+ // replies "I've started that, waiting for it to finish" and stops, or it
812
+ // has live background children of its own, which is precisely when the
813
+ // harness holds the notification back. Treating a cleanly-ended timeline
814
+ // as proof of completion therefore marked every background agent
815
+ // `completed` the moment it launched, collapsing its card and dropping
816
+ // its spinner while it was still working.
817
+ //
818
+ // "Unknown" only means "still outstanding" while the process the agent
819
+ // runs in is alive. Once it is gone — the run was stopped or crashed, or
820
+ // this transcript is a fork, which copies the parent's conversation rows
821
+ // but not its agent files or queue-operation records — nothing can still
822
+ // report, and reading that as `running` pinned a spinner on the card
823
+ // forever.
824
+ //
825
+ // A synchronous agent hands its answer back inline on its own tool
826
+ // result, so its status never depends on a notification or the process.
827
+ const unreportedStatus = isAsyncLaunch
828
+ ? launchedByLiveRun(message) ? 'running' : 'stopped'
829
+ : 'completed';
830
+
831
+ if (launch === 'agent') {
832
+ const agentId = String(message.toolUseResult.agentId);
833
+ const subagent = subagentsById.get(agentId);
834
+ const status: SubagentInfo['status'] = notification
835
+ ? notification.status === 'completed' ? 'completed' : 'failed'
836
+ : unreportedStatus;
837
+
838
+ if (subagent && subagent.activity.length > 0) {
499
839
  message.subagentTools = subagent.activity;
500
840
  }
841
+ // The agent's own transcript names its type, model and timeline, but
842
+ // it is not what says whether the agent finished, and it can be
843
+ // missing — a fork copies only the parent's `.jsonl`. The card reads
844
+ // its status off `subagent`, so the launch row publishes one with or
845
+ // without it; otherwise a notification saying "completed" left the
846
+ // card `running`.
501
847
  message.subagent = {
502
- ...subagent.info,
503
- description: subagent.info.description
848
+ id: agentId,
849
+ ...subagent?.info,
850
+ description: subagent?.info.description
504
851
  ?? (typeof message.toolUseResult?.description === 'string' ? message.toolUseResult.description : undefined),
505
- model: subagent.info.model
852
+ model: subagent?.info.model
506
853
  ?? (typeof message.toolUseResult?.resolvedModel === 'string' ? message.toolUseResult.resolvedModel : undefined),
507
- status: isAwaitingAsyncAgent
508
- ? 'running'
509
- : notification && notification.status !== 'completed'
510
- ? 'failed'
511
- : 'completed',
854
+ status,
855
+ };
856
+ } else if (launch === 'workflow') {
857
+ // The notification's own word is kept here, `stopped` included: a
858
+ // workflow the user stopped is not one that failed.
859
+ const status: WorkflowInfo['status'] = notification
860
+ ? notification.status === 'completed' || notification.status === 'stopped' ? notification.status : 'failed'
861
+ : unreportedStatus;
862
+ // The journal marks an agent `started` and later `result`/`failed`.
863
+ // Once the run itself has settled, a `started` with no second record
864
+ // is not still going: the run was stopped under it, or a resume re-ran
865
+ // the step under a new id. Drawing those as running put a pulsing dot
866
+ // on a finished card.
867
+ const agents = (workflowAgentsByDir.get(String(message.toolUseResult.transcriptDir ?? '')) ?? [])
868
+ .map((agent) => (status !== 'running' && agent.status === 'running' ? { ...agent, status: 'stopped' as const } : agent));
869
+ const countWith = (agentStatus: WorkflowAgentInfo['status']) =>
870
+ agents.filter((agent) => agent.status === agentStatus).length;
871
+
872
+ message.workflow = {
873
+ runId: String(message.toolUseResult.runId ?? ''),
874
+ name: String(message.toolUseResult.workflowName ?? ''),
875
+ description: typeof message.toolUseResult.summary === 'string' ? message.toolUseResult.summary : undefined,
876
+ status,
877
+ agents,
878
+ agentCounts: {
879
+ total: agents.length,
880
+ completed: countWith('completed'),
881
+ failed: countWith('failed'),
882
+ running: countWith('running'),
883
+ stopped: countWith('stopped'),
884
+ },
885
+ scriptPath: typeof message.toolUseResult.scriptPath === 'string' ? message.toolUseResult.scriptPath : undefined,
512
886
  };
513
887
  }
514
888
 
515
889
  if (notification) {
516
- replaceAgentToolResultContent(message, notification.result || notification.summary);
517
- foldedNotificationUuids.add(notification.sourceUuid);
518
- } else if (message.toolUseResult?.isAsync === true) {
519
- // Without a notification there is no answer to show, and the launch
520
- // acknowledgement is internal bookkeeping the user must never read.
521
- replaceAgentToolResultContent(message, '');
890
+ replaceLaunchToolResultContent(message, notification.result || notification.summary);
891
+ for (const sourceUuid of notification.sourceUuids) {
892
+ foldedNotificationUuids.add(sourceUuid);
893
+ }
894
+ } else if (isAsyncLaunch && launch !== 'bash') {
895
+ // Without a notification there is no answer to show, and an agent's
896
+ // or workflow's launch acknowledgement is internal bookkeeping the
897
+ // user must never read. A backgrounded command's acknowledgement is
898
+ // different: it names the output file, which is the only handle on
899
+ // the command's output until it reports, so it stays.
900
+ replaceLaunchToolResultContent(message, '');
522
901
  }
523
902
  }
524
903
 
@@ -638,15 +1017,185 @@ function buildLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): strin
638
1017
  return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
639
1018
  }
640
1019
 
1020
+ /** The `task_status` fields of a normalized message, minus the envelope. */
1021
+ type ClaudeTaskStatusEvent = Pick<
1022
+ NormalizedMessage,
1023
+ 'event' | 'taskId' | 'toolUseId' | 'taskType' | 'workflowName' | 'description' | 'status' | 'summary' | 'usage' | 'outputFile' | 'agents'
1024
+ >;
1025
+
1026
+ const readOptionalString = (value: unknown): string | undefined =>
1027
+ typeof value === 'string' && value ? value : undefined;
1028
+
641
1029
  /**
642
- * Claude local-command stdout may contain ANSI styling codes because it was
643
- * captured from the terminal. The web chat should receive readable plain text.
1030
+ * The model an assistant row reports, or undefined when there is nothing
1031
+ * honest to report.
1032
+ *
1033
+ * Rows Claude Code fabricated locally — a usage-limit notice, an API-error
1034
+ * placeholder — are stamped `<synthetic>`; labelling one with a model would
1035
+ * claim a request that never ran, so those are dropped here rather than
1036
+ * guessed at further down.
644
1037
  */
645
- function stripAnsiFormatting(text: string): string {
646
- return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
1038
+ const readAssistantModel = (value: unknown): string | undefined => {
1039
+ const model = readOptionalString(value)?.trim();
1040
+ return model && !isPlaceholderProviderModel(model) ? model : undefined;
1041
+ };
1042
+
1043
+ /** The CLI's `{total_tokens, tool_uses, duration_ms}` in the app's spelling. */
1044
+ function readTaskUsage(value: unknown): TaskUsage | undefined {
1045
+ const usage = readObjectRecord(value);
1046
+ if (!usage) {
1047
+ return undefined;
1048
+ }
1049
+ return {
1050
+ totalTokens: Number(usage.total_tokens) || 0,
1051
+ toolUses: Number(usage.tool_uses) || 0,
1052
+ durationMs: Number(usage.duration_ms) || 0,
1053
+ };
647
1054
  }
648
1055
 
1056
+ const readOptionalNumber = (value: unknown): number | undefined =>
1057
+ typeof value === 'number' && Number.isFinite(value) ? value : undefined;
1058
+
1059
+ /**
1060
+ * The SDK's word on one workflow agent, in the app's spelling.
1061
+ *
1062
+ * The SDK reports `start` for an agent the script has queued as well as one
1063
+ * that is running; only an `agentId` — the transcript the agent writes — tells
1064
+ * the two apart. `progress` is a running agent that has reported, `done` one
1065
+ * that finished, and anything else (`error`, `failed`) one that did not.
1066
+ */
1067
+ function readWorkflowAgentState(state: unknown, agentId: string | undefined): WorkflowAgentProgress['state'] {
1068
+ switch (state) {
1069
+ case 'start':
1070
+ return agentId ? 'running' : 'queued';
1071
+ case 'progress':
1072
+ return 'running';
1073
+ case 'done':
1074
+ return 'done';
1075
+ default:
1076
+ return 'failed';
1077
+ }
1078
+ }
1079
+
1080
+ /**
1081
+ * The per-agent progress a workflow's `task_progress` carries in its
1082
+ * undocumented `workflow_progress`, or undefined when the event has none (an
1083
+ * agent's or shell command's progress, or a workflow that has not spawned).
1084
+ */
1085
+ function readWorkflowAgentsProgress(value: unknown): WorkflowAgentProgress[] | undefined {
1086
+ if (!Array.isArray(value)) {
1087
+ return undefined;
1088
+ }
1089
+
1090
+ const agents: WorkflowAgentProgress[] = [];
1091
+ for (const raw of value) {
1092
+ const entry = readObjectRecord(raw);
1093
+ const index = readOptionalNumber(entry?.index);
1094
+ if (!entry || entry.type !== 'workflow_agent' || index === undefined) {
1095
+ continue;
1096
+ }
1097
+ const agentId = readOptionalString(entry.agentId);
1098
+ agents.push({
1099
+ index,
1100
+ label: readOptionalString(entry.label),
1101
+ phase: readOptionalString(entry.phaseTitle),
1102
+ agentId,
1103
+ model: readOptionalString(entry.model),
1104
+ state: readWorkflowAgentState(entry.state, agentId),
1105
+ startedAt: readOptionalNumber(entry.startedAt),
1106
+ lastToolName: readOptionalString(entry.lastToolName),
1107
+ lastToolSummary: readOptionalString(entry.lastToolSummary),
1108
+ promptPreview: readOptionalString(entry.promptPreview),
1109
+ tokens: readOptionalNumber(entry.tokens),
1110
+ toolCalls: readOptionalNumber(entry.toolCalls),
1111
+ durationMs: readOptionalNumber(entry.durationMs),
1112
+ resultPreview: readOptionalString(entry.resultPreview),
1113
+ });
1114
+ }
1115
+ return agents;
1116
+ }
1117
+
1118
+ /**
1119
+ * Maps one of the SDK's four background-task `system` events onto the
1120
+ * `task_status` fields, or returns null for any other `system` subtype.
1121
+ *
1122
+ * `task_started` and `task_progress` name the tool call that launched the
1123
+ * task; `task_updated` carries only the task id and a patch of what changed,
1124
+ * so the client has to remember the id from the start event. The patch's
1125
+ * `killed` is spelled `stopped` here, the word the task notification and the
1126
+ * history reader already use for a task that ended without an outcome.
1127
+ */
1128
+ function readTaskStatusEvent(raw: AnyRecord): ClaudeTaskStatusEvent | null {
1129
+ const taskId = readOptionalString(raw.task_id);
1130
+ if (!taskId) {
1131
+ return null;
1132
+ }
1133
+
1134
+ const shared = {
1135
+ taskId,
1136
+ toolUseId: readOptionalString(raw.tool_use_id),
1137
+ };
1138
+
1139
+ switch (raw.subtype) {
1140
+ case 'task_started':
1141
+ return {
1142
+ ...shared,
1143
+ event: 'started',
1144
+ taskType: readOptionalString(raw.task_type),
1145
+ workflowName: readOptionalString(raw.workflow_name),
1146
+ description: readOptionalString(raw.description),
1147
+ };
1148
+ case 'task_progress':
1149
+ // A workflow's progress reports on each agent it spawned. Its
1150
+ // task-level `last_tool_name` is the current agent's label rather than
1151
+ // a tool — and nothing draws a task's, so it is not forwarded; the
1152
+ // client reads the current agent from `agents`.
1153
+ return {
1154
+ ...shared,
1155
+ event: 'progress',
1156
+ description: readOptionalString(raw.description),
1157
+ summary: readOptionalString(raw.summary),
1158
+ usage: readTaskUsage(raw.usage),
1159
+ agents: readWorkflowAgentsProgress(raw.workflow_progress),
1160
+ };
1161
+ case 'task_updated': {
1162
+ const patched = readOptionalString(readObjectRecord(raw.patch)?.status);
1163
+ return {
1164
+ ...shared,
1165
+ event: 'updated',
1166
+ status: patched === 'killed' ? 'stopped' : patched,
1167
+ };
1168
+ }
1169
+ case 'task_notification':
1170
+ return {
1171
+ ...shared,
1172
+ event: 'notification',
1173
+ status: readOptionalString(raw.status),
1174
+ summary: readOptionalString(raw.summary),
1175
+ usage: readTaskUsage(raw.usage),
1176
+ outputFile: readOptionalString(raw.output_file),
1177
+ };
1178
+ default:
1179
+ return null;
1180
+ }
1181
+ }
1182
+
1183
+ type ClaudeSessionsProviderOptions = {
1184
+ /**
1185
+ * When the CLI run behind a session started, or null when none is up.
1186
+ * Injected so history tests can pin what a stopped run reads as without
1187
+ * driving a real SDK run.
1188
+ */
1189
+ getLiveRunStartTime?: LiveRunStartTimeProbe;
1190
+ };
1191
+
649
1192
  export class ClaudeSessionsProvider implements IProviderSessions {
1193
+ private readonly getLiveRunStartTime: LiveRunStartTimeProbe;
1194
+
1195
+ constructor({ getLiveRunStartTime = getClaudeSDKSessionStartTime }: ClaudeSessionsProviderOptions = {}) {
1196
+ this.getLiveRunStartTime = getLiveRunStartTime;
1197
+ }
1198
+
650
1199
  /**
651
1200
  * Normalizes one Claude JSONL entry or live SDK stream event into the shared
652
1201
  * message shape consumed by REST and WebSocket clients.
@@ -691,6 +1240,86 @@ export class ClaudeSessionsProvider implements IProviderSessions {
691
1240
  const ts = raw.timestamp || new Date().toISOString();
692
1241
  const baseId = raw.uuid || generateMessageId('claude');
693
1242
 
1243
+ // The live stream reports on background work — a launched workflow, agent
1244
+ // or shell command — through four `system` subtypes that the transcript
1245
+ // never records. They normalize to one `task_status` kind so the client
1246
+ // can keep a workflow's card current while it runs; before this they fell
1247
+ // through every branch below and were dropped.
1248
+ if (raw.type === 'system') {
1249
+ const taskStatus = readTaskStatusEvent(raw);
1250
+ if (taskStatus) {
1251
+ return [createNormalizedMessage({
1252
+ id: baseId,
1253
+ sessionId,
1254
+ timestamp: ts,
1255
+ provider: PROVIDER,
1256
+ kind: 'task_status',
1257
+ ...taskStatus,
1258
+ })];
1259
+ }
1260
+ }
1261
+
1262
+ // A compaction is the most expensive thing a long session does without
1263
+ // being asked, and neither record that describes it survives the branches
1264
+ // below: `system` events normalize to nothing. Both become one ordinary
1265
+ // assistant row, so a client that knows nothing of `compact` still reads
1266
+ // the sentence, while one that does can draw it as a row of its own.
1267
+ if (raw.type === 'system' && (raw.subtype === 'compact_boundary' || raw.subtype === 'status')) {
1268
+ const compactionRow = (content: string, compact: CompactionInfo): NormalizedMessage[] => [
1269
+ createNormalizedMessage({
1270
+ id: `${baseId}_compact`,
1271
+ sessionId,
1272
+ timestamp: ts,
1273
+ provider: PROVIDER,
1274
+ kind: 'text',
1275
+ role: 'assistant',
1276
+ content,
1277
+ compact,
1278
+ }),
1279
+ ];
1280
+
1281
+ if (raw.subtype === 'compact_boundary') {
1282
+ // `compact_metadata` live, `compactMetadata` on disk.
1283
+ const metadata = readObjectRecord(raw.compact_metadata ?? raw.compactMetadata) ?? {};
1284
+ const trigger = metadata.trigger === 'manual' ? 'manual' : 'auto';
1285
+ const preTokens = Number(metadata.pre_tokens ?? metadata.preTokens) || 0;
1286
+ const postTokens = Number(metadata.post_tokens ?? metadata.postTokens) || 0;
1287
+ const durationMs = Number(metadata.duration_ms ?? metadata.durationMs) || 0;
1288
+ const said = ['Compacted', trigger];
1289
+ if (preTokens) {
1290
+ said.push(postTokens
1291
+ ? `${formatTokenCount(preTokens)} → ${formatTokenCount(postTokens)} tokens`
1292
+ : `${formatTokenCount(preTokens)} tokens`);
1293
+ }
1294
+ if (durationMs) {
1295
+ said.push(formatDuration(durationMs));
1296
+ }
1297
+
1298
+ return compactionRow(said.join(' · '), {
1299
+ phase: 'done',
1300
+ trigger,
1301
+ preTokens,
1302
+ postTokens,
1303
+ durationMs,
1304
+ });
1305
+ }
1306
+
1307
+ // A status message reports several things, and compaction is the only one
1308
+ // with anything to say: every other turn sends `requesting` or nothing.
1309
+ if (raw.status === 'compacting') {
1310
+ return compactionRow('Compacting conversation…', { phase: 'running' });
1311
+ }
1312
+ if (raw.compact_result === 'failed') {
1313
+ const error = typeof raw.compact_error === 'string' ? raw.compact_error : null;
1314
+ return compactionRow(`Compaction failed${error ? `: ${error}` : ''}`, {
1315
+ phase: 'failed',
1316
+ error,
1317
+ });
1318
+ }
1319
+
1320
+ return [];
1321
+ }
1322
+
694
1323
  if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
695
1324
  if (Array.isArray(raw.message.content)) {
696
1325
  // Image attachments sent through the SDK are persisted as base64
@@ -718,7 +1347,14 @@ export class ClaudeSessionsProvider implements IProviderSessions {
718
1347
  toolId: part.tool_use_id,
719
1348
  content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
720
1349
  isError: Boolean(part.is_error),
721
- toolUseResult: raw.toolUseResult,
1350
+ // `toolUseResult` on disk, `tool_use_result` on the live SDK
1351
+ // stream. Reading only the transcript key meant a live agent
1352
+ // launch reached the client with no `isAsync`, so the card could
1353
+ // not tell the launch acknowledgement from an answer. The stream
1354
+ // value is the tool's whole structured output — an Edit carries
1355
+ // the file it edited — so it gets the same per-string cap the
1356
+ // transcript path applies before anything is sent or buffered.
1357
+ toolUseResult: truncateNestedOutput(raw.toolUseResult ?? raw.tool_use_result),
722
1358
  }));
723
1359
  } else if (part.type === 'text') {
724
1360
  const text = part.text || '';
@@ -840,7 +1476,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
840
1476
  */
841
1477
  const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
842
1478
  if (localCommandStdout !== null) {
843
- const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
1479
+ const stdoutText = stripAnsiSequences(localCommandStdout).trim();
844
1480
  if (stdoutText) {
845
1481
  messages.push(createNormalizedMessage({
846
1482
  id: baseId,
@@ -917,6 +1553,13 @@ export class ClaudeSessionsProvider implements IProviderSessions {
917
1553
  }
918
1554
 
919
1555
  if (raw.message?.role === 'assistant' && raw.message?.content) {
1556
+ // Which model actually answered. It rides on the assistant row itself in
1557
+ // both the transcript and the live SDK stream, so history renders it
1558
+ // retroactively with no migration. Only prose carries it: a tool call has
1559
+ // no footer to show it in, and a locally-fabricated row (`<synthetic>`,
1560
+ // e.g. the usage-limit notice) gets no label rather than a guessed one.
1561
+ const answeredByModel = readAssistantModel(raw.message.model);
1562
+
920
1563
  if (Array.isArray(raw.message.content)) {
921
1564
  let partIndex = 0;
922
1565
  for (const part of raw.message.content) {
@@ -929,6 +1572,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
929
1572
  kind: 'text',
930
1573
  role: 'assistant',
931
1574
  content: part.text,
1575
+ model: answeredByModel,
932
1576
  }));
933
1577
  } else if (part.type === 'tool_use') {
934
1578
  messages.push(createNormalizedMessage({
@@ -962,6 +1606,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
962
1606
  kind: 'text',
963
1607
  role: 'assistant',
964
1608
  content: raw.message.content,
1609
+ model: answeredByModel,
965
1610
  }));
966
1611
  }
967
1612
  return messages;
@@ -1020,6 +1665,67 @@ export class ClaudeSessionsProvider implements IProviderSessions {
1020
1665
  return { found: true, resumeThroughId: null };
1021
1666
  }
1022
1667
 
1668
+ /**
1669
+ * Reads one workflow agent's timeline from the transcript the run wrote for
1670
+ * it, under `<projectDir>/<providerSessionId>/subagents/workflows/<runId>/`
1671
+ * — the only place it exists, since the SDK never streams a workflow agent's
1672
+ * rows to the parent session.
1673
+ *
1674
+ * The journal beside it settles the agent when it has; short of that the
1675
+ * agent is running only while the process that spawned it is — the same
1676
+ * rule history applies to a launch row, keyed here on the agent's first row
1677
+ * rather than the launch's.
1678
+ */
1679
+ async readWorkflowAgentActivity(
1680
+ sessionId: string,
1681
+ runId: string,
1682
+ agentId: string,
1683
+ ): Promise<WorkflowAgentActivity | null> {
1684
+ const session = sessionsDb.getSessionById(sessionId);
1685
+ const jsonlPath = session?.jsonl_path;
1686
+ const providerSessionId = session?.provider_session_id;
1687
+ if (!jsonlPath || !providerSessionId) {
1688
+ return null;
1689
+ }
1690
+
1691
+ const transcriptDir = path.join(path.dirname(jsonlPath), providerSessionId, 'subagents', 'workflows', runId);
1692
+ const transcriptPath = path.join(transcriptDir, `agent-${agentId}.jsonl`);
1693
+ try {
1694
+ await fsp.access(transcriptPath);
1695
+ } catch {
1696
+ return null;
1697
+ }
1698
+
1699
+ const [transcript, journalAgents] = await Promise.all([
1700
+ readClaudeSubagentTranscript(transcriptPath),
1701
+ readWorkflowJournal(transcriptDir),
1702
+ ]);
1703
+ const journalAgent = journalAgents.find((agent) => agent.id === agentId);
1704
+
1705
+ let status: WorkflowAgentActivity['agent']['status'];
1706
+ if (journalAgent && journalAgent.status !== 'running') {
1707
+ status = journalAgent.status;
1708
+ } else {
1709
+ const liveRunStartedAt = this.getLiveRunStartTime(sessionId) ?? this.getLiveRunStartTime(providerSessionId);
1710
+ status = liveRunStartedAt !== null && Date.parse(transcript.startedAt ?? '') >= liveRunStartedAt
1711
+ ? 'running'
1712
+ : 'stopped';
1713
+ }
1714
+
1715
+ return {
1716
+ agent: {
1717
+ id: agentId,
1718
+ label: journalAgent?.label,
1719
+ model: transcript.model,
1720
+ status,
1721
+ },
1722
+ activity: transcript.activity
1723
+ .slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
1724
+ .map(truncateSubagentActivity),
1725
+ activityCount: transcript.activity.length,
1726
+ };
1727
+ }
1728
+
1023
1729
  /**
1024
1730
  * Loads Claude JSONL history for a project/session and returns normalized
1025
1731
  * messages, preserving the existing pagination behavior from projects.js.
@@ -1035,7 +1741,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
1035
1741
  try {
1036
1742
  // Load full history first so `total` reflects frontend-normalized messages,
1037
1743
  // not raw JSONL records.
1038
- result = await getSessionMessages(sessionId, providerSessionId, null, 0);
1744
+ result = await getSessionMessages(sessionId, providerSessionId, null, 0, this.getLiveRunStartTime);
1039
1745
  } catch (error) {
1040
1746
  const message = error instanceof Error ? error.message : String(error);
1041
1747
  console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
@@ -1054,6 +1760,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
1054
1760
  isError: Boolean(part.is_error),
1055
1761
  subagentTools: raw.subagentTools,
1056
1762
  subagent: raw.subagent,
1763
+ workflow: raw.workflow,
1057
1764
  toolUseResult: raw.toolUseResult,
1058
1765
  });
1059
1766
  }
@@ -1082,6 +1789,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
1082
1789
  };
1083
1790
  msg.subagentTools = toolResult.subagentTools;
1084
1791
  msg.subagent = toolResult.subagent;
1792
+ msg.workflow = toolResult.workflow;
1085
1793
  }
1086
1794
  }
1087
1795