@cloudcli-ai/cloudcli 1.34.0 → 1.35.0

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 (324) hide show
  1. package/README.de.md +9 -1
  2. package/README.ja.md +8 -0
  3. package/README.ko.md +9 -1
  4. package/README.md +16 -3
  5. package/README.ru.md +9 -1
  6. package/README.tr.md +8 -1
  7. package/README.zh-CN.md +9 -1
  8. package/README.zh-TW.md +9 -1
  9. package/dist/api-docs.html +36 -18
  10. package/dist/assets/index-CtwxqzGG.js +1407 -0
  11. package/dist/assets/index-DlvtxFyt.css +32 -0
  12. package/dist/index.html +2 -2
  13. package/dist-server/server/browser-use-mcp.js +350 -0
  14. package/dist-server/server/browser-use-mcp.js.map +1 -0
  15. package/dist-server/server/claude-sdk.js +25 -5
  16. package/dist-server/server/claude-sdk.js.map +1 -1
  17. package/dist-server/server/cli.js +14 -6
  18. package/dist-server/server/cli.js.map +1 -1
  19. package/dist-server/server/computer-use-agent.js +242 -0
  20. package/dist-server/server/computer-use-agent.js.map +1 -0
  21. package/dist-server/server/computer-use-mcp.js +519 -0
  22. package/dist-server/server/computer-use-mcp.js.map +1 -0
  23. package/dist-server/server/cursor-cli.js +27 -11
  24. package/dist-server/server/cursor-cli.js.map +1 -1
  25. package/dist-server/server/gemini-cli.js +17 -2
  26. package/dist-server/server/gemini-cli.js.map +1 -1
  27. package/dist-server/server/index.js +149 -40
  28. package/dist-server/server/index.js.map +1 -1
  29. package/dist-server/server/load-env.js +1 -1
  30. package/dist-server/server/load-env.js.map +1 -1
  31. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  32. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  33. package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
  34. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  35. package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
  36. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  37. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  38. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  39. package/dist-server/server/modules/computer-use/actions/raw-action-dispatcher.js +55 -0
  40. package/dist-server/server/modules/computer-use/actions/raw-action-dispatcher.js.map +1 -0
  41. package/dist-server/server/modules/computer-use/actions/raw-action-types.js +2 -0
  42. package/dist-server/server/modules/computer-use/actions/raw-action-types.js.map +1 -0
  43. package/dist-server/server/modules/computer-use/computer-executor.js +212 -0
  44. package/dist-server/server/modules/computer-use/computer-executor.js.map +1 -0
  45. package/dist-server/server/modules/computer-use/computer-semantics.service.js +422 -0
  46. package/dist-server/server/modules/computer-use/computer-semantics.service.js.map +1 -0
  47. package/dist-server/server/modules/computer-use/computer-use-mcp.routes.js +126 -0
  48. package/dist-server/server/modules/computer-use/computer-use-mcp.routes.js.map +1 -0
  49. package/dist-server/server/modules/computer-use/computer-use.routes.js +196 -0
  50. package/dist-server/server/modules/computer-use/computer-use.routes.js.map +1 -0
  51. package/dist-server/server/modules/computer-use/computer-use.service.js +759 -0
  52. package/dist-server/server/modules/computer-use/computer-use.service.js.map +1 -0
  53. package/dist-server/server/modules/computer-use/desktop-agent-relay.service.js +126 -0
  54. package/dist-server/server/modules/computer-use/desktop-agent-relay.service.js.map +1 -0
  55. package/dist-server/server/modules/computer-use/index.js +3 -0
  56. package/dist-server/server/modules/computer-use/index.js.map +1 -0
  57. package/dist-server/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.js +59 -0
  58. package/dist-server/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.js.map +1 -0
  59. package/dist-server/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.js +5 -0
  60. package/dist-server/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.js.map +1 -0
  61. package/dist-server/server/modules/computer-use/semantics/adapters/semantic-adapter.js +2 -0
  62. package/dist-server/server/modules/computer-use/semantics/adapters/semantic-adapter.js.map +1 -0
  63. package/dist-server/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.js +5 -0
  64. package/dist-server/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.js.map +1 -0
  65. package/dist-server/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
  66. package/dist-server/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
  67. package/dist-server/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
  68. package/dist-server/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
  69. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-process.js +106 -0
  70. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-process.js.map +1 -0
  71. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.js +78 -0
  72. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.js.map +1 -0
  73. package/dist-server/server/modules/computer-use/semantics/semantic-session-store.js +68 -0
  74. package/dist-server/server/modules/computer-use/semantics/semantic-session-store.js.map +1 -0
  75. package/dist-server/server/modules/computer-use/semantics/semantic-tool-dispatcher.js +16 -0
  76. package/dist-server/server/modules/computer-use/semantics/semantic-tool-dispatcher.js.map +1 -0
  77. package/dist-server/server/modules/computer-use/semantics/semantic-types.js +2 -0
  78. package/dist-server/server/modules/computer-use/semantics/semantic-types.js.map +1 -0
  79. package/dist-server/server/modules/database/index.js +1 -0
  80. package/dist-server/server/modules/database/index.js.map +1 -1
  81. package/dist-server/server/modules/database/migrations.js +23 -1
  82. package/dist-server/server/modules/database/migrations.js.map +1 -1
  83. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  84. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  85. package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
  86. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
  87. package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
  88. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  89. package/dist-server/server/modules/database/schema.js +26 -0
  90. package/dist-server/server/modules/database/schema.js.map +1 -1
  91. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  92. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
  93. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  94. package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  96. package/dist-server/server/modules/notifications/index.js +4 -0
  97. package/dist-server/server/modules/notifications/index.js.map +1 -0
  98. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  99. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  100. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  101. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  102. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
  103. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  104. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  105. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  106. package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
  107. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  108. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
  109. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  110. package/dist-server/server/modules/providers/index.js +1 -0
  111. package/dist-server/server/modules/providers/index.js.map +1 -1
  112. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  113. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  114. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  115. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  116. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  117. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  118. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  119. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  121. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  122. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  123. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  124. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  125. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  127. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  128. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  129. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  130. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  131. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  132. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  133. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  134. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  135. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  136. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  137. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  138. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  139. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  140. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  141. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  142. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  143. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  144. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  145. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  146. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  147. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  148. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  149. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  150. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  151. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  152. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  153. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  154. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  155. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  156. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  157. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  158. package/dist-server/server/modules/websocket/index.js +1 -0
  159. package/dist-server/server/modules/websocket/index.js.map +1 -1
  160. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  161. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  162. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  163. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  164. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  165. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  166. package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js +39 -0
  167. package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js.map +1 -0
  168. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  169. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  170. package/dist-server/server/modules/websocket/services/websocket-server.service.js +10 -0
  171. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  172. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  173. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  174. package/dist-server/server/openai-codex.js +24 -14
  175. package/dist-server/server/openai-codex.js.map +1 -1
  176. package/dist-server/server/opencode-cli.js +21 -8
  177. package/dist-server/server/opencode-cli.js.map +1 -1
  178. package/dist-server/server/opencode-cli.test.js +19 -1
  179. package/dist-server/server/opencode-cli.test.js.map +1 -1
  180. package/dist-server/server/routes/settings.js +1 -1
  181. package/dist-server/server/routes/settings.js.map +1 -1
  182. package/dist-server/server/services/notification-orchestrator.js +1 -176
  183. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  184. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  185. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  186. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  187. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  188. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  189. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  190. package/dist-server/server/shared/utils.js +73 -1
  191. package/dist-server/server/shared/utils.js.map +1 -1
  192. package/dist-server/server/voice-proxy.js +226 -0
  193. package/dist-server/server/voice-proxy.js.map +1 -0
  194. package/electron/assets/logo-macos.icns +0 -0
  195. package/electron/assets/logo-macos.png +0 -0
  196. package/electron/assets/logo-windows.ico +0 -0
  197. package/electron/cloud.js +260 -0
  198. package/electron/computerAgent.js +290 -0
  199. package/electron/desktopNotifications.js +378 -0
  200. package/electron/desktopWindow.js +781 -0
  201. package/electron/launcher/index.html +14 -0
  202. package/electron/launcher/launcher.css +801 -0
  203. package/electron/launcher/launcher.js +829 -0
  204. package/electron/localServer.js +550 -0
  205. package/electron/main.js +1060 -0
  206. package/electron/preload.cjs +63 -0
  207. package/electron/scripts/generate-macos-icon.js +62 -0
  208. package/electron/serverInstaller.js +277 -0
  209. package/electron/tabs.js +87 -0
  210. package/electron/viewHost.js +331 -0
  211. package/package.json +86 -5
  212. package/public/api-docs.html +36 -18
  213. package/scripts/build-computer-semantics.mjs +133 -0
  214. package/scripts/copy-computer-semantics-bin.mjs +24 -0
  215. package/scripts/release/build-server-bundle.js +176 -0
  216. package/scripts/release/prepare-desktop-app.js +158 -0
  217. package/server/browser-use-mcp.ts +384 -0
  218. package/server/claude-sdk.js +27 -5
  219. package/server/cli.js +15 -6
  220. package/server/computer-use-agent.ts +279 -0
  221. package/server/computer-use-mcp.ts +574 -0
  222. package/server/cursor-cli.js +27 -11
  223. package/server/gemini-cli.js +17 -2
  224. package/server/index.js +151 -47
  225. package/server/load-env.js +1 -1
  226. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  227. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  228. package/server/modules/browser-use/browser-use.service.ts +836 -0
  229. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  230. package/server/modules/computer-use/actions/raw-action-dispatcher.ts +67 -0
  231. package/server/modules/computer-use/actions/raw-action-types.ts +28 -0
  232. package/server/modules/computer-use/computer-executor.ts +242 -0
  233. package/server/modules/computer-use/computer-semantics.service.ts +460 -0
  234. package/server/modules/computer-use/computer-use-mcp.routes.ts +141 -0
  235. package/server/modules/computer-use/computer-use.routes.ts +211 -0
  236. package/server/modules/computer-use/computer-use.service.ts +920 -0
  237. package/server/modules/computer-use/desktop-agent-relay.service.ts +158 -0
  238. package/server/modules/computer-use/index.ts +2 -0
  239. package/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.ts +82 -0
  240. package/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.ts +5 -0
  241. package/server/modules/computer-use/semantics/adapters/semantic-adapter.ts +23 -0
  242. package/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.ts +5 -0
  243. package/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
  244. package/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
  245. package/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
  246. package/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
  247. package/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift +467 -0
  248. package/server/modules/computer-use/semantics/helpers/semantic-helper-process.ts +124 -0
  249. package/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.ts +97 -0
  250. package/server/modules/computer-use/semantics/helpers/windows/CloudCLISemantics.csproj +11 -0
  251. package/server/modules/computer-use/semantics/helpers/windows/Program.cs +534 -0
  252. package/server/modules/computer-use/semantics/semantic-session-store.ts +87 -0
  253. package/server/modules/computer-use/semantics/semantic-tool-dispatcher.ts +17 -0
  254. package/server/modules/computer-use/semantics/semantic-types.ts +58 -0
  255. package/server/modules/database/index.ts +1 -0
  256. package/server/modules/database/migrations.ts +25 -0
  257. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  258. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  259. package/server/modules/database/repositories/sessions.db.ts +227 -23
  260. package/server/modules/database/schema.ts +27 -0
  261. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  262. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  263. package/server/modules/notifications/index.ts +13 -0
  264. package/server/modules/notifications/notifications.routes.ts +127 -0
  265. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  266. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  267. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  268. package/server/modules/projects/services/project-management.service.ts +0 -8
  269. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  270. package/server/modules/providers/README.md +1 -1
  271. package/server/modules/providers/index.ts +1 -0
  272. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  273. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  274. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  275. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  276. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  277. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  278. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  279. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  280. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  281. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  282. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  283. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  284. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  285. package/server/modules/providers/provider.routes.ts +167 -1
  286. package/server/modules/providers/services/mcp.service.ts +26 -0
  287. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  288. package/server/modules/providers/services/provider-models.service.ts +1 -1
  289. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  290. package/server/modules/providers/services/sessions.service.ts +79 -4
  291. package/server/modules/providers/services/skills.service.ts +25 -1
  292. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  293. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  294. package/server/modules/providers/tests/skills.test.ts +212 -0
  295. package/server/modules/websocket/README.md +33 -27
  296. package/server/modules/websocket/index.ts +1 -0
  297. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  298. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  299. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  300. package/server/modules/websocket/services/desktop-agent-websocket.service.ts +51 -0
  301. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  302. package/server/modules/websocket/services/websocket-server.service.ts +12 -0
  303. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  304. package/server/openai-codex.js +24 -14
  305. package/server/opencode-cli.js +21 -8
  306. package/server/opencode-cli.test.js +20 -1
  307. package/server/routes/settings.js +6 -1
  308. package/server/services/notification-orchestrator.js +3 -224
  309. package/server/services/tests/notification-orchestrator.test.js +80 -0
  310. package/server/shared/interfaces.ts +15 -0
  311. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  312. package/server/shared/types.ts +84 -1
  313. package/server/shared/utils.ts +95 -1
  314. package/server/voice-proxy.js +224 -0
  315. package/dist/assets/index-73Xw3i9v.css +0 -32
  316. package/dist/assets/index-BIHaviaA.js +0 -1361
  317. package/dist/modelConstants.js +0 -854
  318. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  319. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  320. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  321. package/public/modelConstants.js +0 -854
  322. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  323. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  324. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -33,4 +33,12 @@ export class GeminiSkillsProvider extends SkillsProvider {
33
33
  },
