@cloudcli-ai/cloudcli 1.37.2 → 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 (453) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.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-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.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-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +11 -4
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.module.js +9 -2
  65. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  66. package/dist-server/server/modules/agent/agent.routes.js +177 -130
  67. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  68. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  69. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  70. package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
  71. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  73. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  74. package/dist-server/server/modules/database/connection.js +1 -1
  75. package/dist-server/server/modules/database/connection.js.map +1 -1
  76. package/dist-server/server/modules/database/index.js +5 -0
  77. package/dist-server/server/modules/database/index.js.map +1 -1
  78. package/dist-server/server/modules/database/migrations.js +45 -1
  79. package/dist-server/server/modules/database/migrations.js.map +1 -1
  80. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  81. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  83. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  84. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  85. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  86. package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
  87. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  88. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  89. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  90. package/dist-server/server/modules/database/schema.js +99 -0
  91. package/dist-server/server/modules/database/schema.js.map +1 -1
  92. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  93. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  94. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  96. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  97. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  98. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  99. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  100. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  101. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  102. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  103. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  104. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  105. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  106. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  107. package/dist-server/server/modules/git/git.routes.js +92 -5
  108. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  109. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  110. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  111. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  112. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  113. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  114. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  115. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  116. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  117. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  118. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  119. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  120. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  121. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  122. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  123. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  124. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  125. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  126. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  127. package/dist-server/server/modules/providers/index.js +3 -0
  128. package/dist-server/server/modules/providers/index.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
  130. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  132. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  133. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
  134. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  135. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
  136. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  137. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  138. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
  140. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  142. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  143. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  144. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  146. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  147. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  148. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  149. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
  150. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
  152. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  155. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
  156. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  157. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  158. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  160. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
  162. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  164. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
  165. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  166. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  167. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  168. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  169. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/provider.routes.js +134 -2
  171. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  172. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  173. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  174. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  175. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  176. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
  177. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  179. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  181. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  182. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  183. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  185. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
  187. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  188. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  189. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  190. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  191. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  192. package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
  193. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
  195. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  196. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  197. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  199. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  201. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  203. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  205. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  206. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
  207. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  209. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  210. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  211. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  212. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  213. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  215. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  217. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
  219. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  221. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  222. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
  223. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  224. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  225. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  226. package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
  227. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  228. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  229. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  230. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  231. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  232. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  233. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  234. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  235. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  236. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  237. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  239. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  240. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
  241. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  242. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  243. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  244. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  245. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  246. package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
  247. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  248. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  249. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  250. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  251. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  252. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  253. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  254. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
  255. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  256. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  257. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  258. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
  259. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  260. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  261. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  262. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  263. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  264. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  265. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  266. package/dist-server/server/modules/user/user.module.js +10 -1
  267. package/dist-server/server/modules/user/user.module.js.map +1 -1
  268. package/dist-server/server/modules/user/user.routes.js +44 -0
  269. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  270. package/dist-server/server/modules/user/user.service.js +75 -0
  271. package/dist-server/server/modules/user/user.service.js.map +1 -1
  272. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  273. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  274. package/dist-server/server/modules/websocket/index.js +7 -0
  275. package/dist-server/server/modules/websocket/index.js.map +1 -1
  276. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
  277. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  278. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  279. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  280. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
  281. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  282. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  283. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  284. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
  285. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  286. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  287. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  288. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  289. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  290. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  291. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  292. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  293. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  294. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  295. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  296. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
  297. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  298. package/dist-server/server/shared/claude-cli-path.js +22 -5
  299. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  300. package/dist-server/server/shared/index.js +3 -0
  301. package/dist-server/server/shared/index.js.map +1 -0
  302. package/dist-server/server/shared/message-unification.js +376 -0
  303. package/dist-server/server/shared/message-unification.js.map +1 -0
  304. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  305. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  306. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  307. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  308. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  309. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  310. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  311. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  312. package/dist-server/server/shared/utils.js +160 -0
  313. package/dist-server/server/shared/utils.js.map +1 -1
  314. package/electron/desktopWindow.js +6 -0
  315. package/electron/viewHost.js +25 -2
  316. package/package.json +20 -17
  317. package/server/index.ts +16 -5
  318. package/server/modules/agent/agent.module.ts +11 -1
  319. package/server/modules/agent/agent.routes.ts +200 -142
  320. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  321. package/server/modules/agent/tests/agent.routes.test.ts +51 -2
  322. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  323. package/server/modules/database/connection.ts +2 -1
  324. package/server/modules/database/index.ts +10 -0
  325. package/server/modules/database/migrations.ts +51 -0
  326. package/server/modules/database/repositories/provider-models.ts +79 -6
  327. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  328. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  329. package/server/modules/database/repositories/sessions.db.ts +204 -3
  330. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  331. package/server/modules/database/schema.ts +103 -0
  332. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  333. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  334. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  335. package/server/modules/file-tree/file-tree.module.ts +2 -1
  336. package/server/modules/file-tree/file-tree.service.ts +49 -7
  337. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  338. package/server/modules/git/git-branch-diff.service.ts +204 -0
  339. package/server/modules/git/git-branch.service.ts +2 -12
  340. package/server/modules/git/git.routes.ts +100 -6
  341. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  342. package/server/modules/projects/project-clone.routes.ts +142 -0
  343. package/server/modules/projects/projects.routes.ts +16 -81
  344. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  345. package/server/modules/projects/services/project-clone.service.ts +102 -34
  346. package/server/modules/projects/services/project-delete.service.ts +13 -1
  347. package/server/modules/projects/services/project-star.service.ts +2 -2
  348. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  349. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  350. package/server/modules/providers/index.ts +4 -0
  351. package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
  352. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  353. package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
  354. package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
  355. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  356. package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
  357. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  358. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  359. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  360. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  361. package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
  362. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
  363. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  364. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
  365. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  366. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  367. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
  368. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  369. package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
  370. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  371. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  372. package/server/modules/providers/provider.routes.ts +162 -2
  373. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  374. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  375. package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
  376. package/server/modules/providers/services/provider-models.service.ts +71 -4
  377. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  378. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  379. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  380. package/server/modules/providers/services/session-history-cache.service.ts +150 -0
  381. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  382. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  383. package/server/modules/providers/services/sessions.service.ts +309 -17
  384. package/server/modules/providers/tests/claude-auth.test.ts +208 -0
  385. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  386. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  387. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  388. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  389. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  390. package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
  391. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  392. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  393. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  394. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  395. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  396. package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
  397. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  398. package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
  399. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  400. package/server/modules/providers/tests/opencode-models.test.ts +266 -8
  401. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  402. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  403. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  404. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  405. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  406. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  407. package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
  408. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  409. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  410. package/server/modules/providers/tests/sessions.service.test.ts +303 -1
  411. package/server/modules/providers/tests/skills.test.ts +21 -5
  412. package/server/modules/scheduled-messages/index.ts +8 -0
  413. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  414. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
  415. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  416. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
  417. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  418. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  419. package/server/modules/user/tests/user.service.test.ts +90 -0
  420. package/server/modules/user/user.module.ts +10 -1
  421. package/server/modules/user/user.routes.ts +44 -0
  422. package/server/modules/user/user.service.ts +105 -0
  423. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  424. package/server/modules/websocket/README.md +4 -2
  425. package/server/modules/websocket/index.ts +8 -0
  426. package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
  427. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  428. package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
  429. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  430. package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
  431. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  432. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  433. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  434. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  435. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  436. package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
  437. package/server/shared/claude-cli-path.ts +24 -6
  438. package/server/shared/index.ts +3 -0
  439. package/server/shared/interfaces.ts +91 -0
  440. package/server/shared/message-unification.ts +449 -0
  441. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  442. package/server/shared/tests/message-unification.test.ts +218 -0
  443. package/server/shared/tests/read-only-roots.test.ts +139 -0
  444. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  445. package/server/shared/types.ts +382 -7
  446. package/server/shared/utils.ts +176 -0
  447. package/dist/assets/channel-_falYbU1.js +0 -1
  448. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  449. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  450. package/dist/assets/index-CzPLBvXT.css +0 -32
  451. package/dist/assets/index-DFkBBuSA.js +0 -1549
  452. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  453. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -4,17 +4,67 @@ import path from 'node:path';
