@cloudcli-ai/cloudcli 1.33.3 → 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 (329) 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 +26 -6
  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-models.provider.js +5 -0
  113. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  114. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  115. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  116. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  117. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  118. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  119. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  120. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  121. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  122. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  123. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  124. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  125. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  127. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  128. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  129. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  130. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  131. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  132. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  133. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  134. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  135. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  136. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  137. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  138. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  139. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  140. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  141. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  142. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  143. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  144. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  145. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  146. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  147. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  148. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  149. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  150. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  151. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  152. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  153. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  154. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  155. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  156. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  157. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  158. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  159. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  160. package/dist-server/server/modules/websocket/index.js +1 -0
  161. package/dist-server/server/modules/websocket/index.js.map +1 -1
  162. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  163. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  164. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  165. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  166. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  167. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  168. package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js +39 -0
  169. package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js.map +1 -0
  170. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  171. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  172. package/dist-server/server/modules/websocket/services/websocket-server.service.js +10 -0
  173. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  174. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  175. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  176. package/dist-server/server/openai-codex.js +24 -14
  177. package/dist-server/server/openai-codex.js.map +1 -1
  178. package/dist-server/server/opencode-cli.js +21 -8
  179. package/dist-server/server/opencode-cli.js.map +1 -1
  180. package/dist-server/server/opencode-cli.test.js +19 -1
  181. package/dist-server/server/opencode-cli.test.js.map +1 -1
  182. package/dist-server/server/routes/agent.js +1 -1
  183. package/dist-server/server/routes/settings.js +1 -1
  184. package/dist-server/server/routes/settings.js.map +1 -1
  185. package/dist-server/server/services/notification-orchestrator.js +1 -176
  186. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  187. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  188. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  189. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  190. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  191. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  192. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  193. package/dist-server/server/shared/utils.js +73 -1
  194. package/dist-server/server/shared/utils.js.map +1 -1
  195. package/dist-server/server/voice-proxy.js +226 -0
  196. package/dist-server/server/voice-proxy.js.map +1 -0
  197. package/electron/assets/logo-macos.icns +0 -0
  198. package/electron/assets/logo-macos.png +0 -0
  199. package/electron/assets/logo-windows.ico +0 -0
  200. package/electron/cloud.js +260 -0
  201. package/electron/computerAgent.js +290 -0
  202. package/electron/desktopNotifications.js +378 -0
  203. package/electron/desktopWindow.js +781 -0
  204. package/electron/launcher/index.html +14 -0
  205. package/electron/launcher/launcher.css +801 -0
  206. package/electron/launcher/launcher.js +829 -0
  207. package/electron/localServer.js +550 -0
  208. package/electron/main.js +1060 -0
  209. package/electron/preload.cjs +63 -0
  210. package/electron/scripts/generate-macos-icon.js +62 -0
  211. package/electron/serverInstaller.js +277 -0
  212. package/electron/tabs.js +87 -0
  213. package/electron/viewHost.js +331 -0
  214. package/package.json +86 -5
  215. package/public/api-docs.html +36 -18
  216. package/scripts/build-computer-semantics.mjs +133 -0
  217. package/scripts/copy-computer-semantics-bin.mjs +24 -0
  218. package/scripts/release/build-server-bundle.js +176 -0
  219. package/scripts/release/prepare-desktop-app.js +158 -0
  220. package/server/browser-use-mcp.ts +384 -0
  221. package/server/claude-sdk.js +28 -6
  222. package/server/cli.js +15 -6
  223. package/server/computer-use-agent.ts +279 -0
  224. package/server/computer-use-mcp.ts +574 -0
  225. package/server/cursor-cli.js +27 -11
  226. package/server/gemini-cli.js +17 -2
  227. package/server/index.js +151 -47
  228. package/server/load-env.js +1 -1
  229. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  230. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  231. package/server/modules/browser-use/browser-use.service.ts +836 -0
  232. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  233. package/server/modules/computer-use/actions/raw-action-dispatcher.ts +67 -0
  234. package/server/modules/computer-use/actions/raw-action-types.ts +28 -0
  235. package/server/modules/computer-use/computer-executor.ts +242 -0
  236. package/server/modules/computer-use/computer-semantics.service.ts +460 -0
  237. package/server/modules/computer-use/computer-use-mcp.routes.ts +141 -0
  238. package/server/modules/computer-use/computer-use.routes.ts +211 -0
  239. package/server/modules/computer-use/computer-use.service.ts +920 -0
  240. package/server/modules/computer-use/desktop-agent-relay.service.ts +158 -0
  241. package/server/modules/computer-use/index.ts +2 -0
  242. package/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.ts +82 -0
  243. package/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.ts +5 -0
  244. package/server/modules/computer-use/semantics/adapters/semantic-adapter.ts +23 -0
  245. package/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.ts +5 -0
  246. package/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
  247. package/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
  248. package/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
  249. package/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
  250. package/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift +467 -0
  251. package/server/modules/computer-use/semantics/helpers/semantic-helper-process.ts +124 -0
  252. package/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.ts +97 -0
  253. package/server/modules/computer-use/semantics/helpers/windows/CloudCLISemantics.csproj +11 -0
  254. package/server/modules/computer-use/semantics/helpers/windows/Program.cs +534 -0
  255. package/server/modules/computer-use/semantics/semantic-session-store.ts +87 -0
  256. package/server/modules/computer-use/semantics/semantic-tool-dispatcher.ts +17 -0
  257. package/server/modules/computer-use/semantics/semantic-types.ts +58 -0
  258. package/server/modules/database/index.ts +1 -0
  259. package/server/modules/database/migrations.ts +25 -0
  260. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  261. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  262. package/server/modules/database/repositories/sessions.db.ts +227 -23
  263. package/server/modules/database/schema.ts +27 -0
  264. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  265. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  266. package/server/modules/notifications/index.ts +13 -0
  267. package/server/modules/notifications/notifications.routes.ts +127 -0
  268. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  269. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  270. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  271. package/server/modules/projects/services/project-management.service.ts +0 -8
  272. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  273. package/server/modules/providers/README.md +1 -1
  274. package/server/modules/providers/index.ts +1 -0
  275. package/server/modules/providers/list/claude/claude-models.provider.ts +5 -0
  276. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  277. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  278. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  279. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  280. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  281. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  282. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  283. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  284. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  285. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  286. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  287. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  288. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  289. package/server/modules/providers/provider.routes.ts +167 -1
  290. package/server/modules/providers/services/mcp.service.ts +26 -0
  291. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  292. package/server/modules/providers/services/provider-models.service.ts +1 -1
  293. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  294. package/server/modules/providers/services/sessions.service.ts +79 -4
  295. package/server/modules/providers/services/skills.service.ts +25 -1
  296. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  297. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  298. package/server/modules/providers/tests/skills.test.ts +212 -0
  299. package/server/modules/websocket/README.md +33 -27
  300. package/server/modules/websocket/index.ts +1 -0
  301. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  302. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  303. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  304. package/server/modules/websocket/services/desktop-agent-websocket.service.ts +51 -0
  305. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  306. package/server/modules/websocket/services/websocket-server.service.ts +12 -0
  307. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  308. package/server/openai-codex.js +24 -14
  309. package/server/opencode-cli.js +21 -8
  310. package/server/opencode-cli.test.js +20 -1
  311. package/server/routes/agent.js +1 -1
  312. package/server/routes/settings.js +6 -1
  313. package/server/services/notification-orchestrator.js +3 -224
  314. package/server/services/tests/notification-orchestrator.test.js +80 -0
  315. package/server/shared/interfaces.ts +15 -0
  316. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  317. package/server/shared/types.ts +84 -1
  318. package/server/shared/utils.ts +95 -1
  319. package/server/voice-proxy.js +224 -0
  320. package/dist/assets/index-73Xw3i9v.css +0 -32
  321. package/dist/assets/index-9n7A21AT.js +0 -1361
  322. package/dist/modelConstants.js +0 -848
  323. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  324. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  325. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  326. package/public/modelConstants.js +0 -848
  327. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  328. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  329. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -35,7 +35,10 @@ const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
