@cloudcli-ai/cloudcli 1.37.1 → 1.37.3

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 (402) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/abnfDiagram-N423BO3Z-CJntskwA.js +1 -0
  3. package/dist/assets/arc-CIfH4uMN.js +1 -0
  4. package/dist/assets/architectureDiagram-T3A2C74G-_IpEkukg.js +36 -0
  5. package/dist/assets/blockDiagram-VBNYF7ZC-BttxrpgZ.js +132 -0
  6. package/dist/assets/c4Diagram-5PPSVZJV-g51dSRr1.js +10 -0
  7. package/dist/assets/channel-C5xU9jMw.js +1 -0
  8. package/dist/assets/chunk-2GRJ4B5K-D_-LbQUa.js +1 -0
  9. package/dist/assets/chunk-2Q5K7J3B-BtlHzwaO.js +1 -0
  10. package/dist/assets/chunk-5RXB4S5H-UzkpvpAT.js +231 -0
  11. package/dist/assets/chunk-5VM5RSS4-BKwx5Gag.js +15 -0
  12. package/dist/assets/chunk-6Q2QTUOP-D_KEBH5f.js +88 -0
  13. package/dist/assets/chunk-GF5L2VYU-rK2DGyto.js +206 -0
  14. package/dist/assets/chunk-JWPE2WC7-CeBl_N9i.js +1 -0
  15. package/dist/assets/chunk-KBJHAD2P-BiQLWVIr.js +1 -0
  16. package/dist/assets/chunk-RYQCIY6F-C1knVGV5.js +1 -0
  17. package/dist/assets/chunk-XXDRQBXY-CtKbJkvF.js +1 -0
  18. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
  20. package/dist/assets/cose-bilkent-JH36ORCC-DjX8y40Z.js +1 -0
  21. package/dist/assets/cynefin-VYW2F7L2-DM6YeRba.js +178 -0
  22. package/dist/assets/cynefinDiagram-MW4NZA55-wZJ-vy5B.js +62 -0
  23. package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
  24. package/dist/assets/dagre-VZM6K2ZE-BzP0ejkH.js +4 -0
  25. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  26. package/dist/assets/diagram-7IWD3JNH-BTL_j23J.js +30 -0
  27. package/dist/assets/diagram-B4RE2ZJO-Cf1Norgu.js +3 -0
  28. package/dist/assets/diagram-LBJQPF4R-DPiUEKr-.js +24 -0
  29. package/dist/assets/diagram-Q27KOJAE-CTcTpiz5.js +24 -0
  30. package/dist/assets/diagram-UB23O5K3-DPMSdYm0.js +41 -0
  31. package/dist/assets/ebnfDiagram-BXEA7PRR-7yvD6uiF.js +1 -0
  32. package/dist/assets/erDiagram-JOGREHBK-CfgeDoV6.js +85 -0
  33. package/dist/assets/flowDiagram-UKHOOZJN-B27ng5zu.js +156 -0
  34. package/dist/assets/ganttDiagram-PKOTCBZU-CcC6b_7t.js +292 -0
  35. package/dist/assets/gitGraphDiagram-DS77QQ5N-BX5kPGrO.js +106 -0
  36. package/dist/assets/graph-Cx-EqisW.js +1 -0
  37. package/dist/assets/index-BTDHQ6Id.css +32 -0
  38. package/dist/assets/index-sFfz8iBR.js +4931 -0
  39. package/dist/assets/infoDiagram-6WML65LV-CUP-SlN4.js +2 -0
  40. package/dist/assets/init-Gi6I4Gst.js +1 -0
  41. package/dist/assets/ishikawaDiagram-WSZJBQD7-BTTDhOpX.js +70 -0
  42. package/dist/assets/journeyDiagram-NVQOT4AX-ARXcc61m.js +139 -0
  43. package/dist/assets/kanban-definition-27J2QSJJ-BieMpqMA.js +89 -0
  44. package/dist/assets/layout-D6BstSzF.js +1 -0
  45. package/dist/assets/linear-61vxjRnP.js +1 -0
  46. package/dist/assets/map-CV9uif0g.js +1 -0
  47. package/dist/assets/mermaid.core-nhnwkfQx.js +312 -0
  48. package/dist/assets/mindmap-definition-FAOFIHXS-Cm6x5vT1.js +96 -0
  49. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  50. package/dist/assets/pegDiagram-VL7TDLO6-CcYBq_9H.js +1 -0
  51. package/dist/assets/pieDiagram-7S7Q4E2Y-CfvIjfzo.js +39 -0
  52. package/dist/assets/quadrantDiagram-CIZ2JOQS-SgYoWPNb.js +7 -0
  53. package/dist/assets/railroadDiagram-AXF67PYL-D2Mu5E_f.js +1 -0
  54. package/dist/assets/requirementDiagram-LRYGKXZP-Azwe_k4Q.js +84 -0
  55. package/dist/assets/sankeyDiagram-W5VNT64P-DN1Ju3PA.js +40 -0
  56. package/dist/assets/sequenceDiagram-SI44F4Z6-Bj4qC34S.js +162 -0
  57. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  58. package/dist/assets/sizeCapture-X5ZJPWSS-Co4PdklG.js +1 -0
  59. package/dist/assets/stateDiagram-OKZ733FA-nOvL9dRC.js +1 -0
  60. package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
  61. package/dist/assets/swimlanes-SLNWSIFB-CCK3wMid.js +2 -0
  62. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
  63. package/dist/assets/timeline-definition-Z64GVDOM-BC29RNFm.js +120 -0
  64. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  65. package/dist/assets/vennDiagram-T6HMQDX7-pZlZCtEo.js +34 -0
  66. package/dist/assets/wardleyDiagram-T6FBY63Y-De8k2-zM.js +78 -0
  67. package/dist/assets/xychartDiagram-ELKLHX3M-Bk8ykH0w.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/dist-server/server/index.js +8 -5
  70. package/dist-server/server/index.js.map +1 -1
  71. package/dist-server/server/modules/agent/agent.module.js +2 -1
  72. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  73. package/dist-server/server/modules/agent/agent.routes.js +2 -2
  74. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  75. package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
  76. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  77. package/dist-server/server/modules/auth/auth.middleware.js +1 -1
  78. package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
  79. package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
  80. package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
  81. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  82. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  83. package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
  84. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  85. package/dist-server/server/modules/browser-use/index.js +2 -0
  86. package/dist-server/server/modules/browser-use/index.js.map +1 -1
  87. package/dist-server/server/modules/cli/cli.js +9 -3
  88. package/dist-server/server/modules/cli/cli.js.map +1 -1
  89. package/dist-server/server/modules/cli/cli.module.js +0 -1
  90. package/dist-server/server/modules/cli/cli.module.js.map +1 -1
  91. package/dist-server/server/modules/cli/cli.service.js +0 -18
  92. package/dist-server/server/modules/cli/cli.service.js.map +1 -1
  93. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
  94. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
  95. package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
  96. package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
  97. package/dist-server/server/modules/database/connection.js +1 -1
  98. package/dist-server/server/modules/database/connection.js.map +1 -1
  99. package/dist-server/server/modules/database/index.js +5 -0
  100. package/dist-server/server/modules/database/index.js.map +1 -1
  101. package/dist-server/server/modules/database/migrations.js +32 -1
  102. package/dist-server/server/modules/database/migrations.js.map +1 -1
  103. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  104. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  105. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  106. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  107. package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
  108. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  109. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  110. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  111. package/dist-server/server/modules/database/schema.js +92 -0
  112. package/dist-server/server/modules/database/schema.js.map +1 -1
  113. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  114. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  115. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  116. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  117. package/dist-server/server/modules/file-tree/file-tree.service.js +18 -10
  118. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  119. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +62 -11
  120. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  121. package/dist-server/server/modules/git/git-branch.service.js +15 -0
  122. package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
  123. package/dist-server/server/modules/git/git.routes.js +15 -7
  124. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  125. package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
  126. package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
  127. package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
  128. package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
  129. package/dist-server/server/modules/notifications/index.js +3 -1
  130. package/dist-server/server/modules/notifications/index.js.map +1 -1
  131. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
  132. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  133. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  134. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  135. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  136. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  137. package/dist-server/server/modules/providers/index.js +3 -0
  138. package/dist-server/server/modules/providers/index.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
  140. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  142. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  143. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
  144. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +422 -86
  146. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  147. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
  148. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  149. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  150. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  152. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  155. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  156. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  157. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
  158. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
  160. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  162. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
  164. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  165. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  166. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  167. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  168. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  169. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
  171. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  172. package/dist-server/server/modules/providers/provider.routes.js +29 -0
  173. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  174. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
  175. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  176. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  177. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +63 -7
  179. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
  181. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  182. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  183. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  185. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
  187. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  188. package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
  189. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  190. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  191. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  192. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
  193. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  195. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  196. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  197. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  199. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
  201. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  203. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
  205. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  206. package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
  207. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  209. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  210. package/dist-server/server/modules/providers/tests/provider.routes.test.js +35 -0
  211. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  212. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  213. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
  215. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  217. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  219. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
  221. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  222. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  223. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  224. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  225. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  226. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  227. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  228. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
  229. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  230. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  231. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  232. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
  233. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  234. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  235. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  236. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  237. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  239. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  240. package/dist-server/server/modules/user/user.module.js +10 -1
  241. package/dist-server/server/modules/user/user.module.js.map +1 -1
  242. package/dist-server/server/modules/user/user.routes.js +44 -0
  243. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  244. package/dist-server/server/modules/user/user.service.js +75 -0
  245. package/dist-server/server/modules/user/user.service.js.map +1 -1
  246. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  247. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  248. package/dist-server/server/modules/websocket/index.js +7 -0
  249. package/dist-server/server/modules/websocket/index.js.map +1 -1
  250. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
  251. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  252. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  253. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  254. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
  255. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  256. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  257. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  258. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
  259. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  260. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  261. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  262. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  263. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  264. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  265. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  266. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
  267. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  268. package/dist-server/server/shared/claude-cli-path.js +22 -5
  269. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  270. package/dist-server/server/shared/index.js +3 -0
  271. package/dist-server/server/shared/index.js.map +1 -0
  272. package/dist-server/server/shared/message-unification.js +376 -0
  273. package/dist-server/server/shared/message-unification.js.map +1 -0
  274. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  275. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  276. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  277. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  278. package/dist-server/server/shared/utils.js +42 -0
  279. package/dist-server/server/shared/utils.js.map +1 -1
  280. package/package.json +23 -16
  281. package/scripts/promote-dist-server.mjs +50 -0
  282. package/server/index.ts +12 -6
  283. package/server/modules/agent/agent.module.ts +2 -1
  284. package/server/modules/agent/agent.routes.ts +2 -2
  285. package/server/modules/agent/tests/agent.routes.test.ts +33 -1
  286. package/server/modules/auth/auth.middleware.ts +2 -2
  287. package/server/modules/browser-use/browser-use-runtime.ts +10 -0
  288. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  289. package/server/modules/browser-use/browser-use.service.ts +5 -8
  290. package/server/modules/browser-use/index.ts +3 -0
  291. package/server/modules/cli/cli.module.ts +0 -1
  292. package/server/modules/cli/cli.service.ts +0 -20
  293. package/server/modules/cli/cli.ts +9 -3
  294. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
  295. package/server/modules/cli/tests/cli.service.test.ts +0 -3
  296. package/server/modules/database/connection.ts +2 -1
  297. package/server/modules/database/index.ts +10 -0
  298. package/server/modules/database/migrations.ts +37 -0
  299. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  300. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  301. package/server/modules/database/repositories/sessions.db.ts +196 -3
  302. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  303. package/server/modules/database/schema.ts +96 -0
  304. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  305. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  306. package/server/modules/file-tree/file-tree.service.ts +21 -10
  307. package/server/modules/file-tree/tests/file-tree.service.test.ts +65 -11
  308. package/server/modules/git/git-branch.service.ts +43 -0
  309. package/server/modules/git/git.routes.ts +16 -9
  310. package/server/modules/git/tests/git-branch.service.test.ts +60 -0
  311. package/server/modules/git/tests/git-init.routes.test.ts +83 -0
  312. package/server/modules/notifications/index.ts +2 -0
  313. package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
  314. package/server/modules/projects/services/project-delete.service.ts +13 -1
  315. package/server/modules/projects/services/project-star.service.ts +2 -2
  316. package/server/modules/providers/index.ts +4 -0
  317. package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
  318. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  319. package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
  320. package/server/modules/providers/list/claude/claude-runtime.provider.js +463 -93
  321. package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
  322. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  323. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  324. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  325. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  326. package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
  327. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
  328. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  329. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
  330. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  331. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  332. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  333. package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
  334. package/server/modules/providers/provider.routes.ts +36 -0
  335. package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
  336. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  337. package/server/modules/providers/services/session-conversations-search.service.ts +94 -7
  338. package/server/modules/providers/services/session-history-cache.service.ts +139 -0
  339. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  340. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  341. package/server/modules/providers/services/sessions.service.ts +201 -9
  342. package/server/modules/providers/tests/claude-auth.test.ts +58 -0
  343. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  344. package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
  345. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  346. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  347. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  348. package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
  349. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  350. package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
  351. package/server/modules/providers/tests/opencode-models.test.ts +166 -8
  352. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  353. package/server/modules/providers/tests/provider.routes.test.ts +61 -0
  354. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  355. package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
  356. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  357. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  358. package/server/modules/providers/tests/sessions.service.test.ts +67 -1
  359. package/server/modules/providers/tests/skills.test.ts +21 -5
  360. package/server/modules/scheduled-messages/index.ts +8 -0
  361. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  362. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
  363. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  364. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
  365. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  366. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  367. package/server/modules/user/tests/user.service.test.ts +90 -0
  368. package/server/modules/user/user.module.ts +10 -1
  369. package/server/modules/user/user.routes.ts +44 -0
  370. package/server/modules/user/user.service.ts +105 -0
  371. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  372. package/server/modules/websocket/index.ts +8 -0
  373. package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
  374. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  375. package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
  376. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  377. package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
  378. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  379. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  380. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  381. package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
  382. package/server/shared/claude-cli-path.ts +24 -6
  383. package/server/shared/index.ts +3 -0
  384. package/server/shared/interfaces.ts +62 -0
  385. package/server/shared/message-unification.ts +449 -0
  386. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  387. package/server/shared/tests/message-unification.test.ts +218 -0
  388. package/server/shared/types.ts +123 -3
  389. package/server/shared/utils.ts +50 -0
  390. package/server/tsconfig.json +6 -2
  391. package/README.de.md +0 -257
  392. package/README.ja.md +0 -249
  393. package/README.ko.md +0 -249
  394. package/README.ru.md +0 -257
  395. package/README.tr.md +0 -258
  396. package/README.zh-CN.md +0 -249
  397. package/README.zh-TW.md +0 -249
  398. package/dist/assets/index-D3jfzuKN.css +0 -32
  399. package/dist/assets/index-DTMWjJj8.js +0 -1550
  400. package/dist-server/server/constants/config.js +0 -6
  401. package/dist-server/server/constants/config.js.map +0 -1
  402. package/server/constants/config.ts +0 -5