4
4
  import readline from 'node:readline';
5
5
 
6
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
7
- import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
7
+ import type {
8
+ AnyRecord,
9
+ CompactionInfo,
10
+ FetchHistoryOptions,
11
+ FetchHistoryResult,
12
+ NormalizedMessage,
13
+ SubagentActivity,
14
+ SubagentInfo,
15
+ TaskUsage,
16
+ WorkflowAgentActivity,
17
+ WorkflowAgentInfo,
18
+ WorkflowAgentProgress,
19
+ WorkflowInfo,
20
+ } from '@/shared/types.js';
8
21
  import { parseFilesInputTag } from '@/shared/image-attachments.js';
9
- import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js';
22
+ import { prepareTranscriptMessages, truncateNestedOutput } from '@/shared/message-unification.js';
23
+ import {
24
+ createNormalizedMessage,
25
+ generateMessageId,
26
+ isPlaceholderProviderModel,
27
+ readObjectRecord,
28
+ sliceTailPage,
29
+ stripAnsiSequences,
30
+ truncateSubagentActivity,
31
+ } from '@/shared/utils.js';
10
32
  import { sessionsDb } from '@/modules/database/index.js';
33
+ import { getClaudeSDKSessionStartTime } from '@/modules/providers/list/claude/claude-runtime.provider.js';
34
+ import { summarizeClaudeTokenUsage } from '@/modules/providers/services/provider-token-usage.service.js';
11
35
 
