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