35
35
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
36
36
 
37
37
  type ShellWebSocketDependencies = {
38
- getSessionById: (sessionId: string) => { cliSessionId?: string } | null | undefined;
38
+ resolveProviderSessionId: (
39
+ sessionId: string,
40
+ provider: string,
41
+ ) => string | null | undefined;
39
42
  stripAnsiSequences: (content: string) => string;
40
43
  normalizeDetectedUrl: (url: string) => string | null;
41
44
  extractUrlsFromText: (content: string) => string[];
@@ -76,6 +79,36 @@ function parseShellMessage(rawMessage: RawData): ShellIncomingMessage | null {
76
79
  return payload as ShellIncomingMessage;
77
80
  }
78
81
 
82
+ const SAFE_SESSION_ID_PATTERN = /^[a-zA-Z0-9_.\-:]+$/;
83
+
84
+ function resolveResumeSessionId(
85
+ message: ShellIncomingMessage,
86
+ dependencies: ShellWebSocketDependencies
87
+ ): string {
88
+ const hasSession = readBoolean(message.hasSession);
89
+ const sessionId = readString(message.sessionId);
90
+ const provider = readString(message.provider, 'claude');
91
+
92
+ if (!hasSession || !sessionId) {
93
+ return '';
94
+ }
95
+
96
+ let resumeSessionId: string | null | undefined;
97
+ try {
98
+ resumeSessionId = dependencies.resolveProviderSessionId(sessionId, provider);
99
+ } catch (error) {
100
+ console.error('Failed to resolve provider session ID:', error);
101
+ resumeSessionId = undefined;
102
+ }
103
+
104
+ const resolvedSessionId = resumeSessionId === undefined ? sessionId : resumeSessionId;
105
+ if (!resolvedSessionId || !SAFE_SESSION_ID_PATTERN.test(resolvedSessionId)) {
106
+ return '';
107
+ }
108
+
109
+ return resolvedSessionId;
110
+ }
111
+
79
112
  /**
80
113
  * Resolves provider command line for plain shell and agent-backed shell modes.
81
114
  */
@@ -84,10 +117,9 @@ function buildShellCommand(
84
117
  dependencies: ShellWebSocketDependencies
85
118
  ): string {
86
119
  const hasSession = readBoolean(message.hasSession);
87
- const sessionId = readString(message.sessionId);
88
120
  const initialCommand = readString(message.initialCommand);
89
121
  const provider = readString(message.provider, 'claude');
90
- const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
122
+ const resumeSessionId = resolveResumeSessionId(message, dependencies);
91
123
  const isPlainShell =
92
124
  readBoolean(message.isPlainShell) ||
93
125
  (!!initialCommand && !hasSession) ||
@@ -98,62 +130,103 @@ function buildShellCommand(
98
130
  }
99
131
 
100
132
  if (provider === 'cursor') {
101
- if (hasSession && sessionId) {
102
- return `cursor-agent --resume="${sessionId}"`;
133
+ if (resumeSessionId) {
134
+ return `cursor-agent --resume="${resumeSessionId}"`;
103
135
  }
104
136
  return 'cursor-agent';
105
137
  }
106
138
 
107
139
  if (provider === 'codex') {
108
- if (hasSession && sessionId) {
140
+ if (resumeSessionId) {
109
141
  if (os.platform() === 'win32') {
110
- return `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
142
+ return `codex resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
111
143
  }
112
- return `codex resume "${sessionId}" || codex`;
144
+ return `codex resume "${resumeSessionId}" || codex`;
113
145
  }
114
146
  return 'codex';
115
147
  }
116
148
 
117
149
  if (provider === 'gemini') {
118
150
  const command = initialCommand || 'gemini';
119
- let resumeId = sessionId;
120
- if (hasSession && sessionId) {
121
- try {
122
- const existingSession = dependencies.getSessionById(sessionId);
123
- if (existingSession && existingSession.cliSessionId) {
124
- resumeId = existingSession.cliSessionId;
125
- if (!safeSessionIdPattern.test(resumeId)) {
126
- resumeId = '';
127
- }
128
- }
129
- } catch (error) {
130
- console.error('Failed to get Gemini CLI session ID:', error);
131
- }
132
- }
133
-
134
- if (hasSession && resumeId) {
135
- return `${command} --resume "${resumeId}"`;
151
+ if (resumeSessionId) {
152
+ return `${command} --resume "${resumeSessionId}"`;
136
153
  }
137
154
  return command;
138
155
  }
139
156
 
140
157
  if (provider === 'opencode') {
141
- if (hasSession && sessionId) {
142
- return `opencode --session "${sessionId}"`;
158
+ if (resumeSessionId) {
159
+ return `opencode --session "${resumeSessionId}"`;
143
160
  }
144
161
  return initialCommand || 'opencode';
145
162
  }
146
163
 
147
164
  const command = initialCommand || 'claude';
148
- if (hasSession && sessionId) {
165
+ if (resumeSessionId) {
149
166
  if (os.platform() === 'win32') {
150
- return `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
167
+ return `claude --resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
151
168
  }
152
- return `claude --resume "${sessionId}" || claude`;
169
+ return `claude --resume "${resumeSessionId}" || claude`;
153
170
  }
154
171
  return command;
155
172
  }
156
173
 
174
+ function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
175
+ const resolvedKey = Object.keys(env).find((envKey) => envKey.toLowerCase() === key.toLowerCase());
176
+ return resolvedKey ? env[resolvedKey] : undefined;
177
+ }
178
+
179
+ function getPathEnvKey(env: NodeJS.ProcessEnv): string {
180
+ return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
181
+ }
182
+
183
+ function prioritizeUserNpmGlobalBin(env: NodeJS.ProcessEnv): { key: string; value: string | undefined } {
184
+ const pathKey = getPathEnvKey(env);
185
+ const currentPath = env[pathKey];
186
+ if (!currentPath) {
187
+ return { key: pathKey, value: currentPath };
188
+ }
189
+
190
+ const delimiter = path.delimiter;
191
+ const pathEntries = currentPath.split(delimiter).filter(Boolean);
192
+ const npmPrefix = readEnvValue(env, 'npm_config_prefix');
193
+ const appData = readEnvValue(env, 'APPDATA');
194
+ const candidates = [
195
+ npmPrefix || '',
196
+ npmPrefix ? path.join(npmPrefix, 'bin') : '',
197
+ appData ? path.join(appData, 'npm') : '',
198
+ path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
199
+ path.join(os.homedir(), '.npm-global', 'bin'),
200
+ ].filter(Boolean);
201
+
202
+ const normalizedPathEntries = pathEntries.map((entry) => os.platform() === 'win32' ? entry.toLowerCase() : entry);
203
+ const preferredEntries = candidates.filter((candidate, index) => {
204
+ const normalizedCandidate = os.platform() === 'win32' ? candidate.toLowerCase() : candidate;
205
+ return (
206
+ candidates.indexOf(candidate) === index &&
207
+ normalizedPathEntries.includes(normalizedCandidate)
208
+ );
209
+ });
210
+
211
+ if (preferredEntries.length === 0) {
212
+ return { key: pathKey, value: currentPath };
213
+ }
214
+
215
+ const normalizedPreferredEntries = preferredEntries.map((entry) =>
216
+ os.platform() === 'win32' ? entry.toLowerCase() : entry
217
+ );
218
+
219
+ const value = [
220
+ ...preferredEntries,
221
+ ...pathEntries.filter((entry) => {
222
+ const normalizedEntry = os.platform() === 'win32' ? entry.toLowerCase() : entry;
223
+ return !normalizedPreferredEntries.includes(normalizedEntry);
224
+ }),
225
+ ].join(delimiter);
226
+
227
+ return { key: pathKey, value };
228
+ }
229
+
157
230
  /**
158
231
  * Handles websocket connections used by the standalone shell terminal UI.
159
232
  */
@@ -261,11 +334,13 @@ export function handleShellConnection(
261
334
  }
262
335
 
263
336
  const shellCommand = buildShellCommand(data, dependencies);
337
+ const resumeSessionId = resolveResumeSessionId(data, dependencies);
264
338
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
265
339
  const shellArgs =
266
340
  os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
267
341
  const termCols = readNumber(data.cols, 80);
268
342
  const termRows = readNumber(data.rows, 24);
343
+ const prioritizedPath = prioritizeUserNpmGlobalBin(process.env);
269
344
 
270
345
  shellProcess = pty.spawn(shell, shellArgs, {
271
346
  name: 'xterm-256color',
@@ -274,6 +349,7 @@ export function handleShellConnection(
274
349
  cwd: resolvedProjectPath,
275
350
  env: {
276
351
  ...process.env,
352
+ [prioritizedPath.key]: prioritizedPath.value,
277
353
  TERM: 'xterm-256color',
278
354
  COLORTERM: 'truecolor',
279
355
  FORCE_COLOR: '3',
@@ -406,8 +482,8 @@ export function handleShellConnection(
406
482
  : provider === 'opencode'
407
483
  ? 'OpenCode'
408
484
  : 'Claude';
409
- welcomeMsg = hasSession
410
- ? `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n`
485
+ welcomeMsg = hasSession && resumeSessionId
486
+ ? `\x1b[36mResuming ${providerName} session ${resumeSessionId} in: ${projectPath}\x1b[0m\r\n`
411
487
  : `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
412
488
  }
413
489
 
@@ -6,6 +6,8 @@ import { handleChatConnection } from '@/modules/websocket/services/chat-websocke
6
6
  import { verifyWebSocketClient } from '@/modules/websocket/services/websocket-auth.service.js';
7
7
  import { handlePluginWsProxy } from '@/modules/websocket/services/plugin-websocket-proxy.service.js';
8
8
  import { handleShellConnection } from '@/modules/websocket/services/shell-websocket.service.js';
9
+ import { handleDesktopAgentConnection } from '@/modules/websocket/services/desktop-agent-websocket.service.js';
10
+ import { handleDesktopNotificationsConnection } from '@/modules/notifications/index.js';
9
11
  import type { AuthenticatedWebSocketRequest } from '@/shared/types.js';
10
12
 
11
13
  type WebSocketServerDependencies = {
@@ -63,6 +65,16 @@ export function createWebSocketServer(
63
65
  return;
64
66
  }
65
67
 
68
+ if (pathname === '/desktop-agent') {
69
+ handleDesktopAgentConnection(ws, incomingRequest);
70
+ return;
71
+ }
72
+
73
+ if (pathname === '/desktop-notifications') {
74
+ handleDesktopNotificationsConnection(ws, incomingRequest);
75
+ return;
76
+ }
77
+
66
78
  if (pathname.startsWith('/plugin-ws/')) {
67
79
  handlePluginWsProxy(ws, pathname, dependencies.getPluginPort);
68
80
  return;
@@ -0,0 +1,244 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
8
+ import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
9
+ import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
10
+
11
+ /**
12
+ * Minimal stand-in for a websocket connection: collects every JSON frame the
13
+ * gateway writer forwards so assertions can inspect the outbound protocol.
14
+ */
15
+ class FakeConnection {
16
+ readyState = 1; // WS_OPEN_STATE
17
+ frames: Array<Record<string, unknown>> = [];
18
+
19
+ send(data: string): void {
20
+ this.frames.push(JSON.parse(data) as Record<string, unknown>);
21
+ }
22
+ }
23
+
24
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
25
+ const previousDatabasePath = process.env.DATABASE_PATH;
26
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'chat-run-registry-'));
27
+ const databasePath = path.join(tempDirectory, 'auth.db');
28
+
29
+ closeConnection();
30
+ process.env.DATABASE_PATH = databasePath;
31
+ await initializeDatabase();
32
+
33
+ try {
34
+ await runTest();
35
+ } finally {
36
+ connectedClients.clear();
37
+ chatRunRegistry.clearAll();
38
+ closeConnection();
39
+ if (previousDatabasePath === undefined) {
40
+ delete process.env.DATABASE_PATH;
41
+ } else {
42
+ process.env.DATABASE_PATH = previousDatabasePath;
43
+ }
44
+ await rm(tempDirectory, { recursive: true, force: true });
45
+ }
46
+ }
47
+
48
+ test('live events are remapped to the app session id and sequenced', async () => {
49
+ await withIsolatedDatabase(() => {
50
+ sessionsDb.createAppSession('app-run-1', 'claude', '/workspace/demo');
51
+ const connection = new FakeConnection();
52
+ const run = chatRunRegistry.startRun({
53
+ appSessionId: 'app-run-1',
54
+ provider: 'claude',
55
+ providerSessionId: null,
56
+ connection,
57
+ userId: 'user-1',
58
+ });
59
+ assert.ok(run);
60
+
61
+ run.writer.send({ kind: 'stream_delta', provider: 'claude', sessionId: 'provider-id-9', content: 'hello' });
62
+ run.writer.send({ kind: 'text', provider: 'claude', sessionId: 'provider-id-9', content: 'hello world' });
63
+
64
+ assert.equal(connection.frames.length, 2);
65
+ assert.equal(connection.frames[0]?.sessionId, 'app-run-1');
66
+ assert.equal(connection.frames[0]?.seq, 1);
67
+ assert.equal(connection.frames[1]?.sessionId, 'app-run-1');
68
+ assert.equal(connection.frames[1]?.seq, 2);
69
+ });
70
+ });
71
+
72
+ test('session_created is swallowed and persisted as the provider-id mapping', async () => {
73
+ await withIsolatedDatabase(() => {
74
+ sessionsDb.createAppSession('app-run-2', 'cursor', '/workspace/demo');
75
+ const connection = new FakeConnection();
76
+ connectedClients.add(connection as never);
77
+ const run = chatRunRegistry.startRun({
78
+ appSessionId: 'app-run-2',
79
+ provider: 'cursor',
80
+ providerSessionId: null,
81
+ connection,
82
+ userId: null,
83
+ });
84
+ assert.ok(run);
85
+
86
+ run.writer.send({
87
+ kind: 'session_created',
88
+ provider: 'cursor',
89
+ sessionId: 'cursor-native-7',
90
+ newSessionId: 'cursor-native-7',
91
+ });
92
+
93
+ // The provider-native event itself is never forwarded...
94
+ const sessionUpserts = connection.frames.filter((frame) => frame.kind === 'session_upserted');
95
+ assert.equal(sessionUpserts.length, 1);
96
+ assert.equal(sessionUpserts[0]?.sessionId, 'app-run-2');
97
+ assert.equal(sessionUpserts[0]?.providerSessionId, 'cursor-native-7');
98
+ // ...but the canonical mapping is recorded and persisted in the database.
99
+ assert.equal(run.providerSessionId, 'cursor-native-7');
100
+ assert.equal(sessionsDb.getSessionById('app-run-2')?.provider_session_id, 'cursor-native-7');
101
+ });
102
+ });
103
+
104
+ test('complete marks the run finished and duplicate completes are dropped', async () => {
105
+ await withIsolatedDatabase(() => {
106
+ sessionsDb.createAppSession('app-run-3', 'codex', '/workspace/demo');
107
+ const connection = new FakeConnection();
108
+ const run = chatRunRegistry.startRun({
109
+ appSessionId: 'app-run-3',
110
+ provider: 'codex',
111
+ providerSessionId: null,
112
+ connection,
113
+ userId: null,
114
+ });
115
+ assert.ok(run);
116
+
117
+ run.writer.send({ kind: 'complete', provider: 'codex', sessionId: 'native-3', exitCode: 0 });
118
+ // Late duplicate from a killed runtime's exit handler.
119
+ run.writer.send({ kind: 'complete', provider: 'codex', sessionId: 'native-3', exitCode: 1 });
120
+
121
+ const completes = connection.frames.filter((frame) => frame.kind === 'complete');
122
+ assert.equal(completes.length, 1);
123
+ assert.equal(completes[0]?.actualSessionId, 'app-run-3');
124
+ assert.equal(chatRunRegistry.isProcessing('app-run-3'), false);
125
+
126
+ // completeRun is also a no-op once the run already completed.
127
+ chatRunRegistry.completeRun('app-run-3', { exitCode: 1 });
128
+ assert.equal(connection.frames.filter((frame) => frame.kind === 'complete').length, 1);
129
+ });
130
+ });
131
+
132
+ test('listRunningRuns returns only currently running app sessions', async () => {
133
+ await withIsolatedDatabase(() => {
134
+ sessionsDb.createAppSession('app-run-7', 'claude', '/workspace/demo');
135
+ sessionsDb.createAppSession('app-run-8', 'codex', '/workspace/demo');
136
+ const connection = new FakeConnection();
137
+
138
+ const completedRun = chatRunRegistry.startRun({
139
+ appSessionId: 'app-run-7',
140
+ provider: 'claude',
141
+ providerSessionId: null,
142
+ connection,
143
+ userId: null,
144
+ });
145
+ assert.ok(completedRun);
146
+
147
+ const runningRun = chatRunRegistry.startRun({
148
+ appSessionId: 'app-run-8',
149
+ provider: 'codex',
150
+ providerSessionId: null,
151
+ connection,
152
+ userId: null,
153
+ });
154
+ assert.ok(runningRun);
155
+
156
+ chatRunRegistry.completeRun('app-run-7', { exitCode: 0 });
157
+
158
+ const runningSessions = chatRunRegistry.listRunningRuns();
159
+ assert.deepEqual(runningSessions.map((session) => session.sessionId), ['app-run-8']);
160
+ assert.equal(runningSessions[0]?.provider, 'codex');
161
+ });
162
+ });
163
+
164
+ test('replayEvents returns only events after the requested seq', async () => {
165
+ await withIsolatedDatabase(() => {
166
+ sessionsDb.createAppSession('app-run-4', 'claude', '/workspace/demo');
167
+ const connection = new FakeConnection();
168
+ const run = chatRunRegistry.startRun({
169
+ appSessionId: 'app-run-4',
170
+ provider: 'claude',
171
+ providerSessionId: null,
172
+ connection,
173
+ userId: null,
174
+ });
175
+ assert.ok(run);
176
+
177
+ run.writer.send({ kind: 'stream_delta', provider: 'claude', sessionId: 'x', content: 'a' });
178
+ run.writer.send({ kind: 'stream_delta', provider: 'claude', sessionId: 'x', content: 'b' });
179
+ run.writer.send({ kind: 'stream_delta', provider: 'claude', sessionId: 'x', content: 'c' });
180
+
181
+ const replayed = chatRunRegistry.replayEvents('app-run-4', 1);
182
+ assert.deepEqual(replayed.map((event) => event.content), ['b', 'c']);
183
+ assert.deepEqual(replayed.map((event) => event.seq), [2, 3]);
184
+ });
185
+ });
186
+
187
+ test('attachConnection reroutes the live stream to a new socket', async () => {
188
+ await withIsolatedDatabase(() => {
189
+ sessionsDb.createAppSession('app-run-5', 'gemini', '/workspace/demo');
190
+ const firstConnection = new FakeConnection();
191
+ const run = chatRunRegistry.startRun({
192
+ appSessionId: 'app-run-5',
193
+ provider: 'gemini',
194
+ providerSessionId: null,
195
+ connection: firstConnection,
196
+ userId: null,
197
+ });
198
+ assert.ok(run);
199
+
200
+ run.writer.send({ kind: 'stream_delta', provider: 'gemini', sessionId: 'g', content: 'before' });
201
+
202
+ const secondConnection = new FakeConnection();
203
+ assert.equal(chatRunRegistry.attachConnection('app-run-5', secondConnection), true);
204
+ run.writer.send({ kind: 'stream_delta', provider: 'gemini', sessionId: 'g', content: 'after' });
205
+
206
+ assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before']);
207
+ assert.deepEqual(secondConnection.frames.map((frame) => frame.content), ['after']);
208
+ });
209
+ });
210
+
211
+ test('startRun rejects a second concurrent run for the same session', async () => {
212
+ await withIsolatedDatabase(() => {
213
+ sessionsDb.createAppSession('app-run-6', 'opencode', '/workspace/demo');
214
+ const connection = new FakeConnection();
215
+ const first = chatRunRegistry.startRun({
216
+ appSessionId: 'app-run-6',
217
+ provider: 'opencode',
218
+ providerSessionId: null,
219
+ connection,
220
+ userId: null,
221
+ });
222
+ assert.ok(first);
223
+
224
+ const second = chatRunRegistry.startRun({
225
+ appSessionId: 'app-run-6',
226
+ provider: 'opencode',
227
+ providerSessionId: null,
228
+ connection,
229
+ userId: null,
230
+ });
231
+ assert.equal(second, null);
232
+
233
+ // After the run finishes a new one is allowed again.
234
+ chatRunRegistry.completeRun('app-run-6', { exitCode: 0 });
235
+ const third = chatRunRegistry.startRun({
236
+ appSessionId: 'app-run-6',
237
+ provider: 'opencode',
238
+ providerSessionId: null,
239
+ connection,
240
+ userId: null,
241
+ });
242
+ assert.ok(third);
243
+ });
244
+ });
@@ -18,7 +18,7 @@ import { notifyRunFailed, notifyRunStopped } from './services/notification-orche
18
18
  import { sessionsService } from './modules/providers/services/sessions.service.js';
19
19
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
20
20
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
21
- import { createNormalizedMessage } from './shared/utils.js';
21
+ import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
22
22
 
23
23
  // Track active sessions
24
24
  const activeCodexSessions = new Map();
@@ -352,21 +352,26 @@ export async function queryCodex(command, options = {}, ws) {
352
352
  }
353
353
  }
354
354
 
355
- // Send completion event
356
- if (!terminalFailure) {
357
- sendMessage(ws, createNormalizedMessage({
358
- kind: 'complete',
359
- actualSessionId: capturedSessionId || thread.id || sessionId || null,
360
- sessionId: capturedSessionId || sessionId || null,
361
- provider: 'codex'
362
- }));
363
- notifyRunStopped({
364
- userId: ws?.userId || null,
355
+ // Send the terminal completion event — skipped for aborted runs, whose
356
+ // terminal `complete` (aborted: true) was already sent by abort-session.
357
+ const runSession = capturedSessionId ? activeCodexSessions.get(capturedSessionId) : null;
358
+ const runAborted = runSession?.status === 'aborted' || abortController.signal.aborted;
359
+ if (!runAborted) {
360
+ sendMessage(ws, createCompleteMessage({
365
361
  provider: 'codex',
366
362
  sessionId: capturedSessionId || sessionId || null,
367
- sessionName: sessionSummary,
368
- stopReason: 'completed'
369
- });
363
+ actualSessionId: capturedSessionId || thread.id || sessionId || null,
364
+ exitCode: terminalFailure ? 1 : 0,
365
+ }));
366
+ if (!terminalFailure) {
367
+ notifyRunStopped({
368
+ userId: ws?.userId || null,
369
+ provider: 'codex',
370
+ sessionId: capturedSessionId || sessionId || null,
371
+ sessionName: sessionSummary,
372
+ stopReason: 'completed'
373
+ });
374
+ }
370
375
  }
371
376
 
372
377
  } catch (error) {
@@ -386,6 +391,11 @@ export async function queryCodex(command, options = {}, ws) {
386
391
  : error.message;
387
392
 
388
393
  sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
394
+ sendMessage(ws, createCompleteMessage({
395
+ provider: 'codex',
396
+ sessionId: capturedSessionId || sessionId || null,
397
+ exitCode: 1,
398
+ }));
389
399
  if (!terminalFailure) {
390
400
  notifyRunFailed({
391
401
  userId: ws?.userId || null,
@@ -8,7 +8,7 @@ import { sessionsService } from './modules/providers/services/sessions.service.j
8
8
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
9
9
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
10
10
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
11
- import { createNormalizedMessage, getOpenCodeDatabasePath } from './shared/utils.js';
11
+ import { createCompleteMessage, createNormalizedMessage, getOpenCodeDatabasePath } from './shared/utils.js';
12
12
 
13
13
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
14
14
 
@@ -92,6 +92,9 @@ async function spawnOpenCode(command, options = {}, ws) {
92
92
  let stdoutLineBuffer = '';
93
93
  let terminalNotificationSent = false;
94
94
  let opencodeProcess = null;
95
+ // Unified lifecycle contract: exactly one terminal `complete` per run
96
+ // (close and error handlers can both fire for spawn failures).
97
+ let completeSent = false;
95
98
 
96
99
  const notifyTerminalState = ({ code = null, error = null } = {}) => {
97
100
  if (terminalNotificationSent) {
@@ -191,6 +194,10 @@ async function spawnOpenCode(command, options = {}, ws) {
191
194
 
192
195
  void providerModelsService.resolveResumeModel('opencode', sessionId, model).then((resolvedModel) => {
193
196
  const args = ['run', '--format', 'json'];
197
+ // OpenCode's `run` command owns workspace selection through `--dir`.
198
+ // Relying on the child-process cwd alone is not enough on Linux, where
199
+ // the CLI can still resolve the session under the server install dir.
200
+ args.push('--dir', workingDir);
194
201
  if (sessionId) {
195
202
  args.push('--session', sessionId);
196
203
  }
@@ -256,13 +263,12 @@ async function spawnOpenCode(command, options = {}, ws) {
256
263
  }));
257
264
  }
258
265
 
259
- ws.send(createNormalizedMessage({
260
- kind: 'complete',
261
- exitCode: code,
262
- isNewSession: !sessionId && !!command,
263
- sessionId: finalSessionId,
264
- provider: 'opencode',
265
- }));
266
+ // Terminal complete — skipped for aborted runs (abort-session
267
+ // already sent the aborted complete on this run's behalf).
268
+ if (!completeSent && !opencodeProcess.aborted) {
269
+ completeSent = true;
270
+ ws.send(createCompleteMessage({ provider: 'opencode', sessionId: finalSessionId, exitCode: code }));
271
+ }
266
272
 
267
273
  if (code === 0) {
268
274
  notifyTerminalState({ code });
@@ -302,6 +308,10 @@ async function spawnOpenCode(command, options = {}, ws) {
302
308
  sessionId: finalSessionId,
303
309
  provider: 'opencode',
304
310
  }));
311
+ if (!completeSent && !opencodeProcess.aborted) {
312
+ completeSent = true;
313
+ ws.send(createCompleteMessage({ provider: 'opencode', sessionId: finalSessionId, exitCode: 1 }));
314
+ }
305
315
  notifyTerminalState({ error });
306
316
  reject(error);
307
317
  });
@@ -315,6 +325,9 @@ function abortOpenCodeSession(sessionId) {
315
325
  return false;
316
326
  }
317
327
 
328
+ // The abort handler sends the terminal complete (aborted: true); flag the
329
+ // process so its close handler does not emit a second one.
330
+ process.aborted = true;
318
331
  process.kill('SIGTERM');
319
332
  activeOpenCodeProcesses.delete(sessionId);
320
333
  return true;