12
36
  const PROVIDER = 'claude';
13
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
+
52
+ /**
53
+ * Upper bound on how much of a subagent's timeline is sent to the client. A
54
+ * long-running agent can record hundreds of tool calls, and the transcript only
55
+ * ever shows them behind a collapsed header, so shipping the whole history on
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.
59
+ */
60
+ const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
61
+
14
62
  type ClaudeToolResult = {
15
63
  content: unknown;
16
64
  isError: boolean;
17
- subagentTools?: unknown;
65
+ subagentTools?: SubagentActivity[];
66
+ subagent?: SubagentInfo;
67
+ workflow?: WorkflowInfo;
18
68
  toolUseResult?: unknown;
19
69
  };
20
70
 
@@ -36,8 +86,24 @@ type ClaudeHistoryMessagesResult =
36
86
  limit?: number | null;
37
87
  };
38
88
 
39
- async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
40
- const tools: AnyRecord[] = [];
89
+ type ClaudeSubagentTranscript = {
90
+ activity: SubagentActivity[];
91
+ model?: string;
92
+ /** When the agent's first row was written: the moment it was spawned. */
93
+ startedAt?: string;
94
+ };
95
+
96
+ /**
97
+ * Flattens one subagent transcript into the shared activity timeline.
98
+ *
99
+ * Assistant prose and reasoning are kept alongside the tool calls so the
100
+ * transcript can replay what the agent actually did rather than listing tool
101
+ * names with no narrative.
102
+ */
103
+ async function readClaudeSubagentTranscript(filePath: string): Promise<ClaudeSubagentTranscript> {
104
+ const activity: SubagentActivity[] = [];
105
+ const transcript: ClaudeSubagentTranscript = { activity };
106
+ const toolsById = new Map<string, SubagentActivity>();
41
107
 
42
108
  try {
43
109
  const fileStream = fs.createReadStream(filePath);
@@ -53,16 +119,37 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
53
119
 
54
120
  try {
55
121
  const entry = JSON.parse(line) as AnyRecord;
122
+ const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
123
+ if (timestamp && !transcript.startedAt) {
124
+ transcript.startedAt = timestamp;
125
+ }
56
126
 
57
127
  if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
128
+ if (typeof entry.message.model === 'string') {
129
+ transcript.model = entry.message.model;
130
+ }
131
+
58
132
  for (const part of entry.message.content as AnyRecord[]) {
59
133
  if (part.type === 'tool_use') {
60
- tools.push({
61
- toolId: part.id,
62
- toolName: part.name,
134
+ const tool: SubagentActivity = {
135
+ kind: 'tool',
136
+ toolId: String(part.id ?? ''),
137
+ toolName: String(part.name ?? 'Tool'),
63
138
  toolInput: part.input,
64
- timestamp: entry.timestamp,
65
- });
139
+ timestamp,
140
+ };
141
+ activity.push(tool);
142
+ if (tool.toolId) {
143
+ toolsById.set(tool.toolId, tool);
144
+ }
145
+ continue;
146
+ }
147
+ if (part.type === 'text' && typeof part.text === 'string' && part.text.trim()) {
148
+ activity.push({ kind: 'text', content: part.text, timestamp });
149
+ continue;
150
+ }
151
+ if (part.type === 'thinking' && typeof part.thinking === 'string' && part.thinking.trim()) {
152
+ activity.push({ kind: 'thinking', content: part.thinking, timestamp });
66
153
  }
67
154
  }
68
155
  }
@@ -73,7 +160,7 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
73
160
  continue;
74
161
  }
75
162
 
76
- const tool = tools.find((candidate) => candidate.toolId === part.tool_use_id);
163
+ const tool = toolsById.get(String(part.tool_use_id ?? ''));
77
164
  if (!tool) {
78
165
  continue;
79
166
  }
@@ -99,14 +186,514 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
99
186
  console.warn(`Error parsing agent file ${filePath}:`, message);
100
187
  }
101
188
 