34
34
  ];
35
35
  }
36
+
37
+ protected async getGlobalSkillSource(): Promise<ProviderSkillSource> {
38
+ return {
39
+ scope: 'user',
40
+ rootDir: path.join(os.homedir(), '.gemini', 'skills'),
41
+ commandPrefix: '/',
42
+ };
43
+ }
36
44
  }
@@ -112,7 +112,21 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
112
112
  }
113
113
 
114
114
  const fallbackTitle = 'Untitled OpenCode Session';
115
- const existingSession = sessionsDb.getSessionById(sessionId);
115
+ const pendingAppSession = sessionsDb.getSessionByProviderSessionId(sessionId)
116
+ ?? sessionsDb.getSessionById(sessionId)
117
+ ?? sessionsDb.findLatestPendingAppSession(this.provider, projectPath);
118
+ if (pendingAppSession && !pendingAppSession.provider_session_id) {
119
+ // Slow networks can let the sqlite watcher index opencode.db before the
120
+ // runtime reports its provider id back through the websocket mapping.
121
+ // Bind that id to the fresh app row first so the watcher does not create
122
+ // a temporary provider-id sidebar entry for the same session.
123
+ sessionsDb.assignProviderSessionId(pendingAppSession.session_id, sessionId);
124
+ }
125
+
126
+ // App-created sessions are keyed by an app id, so disk-discovered provider
127
+ // ids must be resolved through the provider-id mapping first.
128
+ const existingSession = sessionsDb.getSessionByProviderSessionId(sessionId)
129
+ ?? sessionsDb.getSessionById(sessionId);
116
130
  const existingName = existingSession?.custom_name;