@@ -11,6 +11,12 @@ import { AppError, getOpenCodeDatabasePath } from '@/shared/utils.js';
11
11
 
12
12
  type SessionRow = NonNullable<ReturnType<typeof sessionsDb.getSessionById>>;
13
13
 
14
+ type FileTail = {
15
+ content: string;
16
+ /** True when `content` is the whole file, not just its trailing bytes. */
17
+ isComplete: boolean;
18
+ };
19
+
14
20
  type ProviderTokenUsageServiceDependencies = {
15
21
  getSessionById: (sessionId: string) => SessionRow | null | undefined;
16
22
  getHomeDirectory: () => string;
@@ -18,7 +24,9 @@ type ProviderTokenUsageServiceDependencies = {
18
24
  fileExists: (filePath: string) => boolean;
19
25
  readDirectory: (directoryPath: string) => Promise<Dirent[]>;
20
26
  readTextFile: (filePath: string) => Promise<string>;
27
+ readTextFileTail: (filePath: string, maxBytes: number) => Promise<FileTail>;
21
28
  getClaudeContextWindow: () => string | undefined;
29
+ isProviderSessionSuperseded: (providerSessionId: string, provider: string) => boolean;
22
30
  };
23
31
 
24
32
  type TokenUsageResult = {
@@ -45,6 +53,14 @@ type OpenCodeTokenRow = {
45
53
  cacheWriteTokens: number | null;
46
54
  };
47
55
 
56
+ /**
57
+ * Both JSONL usage readers below only need the newest usage row, which sits at
58
+ * or near the end of the transcript. Reading just this much of the tail keeps
59
+ * a session-open usage lookup O(1) in the transcript's size; the rare tail
60
+ * with no usage row at all falls back to the full read.
61
+ */
62
+ const TOKEN_USAGE_TAIL_BYTES = 4 * 1024 * 1024;
63
+
48
64
  const defaultDependencies: ProviderTokenUsageServiceDependencies = {
49
65
  getSessionById: (sessionId) => sessionsDb.getSessionById(sessionId),
50
66
  getHomeDirectory: () => os.homedir(),
@@ -52,7 +68,32 @@ const defaultDependencies: ProviderTokenUsageServiceDependencies = {
52
68
  fileExists: (filePath) => fsSync.existsSync(filePath),
53
69
  readDirectory: (directoryPath) => fsp.readdir(directoryPath, { withFileTypes: true }),
54
70
  readTextFile: (filePath) => fsp.readFile(filePath, 'utf8'),
71
+ readTextFileTail: async (filePath, maxBytes) => {
72
+ const handle = await fsp.open(filePath, 'r');
73
+ try {
74
+ const { size } = await handle.stat();
75
+ if (size <= maxBytes) {
76
+ return { content: await handle.readFile({ encoding: 'utf8' }), isComplete: true };
77
+ }
78
+
79
+ const buffer = Buffer.alloc(maxBytes);
80
+ await handle.read(buffer, 0, maxBytes, size - maxBytes);
81
+ const content = buffer.toString('utf8');
82
+ // The window almost never starts on a row boundary; dropping everything
83
+ // up to the first newline discards the partial row (and any split
84
+ // multi-byte character with it).
85
+ const firstNewline = content.indexOf('\n');
86
+ return {
87
+ content: firstNewline === -1 ? '' : content.slice(firstNewline + 1),
88
+ isComplete: false,
89
+ };
90
+ } finally {
91
+ await handle.close();
92
+ }
93
+ },
55
94
  getClaudeContextWindow: () => process.env.CONTEXT_WINDOW,
95
+ isProviderSessionSuperseded: (providerSessionId, provider) =>
96
+ sessionsDb.isProviderSessionSuperseded(providerSessionId, provider),
56
97
  };
57
98
 
58
99
  function readUsageNumber(value: unknown): number {
@@ -92,11 +133,8 @@ async function findCodexSessionFile(
92
133
  return null;
93
134
  }
94
135
 
95
- function readCodexTokenUsage(fileContent: string): TokenUsageResult {
96
- let inputTokens = 0;
97
- let outputTokens = 0;
98
- let totalTokens = 0;
99
- let contextWindow = 200_000;
136
+ /** Newest `token_count` snapshot in the given JSONL text, or null when it has none. */
137
+ function findCodexTokenUsage(fileContent: string): TokenUsageResult | null {
100
138
  const lines = fileContent.trim().split('\n');
101
139
 
102
140
  for (let index = lines.length - 1; index >= 0; index -= 1) {
@@ -109,58 +147,101 @@ function readCodexTokenUsage(fileContent: string): TokenUsageResult {
109
147
  continue;
110
148
  }
111
149
 
150
+ let inputTokens = 0;
151
+ let outputTokens = 0;
152
+ let totalTokens = 0;
112
153
  if (tokenInfo.total_token_usage) {
113
154
  inputTokens = readUsageNumber(tokenInfo.total_token_usage.input_tokens);
114
155
  outputTokens = readUsageNumber(tokenInfo.total_token_usage.output_tokens);
115
156
  totalTokens = readUsageNumber(tokenInfo.total_token_usage.total_tokens)
116
157
  || inputTokens + outputTokens;
117
158
  }
118
- contextWindow = readUsageNumber(tokenInfo.model_context_window) || contextWindow;
119
- break;
159
+ return {
160
+ used: totalTokens,
161
+ total: readUsageNumber(tokenInfo.model_context_window) || 200_000,
162
+ inputTokens,
163
+ outputTokens,
164
+ breakdown: { input: inputTokens, output: outputTokens },
165
+ };
120
166
  } catch {
121
167
  // A provider may be writing the last JSONL line while this read happens.
122
168
  }
123
169
  }
124
170
 
171
+ return null;
172
+ }
173
+
174
+ function emptyCodexTokenUsage(): TokenUsageResult {
125
175
  return {
126
- used: totalTokens,
127
- total: contextWindow,
128
- inputTokens,
129
- outputTokens,
130
- breakdown: { input: inputTokens, output: outputTokens },
176
+ used: 0,
177
+ total: 200_000,
178
+ inputTokens: 0,
179
+ outputTokens: 0,
180
+ breakdown: { input: 0, output: 0 },
131
181
  };
132
182
  }
133
183
 
134
- function readClaudeTokenUsage(fileContent: string, configuredContextWindow: string | undefined): TokenUsageResult {
184
+ /**
185
+ * Latest context-window usage from a Claude transcript's already-parsed rows.
186
+ *
187
+ * Exported because the session-messages reader hands the same usage back on
188
+ * every history page, the way the Codex and OpenCode readers do. Without that,
189
+ * a Claude session's counter only moved when the session was reselected, and
190
+ * the store's "this provider reports no usage" path overwrote it with zero.
191
+ *
192
+ * Reads the newest assistant turn only: `input_tokens + cache_read +
193
+ * cache_creation` is that one request's whole prompt, i.e. what the context
194
+ * window currently holds. Summing turns would count the same cached prefix
195
+ * once per turn.
196
+ */
197
+ export function summarizeClaudeTokenUsage(
198
+ entries: AnyRecord[],
199
+ configuredContextWindow: string | undefined = process.env.CONTEXT_WINDOW,
200
+ ): TokenUsageResult {
135
201
  let inputTokens = 0;
136
202
  let outputTokens = 0;
137
203
  let cacheReadTokens = 0;
138
204
  let cacheCreationTokens = 0;
139
- const lines = fileContent.trim().split('\n');
140
205
 
141
- for (let index = lines.length - 1; index >= 0; index -= 1) {
142
- try {
143
- const entry = JSON.parse(lines[index]) as AnyRecord;
144
- const usage = entry.type === 'assistant' ? entry.message?.usage : null;
145
- if (!usage) {
146
- continue;
147
- }
206
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
207
+ const entry = entries[index];
208
+ // A subagent's turns report the subagent's context window, not this
209
+ // conversation's; reading one makes the counter drop to the subagent's
210
+ // number and bounce back on the next main-thread turn.
211
+ if (entry?.isSidechain === true) {
212
+ continue;
213
+ }
148
214
 
149
- const directInputTokens = readUsageNumber(usage.input_tokens ?? usage.inputTokens);
150
- cacheReadTokens = readUsageNumber(
151
- usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? usage.cacheReadTokens,
152
- );
153
- cacheCreationTokens = readUsageNumber(
154
- usage.cache_creation_input_tokens
155
- ?? usage.cacheCreationInputTokens
156
- ?? usage.cacheCreationTokens,
157
- );
158
- inputTokens = directInputTokens + cacheReadTokens + cacheCreationTokens;
159
- outputTokens = readUsageNumber(usage.output_tokens ?? usage.outputTokens);
160
- break;
161
- } catch {
162
- // Skip malformed lines without discarding usage from earlier messages.
215
+ const usage = entry?.type === 'assistant' ? entry.message?.usage : null;
216
+ if (!usage) {
217
+ continue;
218
+ }
219
+
220
+ const directInputTokens = readUsageNumber(usage.input_tokens ?? usage.inputTokens);
221
+ const rowCacheReadTokens = readUsageNumber(
222
+ usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? usage.cacheReadTokens,
223
+ );
224
+ const rowCacheCreationTokens = readUsageNumber(
225
+ usage.cache_creation_input_tokens
226
+ ?? usage.cacheCreationInputTokens
227
+ ?? usage.cacheCreationTokens,
228
+ );
229
+ const rowInputTokens = directInputTokens + rowCacheReadTokens + rowCacheCreationTokens;
230
+ const rowOutputTokens = readUsageNumber(usage.output_tokens ?? usage.outputTokens);
231
+
232
+ // `<synthetic>` rows — interrupts, API errors, "No response requested" —
233
+ // are written with an all-zero usage block rather than none at all. They
234
+ // never carried a prompt, so treating one as the newest turn zeroed a
235
+ // counter that a live event had just set correctly.
236
+ if (rowInputTokens === 0 && rowOutputTokens === 0) {
237
+ continue;
163
238
  }
239
+
240
+ cacheReadTokens = rowCacheReadTokens;
241
+ cacheCreationTokens = rowCacheCreationTokens;
242
+ inputTokens = rowInputTokens;
243
+ outputTokens = rowOutputTokens;
244
+ break;
164
245
  }
165
246
 
166
247
  const parsedContextWindow = Number.parseInt(configuredContextWindow ?? '', 10);
@@ -179,6 +260,22 @@ function readClaudeTokenUsage(fileContent: string, configuredContextWindow: stri
179
260
  };
180
261
  }
181
262
 
263
+ function parseClaudeUsageEntries(fileContent: string): AnyRecord[] {
264
+ const entries: AnyRecord[] = [];
265
+ for (const line of fileContent.trim().split('\n')) {
266
+ try {
267
+ entries.push(JSON.parse(line) as AnyRecord);
268
+ } catch {
269
+ // Skip malformed lines without discarding usage from earlier messages.
270
+ }
271
+ }
272
+ return entries;
273
+ }
274
+
275
+ function claudeEntriesHaveUsage(entries: AnyRecord[]): boolean {
276
+ return entries.some((entry) => entry?.type === 'assistant' && entry.message?.usage);
277
+ }
278
+
182
279
  function readOpenCodeTokenUsage(databasePath: string, providerSessionId: string): TokenUsageResult {
183
280
  const database = new Database(databasePath, { readonly: true, fileMustExist: true });
184
281
  try {
@@ -264,6 +361,24 @@ export function createProviderTokenUsageService(
264
361
  });
265
362
  }
266
363
 
364
+ // The fallback covers rows whose provider id was never recorded, and for
365
+ // a session discovered from disk the app id *is* its provider id. That
366
+ // stops being true the moment an edit rewinds the conversation off a
367
+ // thread: until the replacement run announces its own id, the fallback
368
+ // would resolve the retired transcript and report the discarded
369
+ // conversation's usage against an empty one.
370
+ if (
371
+ !session.provider_session_id
372
+ && dependencies.isProviderSessionSuperseded(sessionId, session.provider)
373
+ ) {
374
+ return {
375
+ used: 0,
376
+ inputTokens: 0,
377
+ outputTokens: 0,
378
+ breakdown: { input: 0, output: 0 },
379
+ };
380
+ }
381
+
267
382
  const providerSessionId = session.provider_session_id || sessionId;
268
383
 
269
384
  if (session.provider === 'cursor') {
@@ -307,8 +422,16 @@ export function createProviderTokenUsageService(
307
422
  });
308
423
  }
309
424
 
310
- const fileContent = await dependencies.readTextFile(sessionFilePath);
311
- return readCodexTokenUsage(fileContent);
425
+ const tail = await dependencies.readTextFileTail(sessionFilePath, TOKEN_USAGE_TAIL_BYTES);
426
+ const tailUsage = findCodexTokenUsage(tail.content);
427
+ if (tailUsage || tail.isComplete) {
428
+ return tailUsage ?? emptyCodexTokenUsage();
429
+ }
430
+
431
+ // A tail this large with no token_count row is pathological, but the
432
+ // whole file is still authoritative when it happens.
433
+ return findCodexTokenUsage(await dependencies.readTextFile(sessionFilePath))
434
+ ?? emptyCodexTokenUsage();
312
435
  }
313
436
 
314
437
  let sessionFilePath = session.jsonl_path;
@@ -345,8 +468,12 @@ export function createProviderTokenUsageService(
345
468
  });
346
469
  }
347
470
 
348
- const fileContent = await dependencies.readTextFile(sessionFilePath);
349
- return readClaudeTokenUsage(fileContent, dependencies.getClaudeContextWindow());
471
+ const tail = await dependencies.readTextFileTail(sessionFilePath, TOKEN_USAGE_TAIL_BYTES);
472
+ let entries = parseClaudeUsageEntries(tail.content);
473
+ if (!claudeEntriesHaveUsage(entries) && !tail.isComplete) {
474
+ entries = parseClaudeUsageEntries(await dependencies.readTextFile(sessionFilePath));
475
+ }
476
+ return summarizeClaudeTokenUsage(entries, dependencies.getClaudeContextWindow());
350
477
  },
351
478
  };