102
- return tools;
189
+ return transcript;
190
+ }
191
+
192
+ type ClaudeSubagentMeta = {
193
+ agentType?: string;
194
+ description?: string;
195
+ };
196
+
197
+ /** Reads the sidecar `.meta.json` Claude writes next to a subagent transcript. */
198
+ async function readClaudeSubagentMeta(metaPath: string): Promise<ClaudeSubagentMeta> {
199
+ try {
200
+ const parsed = JSON.parse(await fsp.readFile(metaPath, 'utf8')) as AnyRecord;
201
+ return {
202
+ agentType: typeof parsed.agentType === 'string' ? parsed.agentType : undefined,
203
+ description: typeof parsed.description === 'string' ? parsed.description : undefined,
204
+ };
205
+ } catch {
206
+ return {};
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Resolves where a subagent's transcript lives.
212
+ *
213
+ * Current Claude versions write it to
214
+ * `<projectDir>/<providerSessionId>/subagents/agent-<agentId>.jsonl`; older
215
+ * ones dropped it next to the parent transcript. Both are checked, newest
216
+ * layout first, because a project directory usually holds sessions from
217
+ * several CLI versions.
218
+ */
219
+ async function findClaudeSubagentTranscript(
220
+ projectDirectory: string,
221
+ providerSessionId: string,
222
+ agentId: string,
223
+ ): Promise<{ transcriptPath: string; metaPath: string } | null> {
224
+ const candidates = [
225
+ path.join(projectDirectory, providerSessionId, 'subagents', `agent-${agentId}.jsonl`),
226
+ path.join(projectDirectory, `agent-${agentId}.jsonl`),
227
+ ];
228
+
229
+ for (const transcriptPath of candidates) {
230
+ try {
231
+ await fsp.access(transcriptPath);
232
+ return { transcriptPath, metaPath: transcriptPath.replace(/\.jsonl$/, '.meta.json') };
233
+ } catch {
234
+ // Try the next layout.
235
+ }
236
+ }
237
+
238
+ return null;
239
+ }
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
+
336
+ type ClaudeTaskNotification = {
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[];
346
+ toolUseId: string;
347
+ status: string;
348
+ summary: string;
349
+ result: string;
350
+ };
351
+
352
+ function readTaggedValue(content: string, tagName: string): string {
353
+ const match = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`).exec(content);
354
+ return match ? match[1].trim() : '';
355
+ }
356
+
357
+ /**
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.
398
+ *
399
+ * Only notifications that name a tool-use id are collected: without one there
400
+ * is no card to fold them into, and they must keep rendering on their own.
401
+ */
402
+ function collectTaskNotifications(messages: AnyRecord[]): Map<string, ClaudeTaskNotification> {
403
+ const notifications = new Map<string, ClaudeTaskNotification>();
404
+
405
+ for (const message of messages) {
406
+ for (const text of readTaskNotificationTexts(message)) {
407
+ if (!text.trimStart().startsWith('<task-notification>')) {
408
+ continue;
409
+ }
410
+
411
+ const toolUseId = readTaggedValue(text, 'tool-use-id');
412
+ if (!toolUseId) {
413
+ continue;
414
+ }
415
+
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
+ }
422
+ notifications.set(toolUseId, {
423
+ sourceUuids,
424
+ toolUseId,
425
+ status: readTaggedValue(text, 'status') || 'completed',
426
+ summary: readTaggedValue(text, 'summary'),
427
+ result: readTaggedValue(text, 'result'),
428
+ });
429
+ }
430
+ }
431
+
432
+ return notifications;
433
+ }
434
+
435
+ /** Reads the `tool_use_id` off the tool-result row that launched background work. */
436
+ function readLaunchToolUseId(message: AnyRecord): string | null {
437
+ const content = message.message?.content;
438
+ if (!Array.isArray(content)) {
439
+ return null;
440
+ }
441
+
442
+ for (const part of content) {
443
+ if (part?.type === 'tool_result' && typeof part.tool_use_id === 'string') {
444
+ return part.tool_use_id;
445
+ }
446
+ }
447
+
448
+ return null;
449
+ }
450
+
451
+ /** Swaps a launch acknowledgement for the work's actual answer. */
452
+ function replaceLaunchToolResultContent(message: AnyRecord, replacement: string): void {
453
+ const content = message.message?.content;
454
+ if (!Array.isArray(content)) {
455
+ return;
456
+ }
457
+
458
+ for (const part of content) {
459
+ if (part?.type === 'tool_result') {
460
+ part.content = replacement;
461
+ }
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Reads a Claude transcript's rows as a graph keyed by `uuid`.
467
+ *
468
+ * The file is append-only and every row names its predecessor in `parentUuid`,
469
+ * so a conversation is a path through it rather than the whole file. Editing a
470
+ * sent message makes a second path appear alongside the first.
471
+ */
472
+ async function readTranscriptRows(jsonlPath: string, providerSessionId: string): Promise<AnyRecord[]> {
473
+ const rows: AnyRecord[] = [];
474
+ const fileStream = fs.createReadStream(jsonlPath);
475
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
476
+
477
+ for await (const line of rl) {
478
+ if (!line.trim()) {
479
+ continue;
480
+ }
481
+ try {
482
+ const entry = JSON.parse(line) as AnyRecord;
483
+ if (entry.sessionId === providerSessionId) {
484
+ rows.push(entry);
485
+ }
486
+ } catch {
487
+ // A row can be half-written while the CLI is streaming into the file.
488
+ }
489
+ }
490
+
491
+ return rows;
492
+ }
493
+
494
+ /** True for a row the user typed, as opposed to a tool result or an injected note. */
495
+ function isUserPromptRow(row: AnyRecord): boolean {
496
+ if (row.type !== 'user' || row.isMeta === true || row.isCompactSummary === true) {
497
+ return false;
498
+ }
499
+
500
+ const content = row.message?.content;
501
+ if (Array.isArray(content)) {
502
+ return content.some((part: AnyRecord) => part?.type === 'text' || part?.type === 'image');
503
+ }
504
+
505
+ return typeof content === 'string' && content.length > 0;
506
+ }
507
+
508
+ /**
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));
103
576
  }
104
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.
607
+ *
608
+ * When a message is edited, Claude resumes the conversation partway and appends
609
+ * the replacement, so two prompts end up sharing one parent and the file holds
610
+ * both the abandoned attempt and the live one. A flat read would show them
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.
622
+ *
623
+ * Only sibling *prompts* are treated as a fork. Branch points made by parallel
624
+ * tool calls are extremely common — one assistant turn writes several chained
625
+ * rows and each tool result parents onto its own — and pruning those would
626
+ * delete tool output from every transcript in the app.
627
+ */
628
+ function dropSupersededPromptBranches(rows: AnyRecord[]): AnyRecord[] {
629
+ const promptSiblings = new Map<string, AnyRecord[]>();
630
+ for (const row of rows) {
631
+ if (typeof row.parentUuid !== 'string' || !isUserPromptRow(row)) {
632
+ continue;
633
+ }
634
+ const siblings = promptSiblings.get(row.parentUuid);
635
+ if (siblings) {
636
+ siblings.push(row);
637
+ } else {
638
+ promptSiblings.set(row.parentUuid, [row]);
639
+ }
640
+ }
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
+
654
+ const supersededRoots = new Set<string>();
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
+ }
664
+ }
665
+ for (const row of siblings) {
666
+ if (row !== live && typeof row.uuid === 'string') {
667
+ supersededRoots.add(row.uuid);
668
+ }
669
+ }
670
+ }
671
+
672
+ if (supersededRoots.size === 0) {
673
+ return rows;
674
+ }
675
+
676
+ const abandoned = new Set(supersededRoots);
677
+ // Rows are appended in order, so one forward pass propagates each superseded
678
+ // root to its whole subtree.
679
+ for (const row of rows) {
680
+ if (typeof row.parentUuid === 'string' && abandoned.has(row.parentUuid) && typeof row.uuid === 'string') {
681
+ abandoned.add(row.uuid);
682
+ }
683
+ }
684
+
685
+ return rows.filter((row) => typeof row.uuid !== 'string' || !abandoned.has(row.uuid));
686
+ }
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
+
105
691
  async function getSessionMessages(
106
692
  sessionId: string,
107
693
  providerSessionId: string,
108
694
  limit: number | null,
109
695
  offset: number,
696
+ getLiveRunStartTime: LiveRunStartTimeProbe,
110
697
  ): Promise<ClaudeHistoryMessagesResult> {
111
698
  try {
112
699
  // The DB row is keyed by the app-facing session id, while the JSONL rows
@@ -118,32 +705,10 @@ async function getSessionMessages(
118
705
  }
119
706
 
120
707
  const projectDir = path.dirname(jsonLPath);
121
- const files = await fsp.readdir(projectDir);
122
- const agentFiles = files.filter((file) => file.endsWith('.jsonl') && file.startsWith('agent-'));
123
708
 
124
- const messages: AnyRecord[] = [];
125
- const agentToolsCache = new Map<string, AnyRecord[]>();
126
-
127
- const fileStream = fs.createReadStream(jsonLPath);
128
- const rl = readline.createInterface({
129
- input: fileStream,
130
- crlfDelay: Infinity,
131
- });
132
-
133
- for await (const line of rl) {
134
- if (!line.trim()) {
135
- continue;
136
- }
137
-
138
- try {
139
- const entry = JSON.parse(line) as AnyRecord;
140
- if (entry.sessionId === providerSessionId) {
141
- messages.push(entry);
142
- }
143
- } catch {
144
- // Skip malformed JSONL lines that can happen during concurrent writes.
145
- }
146
- }
709
+ const messages = dropSupersededPromptBranches(
710
+ await readTranscriptRows(jsonLPath, providerSessionId),
711
+ );
147
712
 
148
713
  const agentIds = new Set<string>();
149
714
  for (const message of messages) {
@@ -153,30 +718,192 @@ async function getSessionMessages(
153
718
  }
154
719
  }
155
720
 
721
+ // Read each spawned agent's own transcript once, then hang it off every
722
+ // row that references it. Status is not part of this: the transcript
723
+ // cannot say whether the agent finished (see the loop below).
724
+ const subagentsById = new Map<string, {
725
+ activity: SubagentActivity[];
726
+ info: Omit<SubagentInfo, 'status'>;
727
+ }>();
156
728
  for (const agentId of agentIds) {
157
- const agentFileName = `agent-${agentId}.jsonl`;
158
- if (!agentFiles.includes(agentFileName)) {
729
+ const located = await findClaudeSubagentTranscript(projectDir, providerSessionId, agentId);
730
+ if (!located) {
159
731
  continue;
160
732
  }
161
733
 
162
- const agentFilePath = path.join(projectDir, agentFileName);
163
- const tools = await parseAgentTools(agentFilePath);
164
- agentToolsCache.set(agentId, tools);
734
+ const [transcript, meta] = await Promise.all([
735
+ readClaudeSubagentTranscript(located.transcriptPath),
736
+ readClaudeSubagentMeta(located.metaPath),
737
+ ]);
738
+
739
+ subagentsById.set(agentId, {
740
+ activity: transcript.activity
741
+ .slice(-MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
742
+ .map(truncateSubagentActivity),
743
+ info: {
744
+ id: agentId,
745
+ name: meta.agentType,
746
+ type: meta.agentType,
747
+ description: meta.description,
748
+ model: transcript.model,
749
+ activityCount: transcript.activity.length,
750
+ },
751
+ });
165
752
  }
166
753
 
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[]>();
167
758
  for (const message of messages) {
168
- const agentId = message.toolUseResult?.agentId;
169
- if (!agentId) {
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.
779
+ const notificationsByToolUseId = collectTaskNotifications(messages);
780
+ const foldedNotificationUuids = new Set<string>();
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
+
793
+ for (const message of messages) {
794
+ const launch = readBackgroundLaunch(message.toolUseResult);
795
+ if (!launch) {
170
796
  continue;
171
797
  }
172
798
 
173
- const agentTools = agentToolsCache.get(String(agentId));
174
- if (agentTools && agentTools.length > 0) {
175
- message.subagentTools = agentTools;
799
+ const toolUseId = readLaunchToolUseId(message);
800
+ const notification = toolUseId ? notificationsByToolUseId.get(toolUseId) : undefined;
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) {
839
+ message.subagentTools = subagent.activity;
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`.
847
+ message.subagent = {
848
+ id: agentId,
849
+ ...subagent?.info,
850
+ description: subagent?.info.description
851
+ ?? (typeof message.toolUseResult?.description === 'string' ? message.toolUseResult.description : undefined),
852
+ model: subagent?.info.model
853
+ ?? (typeof message.toolUseResult?.resolvedModel === 'string' ? message.toolUseResult.resolvedModel : undefined),
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,
886
+ };
887
+ }
888
+
889
+ if (notification) {
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, '');
176
901
  }
177
902
  }