117
131
  const nextName = existingName && existingName !== fallbackTitle
118
132
  ? existingName
@@ -120,7 +134,9 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
120
134
 
121
135
  // OpenCode stores every session in one shared sqlite database, so jsonl_path
122
136
  // must stay null to avoid deleting opencode.db when one app session is removed.
123
- sessionsDb.createSession(
137
+ // Return the canonical stored row id so watcher-triggered sidebar updates
138
+ // stay on the app session once provider_session_id has already been mapped.
139
+ return sessionsDb.createSession(
124
140
  sessionId,
125
141
  this.provider,
126
142
  projectPath,
@@ -129,8 +145,6 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
129
145
  normalizeProviderTimestamp(row.time_updated ?? row.time_created),
130
146
  null,
131
147
  );
132
-
133
- return sessionId;
134
148
  }
135
149
 
136
150
  private readFirstUserText(db: Database.Database, sessionId: string): string | undefined {
@@ -12,6 +12,7 @@ import {
12
12
  readObjectRecord,
13
13
  readJsonRecord,
14
14
  readOptionalString,
15
+ sliceTailPage,
15
16
  } from '@/shared/utils.js';
16
17
 
17
18
  const PROVIDER = 'opencode';
@@ -325,6 +326,9 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
325
326
  options: FetchHistoryOptions = {},
326
327
  ): Promise<FetchHistoryResult> {
327
328
  const { limit = null, offset = 0 } = options;
329
+ // OpenCode's shared sqlite database keys messages by the provider-native
330
+ // session id, not the app-facing id this method is addressed with.
331
+ const providerSessionId = options.providerSessionId ?? sessionId;
328
332
  const db = openOpenCodeDatabase();
329
333
  if (!db) {
330
334
  return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
@@ -349,27 +353,20 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
349
353
  m.id,
350
354
  COALESCE(p.time_created, 0),
351
355
  p.id
352
- `).all(sessionId) as OpenCodeHistoryRow[];
356
+ `).all(providerSessionId) as OpenCodeHistoryRow[];
353
357
 
354
358
  const normalized = this.normalizeHistoryRows(rows, sessionId);
355
- const tokenUsage = aggregateOpenCodeSessionTokenUsage(db, sessionId);
359
+ const tokenUsage = aggregateOpenCodeSessionTokenUsage(db, providerSessionId);
356
360
 
357
361
  const normalizedOffset = Math.max(0, offset);
358
362
  const normalizedLimit = limit === null ? null : Math.max(0, limit);
359
363
  const total = normalized.length;
360
- const messages = normalizedLimit === null
361
- ? normalized
362
- : normalized.slice(
363
- Math.max(0, total - normalizedOffset - normalizedLimit),
364
- Math.max(0, total - normalizedOffset),
365
- );
364
+ const { page, hasMore } = sliceTailPage(normalized, normalizedLimit, normalizedOffset);
366
365
 
367
366
  return {
368
- messages,
367
+ messages: page,
369
368
  total,
370
- hasMore: normalizedLimit === null
371
- ? false
372
- : Math.max(0, total - normalizedOffset - normalizedLimit) > 0,
369
+ hasMore,
373
370
  offset: normalizedOffset,
374
371
  limit: normalizedLimit,
375
372
  tokenUsage,
@@ -1,6 +1,7 @@
1
1
  import express, { type Request, type Response } from 'express';
2
2
 
3
3
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
+ import { providerCapabilitiesService } from '@/modules/providers/services/provider-capabilities.service.js';
4
5
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
5
6
  import { providerModelsService } from '@/modules/providers/services/provider-models.service.js';
6
7
  import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
@@ -11,6 +12,8 @@ import type {
11
12
  McpScope,
12
13
  McpTransport,
13
14
  ProviderChangeActiveModelInput,
15
+ ProviderSkillCreateFile,
16
+ ProviderSkillCreateInput,
14
17
  UpsertProviderMcpServerInput,
15
18
  } from '@/shared/types.js';
16
19
  import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
@@ -178,6 +181,104 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
178
181
  };
179
182
  };
