wind-agent-cli 1.37.2 → 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 (374) hide show
  1. package/LICENSE +718 -718
  2. package/README.md +1 -1
  3. package/dist/api-docs.html +901 -901
  4. package/dist/assets/{CodeEditor-B2Wf7Fg0.js → CodeEditor-BqmxpZ_D.js} +1 -1
  5. package/dist/assets/{FileTree-N2IkkHlv.js → FileTree-DLE7Ioai.js} +1 -1
  6. package/dist/assets/{GitPanel-DZCeuFZ2.js → GitPanel-f8RMRa8D.js} +1 -1
  7. package/dist/assets/{MarkdownPreview-C0tqYnVu.js → MarkdownPreview-93h_1tJa.js} +1 -1
  8. package/dist/assets/{PluginTabContent-Bnuf-fb8.js → PluginTabContent-DU1jjkXP.js} +1 -1
  9. package/dist/assets/{Shell-BfFnMZ-f.js → Shell-CN_2MRIM.js} +1 -1
  10. package/dist/assets/{StandaloneShell-QmyYwNAl.js → StandaloneShell-D0avS8UE.js} +1 -1
  11. package/dist/assets/{TaskMasterPanel-fMsmvN9L.js → TaskMasterPanel-Cpf1Q5Ph.js} +3 -3
  12. package/dist/assets/{abnfDiagram-N423BO3Z-DjPU7HLs.js → abnfDiagram-N423BO3Z-p3aJIm4T.js} +1 -1
  13. package/dist/assets/{arc-Pdq6Tvr3.js → arc-CTeud6Fl.js} +1 -1
  14. package/dist/assets/{architectureDiagram-T3A2C74G-D0JnT0Vh.js → architectureDiagram-T3A2C74G-CG7kRCJU.js} +1 -1
  15. package/dist/assets/{blockDiagram-VBNYF7ZC-DpaEf3mv.js → blockDiagram-VBNYF7ZC-oSjQ1SgE.js} +1 -1
  16. package/dist/assets/{c4Diagram-5PPSVZJV-MpqlPGFi.js → c4Diagram-5PPSVZJV-RkzF0DJV.js} +1 -1
  17. package/dist/assets/channel-BfQbarn9.js +1 -0
  18. package/dist/assets/{chunk-2GRJ4B5K-DoSdNo8u.js → chunk-2GRJ4B5K-B07uZ07O.js} +1 -1
  19. package/dist/assets/{chunk-2Q5K7J3B-CE7I3uMn.js → chunk-2Q5K7J3B-DNSfO7xS.js} +1 -1
  20. package/dist/assets/{chunk-5RXB4S5H-B4UQ8snD.js → chunk-5RXB4S5H-BTeYcgyb.js} +1 -1
  21. package/dist/assets/{chunk-5VM5RSS4-B55XgqHZ.js → chunk-5VM5RSS4-B7vZV0zC.js} +1 -1
  22. package/dist/assets/{chunk-6Q2QTUOP-BS1KpefW.js → chunk-6Q2QTUOP-DuS_xUGG.js} +1 -1
  23. package/dist/assets/{chunk-GF5L2VYU-DkEPedJP.js → chunk-GF5L2VYU-CKLZ3iVD.js} +1 -1
  24. package/dist/assets/{chunk-JWPE2WC7-DOtkNhMm.js → chunk-JWPE2WC7-DD2hcXvs.js} +1 -1
  25. package/dist/assets/{chunk-KBJHAD2P-BXLjviqM.js → chunk-KBJHAD2P-Cz-l38qp.js} +1 -1
  26. package/dist/assets/{chunk-RYQCIY6F-C50fjNNz.js → chunk-RYQCIY6F-DGFIU1YM.js} +1 -1
  27. package/dist/assets/{chunk-XXDRQBXY-QBYqCkb7.js → chunk-XXDRQBXY-CmfzoYFl.js} +1 -1
  28. package/dist/assets/classDiagram-JCYQIIEL-Bpfe3kq7.js +1 -0
  29. package/dist/assets/classDiagram-v2-OCEON4UE-Bpfe3kq7.js +1 -0
  30. package/dist/assets/{code-xml-VK1YfShP.js → code-xml-CLpHzEOP.js} +1 -1
  31. package/dist/assets/{cose-bilkent-JH36ORCC-DrVTo1Nh.js → cose-bilkent-JH36ORCC-DlFcLXCB.js} +1 -1
  32. package/dist/assets/{cynefin-VYW2F7L2-DnJa_OlQ.js → cynefin-VYW2F7L2-P1HecReO.js} +1 -1
  33. package/dist/assets/{cynefinDiagram-MW4NZA55-CCxBjw9E.js → cynefinDiagram-MW4NZA55-COgduz5P.js} +1 -1
  34. package/dist/assets/{dagre-VZM6K2ZE-C_GvE4lu.js → dagre-VZM6K2ZE-Bbb-siGp.js} +1 -1
  35. package/dist/assets/{diagram-7IWD3JNH-pFzs9HEu.js → diagram-7IWD3JNH-9CWGFMYR.js} +1 -1
  36. package/dist/assets/{diagram-B4RE2ZJO-CcehQC7x.js → diagram-B4RE2ZJO-TWK59T6V.js} +1 -1
  37. package/dist/assets/{diagram-LBJQPF4R-hfKcrrxe.js → diagram-LBJQPF4R-ErMC9yaa.js} +1 -1
  38. package/dist/assets/{diagram-Q27KOJAE-C1semJfK.js → diagram-Q27KOJAE-Dt_-C7hV.js} +1 -1
  39. package/dist/assets/{diagram-UB23O5K3-57hvXQXO.js → diagram-UB23O5K3-BJ1DnRAF.js} +1 -1
  40. package/dist/assets/{ebnfDiagram-BXEA7PRR-9KIAlAVw.js → ebnfDiagram-BXEA7PRR-CE1YaQ9s.js} +1 -1
  41. package/dist/assets/{erDiagram-JOGREHBK-B7OzJ767.js → erDiagram-JOGREHBK-Bqk7N9aY.js} +1 -1
  42. package/dist/assets/{flowDiagram-UKHOOZJN-CJV8fQPR.js → flowDiagram-UKHOOZJN-DEr2II5V.js} +1 -1
  43. package/dist/assets/{ganttDiagram-PKOTCBZU-CbMNWWQu.js → ganttDiagram-PKOTCBZU-CWYAUeYN.js} +1 -1
  44. package/dist/assets/{gitGraphDiagram-DS77QQ5N-I_vZ5waO.js → gitGraphDiagram-DS77QQ5N-tdEcEIM6.js} +1 -1
  45. package/dist/assets/{index-ZHwIojzi.js → index-B-sTfSZq.js} +1 -1
  46. package/dist/assets/{index-GFhXZyl2.js → index-BSe3UiFA.js} +1 -1
  47. package/dist/assets/{index-DFnVoqpx.js → index-B_FDMWar.js} +5 -5
  48. package/dist/assets/{index-DlSeAkw_.js → index-CWJtg2pR.js} +1 -1
  49. package/dist/assets/{infoDiagram-6WML65LV-Dh20Tsbq.js → infoDiagram-6WML65LV-A3IqRUnO.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DH2Sm3ko.js → ishikawaDiagram-WSZJBQD7-B2uL6bkK.js} +1 -1
  51. package/dist/assets/{journeyDiagram-NVQOT4AX-BNFQqlVu.js → journeyDiagram-NVQOT4AX-D9JB-hRW.js} +1 -1
  52. package/dist/assets/{kanban-definition-27J2QSJJ-B3iL0ztJ.js → kanban-definition-27J2QSJJ-DmlgiV7K.js} +1 -1
  53. package/dist/assets/{linear-Cwe9dEKA.js → linear-Ct_eoJQk.js} +1 -1
  54. package/dist/assets/{mermaid.core-BYRQSshi.js → mermaid.core-CQ698HNn.js} +5 -5
  55. package/dist/assets/{mindmap-definition-FAOFIHXS-BgfBYMzr.js → mindmap-definition-FAOFIHXS-DGtMgQhf.js} +1 -1
  56. package/dist/assets/{pegDiagram-VL7TDLO6-CNYpECr8.js → pegDiagram-VL7TDLO6-CO6vP4n9.js} +1 -1
  57. package/dist/assets/{pieDiagram-7S7Q4E2Y-DeSrxc4I.js → pieDiagram-7S7Q4E2Y-DMkt_Zfr.js} +1 -1
  58. package/dist/assets/{quadrantDiagram-CIZ2JOQS-BXVVQsa6.js → quadrantDiagram-CIZ2JOQS-DgNxRz6t.js} +1 -1
  59. package/dist/assets/{railroadDiagram-AXF67PYL-JTroIW8F.js → railroadDiagram-AXF67PYL-DKz9UqA9.js} +1 -1
  60. package/dist/assets/{requirementDiagram-LRYGKXZP-C1Z_inLz.js → requirementDiagram-LRYGKXZP-L84XyGgV.js} +1 -1
  61. package/dist/assets/{sankeyDiagram-W5VNT64P-DCJgKFUk.js → sankeyDiagram-W5VNT64P-CQqWbJ3j.js} +1 -1
  62. package/dist/assets/{sequenceDiagram-SI44F4Z6-D8Wrx5Vc.js → sequenceDiagram-SI44F4Z6--tnzS2tb.js} +1 -1
  63. package/dist/assets/{sizeCapture-X5ZJPWSS-CXqD68vD.js → sizeCapture-X5ZJPWSS-DujwEOtm.js} +1 -1
  64. package/dist/assets/{stateDiagram-OKZ733FA-CF0Ekqjo.js → stateDiagram-OKZ733FA-BK3TcZY2.js} +1 -1
  65. package/dist/assets/stateDiagram-v2-UEYNNEHI-_medyomK.js +1 -0
  66. package/dist/assets/{swimlanes-SLNWSIFB-DVIax_k4.js → swimlanes-SLNWSIFB-naX-Uv1B.js} +2 -2
  67. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DstIv3TC.js +8 -0
  68. package/dist/assets/{timeline-definition-Z64GVDOM-DSqk3ApG.js → timeline-definition-Z64GVDOM-C-vP_ddU.js} +1 -1
  69. package/dist/assets/{vennDiagram-T6HMQDX7-DNbTqyG5.js → vennDiagram-T6HMQDX7-DN-I7o-7.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-T6FBY63Y-BNCpsFZV.js → wardleyDiagram-T6FBY63Y-Db9dsaXv.js} +1 -1
  71. package/dist/assets/{xychartDiagram-ELKLHX3M-XsVZphHr.js → xychartDiagram-ELKLHX3M-DE6OxW_p.js} +1 -1
  72. package/dist/clear-cache.html +85 -85
  73. package/dist/convert-icons.md +52 -52
  74. package/dist/favicon.svg +8 -8
  75. package/dist/generate-icons.js +48 -48
  76. package/dist/icons/codex-white.svg +3 -3
  77. package/dist/icons/codex.svg +3 -3
  78. package/dist/icons/cursor-white.svg +11 -11
  79. package/dist/icons/icon-128x128.svg +11 -11
  80. package/dist/icons/icon-144x144.svg +11 -11
  81. package/dist/icons/icon-152x152.svg +11 -11
  82. package/dist/icons/icon-192x192.svg +11 -11
  83. package/dist/icons/icon-384x384.svg +11 -11
  84. package/dist/icons/icon-512x512.svg +11 -11
  85. package/dist/icons/icon-72x72.svg +11 -11
  86. package/dist/icons/icon-96x96.svg +11 -11
  87. package/dist/icons/icon-template.svg +11 -11
  88. package/dist/index.html +56 -56
  89. package/dist/logo.svg +17 -17
  90. package/dist/manifest.json +60 -60
  91. package/dist/sw.js +124 -124
  92. package/dist-server/server/modules/cli/cli.service.js +36 -36
  93. package/dist-server/server/modules/cli/sandbox.service.js +38 -38
  94. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +74 -74
  95. package/dist-server/server/modules/commands/commands.routes.js +24 -24
  96. package/dist-server/server/modules/database/migrations.js +190 -190
  97. package/dist-server/server/modules/database/repositories/api-keys.js +3 -3
  98. package/dist-server/server/modules/database/repositories/github-tokens.js +2 -2
  99. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +28 -28
  100. package/dist-server/server/modules/database/repositories/notification-preferences.js +4 -4
  101. package/dist-server/server/modules/database/repositories/projects.db.js +61 -61
  102. package/dist-server/server/modules/database/repositories/provider-models.js +35 -35
  103. package/dist-server/server/modules/database/repositories/push-subscriptions.js +5 -5
  104. package/dist-server/server/modules/database/repositories/scan-state.db.js +5 -5
  105. package/dist-server/server/modules/database/repositories/sessions.db.js +92 -92
  106. package/dist-server/server/modules/database/schema.js +177 -177
  107. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +6 -6
  108. package/dist-server/server/modules/git/git.routes.js +17 -17
  109. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +12 -12
  110. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +9 -9
  111. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +17 -17
  112. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +25 -25
  113. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +27 -27
  114. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +9 -9
  115. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -3
  116. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +96 -96
  117. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +18 -18
  118. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +419 -419
  119. package/electron/cloud.js +260 -260
  120. package/electron/desktopNotifications.js +378 -378
  121. package/electron/desktopWindow.js +766 -766
  122. package/electron/launcher/index.html +14 -14
  123. package/electron/launcher/launcher.css +801 -801
  124. package/electron/launcher/launcher.js +687 -687
  125. package/electron/localServer.js +549 -549
  126. package/electron/main.js +944 -944
  127. package/electron/preload.cjs +60 -60
  128. package/electron/scripts/generate-macos-icon.js +62 -62
  129. package/electron/serverInstaller.js +277 -277
  130. package/electron/tabs.js +87 -87
  131. package/electron/viewHost.js +331 -331
  132. package/package.json +250 -247
  133. package/public/api-docs.html +901 -901
  134. package/scripts/fix-node-pty.js +67 -67
  135. package/scripts/promote-dist-server.mjs +50 -50
  136. package/scripts/release/build-server-bundle.js +176 -176
  137. package/scripts/release/prepare-desktop-app.js +152 -152
  138. package/server/index.ts +399 -399
  139. package/server/load-env.ts +47 -47
  140. package/server/modules/agent/agent.module.ts +52 -52
  141. package/server/modules/agent/agent.routes.ts +1287 -1287
  142. package/server/modules/agent/index.ts +2 -2
  143. package/server/modules/agent/tests/agent.routes.test.ts +205 -205
  144. package/server/modules/assets/assets.routes.ts +150 -150
  145. package/server/modules/assets/index.ts +3 -3
  146. package/server/modules/assets/services/image-assets.service.ts +127 -127
  147. package/server/modules/assets/tests/image-assets.service.test.ts +74 -74
  148. package/server/modules/auth/auth.middleware.ts +163 -163
  149. package/server/modules/auth/auth.module.ts +38 -38
  150. package/server/modules/auth/auth.routes.ts +57 -57
  151. package/server/modules/auth/auth.service.ts +155 -155
  152. package/server/modules/auth/index.ts +9 -9
  153. package/server/modules/auth/tests/auth.service.test.ts +95 -95
  154. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -120
  155. package/server/modules/browser-use/browser-use-mcp.ts +386 -386
  156. package/server/modules/browser-use/browser-use-runtime.ts +10 -10
  157. package/server/modules/browser-use/browser-use.routes.ts +96 -96
  158. package/server/modules/browser-use/browser-use.service.ts +836 -836
  159. package/server/modules/browser-use/index.ts +11 -11
  160. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -10
  161. package/server/modules/cli/cli.module.ts +93 -93
  162. package/server/modules/cli/cli.service.ts +248 -248
  163. package/server/modules/cli/cli.ts +19 -19
  164. package/server/modules/cli/index.ts +2 -2
  165. package/server/modules/cli/sandbox.service.ts +398 -398
  166. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -186
  167. package/server/modules/cli/tests/cli.service.test.ts +86 -86
  168. package/server/modules/cli/tests/sandbox.service.test.ts +58 -58
  169. package/server/modules/commands/commands.module.ts +22 -22
  170. package/server/modules/commands/commands.routes.ts +586 -586
  171. package/server/modules/commands/index.ts +2 -2
  172. package/server/modules/commands/tests/commands.test.ts +109 -109
  173. package/server/modules/database/connection.ts +143 -143
  174. package/server/modules/database/index.ts +17 -17
  175. package/server/modules/database/init-db.ts +17 -17
  176. package/server/modules/database/migrations.ts +515 -515
  177. package/server/modules/database/repositories/api-keys.ts +119 -119
  178. package/server/modules/database/repositories/app-config.ts +53 -53
  179. package/server/modules/database/repositories/credentials.ts +106 -106
  180. package/server/modules/database/repositories/github-tokens.ts +100 -100
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -153
  182. package/server/modules/database/repositories/notification-preferences.ts +117 -117
  183. package/server/modules/database/repositories/projects.db.ts +196 -196
  184. package/server/modules/database/repositories/provider-models.ts +168 -168
  185. package/server/modules/database/repositories/push-subscriptions.ts +80 -80
  186. package/server/modules/database/repositories/scan-state.db.ts +42 -42
  187. package/server/modules/database/repositories/sessions.db.ts +524 -524
  188. package/server/modules/database/repositories/users.ts +140 -140
  189. package/server/modules/database/repositories/vapid-keys.ts +57 -57
  190. package/server/modules/database/schema.ts +210 -210
  191. package/server/modules/database/tests/projects.db.integration.test.ts +72 -72
  192. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -125
  193. package/server/modules/database/tests/sessions-provider-mapping.test.ts +109 -109
  194. package/server/modules/database/tests/sessions.db.integration.test.ts +116 -116
  195. package/server/modules/file-tree/file-tree.module.ts +116 -116
  196. package/server/modules/file-tree/file-tree.routes.ts +258 -258
  197. package/server/modules/file-tree/file-tree.service.ts +675 -675
  198. package/server/modules/file-tree/index.ts +2 -2
  199. package/server/modules/file-tree/tests/file-tree.routes.test.ts +157 -157
  200. package/server/modules/file-tree/tests/file-tree.service.test.ts +371 -371
  201. package/server/modules/git/git-branch.service.ts +43 -43
  202. package/server/modules/git/git-parsing.service.ts +66 -66
  203. package/server/modules/git/git.module.ts +22 -22
  204. package/server/modules/git/git.routes.ts +1596 -1596
  205. package/server/modules/git/index.ts +2 -2
  206. package/server/modules/git/tests/git-branch.service.test.ts +60 -60
  207. package/server/modules/git/tests/git-init.routes.test.ts +132 -132
  208. package/server/modules/git/tests/git.test.ts +106 -106
  209. package/server/modules/notifications/index.ts +24 -24
  210. package/server/modules/notifications/notifications.routes.ts +127 -127
  211. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -124
  212. package/server/modules/notifications/services/notification-orchestrator.service.js +310 -310
  213. package/server/modules/notifications/tests/notification-orchestrator.integration.test.ts +55 -55
  214. package/server/modules/notifications/vapid-keys.service.ts +38 -38
  215. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -109
  216. package/server/modules/plugins/index.ts +9 -9
  217. package/server/modules/plugins/plugin-process.service.ts +217 -217
  218. package/server/modules/plugins/plugin-registry.service.ts +459 -459
  219. package/server/modules/plugins/plugins.module.ts +33 -33
  220. package/server/modules/plugins/plugins.routes.ts +66 -66
  221. package/server/modules/plugins/plugins.service.ts +147 -147
  222. package/server/modules/plugins/tests/plugins.service.test.ts +32 -32
  223. package/server/modules/projects/index.ts +11 -11
  224. package/server/modules/projects/projects.routes.ts +273 -273
  225. package/server/modules/projects/services/project-clone.service.ts +323 -323
  226. package/server/modules/projects/services/project-delete.service.ts +90 -90
  227. package/server/modules/projects/services/project-management.service.ts +144 -144
  228. package/server/modules/projects/services/project-star.service.ts +78 -78
  229. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -248
  230. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +314 -314
  231. package/server/modules/projects/tests/project-clone.service.test.ts +183 -183
  232. package/server/modules/projects/tests/project-management.service.test.ts +117 -117
  233. package/server/modules/projects/tests/project-star.service.test.ts +123 -123
  234. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -105
  235. package/server/modules/providers/README.md +380 -380
  236. package/server/modules/providers/index.ts +10 -10
  237. package/server/modules/providers/list/claude/claude-auth.provider.ts +160 -160
  238. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  239. package/server/modules/providers/list/claude/claude-models.provider.ts +294 -294
  240. package/server/modules/providers/list/claude/claude-runtime.provider.js +1068 -1068
  241. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +203 -203
  242. package/server/modules/providers/list/claude/claude-sessions.provider.ts +684 -684
  243. package/server/modules/providers/list/claude/claude-skills.provider.ts +265 -265
  244. package/server/modules/providers/list/claude/claude.provider.ts +30 -30
  245. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -100
  246. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  247. package/server/modules/providers/list/codex/codex-models.provider.ts +123 -123
  248. package/server/modules/providers/list/codex/codex-runtime.provider.js +533 -533
  249. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +203 -203
  250. package/server/modules/providers/list/codex/codex-sessions.provider.ts +938 -938
  251. package/server/modules/providers/list/codex/codex-skills.provider.ts +73 -73
  252. package/server/modules/providers/list/codex/codex.provider.ts +28 -28
  253. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  254. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  255. package/server/modules/providers/list/cursor/cursor-models.provider.ts +119 -119
  256. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +385 -385
  257. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +159 -159
  258. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +654 -654
  259. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +39 -39
  260. package/server/modules/providers/list/cursor/cursor.provider.ts +30 -30
  261. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +110 -110
  262. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -228
  263. package/server/modules/providers/list/opencode/opencode-models.provider.ts +203 -203
  264. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +434 -434
  265. package/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +261 -261
  266. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +179 -179
  267. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +500 -500
  268. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -78
  269. package/server/modules/providers/list/opencode/opencode.provider.ts +30 -30
  270. package/server/modules/providers/provider.registry.ts +36 -36
  271. package/server/modules/providers/provider.routes.ts +877 -877
  272. package/server/modules/providers/services/mcp.service.ts +109 -109
  273. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  274. package/server/modules/providers/services/provider-capabilities.service.ts +97 -97
  275. package/server/modules/providers/services/provider-models.service.ts +401 -401
  276. package/server/modules/providers/services/provider-runtime.service.ts +108 -108
  277. package/server/modules/providers/services/provider-token-usage.service.ts +357 -357
  278. package/server/modules/providers/services/session-conversations-search.service.ts +1321 -1321
  279. package/server/modules/providers/services/session-synchronizer.service.ts +74 -74
  280. package/server/modules/providers/services/sessions-watcher.service.ts +247 -247
  281. package/server/modules/providers/services/sessions.service.ts +471 -471
  282. package/server/modules/providers/services/skills.service.ts +39 -39
  283. package/server/modules/providers/shared/base/abstract.provider.ts +32 -32
  284. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  285. package/server/modules/providers/shared/skills/skills.provider.ts +287 -287
  286. package/server/modules/providers/tests/claude-auth.test.ts +150 -150
  287. package/server/modules/providers/tests/claude-sessions.test.ts +61 -61
  288. package/server/modules/providers/tests/codex-sessions.test.ts +231 -231
  289. package/server/modules/providers/tests/mcp.test.ts +349 -349
  290. package/server/modules/providers/tests/opencode-models.test.ts +41 -41
  291. package/server/modules/providers/tests/opencode-sessions.test.ts +522 -522
  292. package/server/modules/providers/tests/provider-attachment-history.test.ts +247 -247
  293. package/server/modules/providers/tests/provider-models.service.test.ts +353 -353
  294. package/server/modules/providers/tests/provider-runtime.service.test.ts +138 -138
  295. package/server/modules/providers/tests/provider-token-usage.service.test.ts +186 -186
  296. package/server/modules/providers/tests/provider.routes.test.ts +244 -244
  297. package/server/modules/providers/tests/sessions-details.test.ts +73 -73
  298. package/server/modules/providers/tests/sessions.service.test.ts +127 -127
  299. package/server/modules/providers/tests/skills.test.ts +692 -692
  300. package/server/modules/settings/index.ts +2 -2
  301. package/server/modules/settings/settings.module.ts +50 -50
  302. package/server/modules/settings/settings.routes.ts +51 -51
  303. package/server/modules/settings/settings.service.ts +186 -186
  304. package/server/modules/settings/tests/settings.service.test.ts +54 -54
  305. package/server/modules/system/index.ts +2 -2
  306. package/server/modules/system/system.module.ts +62 -62
  307. package/server/modules/system/system.routes.ts +21 -21
  308. package/server/modules/system/system.service.ts +76 -76
  309. package/server/modules/system/tests/system.service.test.ts +118 -118
  310. package/server/modules/taskmaster/index.ts +2 -2
  311. package/server/modules/taskmaster/taskmaster.module.ts +24 -24
  312. package/server/modules/taskmaster/taskmaster.routes.ts +1482 -1482
  313. package/server/modules/taskmaster/taskmaster.service.ts +132 -132
  314. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +144 -144
  315. package/server/modules/taskmaster/tests/taskmaster.service.test.ts +97 -97
  316. package/server/modules/user/index.ts +2 -2
  317. package/server/modules/user/tests/user.service.test.ts +61 -61
  318. package/server/modules/user/user.module.ts +58 -58
  319. package/server/modules/user/user.routes.ts +50 -50
  320. package/server/modules/user/user.service.ts +85 -85
  321. package/server/modules/voice/index.ts +2 -2
  322. package/server/modules/voice/tests/voice.service.test.ts +98 -98
  323. package/server/modules/voice/voice.module.ts +47 -47
  324. package/server/modules/voice/voice.routes.ts +113 -113
  325. package/server/modules/voice/voice.service.ts +192 -192
  326. package/server/modules/websocket/README.md +273 -273
  327. package/server/modules/websocket/index.ts +3 -3
  328. package/server/modules/websocket/services/chat-run-registry.service.ts +343 -343
  329. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -145
  330. package/server/modules/websocket/services/chat-websocket.service.ts +425 -425
  331. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -65
  332. package/server/modules/websocket/services/shell-websocket.service.ts +502 -502
  333. package/server/modules/websocket/services/websocket-auth.service.ts +59 -59
  334. package/server/modules/websocket/services/websocket-server.service.ts +124 -124
  335. package/server/modules/websocket/services/websocket-state.service.ts +16 -16
  336. package/server/modules/websocket/services/websocket-writer.service.ts +38 -38
  337. package/server/modules/websocket/tests/chat-attachment-filter.test.ts +59 -59
  338. package/server/modules/websocket/tests/chat-run-registry.test.ts +282 -282
  339. package/server/modules/websocket/tests/shell-websocket.service.test.ts +100 -100
  340. package/server/modules/websocket/tests/websocket-heartbeat.service.test.ts +78 -78
  341. package/server/modules/worktrees/index.ts +2 -2
  342. package/server/modules/worktrees/services/worktree-create-and-open.service.ts +53 -53
  343. package/server/modules/worktrees/services/worktree-create.service.ts +104 -104
  344. package/server/modules/worktrees/services/worktree-git.service.ts +207 -207
  345. package/server/modules/worktrees/services/worktree-list.service.ts +143 -143
  346. package/server/modules/worktrees/services/worktree-merge.service.ts +157 -157
  347. package/server/modules/worktrees/services/worktree-open.service.ts +85 -85
  348. package/server/modules/worktrees/services/worktree-remove.service.ts +88 -88
  349. package/server/modules/worktrees/tests/worktree-create-and-open.service.test.ts +68 -68
  350. package/server/modules/worktrees/tests/worktree-create.service.test.ts +142 -142
  351. package/server/modules/worktrees/tests/worktree-git.service.test.ts +84 -84
  352. package/server/modules/worktrees/tests/worktree-list.service.test.ts +45 -45
  353. package/server/modules/worktrees/tests/worktree-merge.service.test.ts +213 -213
  354. package/server/modules/worktrees/tests/worktree-open.service.test.ts +122 -122
  355. package/server/modules/worktrees/tests/worktree-remove.service.test.ts +227 -227
  356. package/server/modules/worktrees/tests/worktrees.routes.test.ts +199 -199
  357. package/server/modules/worktrees/worktrees.module.ts +113 -113
  358. package/server/modules/worktrees/worktrees.routes.ts +116 -116
  359. package/server/shared/claude-cli-path.ts +139 -139
  360. package/server/shared/frontmatter.ts +18 -18
  361. package/server/shared/image-attachments.ts +435 -435
  362. package/server/shared/interfaces.ts +179 -179
  363. package/server/shared/tests/claude-cli-path.test.ts +61 -61
  364. package/server/shared/tests/image-attachments.test.ts +353 -353
  365. package/server/shared/tests/slice-tail-page.test.ts +42 -42
  366. package/server/shared/types.ts +1226 -1226
  367. package/server/shared/utils.ts +1161 -1161
  368. package/server/tsconfig.json +40 -40
  369. package/shared/networkHosts.js +22 -22
  370. package/dist/assets/channel-Bm0ZLE4z.js +0 -1
  371. package/dist/assets/classDiagram-JCYQIIEL-DqEOcNxw.js +0 -1
  372. package/dist/assets/classDiagram-v2-OCEON4UE-DqEOcNxw.js +0 -1
  373. package/dist/assets/stateDiagram-v2-UEYNNEHI-DARLpEFJ.js +0 -1
  374. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CrgDgaY8.js +0 -8