352
479
  }
@@ -1,4 +1,4 @@
1
- import fsSync, { promises as fs } from 'node:fs';
1
+ import fsSync from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import readline from 'node:readline';
4
4
 
@@ -38,6 +38,15 @@ type ProjectConversationResult = {
38
38
  sessions: SessionConversationResult[];
39
39
  };
40
40
 
41
+ type SessionTitleSearchResult = {
42
+ sessionId: string;
43
+ provider: string;
44
+ projectId: string | null;
45
+ projectDisplayName: string;
46
+ sessionTitle: string;
47
+ lastActivity: string | null;
48
+ };
49
+
41
50
  export type SessionConversationSearchProgressUpdate = {
42
51
  projectResult: ProjectConversationResult | null;
43
52
  totalMatches: number;
@@ -49,6 +58,7 @@ type SearchSessionConversationsInput = {
49
58
  query: string;
50
59
  limit: number;
51
60
  signal?: AbortSignal;
61
+ onTitleResults?: (results: SessionTitleSearchResult[]) => void;
52
62
  onProgress?: (update: SessionConversationSearchProgressUpdate) => void;
53
63
  };
54
64
 
@@ -156,6 +166,67 @@ function toSummaryText(customName: string | null, fallback: string | null | unde
156
166
  return trimmedFallback.length > 50 ? `${trimmedFallback.slice(0, 50)}...` : trimmedFallback;
157
167
  }
158
168
 
169
+ /**
170
+ * Finds visible sessions whose displayed title contains the query. Title
171
+ * matches are resolved from the database before transcript scanning so the UI
172
+ * can always present them first, including sessions without a transcript yet.
173
+ */
174
+ function findSessionTitleResults(
175
+ sessions: SessionRepositoryRow[],
176
+ query: string,
177
+ limit: number,
178
+ ): SessionTitleSearchResult[] {
179
+ const normalizedQuery = query.toLocaleLowerCase().replace(/\s+/g, ' ');
180
+ const projectCache = new Map<string, ReturnType<typeof projectsDb.getProjectPath>>();
181
+
182
+ return sessions
183
+ .flatMap((session) => {
184
+ const sessionTitle = toSummaryText(session.custom_name, session.session_id, session.session_id);
185
+ const normalizedTitle = sessionTitle.toLocaleLowerCase().replace(/\s+/g, ' ');
186
+ const matchIndex = normalizedTitle.indexOf(normalizedQuery);
187
+ if (matchIndex === -1) {
188
+ return [];
189
+ }
190
+
191
+ const projectPath = typeof session.project_path === 'string' && session.project_path.trim()
192
+ ? session.project_path.trim()
193
+ : null;
194
+ const projectKey = projectPath ?? UNKNOWN_PROJECT_KEY;
195
+ if (!projectCache.has(projectKey)) {
196
+ projectCache.set(
197
+ projectKey,
198
+ projectPath ? projectsDb.getProjectPath(projectPath) : null,
199
+ );
200
+ }
201
+
202
+ const project = projectCache.get(projectKey) ?? null;
203
+ if (project?.isArchived) {
204
+ return [];
205
+ }
206
+
207
+ return [{
208
+ sessionId: session.session_id,
209
+ provider: session.provider,
210
+ projectId: project?.project_id ?? null,
211
+ projectDisplayName: projectPath
212
+ ? (project?.custom_project_name?.trim() || path.basename(projectPath) || projectPath)
213
+ : 'Unknown Project',
214
+ sessionTitle,
215
+ lastActivity: session.updated_at || session.created_at || null,
216
+ matchIndex,
217
+ }];
218
+ })
219
+ .sort((left, right) => {
220
+ if (left.matchIndex !== right.matchIndex) {
221
+ return left.matchIndex - right.matchIndex;
222
+ }
223
+
224
+ return new Date(right.lastActivity ?? 0).getTime() - new Date(left.lastActivity ?? 0).getTime();
225
+ })
226
+ .slice(0, limit)
227
+ .map(({ matchIndex: _matchIndex, ...result }) => result);
228
+ }
229
+
159
230
  function isInternalContent(content: string): boolean {
160
231
  return INTERNAL_CONTENT_PREFIXES.some((prefix) => content.startsWith(prefix));
161
232
  }
@@ -1071,28 +1142,42 @@ async function parseSessionMatches(
1071
1142
  return null;
1072
1143
  }
1073
1144
 
1145
+ /**
1146
+ * Searches session titles and provider transcripts for the provider search
1147
+ * service and its route-level integration tests.
1148
+ */
1074
1149
  export async function searchConversations(
1075
1150
  query: string,
1076
1151
  limit = 50,
1077
1152
  onProjectResult: ((update: SessionConversationSearchProgressUpdate) => void) | null = null,
1078
1153
  signal: AbortSignal | null = null,
1079
- ): Promise<{ results: ProjectConversationResult[]; totalMatches: number; query: string }> {
1154
+ onTitleResults: ((results: SessionTitleSearchResult[]) => void) | null = null,
1155
+ ): Promise<{
1156
+ results: ProjectConversationResult[];
1157
+ titleResults: SessionTitleSearchResult[];
1158
+ totalMatches: number;
1159
+ query: string;
1160
+ }> {
1080
1161
  const safeQuery = typeof query === 'string' ? query.trim() : '';
1081
1162
  const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? limit : 50, 200));