180
183
 
184
+ const parseProviderSkillCreatePayload = (payload: unknown): ProviderSkillCreateInput => {
185
+ if (!payload || typeof payload !== 'object') {
186
+ throw new AppError('Request body must be an object.', {
187
+ code: 'INVALID_REQUEST_BODY',
188
+ statusCode: 400,
189
+ });
190
+ }
191
+
192
+ const body = payload as Record<string, unknown>;
193
+ const rawEntries = Array.isArray(body.entries)
194
+ ? body.entries
195
+ : typeof body.content === 'string'
196
+ ? [{
197
+ content: body.content,
198
+ directoryName: body.directoryName,
199
+ fileName: body.fileName,
200
+ files: body.files,
201
+ }]
202
+ : null;
203
+
204
+ if (!rawEntries || rawEntries.length === 0) {
205
+ throw new AppError('At least one skill entry is required.', {
206
+ code: 'PROVIDER_SKILLS_REQUIRED',
207
+ statusCode: 400,
208
+ });
209
+ }
210
+
211
+ const entries = rawEntries.map((entry, index) => {
212
+ if (!entry || typeof entry !== 'object') {
213
+ throw new AppError(`Skill entry ${index + 1} must be an object.`, {
214
+ code: 'INVALID_REQUEST_BODY',
215
+ statusCode: 400,
216
+ });
217
+ }
218
+
219
+ const record = entry as Record<string, unknown>;
220
+ const content = typeof record.content === 'string' ? record.content : '';
221
+ const directoryName = readOptionalQueryString(record.directoryName);
222
+ const fileName = readOptionalQueryString(record.fileName);
223
+ const rawFiles = record.files;
224
+
225
+ if (!content.trim()) {
226
+ throw new AppError(`Skill entry ${index + 1} must include markdown content.`, {
227
+ code: 'PROVIDER_SKILL_CONTENT_REQUIRED',
228
+ statusCode: 400,
229
+ });
230
+ }
231
+
232
+ if (rawFiles !== undefined && !Array.isArray(rawFiles)) {
233
+ throw new AppError(`Skill entry ${index + 1} files must be an array.`, {
234
+ code: 'INVALID_REQUEST_BODY',
235
+ statusCode: 400,
236
+ });
237
+ }
238
+
239
+ const files: ProviderSkillCreateFile[] | undefined = rawFiles?.map((file, fileIndex) => {
240
+ if (!file || typeof file !== 'object') {
241
+ throw new AppError(`Skill entry ${index + 1} file ${fileIndex + 1} must be an object.`, {
242
+ code: 'INVALID_REQUEST_BODY',
243
+ statusCode: 400,
244
+ });
245
+ }
246
+
247
+ const fileRecord = file as Record<string, unknown>;
248
+ const relativePath = readOptionalQueryString(fileRecord.relativePath);
249
+ const fileContent = typeof fileRecord.content === 'string' ? fileRecord.content : null;
250
+ const encoding = fileRecord.encoding === 'utf8' || fileRecord.encoding === 'base64'
251
+ ? fileRecord.encoding
252
+ : null;
253
+
254
+ if (!relativePath || fileContent === null || !encoding) {
255
+ throw new AppError(
256
+ `Skill entry ${index + 1} file ${fileIndex + 1} requires relativePath, content, and encoding.`,
257
+ {
258
+ code: 'INVALID_REQUEST_BODY',
259
+ statusCode: 400,
260
+ },
261
+ );
262
+ }
263
+
264
+ return {
265
+ relativePath,
266
+ content: fileContent,
267
+ encoding,
268
+ };
269
+ });
270
+
271
+ return {
272
+ content,
273
+ directoryName,
274
+ fileName,
275
+ files,
276
+ };
277
+ });
278
+
279
+ return { entries };
280
+ };
281
+
181
282
  const parseProvider = (value: unknown): LLMProvider => {
182
283
  const normalized = normalizeProviderParam(value);
183
284
  if (
@@ -319,6 +420,27 @@ router.get(
319
420
  }),
320
421
  );