@@ -1,524 +1,524 @@
1
- import { getConnection } from '@/modules/database/connection.js';
2
- import { projectsDb } from '@/modules/database/repositories/projects.db.js';
3
- import { normalizeProjectPath } from '@/shared/utils.js';
4
-
5
- type SessionRow = {
6
- session_id: string;
7
- provider: string;
8
- provider_session_id: string | null;
9
- project_path: string | null;
10
- jsonl_path: string | null;
11
- custom_name: string | null;
12
- /** Model this session runs with; NULL until the app records one for it. */
13
- model: string | null;
14
- /** Reasoning effort this session runs with; NULL until the app records one. */
15
- effort: string | null;
16
- isArchived: number;
17
- created_at: string;
18
- updated_at: string;
19
- };
20
-
21
- type RecentSessionsPage = {
22
- sessions: SessionRow[];
23
- total: number;
24
- };
25
-
26
- const SESSION_ROW_COLUMNS =
27
- 'session_id, provider, provider_session_id, project_path, jsonl_path, custom_name, model, effort, isArchived, created_at, updated_at';
28
-
29
- const SQLITE_UTC_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
30
-
31
- function normalizeTimestamp(value?: string): string | null {
32
- if (!value) return null;
33
-
34
- // SQLite CURRENT_TIMESTAMP is stored as UTC without a timezone suffix.
35
- // Normalize it here so every session reader returns canonical ISO strings
36
- // and the sidebar never interprets fresh rows as local-time "hours old".
37
- const normalizedValue = SQLITE_UTC_TIMESTAMP_REGEX.test(value)
38
- ? `${value.replace(' ', 'T')}Z`
39
- : value;
40
-
41
- const parsed = new Date(normalizedValue);
42
- if (Number.isNaN(parsed.getTime())) {
43
- return null;
44
- }
45
-
46
- return parsed.toISOString();
47
- }
48
-
49
- function normalizeSessionRow<T extends SessionRow | null | undefined>(row: T): T {
50
- if (!row) {
51
- return row;
52
- }
53
-
54
- return {
55
- ...row,
56
- created_at: normalizeTimestamp(row.created_at) ?? row.created_at,
57
- updated_at: normalizeTimestamp(row.updated_at) ?? row.updated_at,
58
- };
59
- }
60
-
61
- function normalizeSessionRows(rows: SessionRow[]): SessionRow[] {
62
- return rows.map((row) => normalizeSessionRow(row) as SessionRow);
63
- }
64
-
65
- function normalizeProjectPathForProvider(provider: string, projectPath: string): string {
66
- void provider;
67
- return normalizeProjectPath(projectPath);
68
- }
69
-
70
- export const sessionsDb = {
71
- /**
72
- * Upserts one session row discovered on disk by a provider synchronizer.
73
- *
74
- * The given id is the provider-native session id. Rows are keyed by
75
- * `provider_session_id` so a session that was first created by the app
76
- * (with an app-allocated `session_id`) is updated in place once its
77
- * transcript shows up on disk, instead of producing a duplicate row. An
78
- * app-created row keeps its existing name; synchronizer names only update
79
- * rows that were themselves created by indexing provider storage.
80
- */
81
- createSession(
82
- providerSessionId: string,
83
- provider: string,
84
- projectPath: string,
85
- customName?: string,
86
- createdAt?: string,
87
- updatedAt?: string,
88
- jsonlPath?: string | null
89
- ): string {
90
- const db = getConnection();
91
- const createdAtValue = normalizeTimestamp(createdAt);
92
- const updatedAtValue = normalizeTimestamp(updatedAt);
93
- const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
94
-
95
- // First, ensure the project path is recorded in the projects table,
96
- // since it's a foreign key in the sessions table.
97
- projectsDb.createProjectPath(normalizedProjectPath);
98
-
99
- const existing = db
100
- .prepare(
101
- `SELECT session_id FROM sessions
102
- WHERE provider_session_id = ? AND provider = ?
103
- LIMIT 1`
104
- )
105
- .get(providerSessionId, provider) as { session_id: string } | undefined;
106
-
107
- if (existing) {
108
- db.prepare(
109
- `UPDATE sessions SET
110
- provider = ?,
111
- updated_at = COALESCE(?, CURRENT_TIMESTAMP),
112
- project_path = ?,
113
- jsonl_path = ?,
114
- isArchived = 0,
115
- custom_name = CASE
116
- WHEN session_id <> provider_session_id AND custom_name IS NOT NULL THEN custom_name
117
- ELSE COALESCE(?, custom_name)
118
- END
119
- WHERE session_id = ?`
120
- ).run(
121
- provider,
122
- updatedAtValue,
123
- normalizedProjectPath,
124
- jsonlPath ?? null,
125
- customName ?? null,
126
- existing.session_id
127
- );
128
-
129
- return existing.session_id;
130
- }
131
-
132
- // Sessions created outside the app (directly via the provider CLI) are
133
- // keyed by the provider-native id for both columns. The ON CONFLICT path
134
- // covers legacy rows that predate the provider_session_id mapping.
135
- db.prepare(
136
- `INSERT INTO sessions (session_id, provider, provider_session_id, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
137
- VALUES (?, ?, ?, ?, ?, ?, 0, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
138
- ON CONFLICT(session_id) DO UPDATE SET
139
- provider = excluded.provider,
140
- provider_session_id = excluded.provider_session_id,
141
- updated_at = excluded.updated_at,
142
- project_path = excluded.project_path,
143
- jsonl_path = excluded.jsonl_path,
144
- isArchived = 0,
145
- custom_name = CASE
146
- WHEN sessions.session_id <> sessions.provider_session_id AND sessions.custom_name IS NOT NULL
147
- THEN sessions.custom_name
148
- ELSE COALESCE(excluded.custom_name, sessions.custom_name)
149
- END`
150
- ).run(
151
- providerSessionId,
152
- provider,
153
- providerSessionId,
154
- customName ?? null,
155
- normalizedProjectPath,
156
- jsonlPath ?? null,
157
- createdAtValue,
158
- updatedAtValue
159
- );
160
-
161
- return providerSessionId;
162
- },
163
-
164
- /**
165
- * Inserts one app-allocated session row before any provider run happens.
166
- *
167
- * The session gateway uses this when the frontend starts a brand-new chat:
168
- * `session_id` is the stable app-facing id, while `provider_session_id`
169
- * stays NULL until the provider runtime announces its own id and
170
- * `assignProviderSessionId` records the mapping. `customName` is derived
171
- * from the first visible CloudCLI message by the sessions service.
172
- */
173
- createAppSession(
174
- sessionId: string,
175
- provider: string,
176
- projectPath: string,
177
- customName?: string,
178
- ): string {
179
- const db = getConnection();
180
- const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
181
-
182
- projectsDb.createProjectPath(normalizedProjectPath);
183
-
184
- db.prepare(
185
- `INSERT INTO sessions (session_id, provider, provider_session_id, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
186
- VALUES (?, ?, NULL, ?, ?, NULL, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`
187
- ).run(sessionId, provider, customName ?? null, normalizedProjectPath);
188
-
189
- return sessionId;
190
- },
191
-
192
- /**
193
- * Records the provider-native session id for one app-allocated session.
194
- *
195
- * If the filesystem watcher indexed the provider transcript before this
196
- * mapping was recorded (a duplicate row keyed by the provider id exists),
197
- * the duplicate is merged into the app row: its transcript path and name
198
- * are adopted and the duplicate row is removed. Runs in a transaction so
199
- * the sidebar can never observe both rows at once.
200
- */
201
- assignProviderSessionId(sessionId: string, providerSessionId: string): void {
202
- const db = getConnection();
203
-
204
- const merge = db.transaction(() => {
205
- const duplicate = db
206
- .prepare(
207
- `SELECT ${SESSION_ROW_COLUMNS} FROM sessions
208
- WHERE (session_id = ? OR provider_session_id = ?)
209
- AND session_id <> ?
210
- LIMIT 1`
211
- )
212
- .get(providerSessionId, providerSessionId, sessionId) as SessionRow | undefined;
213
-
214
- if (duplicate) {
215
- db.prepare('DELETE FROM sessions WHERE session_id = ?').run(duplicate.session_id);
216
- db.prepare(
217
- `UPDATE sessions SET
218
- provider_session_id = ?,
219
- jsonl_path = COALESCE(jsonl_path, ?),
220
- custom_name = COALESCE(custom_name, ?),
221
- updated_at = CURRENT_TIMESTAMP
222
- WHERE session_id = ?`
223
- ).run(providerSessionId, duplicate.jsonl_path, duplicate.custom_name, sessionId);
224
- return;
225
- }
226
-
227
- db.prepare(
228
- `UPDATE sessions SET
229
- provider_session_id = ?,
230
- updated_at = CURRENT_TIMESTAMP
231
- WHERE session_id = ?`
232
- ).run(providerSessionId, sessionId);
233
- });
234
-
235
- merge();
236
- },
237
-
238
- /**
239
- * Records the model one session runs with.
240
- *
241
- * Called both when the user picks a model for the session and on every send,
242
- * so the row always reflects what the session last ran with and reopening it
243
- * restores that model instead of a catalog default.
244
- */
245
- setSessionModel(sessionId: string, model: string): void {
246
- const db = getConnection();
247
- db.prepare(
248
- `UPDATE sessions
249
- SET model = ?
250
- WHERE session_id = ?`
251
- ).run(model, sessionId);
252
- },
253
-
254
- /**
255
- * Records the reasoning effort one session runs with.
256
- *
257
- * `default` is stored as an explicit choice rather than NULL so reopening
258
- * the session does not inherit a later per-provider effort preference.
259
- */
260
- setSessionEffort(sessionId: string, effort: string): void {
261
- const db = getConnection();
262
- db.prepare(
263
- `UPDATE sessions
264
- SET effort = ?
265
- WHERE session_id = ?`
266
- ).run(effort, sessionId);
267
- },
268
-
269
- updateSessionCustomName(sessionId: string, customName: string): void {
270
- const db = getConnection();
271
- db.prepare(
272
- `UPDATE sessions
273
- SET custom_name = ?
274
- WHERE session_id = ?`
275
- ).run(customName, sessionId);
276
- },
277
-
278
- getSessionById(sessionId: string): SessionRow | null {
279
- const db = getConnection();
280
- const row = db
281
- .prepare(
282
- `SELECT ${SESSION_ROW_COLUMNS}
283
- FROM sessions
284
- WHERE session_id = ?
285
- ORDER BY updated_at DESC
286
- LIMIT 1`
287
- )
288
- .get(sessionId) as SessionRow | undefined;
289
-
290
- return normalizeSessionRow(row) ?? null;
291
- },
292
-
293
- /**
294
- * Resolves one session row through the provider-native id.
295
- *
296
- * The filesystem watcher only knows provider ids (they come from transcript
297
- * file names), so it uses this lookup to translate disk artifacts back to
298
- * the app-facing session row before broadcasting sidebar updates.
299
- */
300
- getSessionByProviderSessionId(providerSessionId: string): SessionRow | null {
301
- const db = getConnection();
302
- const row = db
303
- .prepare(
304
- `SELECT ${SESSION_ROW_COLUMNS}
305
- FROM sessions
306
- WHERE provider_session_id = ?
307
- ORDER BY updated_at DESC
308
- LIMIT 1`
309
- )
310
- .get(providerSessionId) as SessionRow | undefined;
311
-
312
- return normalizeSessionRow(row) ?? null;
313
- },
314
-
315
- /**
316
- * Finds the newest app-created session for a project that is still waiting
317
- * for its provider-native id to be recorded.
318
- *
319
- * Primary intention: OpenCode can expose a new session in its shared
320
- * `opencode.db` before the websocket runtime reports that same provider id
321
- * back to our app. At that moment the sidebar already has an optimistic
322
- * app-owned session row, but the watcher only knows the provider-native id.
323
- *
324
- * Without this lookup, the synchronizer would insert a second row keyed by
325
- * the provider id, then `assignProviderSessionId()` would merge it a moment
326
- * later. That eventually self-heals, but on slow networks the user can still
327
- * briefly see two sidebar sessions for the same conversation.
328
- *
329
- * This helper lets the synchronizer claim the pending app row first, so the
330
- * provider id is attached before any watcher-created row exists. The result
331
- * is simpler than frontend dedupe and keeps the race resolved at the source.
332
- */
333
- findLatestPendingAppSession(provider: string, projectPath: string): SessionRow | null {
334
- const db = getConnection();
335
- const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
336
- const row = db
337
- .prepare(
338
- `SELECT ${SESSION_ROW_COLUMNS}
339
- FROM sessions
340
- WHERE provider = ?
341
- AND project_path = ?
342
- AND provider_session_id IS NULL
343
- AND isArchived = 0
344
- ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC
345
- LIMIT 1`
346
- )
347
- .get(provider, normalizedProjectPath) as SessionRow | undefined;
348
-
349
- return normalizeSessionRow(row) ?? null;
350
- },
351
-
352
- getAllSessions(): SessionRow[] {
353
- const db = getConnection();
354
- const rows = db
355
- .prepare(
356
- `SELECT ${SESSION_ROW_COLUMNS}
357
- FROM sessions
358
- WHERE isArchived = 0`
359
- )
360
- .all() as SessionRow[];
361
-
362
- return normalizeSessionRows(rows);
363
- },
364
-
365
- /**
366
- * Returns one globally ordered page of visible conversations.
367
- *
368
- * Pagination happens after archived sessions and sessions belonging to an
369
- * archived project have been excluded. This keeps the sidebar feed complete
370
- * and correctly ordered across projects instead of flattening only the
371
- * per-project slices already loaded by the client.
372
- */
373
- getRecentSessionsPage(limit: number, offset: number): RecentSessionsPage {
374
- const db = getConnection();
375
- const visibilityClause = `
376
- sessions.isArchived = 0
377
- AND (projects.isArchived IS NULL OR projects.isArchived = 0)
378
- `;
379
- const rows = db
380
- .prepare(
381
- `SELECT sessions.*
382
- FROM sessions
383
- LEFT JOIN projects ON projects.project_path = sessions.project_path
384
- WHERE ${visibilityClause}
385
- ORDER BY julianday(COALESCE(sessions.updated_at, sessions.created_at)) DESC,
386
- sessions.session_id DESC
387
- LIMIT ? OFFSET ?`
388
- )
389
- .all(limit, offset) as SessionRow[];
390
- const countRow = db
391
- .prepare(
392
- `SELECT COUNT(*) AS count
393
- FROM sessions
394
- LEFT JOIN projects ON projects.project_path = sessions.project_path
395
- WHERE ${visibilityClause}`
396
- )
397
- .get() as { count: number } | undefined;
398
-
399
- return {
400
- sessions: normalizeSessionRows(rows),
401
- total: Number(countRow?.count ?? 0),
402
- };
403
- },
404
-
405
- /**
406
- * Archived rows are intentionally queried separately so the caller can render
407
- * them in a dedicated view without reintroducing them into active session lists.
408
- */
409
- getArchivedSessions(): SessionRow[] {
410
- const db = getConnection();
411
- const rows = db
412
- .prepare(
413
- `SELECT ${SESSION_ROW_COLUMNS}
414
- FROM sessions
415
- WHERE isArchived = 1
416
- ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC`
417
- )
418
- .all() as SessionRow[];
419
-
420
- return normalizeSessionRows(rows);
421
- },
422
-
423
- getSessionsByProjectPath(projectPath: string): SessionRow[] {
424
- const db = getConnection();
425
- const normalizedProjectPath = normalizeProjectPath(projectPath);
426
- const rows = db
427
- .prepare(
428
- `SELECT ${SESSION_ROW_COLUMNS}
429
- FROM sessions
430
- WHERE project_path = ?
431
- AND isArchived = 0`
432
- )
433
- .all(normalizedProjectPath) as SessionRow[];
434
-
435
- return normalizeSessionRows(rows);
436
- },
437
-
438
- /**
439
- * Permanent project deletion must see every session row for the path,
440
- * including archived ones, so their transcript files can be cleaned up.
441
- */
442
- getSessionsByProjectPathIncludingArchived(projectPath: string): SessionRow[] {
443
- const db = getConnection();
444
- const normalizedProjectPath = normalizeProjectPath(projectPath);
445
- const rows = db
446
- .prepare(
447
- `SELECT ${SESSION_ROW_COLUMNS}
448
- FROM sessions
449
- WHERE project_path = ?`
450
- )
451
- .all(normalizedProjectPath) as SessionRow[];
452
-
453
- return normalizeSessionRows(rows);
454
- },
455
-
456
- getSessionsByProjectPathPage(projectPath: string, limit: number, offset: number): SessionRow[] {
457
- const db = getConnection();
458
- const normalizedProjectPath = normalizeProjectPath(projectPath);
459
- const rows = db
460
- .prepare(
461
- `SELECT ${SESSION_ROW_COLUMNS}
462
- FROM sessions
463
- WHERE project_path = ?
464
- AND isArchived = 0
465
- ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC
466
- LIMIT ? OFFSET ?`
467
- )
468
- .all(normalizedProjectPath, limit, offset) as SessionRow[];
469
-
470
- return normalizeSessionRows(rows);
471
- },
472
-
473
- countSessionsByProjectPath(projectPath: string): number {
474
- const db = getConnection();
475
- const normalizedProjectPath = normalizeProjectPath(projectPath);
476
- const row = db
477
- .prepare(
478
- `SELECT COUNT(*) AS count
479
- FROM sessions
480
- WHERE project_path = ?
481
- AND isArchived = 0`
482
- )
483
- .get(normalizedProjectPath) as { count: number } | undefined;
484
-
485
- return Number(row?.count ?? 0);
486
- },
487
-
488
- deleteSessionsByProjectPath(projectPath: string): void {
489
- const db = getConnection();
490
- const normalizedProjectPath = normalizeProjectPath(projectPath);
491
- db.prepare(`DELETE FROM sessions WHERE project_path = ?`).run(normalizedProjectPath);
492
- },
493
-
494
- getSessionName(sessionId: string, provider: string): string | null {
495
- const db = getConnection();
496
- const row = db
497
- .prepare(
498
- `SELECT custom_name
499
- FROM sessions
500
- WHERE session_id = ? AND provider = ?`
501
- )
502
- .get(sessionId, provider) as { custom_name: string | null } | undefined;
503
-
504
- return row?.custom_name ?? null;
505
- },
506
-
507
- /**
508
- * Soft-delete and restore both use the same flag update so callers keep the
509
- * row, metadata, and file path intact while toggling visibility.
510
- */
511
- updateSessionIsArchived(sessionId: string, isArchived: boolean): void {
512
- const db = getConnection();
513
- db.prepare(
514
- `UPDATE sessions
515
- SET isArchived = ?
516
- WHERE session_id = ?`
517
- ).run(isArchived ? 1 : 0, sessionId);
518
- },
519
-
520
- deleteSessionById(sessionId: string): boolean {
521
- const db = getConnection();
522
- return db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId).changes > 0;
523
- },
524
- };
1
+ import { getConnection } from '@/modules/database/connection.js';
2
+ import { projectsDb } from '@/modules/database/repositories/projects.db.js';
3
+ import { normalizeProjectPath } from '@/shared/utils.js';
4
+
5
+ type SessionRow = {
6
+ session_id: string;
7
+ provider: string;
8
+ provider_session_id: string | null;
9
+ project_path: string | null;
10
+ jsonl_path: string | null;
11
+ custom_name: string | null;
12
+ /** Model this session runs with; NULL until the app records one for it. */
13
+ model: string | null;
14
+ /** Reasoning effort this session runs with; NULL until the app records one. */
15
+ effort: string | null;
16
+ isArchived: number;
17
+ created_at: string;
18
+ updated_at: string;
19
+ };
20
+
21
+ type RecentSessionsPage = {
22
+ sessions: SessionRow[];
23
+ total: number;
24
+ };
25
+
26
+ const SESSION_ROW_COLUMNS =
27
+ 'session_id, provider, provider_session_id, project_path, jsonl_path, custom_name, model, effort, isArchived, created_at, updated_at';
28
+
29
+ const SQLITE_UTC_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
30
+
31
+ function normalizeTimestamp(value?: string): string | null {
32
+ if (!value) return null;
33
+
34
+ // SQLite CURRENT_TIMESTAMP is stored as UTC without a timezone suffix.
35
+ // Normalize it here so every session reader returns canonical ISO strings
36
+ // and the sidebar never interprets fresh rows as local-time "hours old".
37
+ const normalizedValue = SQLITE_UTC_TIMESTAMP_REGEX.test(value)
38
+ ? `${value.replace(' ', 'T')}Z`
39
+ : value;
40
+
41
+ const parsed = new Date(normalizedValue);
42
+ if (Number.isNaN(parsed.getTime())) {
43
+ return null;
44
+ }
45
+
46
+ return parsed.toISOString();
47
+ }
48
+
49
+ function normalizeSessionRow<T extends SessionRow | null | undefined>(row: T): T {
50
+ if (!row) {
51
+ return row;
52
+ }
53
+
54
+ return {
55
+ ...row,
56
+ created_at: normalizeTimestamp(row.created_at) ?? row.created_at,
57
+ updated_at: normalizeTimestamp(row.updated_at) ?? row.updated_at,
58
+ };
59
+ }
60
+
61
+ function normalizeSessionRows(rows: SessionRow[]): SessionRow[] {
62
+ return rows.map((row) => normalizeSessionRow(row) as SessionRow);
63
+ }
64
+
65
+ function normalizeProjectPathForProvider(provider: string, projectPath: string): string {
66
+ void provider;
67
+ return normalizeProjectPath(projectPath);
68
+ }
69
+
70
+ export const sessionsDb = {
71
+ /**
72
+ * Upserts one session row discovered on disk by a provider synchronizer.
73
+ *
74
+ * The given id is the provider-native session id. Rows are keyed by
75
+ * `provider_session_id` so a session that was first created by the app
76
+ * (with an app-allocated `session_id`) is updated in place once its
77
+ * transcript shows up on disk, instead of producing a duplicate row. An
78
+ * app-created row keeps its existing name; synchronizer names only update
79
+ * rows that were themselves created by indexing provider storage.
80
+ */
81
+ createSession(
82
+ providerSessionId: string,
83
+ provider: string,
84
+ projectPath: string,
85
+ customName?: string,
86
+ createdAt?: string,
87
+ updatedAt?: string,
88
+ jsonlPath?: string | null
89
+ ): string {
90
+ const db = getConnection();
91
+ const createdAtValue = normalizeTimestamp(createdAt);
92
+ const updatedAtValue = normalizeTimestamp(updatedAt);
93
+ const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
94
+
95
+ // First, ensure the project path is recorded in the projects table,
96
+ // since it's a foreign key in the sessions table.
97
+ projectsDb.createProjectPath(normalizedProjectPath);
98
+
99
+ const existing = db
100
+ .prepare(
101
+ `SELECT session_id FROM sessions
102
+ WHERE provider_session_id = ? AND provider = ?
103
+ LIMIT 1`
104
+ )
105
+ .get(providerSessionId, provider) as { session_id: string } | undefined;
106
+
107
+ if (existing) {
108
+ db.prepare(
109
+ `UPDATE sessions SET
110
+ provider = ?,
111
+ updated_at = COALESCE(?, CURRENT_TIMESTAMP),
112
+ project_path = ?,
113
+ jsonl_path = ?,
114
+ isArchived = 0,
115
+ custom_name = CASE
116
+ WHEN session_id <> provider_session_id AND custom_name IS NOT NULL THEN custom_name
117
+ ELSE COALESCE(?, custom_name)
118
+ END
119
+ WHERE session_id = ?`
120
+ ).run(
121
+ provider,
122
+ updatedAtValue,
123
+ normalizedProjectPath,
124
+ jsonlPath ?? null,
125
+ customName ?? null,
126
+ existing.session_id
127
+ );
128
+
129
+ return existing.session_id;
130
+ }
131
+
132
+ // Sessions created outside the app (directly via the provider CLI) are
133
+ // keyed by the provider-native id for both columns. The ON CONFLICT path
134
+ // covers legacy rows that predate the provider_session_id mapping.
135
+ db.prepare(
136
+ `INSERT INTO sessions (session_id, provider, provider_session_id, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
137
+ VALUES (?, ?, ?, ?, ?, ?, 0, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
138
+ ON CONFLICT(session_id) DO UPDATE SET
139
+ provider = excluded.provider,
140
+ provider_session_id = excluded.provider_session_id,
141
+ updated_at = excluded.updated_at,
142
+ project_path = excluded.project_path,
143
+ jsonl_path = excluded.jsonl_path,
144
+ isArchived = 0,
145
+ custom_name = CASE
146
+ WHEN sessions.session_id <> sessions.provider_session_id AND sessions.custom_name IS NOT NULL
147
+ THEN sessions.custom_name
148
+ ELSE COALESCE(excluded.custom_name, sessions.custom_name)
149
+ END`
150
+ ).run(
151
+ providerSessionId,
152
+ provider,
153
+ providerSessionId,
154
+ customName ?? null,
155
+ normalizedProjectPath,
156
+ jsonlPath ?? null,
157
+ createdAtValue,
158
+ updatedAtValue
159
+ );
160
+
161
+ return providerSessionId;
162
+ },
163
+
164
+ /**
165
+ * Inserts one app-allocated session row before any provider run happens.
166
+ *
167
+ * The session gateway uses this when the frontend starts a brand-new chat:
168
+ * `session_id` is the stable app-facing id, while `provider_session_id`
169
+ * stays NULL until the provider runtime announces its own id and
170
+ * `assignProviderSessionId` records the mapping. `customName` is derived
171
+ * from the first visible CloudCLI message by the sessions service.
172
+ */
173
+ createAppSession(
174
+ sessionId: string,
175
+ provider: string,
176
+ projectPath: string,
177
+ customName?: string,
178
+ ): string {
179
+ const db = getConnection();
180
+ const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
181
+
182
+ projectsDb.createProjectPath(normalizedProjectPath);
183
+
184
+ db.prepare(
185
+ `INSERT INTO sessions (session_id, provider, provider_session_id, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
186
+ VALUES (?, ?, NULL, ?, ?, NULL, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`
187
+ ).run(sessionId, provider, customName ?? null, normalizedProjectPath);
188
+
189
+ return sessionId;
190
+ },
191
+
192
+ /**
193
+ * Records the provider-native session id for one app-allocated session.
194
+ *
195
+ * If the filesystem watcher indexed the provider transcript before this
196
+ * mapping was recorded (a duplicate row keyed by the provider id exists),
197
+ * the duplicate is merged into the app row: its transcript path and name
198
+ * are adopted and the duplicate row is removed. Runs in a transaction so
199
+ * the sidebar can never observe both rows at once.
200
+ */
201
+ assignProviderSessionId(sessionId: string, providerSessionId: string): void {
202
+ const db = getConnection();
203
+
204
+ const merge = db.transaction(() => {
205
+ const duplicate = db
206
+ .prepare(
207
+ `SELECT ${SESSION_ROW_COLUMNS} FROM sessions
208
+ WHERE (session_id = ? OR provider_session_id = ?)
209
+ AND session_id <> ?
210
+ LIMIT 1`
211
+ )
212
+ .get(providerSessionId, providerSessionId, sessionId) as SessionRow | undefined;
213
+
214
+ if (duplicate) {
215
+ db.prepare('DELETE FROM sessions WHERE session_id = ?').run(duplicate.session_id);
216
+ db.prepare(
217
+ `UPDATE sessions SET
218
+ provider_session_id = ?,
219
+ jsonl_path = COALESCE(jsonl_path, ?),
220
+ custom_name = COALESCE(custom_name, ?),
221
+ updated_at = CURRENT_TIMESTAMP
222
+ WHERE session_id = ?`
223
+ ).run(providerSessionId, duplicate.jsonl_path, duplicate.custom_name, sessionId);
224
+ return;
225
+ }
226
+
227
+ db.prepare(
228
+ `UPDATE sessions SET
229
+ provider_session_id = ?,
230
+ updated_at = CURRENT_TIMESTAMP
231
+ WHERE session_id = ?`
232
+ ).run(providerSessionId, sessionId);
233
+ });
234
+
235
+ merge();
236
+ },
237
+
238
+ /**
239
+ * Records the model one session runs with.
240
+ *
241
+ * Called both when the user picks a model for the session and on every send,
242
+ * so the row always reflects what the session last ran with and reopening it
243
+ * restores that model instead of a catalog default.
244
+ */
245
+ setSessionModel(sessionId: string, model: string): void {
246
+ const db = getConnection();
247
+ db.prepare(
248
+ `UPDATE sessions
249
+ SET model = ?
250
+ WHERE session_id = ?`
251
+ ).run(model, sessionId);
252
+ },
253
+
254
+ /**
255
+ * Records the reasoning effort one session runs with.
256
+ *
257
+ * `default` is stored as an explicit choice rather than NULL so reopening
258
+ * the session does not inherit a later per-provider effort preference.
259
+ */
260
+ setSessionEffort(sessionId: string, effort: string): void {
261
+ const db = getConnection();
262
+ db.prepare(
263
+ `UPDATE sessions
264
+ SET effort = ?
265
+ WHERE session_id = ?`
266
+ ).run(effort, sessionId);
267
+ },
268
+
269
+ updateSessionCustomName(sessionId: string, customName: string): void {
270
+ const db = getConnection();
271
+ db.prepare(
272
+ `UPDATE sessions
273
+ SET custom_name = ?
274
+ WHERE session_id = ?`
275
+ ).run(customName, sessionId);
276
+ },
277
+
278
+ getSessionById(sessionId: string): SessionRow | null {
279
+ const db = getConnection();
280
+ const row = db
281
+ .prepare(
282
+ `SELECT ${SESSION_ROW_COLUMNS}
283
+ FROM sessions
284
+ WHERE session_id = ?
285
+ ORDER BY updated_at DESC
286
+ LIMIT 1`
287
+ )
288
+ .get(sessionId) as SessionRow | undefined;
289
+
290
+ return normalizeSessionRow(row) ?? null;
291
+ },
292
+
293
+ /**
294
+ * Resolves one session row through the provider-native id.
295
+ *
296
+ * The filesystem watcher only knows provider ids (they come from transcript
297
+ * file names), so it uses this lookup to translate disk artifacts back to
298
+ * the app-facing session row before broadcasting sidebar updates.
299
+ */
300
+ getSessionByProviderSessionId(providerSessionId: string): SessionRow | null {
301
+ const db = getConnection();
302
+ const row = db
303
+ .prepare(
304
+ `SELECT ${SESSION_ROW_COLUMNS}
305
+ FROM sessions
306
+ WHERE provider_session_id = ?
307
+ ORDER BY updated_at DESC
308
+ LIMIT 1`
309
+ )
310
+ .get(providerSessionId) as SessionRow | undefined;
311
+
312
+ return normalizeSessionRow(row) ?? null;
313
+ },
314
+
315
+ /**
316
+ * Finds the newest app-created session for a project that is still waiting
317
+ * for its provider-native id to be recorded.
318
+ *
319
+ * Primary intention: OpenCode can expose a new session in its shared
320
+ * `opencode.db` before the websocket runtime reports that same provider id
321
+ * back to our app. At that moment the sidebar already has an optimistic
322
+ * app-owned session row, but the watcher only knows the provider-native id.
323
+ *
324
+ * Without this lookup, the synchronizer would insert a second row keyed by
325
+ * the provider id, then `assignProviderSessionId()` would merge it a moment
326
+ * later. That eventually self-heals, but on slow networks the user can still
327
+ * briefly see two sidebar sessions for the same conversation.
328
+ *
329
+ * This helper lets the synchronizer claim the pending app row first, so the
330
+ * provider id is attached before any watcher-created row exists. The result
331
+ * is simpler than frontend dedupe and keeps the race resolved at the source.
332
+ */
333
+ findLatestPendingAppSession(provider: string, projectPath: string): SessionRow | null {
334
+ const db = getConnection();
335
+ const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
336
+ const row = db
337
+ .prepare(
338
+ `SELECT ${SESSION_ROW_COLUMNS}
339
+ FROM sessions
340
+ WHERE provider = ?
341
+ AND project_path = ?
342
+ AND provider_session_id IS NULL
343
+ AND isArchived = 0
344
+ ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC
345
+ LIMIT 1`
346
+ )
347
+ .get(provider, normalizedProjectPath) as SessionRow | undefined;
348
+
349
+ return normalizeSessionRow(row) ?? null;
350
+ },
351
+
352
+ getAllSessions(): SessionRow[] {
353
+ const db = getConnection();
354
+ const rows = db
355
+ .prepare(
356
+ `SELECT ${SESSION_ROW_COLUMNS}
357
+ FROM sessions
358
+ WHERE isArchived = 0`
359
+ )
360
+ .all() as SessionRow[];
361
+
362
+ return normalizeSessionRows(rows);
363
+ },
364
+
365
+ /**
366
+ * Returns one globally ordered page of visible conversations.
367
+ *
368
+ * Pagination happens after archived sessions and sessions belonging to an
369
+ * archived project have been excluded. This keeps the sidebar feed complete
370
+ * and correctly ordered across projects instead of flattening only the
371
+ * per-project slices already loaded by the client.
372
+ */
373
+ getRecentSessionsPage(limit: number, offset: number): RecentSessionsPage {
374
+ const db = getConnection();
375
+ const visibilityClause = `
376
+ sessions.isArchived = 0
377
+ AND (projects.isArchived IS NULL OR projects.isArchived = 0)
378
+ `;
379
+ const rows = db
380
+ .prepare(
381
+ `SELECT sessions.*
382
+ FROM sessions
383
+ LEFT JOIN projects ON projects.project_path = sessions.project_path
384
+ WHERE ${visibilityClause}
385
+ ORDER BY julianday(COALESCE(sessions.updated_at, sessions.created_at)) DESC,
386
+ sessions.session_id DESC
387
+ LIMIT ? OFFSET ?`
388
+ )
389
+ .all(limit, offset) as SessionRow[];
390
+ const countRow = db
391
+ .prepare(
392
+ `SELECT COUNT(*) AS count
393
+ FROM sessions
394
+ LEFT JOIN projects ON projects.project_path = sessions.project_path
395
+ WHERE ${visibilityClause}`
396
+ )
397
+ .get() as { count: number } | undefined;
398
+
399
+ return {
400
+ sessions: normalizeSessionRows(rows),
401
+ total: Number(countRow?.count ?? 0),
402
+ };
403
+ },
404
+
405
+ /**
406
+ * Archived rows are intentionally queried separately so the caller can render
407
+ * them in a dedicated view without reintroducing them into active session lists.
408
+ */
409
+ getArchivedSessions(): SessionRow[] {
410
+ const db = getConnection();
411
+ const rows = db
412
+ .prepare(
413
+ `SELECT ${SESSION_ROW_COLUMNS}
414
+ FROM sessions
415
+ WHERE isArchived = 1
416
+ ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC`
417
+ )
418
+ .all() as SessionRow[];
419
+
420
+ return normalizeSessionRows(rows);
421
+ },
422
+
423
+ getSessionsByProjectPath(projectPath: string): SessionRow[] {
424
+ const db = getConnection();
425
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
426
+ const rows = db
427
+ .prepare(
428
+ `SELECT ${SESSION_ROW_COLUMNS}
429
+ FROM sessions
430
+ WHERE project_path = ?
431
+ AND isArchived = 0`
432
+ )
433
+ .all(normalizedProjectPath) as SessionRow[];
434
+
435
+ return normalizeSessionRows(rows);
436
+ },
437
+
438
+ /**
439
+ * Permanent project deletion must see every session row for the path,
440
+ * including archived ones, so their transcript files can be cleaned up.
441
+ */
442
+ getSessionsByProjectPathIncludingArchived(projectPath: string): SessionRow[] {
443
+ const db = getConnection();
444
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
445
+ const rows = db
446
+ .prepare(
447
+ `SELECT ${SESSION_ROW_COLUMNS}
448
+ FROM sessions
449
+ WHERE project_path = ?`
450
+ )
451
+ .all(normalizedProjectPath) as SessionRow[];
452
+
453
+ return normalizeSessionRows(rows);
454
+ },
455
+
456
+ getSessionsByProjectPathPage(projectPath: string, limit: number, offset: number): SessionRow[] {
457
+ const db = getConnection();
458
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
459
+ const rows = db
460
+ .prepare(
461
+ `SELECT ${SESSION_ROW_COLUMNS}
462
+ FROM sessions
463
+ WHERE project_path = ?
464
+ AND isArchived = 0
465
+ ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC
466
+ LIMIT ? OFFSET ?`
467
+ )
468
+ .all(normalizedProjectPath, limit, offset) as SessionRow[];
469
+
470
+ return normalizeSessionRows(rows);
471
+ },
472
+
473
+ countSessionsByProjectPath(projectPath: string): number {
474
+ const db = getConnection();
475
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
476
+ const row = db
477
+ .prepare(
478
+ `SELECT COUNT(*) AS count
479
+ FROM sessions
480
+ WHERE project_path = ?
481
+ AND isArchived = 0`
482
+ )
483
+ .get(normalizedProjectPath) as { count: number } | undefined;
484
+
485
+ return Number(row?.count ?? 0);
486
+ },
487
+
488
+ deleteSessionsByProjectPath(projectPath: string): void {
489
+ const db = getConnection();
490
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
491
+ db.prepare(`DELETE FROM sessions WHERE project_path = ?`).run(normalizedProjectPath);
492
+ },
493
+
494
+ getSessionName(sessionId: string, provider: string): string | null {
495
+ const db = getConnection();
496
+ const row = db
497
+ .prepare(
498
+ `SELECT custom_name
499
+ FROM sessions
500
+ WHERE session_id = ? AND provider = ?`
501
+ )
502
+ .get(sessionId, provider) as { custom_name: string | null } | undefined;
503
+
504
+ return row?.custom_name ?? null;
505
+ },
506
+
507
+ /**
508
+ * Soft-delete and restore both use the same flag update so callers keep the
509
+ * row, metadata, and file path intact while toggling visibility.
510
+ */
511
+ updateSessionIsArchived(sessionId: string, isArchived: boolean): void {
512
+ const db = getConnection();
513
+ db.prepare(
514
+ `UPDATE sessions
515
+ SET isArchived = ?
516
+ WHERE session_id = ?`
517
+ ).run(isArchived ? 1 : 0, sessionId);
518
+ },
519
+
520
+ deleteSessionById(sessionId: string): boolean {
521
+ const db = getConnection();
522
+ return db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId).changes > 0;
523
+ },
524
+ };