1082
1163
  const words = safeQuery.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
1083
1164
 
1084
1165
  if (words.length === 0) {
1085
- return { results: [], totalMatches: 0, query: safeQuery };
1166
+ return { results: [], titleResults: [], totalMatches: 0, query: safeQuery };
1086
1167
  }
1087
1168
 
1088
1169
  const isAborted = () => signal?.aborted === true;
1089
1170
  if (isAborted()) {
1090
- return { results: [], totalMatches: 0, query: safeQuery };
1171
+ return { results: [], titleResults: [], totalMatches: 0, query: safeQuery };
1091
1172
  }
1092
1173
 
1093
- const searchableSessions = normalizeSearchableSessions(sessionsDb.getAllSessions());
1174
+ const activeSessions = sessionsDb.getAllSessions();
1175
+ const titleResults = findSessionTitleResults(activeSessions, safeQuery, safeLimit);
1176
+ onTitleResults?.(titleResults);
1177
+
1178
+ const searchableSessions = normalizeSearchableSessions(activeSessions);
1094
1179
  if (searchableSessions.length === 0) {
1095
- return { results: [], totalMatches: 0, query: safeQuery };
1180
+ return { results: [], titleResults, totalMatches: 0, query: safeQuery };
1096
1181
  }
1097
1182
 