321
422
 
423
+ router.post(
424
+ '/:provider/skills',
425
+ asyncHandler(async (req: Request, res: Response) => {
426
+ const provider = parseProvider(req.params.provider);
427
+ const input = parseProviderSkillCreatePayload(req.body);
428
+ const skills = await providerSkillsService.addProviderSkills(provider, input);
429
+ res.json(createApiSuccessResponse({ provider, skills }));
430
+ }),
431
+ );
432
+
433
+ router.delete(
434
+ '/:provider/skills/:directoryName',
435
+ asyncHandler(async (req: Request, res: Response) => {
436
+ const provider = parseProvider(req.params.provider);
437
+ const result = await providerSkillsService.removeProviderSkill(provider, {
438
+ directoryName: readPathParam(req.params.directoryName, 'directoryName'),
439
+ });
440
+ res.json(createApiSuccessResponse(result));
441
+ }),
442
+ );
443
+
322
444
  // ----------------- MCP routes -----------------
323
445
  router.get(
324
446
  '/:provider/mcp/servers',
@@ -382,7 +504,51 @@ router.post(
382
504
  }),
383
505
  );
384
506
 
507
+ router.get(
508
+ '/capabilities',
509
+ asyncHandler(async (_req: Request, res: Response) => {
510
+ res.json(createApiSuccessResponse({
511
+ providers: providerCapabilitiesService.listAllProviderCapabilities(),
512
+ }));
513
+ }),
514
+ );
515
+
516
+ router.get(
517
+ '/:provider/capabilities',
518
+ asyncHandler(async (req: Request, res: Response) => {
519
+ const provider = parseProvider(req.params.provider);
520
+ res.json(createApiSuccessResponse(
521
+ providerCapabilitiesService.getProviderCapabilities(provider),
522
+ ));
523
+ }),
524
+ );
525
+
385
526
  // ----------------- Session routes -----------------