178
903
 
179
- const sortedMessages = messages.sort(
904
+ const sortedMessages = messages
905
+ .filter((message) => !foldedNotificationUuids.has(String(message.uuid ?? '')))
906
+ .sort(
180
907
  (a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
181
908
  );
182
909
  const total = sortedMessages.length;
@@ -290,20 +1017,213 @@ function buildLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): strin
290
1017
  return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
291
1018
  }
292
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
+
1029
+ /**
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.
1037
+ */
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
+ };
1054
+ }
1055
+
1056
+ const readOptionalNumber = (value: unknown): number | undefined =>
1057
+ typeof value === 'number' && Number.isFinite(value) ? value : undefined;
1058
+
293
1059
  /**
294
- * Claude local-command stdout may contain ANSI styling codes because it was
295
- * captured from the terminal. The web chat should receive readable plain text.
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.
296
1066
  */
297
- function stripAnsiFormatting(text: string): string {
298
- return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
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
+ }
299
1078
  }
300
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
+
301
1192
  export class ClaudeSessionsProvider implements IProviderSessions {
1193
+ private readonly getLiveRunStartTime: LiveRunStartTimeProbe;
1194
+
1195
+ constructor({ getLiveRunStartTime = getClaudeSDKSessionStartTime }: ClaudeSessionsProviderOptions = {}) {
1196
+ this.getLiveRunStartTime = getLiveRunStartTime;
1197
+ }
1198
+
302
1199
  /**
303
1200
  * Normalizes one Claude JSONL entry or live SDK stream event into the shared
304
1201
  * message shape consumed by REST and WebSocket clients.
1202
+ *
1203
+ * Every user turn it produces is stamped with the transcript row's `uuid`,
1204
+ * which is the anchor "edit this message" and "fork from here" address. It is
1205
+ * applied here rather than at each `createNormalizedMessage` call because the
1206
+ * row-shape branches below have several exits, and an anchor missing from one
1207
+ * of them would show up as a message the user silently cannot edit.
305
1208
  */
306
1209
  normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
1210
+ const messages = this.normalizeMessageRows(rawMessage, sessionId);
1211
+ // A synthesized id is useless as an anchor — it changes on every read — so
1212
+ // a row without its own uuid produces messages with no anchor at all.
1213
+ const raw = readObjectRecord(rawMessage);
1214
+ const anchorId = typeof raw?.uuid === 'string' && raw.uuid ? raw.uuid : null;
1215
+ if (anchorId) {
1216
+ for (const message of messages) {
1217
+ if (message.role === 'user') {
1218
+ message.transcriptAnchorId = anchorId;
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ return messages;
1224
+ }
1225
+
1226
+ private normalizeMessageRows(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
307
1227
  const raw = readObjectRecord(rawMessage);
308
1228
  if (!raw) {
309
1229
  return [];
@@ -320,6 +1240,86 @@ export class ClaudeSessionsProvider implements IProviderSessions {
320
1240
  const ts = raw.timestamp || new Date().toISOString();
321
1241
  const baseId = raw.uuid || generateMessageId('claude');
322
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
+
323
1323
  if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
324
1324
  if (Array.isArray(raw.message.content)) {
325
1325
  // Image attachments sent through the SDK are persisted as base64
@@ -347,8 +1347,14 @@ export class ClaudeSessionsProvider implements IProviderSessions {
347
1347
  toolId: part.tool_use_id,
348
1348
  content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
349
1349
  isError: Boolean(part.is_error),
350
- subagentTools: raw.subagentTools,
351
- 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),
352
1358
  }));
353
1359
  } else if (part.type === 'text') {
354
1360
  const text = part.text || '';
@@ -470,7 +1476,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
470
1476
  */
471
1477
  const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
472
1478
  if (localCommandStdout !== null) {
473
- const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
1479
+ const stdoutText = stripAnsiSequences(localCommandStdout).trim();
474
1480
  if (stdoutText) {
475
1481
  messages.push(createNormalizedMessage({
476
1482
  id: baseId,
@@ -547,6 +1553,13 @@ export class ClaudeSessionsProvider implements IProviderSessions {
547
1553
  }
548
1554
 
549
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
+
550
1563
  if (Array.isArray(raw.message.content)) {
551
1564
  let partIndex = 0;
552
1565
  for (const part of raw.message.content) {
@@ -559,6 +1572,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
559
1572
  kind: 'text',
560
1573
  role: 'assistant',
561
1574
  content: part.text,
1575
+ model: answeredByModel,
562
1576
  }));
563
1577
  } else if (part.type === 'tool_use') {
564
1578
  messages.push(createNormalizedMessage({
@@ -592,6 +1606,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
592
1606
  kind: 'text',
593
1607
  role: 'assistant',
594
1608
  content: raw.message.content,
1609
+ model: answeredByModel,
595
1610
  }));
596
1611
  }
597
1612
  return messages;
@@ -600,6 +1615,117 @@ export class ClaudeSessionsProvider implements IProviderSessions {
600
1615
  return messages;
601
1616
  }
602
1617
 
1618
+ /**
1619
+ * Finds the row to resume *through* so that `anchorId`'s turn is replaced.
1620
+ *
1621
+ * Walks up the `parentUuid` chain to the nearest assistant row, because the
1622
+ * SDK's `resumeSessionAt` is documented against assistant message ids — the
1623
+ * user row's immediate parent can be an attachment or an injected note.
1624
+ * Returns `null` when nothing precedes the edited prompt, which means the
1625
+ * conversation should start over rather than resume.
1626
+ */
1627
+ async resolveEditAnchor(
1628
+ sessionId: string,
1629
+ anchorId: string,
1630
+ ): Promise<{ found: boolean; resumeThroughId: string | null }> {
1631
+ const session = sessionsDb.getSessionById(sessionId);
1632
+ const jsonlPath = session?.jsonl_path;
1633
+ const providerSessionId = session?.provider_session_id;
1634
+ if (!jsonlPath || !providerSessionId) {
1635
+ return { found: false, resumeThroughId: null };
1636
+ }
1637
+
1638
+ const rows = await readTranscriptRows(jsonlPath, providerSessionId);
1639
+ const byUuid = new Map<string, AnyRecord>();
1640
+ for (const row of rows) {
1641
+ if (typeof row.uuid === 'string') {
1642
+ byUuid.set(row.uuid, row);
1643
+ }
1644
+ }
1645
+
1646
+ const target = byUuid.get(anchorId);
1647
+ if (!target) {
1648
+ return { found: false, resumeThroughId: null };
1649
+ }
1650
+
1651
+ const visited = new Set<string>([anchorId]);
1652
+ let parentUuid: unknown = target.parentUuid;
1653
+ while (typeof parentUuid === 'string' && !visited.has(parentUuid)) {
1654
+ visited.add(parentUuid);
1655
+ const parent = byUuid.get(parentUuid);
1656
+ if (!parent) {
1657
+ break;
1658
+ }
1659
+ if (parent.type === 'assistant') {
1660
+ return { found: true, resumeThroughId: parentUuid };
1661
+ }
1662
+ parentUuid = parent.parentUuid;
1663
+ }
1664
+
1665
+ return { found: true, resumeThroughId: null };
1666
+ }
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
+
603
1729
  /**
604
1730
  * Loads Claude JSONL history for a project/session and returns normalized
605
1731
  * messages, preserving the existing pagination behavior from projects.js.
@@ -615,7 +1741,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
615
1741
  try {
616
1742
  // Load full history first so `total` reflects frontend-normalized messages,
617
1743
  // not raw JSONL records.
618
- result = await getSessionMessages(sessionId, providerSessionId, null, 0);
1744
+ result = await getSessionMessages(sessionId, providerSessionId, null, 0, this.getLiveRunStartTime);
619
1745
  } catch (error) {
620
1746
  const message = error instanceof Error ? error.message : String(error);
621
1747
  console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
@@ -633,6 +1759,8 @@ export class ClaudeSessionsProvider implements IProviderSessions {
633
1759
  content: part.content,
634
1760
  isError: Boolean(part.is_error),
635
1761
  subagentTools: raw.subagentTools,
1762
+ subagent: raw.subagent,
1763
+ workflow: raw.workflow,
636
1764
  toolUseResult: raw.toolUseResult,
637
1765
  });
638
1766
  }
@@ -660,18 +1788,18 @@ export class ClaudeSessionsProvider implements IProviderSessions {
660
1788
  toolUseResult: toolResult.toolUseResult,
661
1789
  };
662
1790
  msg.subagentTools = toolResult.subagentTools;
1791
+ msg.subagent = toolResult.subagent;
1792
+ msg.workflow = toolResult.workflow;
663
1793
  }
664
1794
  }
665
1795
 
666
- let total = 0;
667
- for (const msg of normalized) {
668
- if (msg.kind !== 'tool_result') {
669
- total += 1;
670
- }
671
- }
1796
+ // Everything the transcript draws, and nothing else — so a page of N rows
1797
+ // is N rows the user sees, and `total` counts the same thing.
1798
+ const transcript = prepareTranscriptMessages(normalized);
1799
+ const total = transcript.length;
672
1800
  const normalizedOffset = Math.max(0, offset);
673
1801
  const normalizedLimit = limit === null ? null : Math.max(0, limit);
674
- const { page, hasMore } = sliceTailPage(normalized, normalizedLimit, normalizedOffset);
1802
+ const { page, hasMore } = sliceTailPage(transcript, normalizedLimit, normalizedOffset);
675
1803
 
676
1804
  return {
677
1805
  messages: page,
@@ -679,6 +1807,10 @@ export class ClaudeSessionsProvider implements IProviderSessions {
679
1807
  hasMore,
680
1808
  offset: normalizedOffset,
681
1809
  limit: normalizedLimit,
1810
+ // Carried on every page, like the Codex and OpenCode readers do, so the
1811
+ // composer's counter tracks the conversation instead of being frozen at
1812
+ // whatever it was when the session was opened.
1813
+ tokenUsage: summarizeClaudeTokenUsage(rawMessages),
682
1814
  };
683
1815
  }
684
1816
  }