1098
1183
  const sessionsByPathKey = new Map<string, SearchableSessionRow[]>();
@@ -1123,7 +1208,7 @@ export async function searchConversations(
1123
1208
  signal ?? undefined,
1124
1209
  );
1125
1210
  if (isAborted() || matchedFileKeys.size === 0) {
1126
- return { results: [], totalMatches: 0, query: safeQuery };
1211
+ return { results: [], titleResults, totalMatches: 0, query: safeQuery };
1127
1212
  }
1128
1213
 
1129
1214
  const matchedSessionKeys = new Set<string>();
@@ -1207,6 +1292,7 @@ export async function searchConversations(
1207
1292
 
1208
1293
  return {
1209
1294
  results,
1295
+ titleResults,
1210
1296
  totalMatches: runtime.totalMatches,
1211
1297
  query: safeQuery,
1212
1298
  };
@@ -1229,6 +1315,7 @@ export const sessionConversationsSearchService = {
1229
1315
  input.limit,
1230
1316
  input.onProgress ?? null,
1231
1317
  input.signal ?? null,
1318
+ input.onTitleResults ?? null,
1232
1319
  );
1233
1320
  },
1234
1321
  };
@@ -0,0 +1,139 @@
1
+ import fsp from 'node:fs/promises';
2
+
3
+ import type { FetchHistoryResult } from '@/shared/types.js';
4
+
5
+ /**
6
+ * Full-transcript cache for session history reads.
7
+ *
8
+ * Every provider history reader materializes the complete normalized
9
+ * transcript and then slices out the requested page, so serving a 20-row page
10
+ * of a large session re-read and re-parsed the whole transcript file on every
11
+ * request — opening a session, each older page while scrolling up, and every
12
+ * post-turn refresh. For a multi-megabyte JSONL that is most of a second of
13
+ * CPU per request.
14
+ *
15
+ * Entries are keyed by app session id and validated with one `stat` per
16
+ * request against the transcript file's identity (path + mtime + size), so
17
+ * only the first read after the file changes pays the parse. Anything that
18
+ * rewrites history (a new turn, an edit, a rewind, a fork) touches the file
19
+ * and invalidates naturally; no explicit invalidation hooks exist or are
20
+ * needed.
21
+ *
22
+ * Only history readers that read `jsonl_path` itself may use this cache —
23
+ * callers pass `transcriptPath: null` for providers whose messages live
24
+ * elsewhere (Cursor's store.db, OpenCode's shared SQLite), which bypasses
25
+ * caching entirely.
26
+ */
27
+
28
+ type CacheEntry = {
29
+ transcriptPath: string;
30
+ mtimeMs: number;
31
+ /** File size in bytes; doubles as the entry's cost against the byte budget. */
32
+ size: number;
33
+ full: FetchHistoryResult;
34
+ };
35
+
36
+ type GetFullHistoryArgs = {
37
+ sessionId: string;
38
+ /** Path of the file the provider's history reader actually parses, or null to bypass. */
39
+ transcriptPath: string | null | undefined;
40
+ /** Loads the complete transcript (`limit: null, offset: 0`) from the provider. */
41
+ loadFull: () => Promise<FetchHistoryResult>;
42
+ };
43
+
44
+ /**
45
+ * A transcript entry's heap cost is roughly the file it was parsed from, so
46
+ * the budget is expressed in file bytes. The newest entry is always retained
47
+ * even when it alone exceeds the budget — evicting it would just re-parse the
48
+ * same file on the next request.
49
+ */
50
+ const MAX_CACHED_TRANSCRIPT_FILE_BYTES = 256 * 1024 * 1024;
51
+ const MAX_CACHE_ENTRIES = 8;
52
+
53
+ export function createSessionHistoryCache(
54
+ maxTotalFileBytes = MAX_CACHED_TRANSCRIPT_FILE_BYTES,
55
+ maxEntries = MAX_CACHE_ENTRIES,
56
+ ) {
57
+ const entries = new Map<string, CacheEntry>();
58
+ const pendingLoads = new Map<string, Promise<FetchHistoryResult>>();
59
+
60
+ function evictOverBudget(): void {
61
+ let totalBytes = 0;
62
+ for (const entry of entries.values()) {
63
+ totalBytes += entry.size;
64
+ }
65
+ for (const key of entries.keys()) {
66
+ if (entries.size <= 1 || (totalBytes <= maxTotalFileBytes && entries.size <= maxEntries)) {
67
+ break;
68
+ }
69
+ totalBytes -= entries.get(key)!.size;
70
+ entries.delete(key);
71
+ }
72
+ }
73
+
74
+ return {
75
+ /**
76
+ * Returns the session's full transcript through the cache, or null when
77
+ * the session is not cacheable (no transcript path, or the file cannot be
78
+ * stat'ed) — the caller then falls back to a plain provider read.
79
+ */
80
+ async getFullHistory({ sessionId, transcriptPath, loadFull }: GetFullHistoryArgs): Promise<FetchHistoryResult | null> {
81
+ if (!transcriptPath) {
82
+ return null;
83
+ }
84
+
85
+ let stat;
86
+ try {
87
+ stat = await fsp.stat(transcriptPath);
88
+ } catch {
89
+ entries.delete(sessionId);
90
+ return null;
91
+ }
92
+ if (!stat.isFile()) {
93
+ entries.delete(sessionId);
94
+ return null;
95
+ }
96
+
97
+ const cached = entries.get(sessionId);
98
+ if (
99
+ cached
100
+ && cached.transcriptPath === transcriptPath
101
+ && cached.mtimeMs === stat.mtimeMs
102
+ && cached.size === stat.size
103
+ ) {
104
+ // Re-insert to mark as most recently used.
105
+ entries.delete(sessionId);
106
+ entries.set(sessionId, cached);
107
+ return cached.full;
108
+ }
109
+
110
+ // Concurrent requests for the same session share one parse. The file may
111
+ // gain rows while the load runs; the pre-load stat is what the entry is
112
+ // keyed by, so the next request would see a changed stat and re-read.
113
+ const pending = pendingLoads.get(sessionId);
114
+ if (pending) {
115
+ return pending;
116
+ }
117
+
118
+ const load = loadFull().then((full) => {
119
+ entries.delete(sessionId);
120
+ entries.set(sessionId, {
121
+ transcriptPath,
122
+ mtimeMs: stat.mtimeMs,
123
+ size: stat.size,
124
+ full,
125
+ });
126
+ evictOverBudget();
127
+ return full;
128
+ });
129
+ pendingLoads.set(sessionId, load);
130
+ try {
131
+ return await load;
132
+ } finally {
133
+ pendingLoads.delete(sessionId);
134
+ }
135
+ },
136
+ };
137
+ }
138
+
139
+ export const sessionHistoryCache = createSessionHistoryCache();