527
+ /**
528
+ * Session gateway entry point: allocates the stable app-facing session id for
529
+ * a brand-new chat. The frontend must call this before the first `chat.send`
530
+ * so the session id in the URL, the store, and the websocket all agree from
531
+ * the very first message — there is no client-visible session-id handoff.
532
+ */
533
+ router.post(
534
+ '/sessions',
535
+ asyncHandler(async (req: Request, res: Response) => {
536
+ const body = (req.body ?? {}) as Record<string, unknown>;
537
+ const provider = parseProvider(body.provider);
538
+ const projectPath = typeof body.projectPath === 'string' ? body.projectPath : '';
539
+ const result = sessionsService.createAppSession(provider, projectPath);
540
+ res.status(201).json(createApiSuccessResponse(result));
541
+ }),
542
+ );
543
+
544
+ router.get(
545
+ '/sessions/running',
546
+ asyncHandler(async (_req: Request, res: Response) => {
547
+ const sessions = sessionsService.listRunningSessions();
548
+ res.json(createApiSuccessResponse({ sessions }));
549
+ }),
550
+ );
551
+
386
552
  router.get(
387
553
  '/sessions/archived',
388
554
  asyncHandler(async (_req: Request, res: Response) => {
@@ -459,7 +625,7 @@ router.get(
459
625
  limit,
460
626
  offset,
461
627
  });
462
- res.json(result);
628
+ res.json(createApiSuccessResponse(result));
463
629
  }),
464
630
  );
465
631
 
@@ -80,4 +80,30 @@ export const providerMcpService = {
80
80
 
81
81
  return results;
82
82
  },
83
+
84
+ /**
85
+ * Removes one MCP server from every provider. Mirrors `addMcpServerToAllProviders`
86
+ * by iterating the live provider registry, so callers stay in sync with which
87
+ * providers exist instead of maintaining their own provider list.
88
+ */
89
+ async removeMcpServerFromAllProviders(
90
+ input: { name: string; scope?: McpScope; workspacePath?: string },
91
+ ): Promise<Array<{ provider: LLMProvider; removed: boolean; error?: string }>> {
92
+ const results: Array<{ provider: LLMProvider; removed: boolean; error?: string }> = [];
93
+ const providers = providerRegistry.listProviders();
94
+ for (const provider of providers) {
95
+ try {
96
+ const result = await provider.mcp.removeServer(input);
97
+ results.push({ provider: provider.id, removed: result.removed });
98
+ } catch (error) {
99
+ results.push({
100
+ provider: provider.id,
101
+ removed: false,
102
+ error: error instanceof Error ? error.message : 'Unknown error',
103
+ });
104
+ }
105
+ }
106
+
107
+ return results;
108
+ },
83
109
  };
@@ -0,0 +1,91 @@
1
+ import type { LLMProvider } from '@/shared/types.js';
2
+
3
+ /**
4
+ * Static, backend-owned description of what one provider integration supports.
5
+ *
6
+ * The frontend renders its composer UI (permission mode picker, image upload,
7
+ * abort button, ...) purely from this shape, which is what keeps the frontend
8
+ * free of per-provider conditionals. New provider features should be exposed
9
+ * here instead of branching on the provider id in React components.
10
+ */
11
+ type ProviderCapabilities = {
12
+ provider: LLMProvider;
13
+ /** Permission modes the provider runtime understands, in cycle order. */
14
+ permissionModes: string[];
15
+ defaultPermissionMode: string;
16
+ /** Whether image attachments can be included in a chat.send. */
17
+ supportsImages: boolean;
18
+ /** Whether an in-flight run can be cancelled via chat.abort. */
19
+ supportsAbort: boolean;
20
+ /** Whether interactive tool permission prompts can reach the UI. */
21
+ supportsPermissionRequests: boolean;
22
+ /** Whether the token-usage endpoint has data for this provider. */
23
+ supportsTokenUsage: boolean;
24
+ };
25
+
26
+ /**
27
+ * The capability matrix mirrors what each runtime actually implements today:
28
+ * - permission modes match the option sets accepted by each CLI/SDK.
29
+ * - only the Claude SDK integration surfaces interactive permission requests.
30
+ * - Cursor has no token usage endpoint support (its store.db has no usage rows).
31
+ */
32
+ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
33
+ claude: {
34
+ provider: 'claude',
35
+ permissionModes: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'],
36
+ defaultPermissionMode: 'default',
37
+ supportsImages: true,
38
+ supportsAbort: true,
39
+ supportsPermissionRequests: true,
40
+ supportsTokenUsage: true,
41
+ },
42
+ cursor: {
43
+ provider: 'cursor',
44
+ permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
45
+ defaultPermissionMode: 'default',
46
+ supportsImages: false,
47
+ supportsAbort: true,
48
+ supportsPermissionRequests: false,
49
+ supportsTokenUsage: false,
50
+ },
51
+ codex: {
52
+ provider: 'codex',
53
+ permissionModes: ['default', 'acceptEdits', 'bypassPermissions'],
54
+ defaultPermissionMode: 'default',
55
+ supportsImages: false,
56
+ supportsAbort: true,
57
+ supportsPermissionRequests: false,
58
+ supportsTokenUsage: true,
59
+ },
60
+ gemini: {
61
+ provider: 'gemini',
62
+ permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
63
+ defaultPermissionMode: 'default',
64
+ supportsImages: false,
65
+ supportsAbort: true,
66
+ supportsPermissionRequests: false,
67
+ supportsTokenUsage: true,
68
+ },
69
+ opencode: {
70
+ provider: 'opencode',
71
+ permissionModes: ['default'],
72
+ defaultPermissionMode: 'default',
73
+ supportsImages: false,
74
+ supportsAbort: true,
75
+ supportsPermissionRequests: false,
76
+ supportsTokenUsage: true,
77
+ },
78
+ };
79
+
80
+ /**
81
+ * Application service exposing the provider capability matrix.
82
+ */
83
+ export const providerCapabilitiesService = {
84
+ getProviderCapabilities(provider: LLMProvider): ProviderCapabilities {
85
+ return PROVIDER_CAPABILITIES[provider];
86
+ },
87
+
88
+ listAllProviderCapabilities(): ProviderCapabilities[] {
89
+ return Object.values(PROVIDER_CAPABILITIES);
90
+ },
91
+ };
@@ -17,7 +17,7 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
17
17
 
18
18
  export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
19
19
  const PROVIDER_MODELS_CACHE_VERSION = 1;
20
- const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
20
+ const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude', 'gemini']);
21
21
 
22
22
  type ProviderModelsServiceDependencies = {
23
23
  resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
@@ -4,10 +4,11 @@ import { promises as fsPromises } from 'node:fs';
4
4
 
5
5
  import chokidar, { type FSWatcher } from 'chokidar';
6
6
 
7
+ import { projectsDb, sessionsDb } from '@/modules/database/index.js';
7
8
  import { sessionSynchronizerService } from '@/modules/providers/services/session-synchronizer.service.js';
8
9
  import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
9
10
  import type { LLMProvider } from '@/shared/types.js';
10
- import { getProjectsWithSessions } from '@/modules/projects/index.js';
11
+ import { generateDisplayName } from '@/modules/projects/index.js';
11
12
 
12
13
  type WatcherEventType = 'add' | 'change';
13
14
 
@@ -58,6 +59,11 @@ const watchers: FSWatcher[] = [];
58
59
  type PendingWatcherUpdate = {
59
60
  providers: Set<LLMProvider>;
60
61
  changeTypes: Set<WatcherEventType>;
62
+ /**
63
+ * Provider-native session ids reported by the synchronizers. They are
64
+ * translated back to app-facing session rows at flush time, because the
65
+ * transcript file names on disk only ever contain provider ids.
66
+ */
61
67
  updatedSessionIds: Set<string>;
62
68
  };
63
69
 
@@ -131,6 +137,50 @@ function queuePendingWatcherUpdate(
131
137
  schedulePendingWatcherFlush();
132
138
  }
133
139
 
140
+ /**
141
+ * Builds one `session_upserted` delta event for a provider-native session id.
142
+ *
143
+ * The event carries everything a sidebar needs to upsert the session in place
144
+ * (session summary plus owning-project metadata), so clients never need a full
145
+ * project-list refetch when a transcript file changes on disk. Returns `null`
146
+ * when the id cannot be resolved to an indexed session row.
147
+ */
148
+ async function buildSessionUpsertedEvent(updatedProviderSessionId: string): Promise<string | null> {
149
+ const row = sessionsDb.getSessionByProviderSessionId(updatedProviderSessionId)
150
+ ?? sessionsDb.getSessionById(updatedProviderSessionId);
151
+ if (!row || row.isArchived) {
152
+ return null;
153
+ }
154
+
155
+ const projectPath = row.project_path;
156
+ const project = projectPath ? projectsDb.getProjectPath(projectPath) : null;
157
+ const displayName = project?.custom_project_name?.trim()
158
+ ? project.custom_project_name
159
+ : await generateDisplayName(path.basename(projectPath ?? '') || (projectPath ?? ''), projectPath);
160
+
161
+ return JSON.stringify({
162
+ kind: 'session_upserted',
163
+ sessionId: row.session_id,
164
+ provider: row.provider,
165
+ session: {
166
+ id: row.session_id,
167
+ summary: row.custom_name || '',
168
+ messageCount: 0,
169
+ lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
170
+ },
171
+ project: project
172
+ ? {
173
+ projectId: project.project_id,
174
+ path: project.project_path,
175
+ fullPath: project.project_path,
176
+ displayName,
177
+ isStarred: Boolean(project.isStarred),
178
+ }
179
+ : null,
180
+ timestamp: new Date().toISOString(),
181
+ });
182
+ }
183
+
134
184
  async function flushPendingWatcherUpdate(): Promise<void> {
135
185
  clearPendingWatcherFlushTimer();
136
186
 
@@ -149,33 +199,29 @@ async function flushPendingWatcherUpdate(): Promise<void> {
149
199
  watcherRefreshInFlight = true;
150
200
 
151
201
  try {
152
- const updatedProjects = await getProjectsWithSessions({ skipSynchronization: true });
153
- const changeTypes = Array.from(queuedUpdate.changeTypes);
154
- const watchProviders = Array.from(queuedUpdate.providers);
155
- const updatedSessionIds = Array.from(queuedUpdate.updatedSessionIds);
156
-
157
- // Backward-compatible fields stay populated with the first queued values.
158
- const updateMessage = JSON.stringify({
159
- type: 'projects_updated',
160
- projects: updatedProjects,
161
- timestamp: new Date().toISOString(),
162
- changeType: changeTypes[0] ?? 'change',
163
- updatedSessionId: updatedSessionIds[0] ?? undefined,
164
- watchProvider: watchProviders[0] ?? undefined,
165
- changeTypes,
166
- updatedSessionIds,
167
- watchProviders,
168
- batched: true,
169
- });
170
-
171
- connectedClients.forEach(client => {
172
- if (client.readyState === WS_OPEN_STATE) {
173
- client.send(updateMessage);
202
+ // Per-session deltas instead of full project snapshots: an upsert of one
203
+ // session can never clobber unrelated client state, so the frontend needs
204
+ // no "suppress updates while a run is active" protection logic.
205
+ const events: string[] = [];
206
+ for (const updatedSessionId of queuedUpdate.updatedSessionIds) {
207
+ const event = await buildSessionUpsertedEvent(updatedSessionId);
208
+ if (event) {
209
+ events.push(event);
174
210
  }
175
- });
211
+ }
212
+
213
+ if (events.length > 0) {
214
+ connectedClients.forEach(client => {
215
+ if (client.readyState === WS_OPEN_STATE) {
216
+ for (const event of events) {
217
+ client.send(event);
218
+ }
219
+ }
220
+ });
221
+ }
176
222
  } catch (error) {
177
223
  const message = error instanceof Error ? error.message : String(error);
178
- console.error('Session watcher refresh failed while broadcasting projects_updated', { error: message });
224
+ console.error('Session watcher refresh failed while broadcasting session_upserted', { error: message });
179
225
  } finally {
180
226
  watcherRefreshInFlight = false;
181
227