wind-agent-cli 1.37.2 → 1.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/LICENSE +718 -718
  2. package/README.md +1 -1
  3. package/dist/api-docs.html +901 -901
  4. package/dist/assets/{CodeEditor-B2Wf7Fg0.js → CodeEditor-BqmxpZ_D.js} +1 -1
  5. package/dist/assets/{FileTree-N2IkkHlv.js → FileTree-DLE7Ioai.js} +1 -1
  6. package/dist/assets/{GitPanel-DZCeuFZ2.js → GitPanel-f8RMRa8D.js} +1 -1
  7. package/dist/assets/{MarkdownPreview-C0tqYnVu.js → MarkdownPreview-93h_1tJa.js} +1 -1
  8. package/dist/assets/{PluginTabContent-Bnuf-fb8.js → PluginTabContent-DU1jjkXP.js} +1 -1
  9. package/dist/assets/{Shell-BfFnMZ-f.js → Shell-CN_2MRIM.js} +1 -1
  10. package/dist/assets/{StandaloneShell-QmyYwNAl.js → StandaloneShell-D0avS8UE.js} +1 -1
  11. package/dist/assets/{TaskMasterPanel-fMsmvN9L.js → TaskMasterPanel-Cpf1Q5Ph.js} +3 -3
  12. package/dist/assets/{abnfDiagram-N423BO3Z-DjPU7HLs.js → abnfDiagram-N423BO3Z-p3aJIm4T.js} +1 -1
  13. package/dist/assets/{arc-Pdq6Tvr3.js → arc-CTeud6Fl.js} +1 -1
  14. package/dist/assets/{architectureDiagram-T3A2C74G-D0JnT0Vh.js → architectureDiagram-T3A2C74G-CG7kRCJU.js} +1 -1
  15. package/dist/assets/{blockDiagram-VBNYF7ZC-DpaEf3mv.js → blockDiagram-VBNYF7ZC-oSjQ1SgE.js} +1 -1
  16. package/dist/assets/{c4Diagram-5PPSVZJV-MpqlPGFi.js → c4Diagram-5PPSVZJV-RkzF0DJV.js} +1 -1
  17. package/dist/assets/channel-BfQbarn9.js +1 -0
  18. package/dist/assets/{chunk-2GRJ4B5K-DoSdNo8u.js → chunk-2GRJ4B5K-B07uZ07O.js} +1 -1
  19. package/dist/assets/{chunk-2Q5K7J3B-CE7I3uMn.js → chunk-2Q5K7J3B-DNSfO7xS.js} +1 -1
  20. package/dist/assets/{chunk-5RXB4S5H-B4UQ8snD.js → chunk-5RXB4S5H-BTeYcgyb.js} +1 -1
  21. package/dist/assets/{chunk-5VM5RSS4-B55XgqHZ.js → chunk-5VM5RSS4-B7vZV0zC.js} +1 -1
  22. package/dist/assets/{chunk-6Q2QTUOP-BS1KpefW.js → chunk-6Q2QTUOP-DuS_xUGG.js} +1 -1
  23. package/dist/assets/{chunk-GF5L2VYU-DkEPedJP.js → chunk-GF5L2VYU-CKLZ3iVD.js} +1 -1
  24. package/dist/assets/{chunk-JWPE2WC7-DOtkNhMm.js → chunk-JWPE2WC7-DD2hcXvs.js} +1 -1
  25. package/dist/assets/{chunk-KBJHAD2P-BXLjviqM.js → chunk-KBJHAD2P-Cz-l38qp.js} +1 -1
  26. package/dist/assets/{chunk-RYQCIY6F-C50fjNNz.js → chunk-RYQCIY6F-DGFIU1YM.js} +1 -1
  27. package/dist/assets/{chunk-XXDRQBXY-QBYqCkb7.js → chunk-XXDRQBXY-CmfzoYFl.js} +1 -1
  28. package/dist/assets/classDiagram-JCYQIIEL-Bpfe3kq7.js +1 -0
  29. package/dist/assets/classDiagram-v2-OCEON4UE-Bpfe3kq7.js +1 -0
  30. package/dist/assets/{code-xml-VK1YfShP.js → code-xml-CLpHzEOP.js} +1 -1
  31. package/dist/assets/{cose-bilkent-JH36ORCC-DrVTo1Nh.js → cose-bilkent-JH36ORCC-DlFcLXCB.js} +1 -1
  32. package/dist/assets/{cynefin-VYW2F7L2-DnJa_OlQ.js → cynefin-VYW2F7L2-P1HecReO.js} +1 -1
  33. package/dist/assets/{cynefinDiagram-MW4NZA55-CCxBjw9E.js → cynefinDiagram-MW4NZA55-COgduz5P.js} +1 -1
  34. package/dist/assets/{dagre-VZM6K2ZE-C_GvE4lu.js → dagre-VZM6K2ZE-Bbb-siGp.js} +1 -1
  35. package/dist/assets/{diagram-7IWD3JNH-pFzs9HEu.js → diagram-7IWD3JNH-9CWGFMYR.js} +1 -1
  36. package/dist/assets/{diagram-B4RE2ZJO-CcehQC7x.js → diagram-B4RE2ZJO-TWK59T6V.js} +1 -1
  37. package/dist/assets/{diagram-LBJQPF4R-hfKcrrxe.js → diagram-LBJQPF4R-ErMC9yaa.js} +1 -1
  38. package/dist/assets/{diagram-Q27KOJAE-C1semJfK.js → diagram-Q27KOJAE-Dt_-C7hV.js} +1 -1
  39. package/dist/assets/{diagram-UB23O5K3-57hvXQXO.js → diagram-UB23O5K3-BJ1DnRAF.js} +1 -1
  40. package/dist/assets/{ebnfDiagram-BXEA7PRR-9KIAlAVw.js → ebnfDiagram-BXEA7PRR-CE1YaQ9s.js} +1 -1
  41. package/dist/assets/{erDiagram-JOGREHBK-B7OzJ767.js → erDiagram-JOGREHBK-Bqk7N9aY.js} +1 -1
  42. package/dist/assets/{flowDiagram-UKHOOZJN-CJV8fQPR.js → flowDiagram-UKHOOZJN-DEr2II5V.js} +1 -1
  43. package/dist/assets/{ganttDiagram-PKOTCBZU-CbMNWWQu.js → ganttDiagram-PKOTCBZU-CWYAUeYN.js} +1 -1
  44. package/dist/assets/{gitGraphDiagram-DS77QQ5N-I_vZ5waO.js → gitGraphDiagram-DS77QQ5N-tdEcEIM6.js} +1 -1
  45. package/dist/assets/{index-ZHwIojzi.js → index-B-sTfSZq.js} +1 -1
  46. package/dist/assets/{index-GFhXZyl2.js → index-BSe3UiFA.js} +1 -1
  47. package/dist/assets/{index-DFnVoqpx.js → index-B_FDMWar.js} +5 -5
  48. package/dist/assets/{index-DlSeAkw_.js → index-CWJtg2pR.js} +1 -1
  49. package/dist/assets/{infoDiagram-6WML65LV-Dh20Tsbq.js → infoDiagram-6WML65LV-A3IqRUnO.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DH2Sm3ko.js → ishikawaDiagram-WSZJBQD7-B2uL6bkK.js} +1 -1
  51. package/dist/assets/{journeyDiagram-NVQOT4AX-BNFQqlVu.js → journeyDiagram-NVQOT4AX-D9JB-hRW.js} +1 -1
  52. package/dist/assets/{kanban-definition-27J2QSJJ-B3iL0ztJ.js → kanban-definition-27J2QSJJ-DmlgiV7K.js} +1 -1
  53. package/dist/assets/{linear-Cwe9dEKA.js → linear-Ct_eoJQk.js} +1 -1
  54. package/dist/assets/{mermaid.core-BYRQSshi.js → mermaid.core-CQ698HNn.js} +5 -5
  55. package/dist/assets/{mindmap-definition-FAOFIHXS-BgfBYMzr.js → mindmap-definition-FAOFIHXS-DGtMgQhf.js} +1 -1
  56. package/dist/assets/{pegDiagram-VL7TDLO6-CNYpECr8.js → pegDiagram-VL7TDLO6-CO6vP4n9.js} +1 -1
  57. package/dist/assets/{pieDiagram-7S7Q4E2Y-DeSrxc4I.js → pieDiagram-7S7Q4E2Y-DMkt_Zfr.js} +1 -1
  58. package/dist/assets/{quadrantDiagram-CIZ2JOQS-BXVVQsa6.js → quadrantDiagram-CIZ2JOQS-DgNxRz6t.js} +1 -1
  59. package/dist/assets/{railroadDiagram-AXF67PYL-JTroIW8F.js → railroadDiagram-AXF67PYL-DKz9UqA9.js} +1 -1
  60. package/dist/assets/{requirementDiagram-LRYGKXZP-C1Z_inLz.js → requirementDiagram-LRYGKXZP-L84XyGgV.js} +1 -1
  61. package/dist/assets/{sankeyDiagram-W5VNT64P-DCJgKFUk.js → sankeyDiagram-W5VNT64P-CQqWbJ3j.js} +1 -1
  62. package/dist/assets/{sequenceDiagram-SI44F4Z6-D8Wrx5Vc.js → sequenceDiagram-SI44F4Z6--tnzS2tb.js} +1 -1
  63. package/dist/assets/{sizeCapture-X5ZJPWSS-CXqD68vD.js → sizeCapture-X5ZJPWSS-DujwEOtm.js} +1 -1
  64. package/dist/assets/{stateDiagram-OKZ733FA-CF0Ekqjo.js → stateDiagram-OKZ733FA-BK3TcZY2.js} +1 -1
  65. package/dist/assets/stateDiagram-v2-UEYNNEHI-_medyomK.js +1 -0
  66. package/dist/assets/{swimlanes-SLNWSIFB-DVIax_k4.js → swimlanes-SLNWSIFB-naX-Uv1B.js} +2 -2
  67. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DstIv3TC.js +8 -0
  68. package/dist/assets/{timeline-definition-Z64GVDOM-DSqk3ApG.js → timeline-definition-Z64GVDOM-C-vP_ddU.js} +1 -1
  69. package/dist/assets/{vennDiagram-T6HMQDX7-DNbTqyG5.js → vennDiagram-T6HMQDX7-DN-I7o-7.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-T6FBY63Y-BNCpsFZV.js → wardleyDiagram-T6FBY63Y-Db9dsaXv.js} +1 -1
  71. package/dist/assets/{xychartDiagram-ELKLHX3M-XsVZphHr.js → xychartDiagram-ELKLHX3M-DE6OxW_p.js} +1 -1
  72. package/dist/clear-cache.html +85 -85
  73. package/dist/convert-icons.md +52 -52
  74. package/dist/favicon.svg +8 -8
  75. package/dist/generate-icons.js +48 -48
  76. package/dist/icons/codex-white.svg +3 -3
  77. package/dist/icons/codex.svg +3 -3
  78. package/dist/icons/cursor-white.svg +11 -11
  79. package/dist/icons/icon-128x128.svg +11 -11
  80. package/dist/icons/icon-144x144.svg +11 -11
  81. package/dist/icons/icon-152x152.svg +11 -11
  82. package/dist/icons/icon-192x192.svg +11 -11
  83. package/dist/icons/icon-384x384.svg +11 -11
  84. package/dist/icons/icon-512x512.svg +11 -11
  85. package/dist/icons/icon-72x72.svg +11 -11
  86. package/dist/icons/icon-96x96.svg +11 -11
  87. package/dist/icons/icon-template.svg +11 -11
  88. package/dist/index.html +56 -56
  89. package/dist/logo.svg +17 -17
  90. package/dist/manifest.json +60 -60
  91. package/dist/sw.js +124 -124
  92. package/dist-server/server/modules/cli/cli.service.js +36 -36
  93. package/dist-server/server/modules/cli/sandbox.service.js +38 -38
  94. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +74 -74
  95. package/dist-server/server/modules/commands/commands.routes.js +24 -24
  96. package/dist-server/server/modules/database/migrations.js +190 -190
  97. package/dist-server/server/modules/database/repositories/api-keys.js +3 -3
  98. package/dist-server/server/modules/database/repositories/github-tokens.js +2 -2
  99. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +28 -28
  100. package/dist-server/server/modules/database/repositories/notification-preferences.js +4 -4
  101. package/dist-server/server/modules/database/repositories/projects.db.js +61 -61
  102. package/dist-server/server/modules/database/repositories/provider-models.js +35 -35
  103. package/dist-server/server/modules/database/repositories/push-subscriptions.js +5 -5
  104. package/dist-server/server/modules/database/repositories/scan-state.db.js +5 -5
  105. package/dist-server/server/modules/database/repositories/sessions.db.js +92 -92
  106. package/dist-server/server/modules/database/schema.js +177 -177
  107. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +6 -6
  108. package/dist-server/server/modules/git/git.routes.js +17 -17
  109. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +12 -12
  110. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +9 -9
  111. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +17 -17
  112. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +25 -25
  113. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +27 -27
  114. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +9 -9
  115. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -3
  116. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +96 -96
  117. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +18 -18
  118. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +419 -419
  119. package/electron/cloud.js +260 -260
  120. package/electron/desktopNotifications.js +378 -378
  121. package/electron/desktopWindow.js +766 -766
  122. package/electron/launcher/index.html +14 -14
  123. package/electron/launcher/launcher.css +801 -801
  124. package/electron/launcher/launcher.js +687 -687
  125. package/electron/localServer.js +549 -549
  126. package/electron/main.js +944 -944
  127. package/electron/preload.cjs +60 -60
  128. package/electron/scripts/generate-macos-icon.js +62 -62
  129. package/electron/serverInstaller.js +277 -277
  130. package/electron/tabs.js +87 -87
  131. package/electron/viewHost.js +331 -331
  132. package/package.json +250 -247
  133. package/public/api-docs.html +901 -901
  134. package/scripts/fix-node-pty.js +67 -67
  135. package/scripts/promote-dist-server.mjs +50 -50
  136. package/scripts/release/build-server-bundle.js +176 -176
  137. package/scripts/release/prepare-desktop-app.js +152 -152
  138. package/server/index.ts +399 -399
  139. package/server/load-env.ts +47 -47
  140. package/server/modules/agent/agent.module.ts +52 -52
  141. package/server/modules/agent/agent.routes.ts +1287 -1287
  142. package/server/modules/agent/index.ts +2 -2
  143. package/server/modules/agent/tests/agent.routes.test.ts +205 -205
  144. package/server/modules/assets/assets.routes.ts +150 -150
  145. package/server/modules/assets/index.ts +3 -3
  146. package/server/modules/assets/services/image-assets.service.ts +127 -127
  147. package/server/modules/assets/tests/image-assets.service.test.ts +74 -74
  148. package/server/modules/auth/auth.middleware.ts +163 -163
  149. package/server/modules/auth/auth.module.ts +38 -38
  150. package/server/modules/auth/auth.routes.ts +57 -57
  151. package/server/modules/auth/auth.service.ts +155 -155
  152. package/server/modules/auth/index.ts +9 -9
  153. package/server/modules/auth/tests/auth.service.test.ts +95 -95
  154. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -120
  155. package/server/modules/browser-use/browser-use-mcp.ts +386 -386
  156. package/server/modules/browser-use/browser-use-runtime.ts +10 -10
  157. package/server/modules/browser-use/browser-use.routes.ts +96 -96
  158. package/server/modules/browser-use/browser-use.service.ts +836 -836
  159. package/server/modules/browser-use/index.ts +11 -11
  160. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -10
  161. package/server/modules/cli/cli.module.ts +93 -93
  162. package/server/modules/cli/cli.service.ts +248 -248
  163. package/server/modules/cli/cli.ts +19 -19
  164. package/server/modules/cli/index.ts +2 -2
  165. package/server/modules/cli/sandbox.service.ts +398 -398
  166. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -186
  167. package/server/modules/cli/tests/cli.service.test.ts +86 -86
  168. package/server/modules/cli/tests/sandbox.service.test.ts +58 -58
  169. package/server/modules/commands/commands.module.ts +22 -22
  170. package/server/modules/commands/commands.routes.ts +586 -586
  171. package/server/modules/commands/index.ts +2 -2
  172. package/server/modules/commands/tests/commands.test.ts +109 -109
  173. package/server/modules/database/connection.ts +143 -143
  174. package/server/modules/database/index.ts +17 -17
  175. package/server/modules/database/init-db.ts +17 -17
  176. package/server/modules/database/migrations.ts +515 -515
  177. package/server/modules/database/repositories/api-keys.ts +119 -119
  178. package/server/modules/database/repositories/app-config.ts +53 -53
  179. package/server/modules/database/repositories/credentials.ts +106 -106
  180. package/server/modules/database/repositories/github-tokens.ts +100 -100
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -153
  182. package/server/modules/database/repositories/notification-preferences.ts +117 -117
  183. package/server/modules/database/repositories/projects.db.ts +196 -196
  184. package/server/modules/database/repositories/provider-models.ts +168 -168
  185. package/server/modules/database/repositories/push-subscriptions.ts +80 -80
  186. package/server/modules/database/repositories/scan-state.db.ts +42 -42
  187. package/server/modules/database/repositories/sessions.db.ts +524 -524
  188. package/server/modules/database/repositories/users.ts +140 -140
  189. package/server/modules/database/repositories/vapid-keys.ts +57 -57
  190. package/server/modules/database/schema.ts +210 -210
  191. package/server/modules/database/tests/projects.db.integration.test.ts +72 -72
  192. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -125
  193. package/server/modules/database/tests/sessions-provider-mapping.test.ts +109 -109
  194. package/server/modules/database/tests/sessions.db.integration.test.ts +116 -116
  195. package/server/modules/file-tree/file-tree.module.ts +116 -116
  196. package/server/modules/file-tree/file-tree.routes.ts +258 -258
  197. package/server/modules/file-tree/file-tree.service.ts +675 -675
  198. package/server/modules/file-tree/index.ts +2 -2
  199. package/server/modules/file-tree/tests/file-tree.routes.test.ts +157 -157
  200. package/server/modules/file-tree/tests/file-tree.service.test.ts +371 -371
  201. package/server/modules/git/git-branch.service.ts +43 -43
  202. package/server/modules/git/git-parsing.service.ts +66 -66
  203. package/server/modules/git/git.module.ts +22 -22
  204. package/server/modules/git/git.routes.ts +1596 -1596
  205. package/server/modules/git/index.ts +2 -2
  206. package/server/modules/git/tests/git-branch.service.test.ts +60 -60
  207. package/server/modules/git/tests/git-init.routes.test.ts +132 -132
  208. package/server/modules/git/tests/git.test.ts +106 -106
  209. package/server/modules/notifications/index.ts +24 -24
  210. package/server/modules/notifications/notifications.routes.ts +127 -127
  211. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -124
  212. package/server/modules/notifications/services/notification-orchestrator.service.js +310 -310
  213. package/server/modules/notifications/tests/notification-orchestrator.integration.test.ts +55 -55
  214. package/server/modules/notifications/vapid-keys.service.ts +38 -38
  215. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -109
  216. package/server/modules/plugins/index.ts +9 -9
  217. package/server/modules/plugins/plugin-process.service.ts +217 -217
  218. package/server/modules/plugins/plugin-registry.service.ts +459 -459
  219. package/server/modules/plugins/plugins.module.ts +33 -33
  220. package/server/modules/plugins/plugins.routes.ts +66 -66
  221. package/server/modules/plugins/plugins.service.ts +147 -147
  222. package/server/modules/plugins/tests/plugins.service.test.ts +32 -32
  223. package/server/modules/projects/index.ts +11 -11
  224. package/server/modules/projects/projects.routes.ts +273 -273
  225. package/server/modules/projects/services/project-clone.service.ts +323 -323
  226. package/server/modules/projects/services/project-delete.service.ts +90 -90
  227. package/server/modules/projects/services/project-management.service.ts +144 -144
  228. package/server/modules/projects/services/project-star.service.ts +78 -78
  229. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -248
  230. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +314 -314
  231. package/server/modules/projects/tests/project-clone.service.test.ts +183 -183
  232. package/server/modules/projects/tests/project-management.service.test.ts +117 -117
  233. package/server/modules/projects/tests/project-star.service.test.ts +123 -123
  234. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -105
  235. package/server/modules/providers/README.md +380 -380
  236. package/server/modules/providers/index.ts +10 -10
  237. package/server/modules/providers/list/claude/claude-auth.provider.ts +160 -160
  238. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  239. package/server/modules/providers/list/claude/claude-models.provider.ts +294 -294
  240. package/server/modules/providers/list/claude/claude-runtime.provider.js +1068 -1068
  241. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +203 -203
  242. package/server/modules/providers/list/claude/claude-sessions.provider.ts +684 -684
  243. package/server/modules/providers/list/claude/claude-skills.provider.ts +265 -265
  244. package/server/modules/providers/list/claude/claude.provider.ts +30 -30
  245. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -100
  246. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  247. package/server/modules/providers/list/codex/codex-models.provider.ts +123 -123
  248. package/server/modules/providers/list/codex/codex-runtime.provider.js +533 -533
  249. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +203 -203
  250. package/server/modules/providers/list/codex/codex-sessions.provider.ts +938 -938
  251. package/server/modules/providers/list/codex/codex-skills.provider.ts +73 -73
  252. package/server/modules/providers/list/codex/codex.provider.ts +28 -28
  253. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  254. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  255. package/server/modules/providers/list/cursor/cursor-models.provider.ts +119 -119
  256. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +385 -385
  257. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +159 -159
  258. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +654 -654
  259. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +39 -39
  260. package/server/modules/providers/list/cursor/cursor.provider.ts +30 -30
  261. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +110 -110
  262. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -228
  263. package/server/modules/providers/list/opencode/opencode-models.provider.ts +203 -203
  264. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +434 -434
  265. package/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +261 -261
  266. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +179 -179
  267. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +500 -500
  268. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -78
  269. package/server/modules/providers/list/opencode/opencode.provider.ts +30 -30
  270. package/server/modules/providers/provider.registry.ts +36 -36
  271. package/server/modules/providers/provider.routes.ts +877 -877
  272. package/server/modules/providers/services/mcp.service.ts +109 -109
  273. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  274. package/server/modules/providers/services/provider-capabilities.service.ts +97 -97
  275. package/server/modules/providers/services/provider-models.service.ts +401 -401
  276. package/server/modules/providers/services/provider-runtime.service.ts +108 -108
  277. package/server/modules/providers/services/provider-token-usage.service.ts +357 -357
  278. package/server/modules/providers/services/session-conversations-search.service.ts +1321 -1321
  279. package/server/modules/providers/services/session-synchronizer.service.ts +74 -74
  280. package/server/modules/providers/services/sessions-watcher.service.ts +247 -247
  281. package/server/modules/providers/services/sessions.service.ts +471 -471
  282. package/server/modules/providers/services/skills.service.ts +39 -39
  283. package/server/modules/providers/shared/base/abstract.provider.ts +32 -32
  284. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  285. package/server/modules/providers/shared/skills/skills.provider.ts +287 -287
  286. package/server/modules/providers/tests/claude-auth.test.ts +150 -150
  287. package/server/modules/providers/tests/claude-sessions.test.ts +61 -61
  288. package/server/modules/providers/tests/codex-sessions.test.ts +231 -231
  289. package/server/modules/providers/tests/mcp.test.ts +349 -349
  290. package/server/modules/providers/tests/opencode-models.test.ts +41 -41
  291. package/server/modules/providers/tests/opencode-sessions.test.ts +522 -522
  292. package/server/modules/providers/tests/provider-attachment-history.test.ts +247 -247
  293. package/server/modules/providers/tests/provider-models.service.test.ts +353 -353
  294. package/server/modules/providers/tests/provider-runtime.service.test.ts +138 -138
  295. package/server/modules/providers/tests/provider-token-usage.service.test.ts +186 -186
  296. package/server/modules/providers/tests/provider.routes.test.ts +244 -244
  297. package/server/modules/providers/tests/sessions-details.test.ts +73 -73
  298. package/server/modules/providers/tests/sessions.service.test.ts +127 -127
  299. package/server/modules/providers/tests/skills.test.ts +692 -692
  300. package/server/modules/settings/index.ts +2 -2
  301. package/server/modules/settings/settings.module.ts +50 -50
  302. package/server/modules/settings/settings.routes.ts +51 -51
  303. package/server/modules/settings/settings.service.ts +186 -186
  304. package/server/modules/settings/tests/settings.service.test.ts +54 -54
  305. package/server/modules/system/index.ts +2 -2
  306. package/server/modules/system/system.module.ts +62 -62
  307. package/server/modules/system/system.routes.ts +21 -21
  308. package/server/modules/system/system.service.ts +76 -76
  309. package/server/modules/system/tests/system.service.test.ts +118 -118
  310. package/server/modules/taskmaster/index.ts +2 -2
  311. package/server/modules/taskmaster/taskmaster.module.ts +24 -24
  312. package/server/modules/taskmaster/taskmaster.routes.ts +1482 -1482
  313. package/server/modules/taskmaster/taskmaster.service.ts +132 -132
  314. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +144 -144
  315. package/server/modules/taskmaster/tests/taskmaster.service.test.ts +97 -97
  316. package/server/modules/user/index.ts +2 -2
  317. package/server/modules/user/tests/user.service.test.ts +61 -61
  318. package/server/modules/user/user.module.ts +58 -58
  319. package/server/modules/user/user.routes.ts +50 -50
  320. package/server/modules/user/user.service.ts +85 -85
  321. package/server/modules/voice/index.ts +2 -2
  322. package/server/modules/voice/tests/voice.service.test.ts +98 -98
  323. package/server/modules/voice/voice.module.ts +47 -47
  324. package/server/modules/voice/voice.routes.ts +113 -113
  325. package/server/modules/voice/voice.service.ts +192 -192
  326. package/server/modules/websocket/README.md +273 -273
  327. package/server/modules/websocket/index.ts +3 -3
  328. package/server/modules/websocket/services/chat-run-registry.service.ts +343 -343
  329. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -145
  330. package/server/modules/websocket/services/chat-websocket.service.ts +425 -425
  331. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -65
  332. package/server/modules/websocket/services/shell-websocket.service.ts +502 -502
  333. package/server/modules/websocket/services/websocket-auth.service.ts +59 -59
  334. package/server/modules/websocket/services/websocket-server.service.ts +124 -124
  335. package/server/modules/websocket/services/websocket-state.service.ts +16 -16
  336. package/server/modules/websocket/services/websocket-writer.service.ts +38 -38
  337. package/server/modules/websocket/tests/chat-attachment-filter.test.ts +59 -59
  338. package/server/modules/websocket/tests/chat-run-registry.test.ts +282 -282
  339. package/server/modules/websocket/tests/shell-websocket.service.test.ts +100 -100
  340. package/server/modules/websocket/tests/websocket-heartbeat.service.test.ts +78 -78
  341. package/server/modules/worktrees/index.ts +2 -2
  342. package/server/modules/worktrees/services/worktree-create-and-open.service.ts +53 -53
  343. package/server/modules/worktrees/services/worktree-create.service.ts +104 -104
  344. package/server/modules/worktrees/services/worktree-git.service.ts +207 -207
  345. package/server/modules/worktrees/services/worktree-list.service.ts +143 -143
  346. package/server/modules/worktrees/services/worktree-merge.service.ts +157 -157
  347. package/server/modules/worktrees/services/worktree-open.service.ts +85 -85
  348. package/server/modules/worktrees/services/worktree-remove.service.ts +88 -88
  349. package/server/modules/worktrees/tests/worktree-create-and-open.service.test.ts +68 -68
  350. package/server/modules/worktrees/tests/worktree-create.service.test.ts +142 -142
  351. package/server/modules/worktrees/tests/worktree-git.service.test.ts +84 -84
  352. package/server/modules/worktrees/tests/worktree-list.service.test.ts +45 -45
  353. package/server/modules/worktrees/tests/worktree-merge.service.test.ts +213 -213
  354. package/server/modules/worktrees/tests/worktree-open.service.test.ts +122 -122
  355. package/server/modules/worktrees/tests/worktree-remove.service.test.ts +227 -227
  356. package/server/modules/worktrees/tests/worktrees.routes.test.ts +199 -199
  357. package/server/modules/worktrees/worktrees.module.ts +113 -113
  358. package/server/modules/worktrees/worktrees.routes.ts +116 -116
  359. package/server/shared/claude-cli-path.ts +139 -139
  360. package/server/shared/frontmatter.ts +18 -18
  361. package/server/shared/image-attachments.ts +435 -435
  362. package/server/shared/interfaces.ts +179 -179
  363. package/server/shared/tests/claude-cli-path.test.ts +61 -61
  364. package/server/shared/tests/image-attachments.test.ts +353 -353
  365. package/server/shared/tests/slice-tail-page.test.ts +42 -42
  366. package/server/shared/types.ts +1226 -1226
  367. package/server/shared/utils.ts +1161 -1161
  368. package/server/tsconfig.json +40 -40
  369. package/shared/networkHosts.js +22 -22
  370. package/dist/assets/channel-Bm0ZLE4z.js +0 -1
  371. package/dist/assets/classDiagram-JCYQIIEL-DqEOcNxw.js +0 -1
  372. package/dist/assets/classDiagram-v2-OCEON4UE-DqEOcNxw.js +0 -1
  373. package/dist/assets/stateDiagram-v2-UEYNNEHI-DARLpEFJ.js +0 -1
  374. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CrgDgaY8.js +0 -8
@@ -1,153 +1,153 @@
1
1
  import fs from 'node:fs';
2
2
  import crypto from 'node:crypto';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- import pty, { type IPty } from 'node-pty';
7
- import { WebSocket, type RawData } from 'ws';
8
-
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import pty, { type IPty } from 'node-pty';
7
+ import { WebSocket, type RawData } from 'ws';
8
+
9
9
  import { parseIncomingJsonObject } from '@/shared/utils.js';
10
10
  import { terminalReplayStore } from '@/modules/websocket/services/terminal-replay-store.js';
11
-
12
- type ShellIncomingMessage = {
13
- type?: string;
14
- data?: string;
15
- cols?: number;
16
- rows?: number;
17
- projectPath?: string;
18
- sessionId?: string;
19
- hasSession?: boolean;
20
- provider?: string;
21
- initialCommand?: string;
22
- isPlainShell?: boolean;
11
+
12
+ type ShellIncomingMessage = {
13
+ type?: string;
14
+ data?: string;
15
+ cols?: number;
16
+ rows?: number;
17
+ projectPath?: string;
18
+ sessionId?: string;
19
+ hasSession?: boolean;
20
+ provider?: string;
21
+ initialCommand?: string;
22
+ isPlainShell?: boolean;
23
23
  forceRestart?: boolean;
24
24
  shellMode?: string;
25
25
  };
26
-
27
- type PtySessionEntry = {
26
+
27
+ type PtySessionEntry = {
28
28
  pty: IPty;
29
29
  ws: WebSocket | null;
30
30
  timeoutId: NodeJS.Timeout | null;
31
- projectPath: string;
32
- sessionId: string | null;
33
- };
34
-
35
- const ptySessionsMap = new Map<string, PtySessionEntry>();
36
- const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
37
- const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
38
- const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
31
+ projectPath: string;
32
+ sessionId: string | null;
33
+ };
34
+
35
+ const ptySessionsMap = new Map<string, PtySessionEntry>();
36
+ const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
37
+ const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
38
+ const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
39
39
  const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
40
40
  const TERMINAL_REPLAY_TRUNCATED_MESSAGE =
41
41
  '\x1b[33m[Earlier terminal output was omitted to limit memory usage]\x1b[0m\r\n';
42
-
43
- function stripAnsiSequences(value: string): string {
44
- return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
45
- }
46
-
47
- function normalizeDetectedUrl(url: string): string | null {
48
- const cleanedUrl = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
49
- if (!cleanedUrl) {
50
- return null;
51
- }
52
-
53
- try {
54
- const parsedUrl = new URL(cleanedUrl);
55
- if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
56
- return null;
57
- }
58
- return parsedUrl.toString();
59
- } catch {
60
- return null;
61
- }
62
- }
63
-
64
- function extractUrlsFromText(value: string): string[] {
65
- const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) ?? [];
66
-
67
- // Terminal width can split a URL across lines, so valid URL characters on
68
- // immediately following lines are joined before the URL is validated.
69
- const wrappedMatches: string[] = [];
70
- const urlContinuationPattern = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
71
- const lines = value.split(/\r?\n/);
72
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
73
- const line = lines[lineIndex].trim();
74
- const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
75
- if (!startMatch) {
76
- continue;
77
- }
78
-
79
- let combinedUrl = startMatch[0];
80
- let continuationIndex = lineIndex + 1;
81
- while (continuationIndex < lines.length) {
82
- const continuation = lines[continuationIndex].trim();
83
- if (!continuation || !urlContinuationPattern.test(continuation)) {
84
- break;
85
- }
86
- combinedUrl += continuation;
87
- continuationIndex += 1;
88
- }
89
-
90
- wrappedMatches.push(combinedUrl);
91
- }
92
-
93
- return Array.from(new Set([...directMatches, ...wrappedMatches]));
94
- }
95
-
96
- function shouldAutoOpenUrlFromOutput(value: string): boolean {
97
- const normalizedOutput = value.toLowerCase();
98
- return (
99
- normalizedOutput.includes("browser didn't open") ||
100
- normalizedOutput.includes('open this url') ||
101
- normalizedOutput.includes('continue in your browser') ||
102
- normalizedOutput.includes('press enter to open') ||
103
- normalizedOutput.includes('open_url:')
104
- );
105
- }
106
-
42
+
43
+ function stripAnsiSequences(value: string): string {
44
+ return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
45
+ }
46
+
47
+ function normalizeDetectedUrl(url: string): string | null {
48
+ const cleanedUrl = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
49
+ if (!cleanedUrl) {
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ const parsedUrl = new URL(cleanedUrl);
55
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
56
+ return null;
57
+ }
58
+ return parsedUrl.toString();
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function extractUrlsFromText(value: string): string[] {
65
+ const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) ?? [];
66
+
67
+ // Terminal width can split a URL across lines, so valid URL characters on
68
+ // immediately following lines are joined before the URL is validated.
69
+ const wrappedMatches: string[] = [];
70
+ const urlContinuationPattern = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
71
+ const lines = value.split(/\r?\n/);
72
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
73
+ const line = lines[lineIndex].trim();
74
+ const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
75
+ if (!startMatch) {
76
+ continue;
77
+ }
78
+
79
+ let combinedUrl = startMatch[0];
80
+ let continuationIndex = lineIndex + 1;
81
+ while (continuationIndex < lines.length) {
82
+ const continuation = lines[continuationIndex].trim();
83
+ if (!continuation || !urlContinuationPattern.test(continuation)) {
84
+ break;
85
+ }
86
+ combinedUrl += continuation;
87
+ continuationIndex += 1;
88
+ }
89
+
90
+ wrappedMatches.push(combinedUrl);
91
+ }
92
+
93
+ return Array.from(new Set([...directMatches, ...wrappedMatches]));
94
+ }
95
+
96
+ function shouldAutoOpenUrlFromOutput(value: string): boolean {
97
+ const normalizedOutput = value.toLowerCase();
98
+ return (
99
+ normalizedOutput.includes("browser didn't open") ||
100
+ normalizedOutput.includes('open this url') ||
101
+ normalizedOutput.includes('continue in your browser') ||
102
+ normalizedOutput.includes('press enter to open') ||
103
+ normalizedOutput.includes('open_url:')
104
+ );
105
+ }
106
+
107
107
  type ShellWebSocketDependencies = {
108
- resolveProviderSessionId: (
109
- sessionId: string,
110
- provider: string,
108
+ resolveProviderSessionId: (
109
+ sessionId: string,
110
+ provider: string,
111
111
  ) => string | null | undefined;
112
112
  spawnPty?: typeof pty.spawn;
113
113
  platform?: () => NodeJS.Platform;
114
114
  environment?: NodeJS.ProcessEnv;
115
115
  };
116
-
117
- /**
118
- * Reads a string field from untyped payloads and falls back when absent.
119
- */
120
- function readString(value: unknown, fallback = ''): string {
121
- return typeof value === 'string' ? value : fallback;
122
- }
123
-
124
- /**
125
- * Reads a boolean field from untyped payloads and falls back when absent.
126
- */
127
- function readBoolean(value: unknown, fallback = false): boolean {
128
- return typeof value === 'boolean' ? value : fallback;
129
- }
130
-
131
- /**
132
- * Reads a finite number field from untyped payloads and falls back when absent.
133
- */
134
- function readNumber(value: unknown, fallback: number): number {
135
- return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
136
- }
137
-
138
- /**
139
- * Parses incoming websocket shell messages and keeps processing safe when
140
- * malformed payloads are received.
141
- */
142
- function parseShellMessage(rawMessage: RawData): ShellIncomingMessage | null {
143
- const payload = parseIncomingJsonObject(rawMessage);
144
- if (!payload) {
145
- return null;
146
- }
147
-
148
- return payload as ShellIncomingMessage;
149
- }
150
-
116
+
117
+ /**
118
+ * Reads a string field from untyped payloads and falls back when absent.
119
+ */
120
+ function readString(value: unknown, fallback = ''): string {
121
+ return typeof value === 'string' ? value : fallback;
122
+ }
123
+
124
+ /**
125
+ * Reads a boolean field from untyped payloads and falls back when absent.
126
+ */
127
+ function readBoolean(value: unknown, fallback = false): boolean {
128
+ return typeof value === 'boolean' ? value : fallback;
129
+ }
130
+
131
+ /**
132
+ * Reads a finite number field from untyped payloads and falls back when absent.
133
+ */
134
+ function readNumber(value: unknown, fallback: number): number {
135
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
136
+ }
137
+
138
+ /**
139
+ * Parses incoming websocket shell messages and keeps processing safe when
140
+ * malformed payloads are received.
141
+ */
142
+ function parseShellMessage(rawMessage: RawData): ShellIncomingMessage | null {
143
+ const payload = parseIncomingJsonObject(rawMessage);
144
+ if (!payload) {
145
+ return null;
146
+ }
147
+
148
+ return payload as ShellIncomingMessage;
149
+ }
150
+
151
151
  const SAFE_SESSION_ID_PATTERN = /^[a-zA-Z0-9_.\-:]+$/;
152
152
  const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
153
153
  const MAX_TERMINAL_BINARY_INPUT_BYTES = 1024;
@@ -158,63 +158,63 @@ const HERDR_PANE_ENVIRONMENT_KEYS = [
158
158
  'HERDR_TAB_ID',
159
159
  'HERDR_WORKSPACE_ID',
160
160
  ] as const;
161
-
162
- function resolveResumeSessionId(
163
- message: ShellIncomingMessage,
164
- dependencies: ShellWebSocketDependencies
165
- ): string {
166
- const hasSession = readBoolean(message.hasSession);
167
- const sessionId = readString(message.sessionId);
168
- const provider = readString(message.provider, 'claude');
169
-
170
- if (!hasSession || !sessionId) {
171
- return '';
172
- }
173
-
174
- let resumeSessionId: string | null | undefined;
175
- try {
176
- resumeSessionId = dependencies.resolveProviderSessionId(sessionId, provider);
177
- } catch (error) {
178
- console.error('Failed to resolve provider session ID:', error);
179
- resumeSessionId = undefined;
180
- }
181
-
182
- const resolvedSessionId = resumeSessionId === undefined ? sessionId : resumeSessionId;
183
- if (!resolvedSessionId || !SAFE_SESSION_ID_PATTERN.test(resolvedSessionId)) {
184
- return '';
185
- }
186
-
187
- return resolvedSessionId;
188
- }
189
-
190
- /**
191
- * Resolves provider command line for plain shell and agent-backed shell modes.
192
- */
161
+
162
+ function resolveResumeSessionId(
163
+ message: ShellIncomingMessage,
164
+ dependencies: ShellWebSocketDependencies
165
+ ): string {
166
+ const hasSession = readBoolean(message.hasSession);
167
+ const sessionId = readString(message.sessionId);
168
+ const provider = readString(message.provider, 'claude');
169
+
170
+ if (!hasSession || !sessionId) {
171
+ return '';
172
+ }
173
+
174
+ let resumeSessionId: string | null | undefined;
175
+ try {
176
+ resumeSessionId = dependencies.resolveProviderSessionId(sessionId, provider);
177
+ } catch (error) {
178
+ console.error('Failed to resolve provider session ID:', error);
179
+ resumeSessionId = undefined;
180
+ }
181
+
182
+ const resolvedSessionId = resumeSessionId === undefined ? sessionId : resumeSessionId;
183
+ if (!resolvedSessionId || !SAFE_SESSION_ID_PATTERN.test(resolvedSessionId)) {
184
+ return '';
185
+ }
186
+
187
+ return resolvedSessionId;
188
+ }
189
+
190
+ /**
191
+ * Resolves provider command line for plain shell and agent-backed shell modes.
192
+ */
193
193
  function buildShellCommand(
194
194
  message: ShellIncomingMessage,
195
195
  dependencies: ShellWebSocketDependencies
196
196
  ): string {
197
- const hasSession = readBoolean(message.hasSession);
198
- const initialCommand = readString(message.initialCommand);
197
+ const hasSession = readBoolean(message.hasSession);
198
+ const initialCommand = readString(message.initialCommand);
199
199
  const provider = readString(message.provider, 'claude');
200
200
  const resumeSessionId = resolveResumeSessionId(message, dependencies);
201
201
  const platform = dependencies.platform?.() ?? os.platform();
202
- const isPlainShell =
203
- readBoolean(message.isPlainShell) ||
204
- (!!initialCommand && !hasSession) ||
205
- provider === 'plain-shell';
206
-
207
- if (isPlainShell) {
208
- return initialCommand;
209
- }
210
-
211
- if (provider === 'cursor') {
212
- if (resumeSessionId) {
213
- return `cursor-agent --resume="${resumeSessionId}"`;
214
- }
215
- return 'cursor-agent';
216
- }
217
-
202
+ const isPlainShell =
203
+ readBoolean(message.isPlainShell) ||
204
+ (!!initialCommand && !hasSession) ||
205
+ provider === 'plain-shell';
206
+
207
+ if (isPlainShell) {
208
+ return initialCommand;
209
+ }
210
+
211
+ if (provider === 'cursor') {
212
+ if (resumeSessionId) {
213
+ return `cursor-agent --resume="${resumeSessionId}"`;
214
+ }
215
+ return 'cursor-agent';
216
+ }
217
+
218
218
  if (provider === 'codex') {
219
219
  const codexCommand = platform === 'win32' ? 'codex.cmd' : 'codex';
220
220
  if (resumeSessionId) {
@@ -224,78 +224,78 @@ function buildShellCommand(
224
224
  return `${codexCommand} resume "${resumeSessionId}" || ${codexCommand}`;
225
225
  }
226
226
  return codexCommand;
227
- }
228
-
229
- if (provider === 'opencode') {
230
- if (resumeSessionId) {
231
- return `opencode --session "${resumeSessionId}"`;
232
- }
233
- return initialCommand || 'opencode';
234
- }
235
-
236
- const command = initialCommand || 'claude';
237
- if (resumeSessionId) {
227
+ }
228
+
229
+ if (provider === 'opencode') {
230
+ if (resumeSessionId) {
231
+ return `opencode --session "${resumeSessionId}"`;
232
+ }
233
+ return initialCommand || 'opencode';
234
+ }
235
+
236
+ const command = initialCommand || 'claude';
237
+ if (resumeSessionId) {
238
238
  if (platform === 'win32') {
239
- return `claude --resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
240
- }
241
- return `claude --resume "${resumeSessionId}" || claude`;
242
- }
243
- return command;
244
- }
245
-
246
- function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
247
- const resolvedKey = Object.keys(env).find((envKey) => envKey.toLowerCase() === key.toLowerCase());
248
- return resolvedKey ? env[resolvedKey] : undefined;
249
- }
250
-
251
- function getPathEnvKey(env: NodeJS.ProcessEnv): string {
252
- return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
253
- }
254
-
239
+ return `claude --resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
240
+ }
241
+ return `claude --resume "${resumeSessionId}" || claude`;
242
+ }
243
+ return command;
244
+ }
245
+
246
+ function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
247
+ const resolvedKey = Object.keys(env).find((envKey) => envKey.toLowerCase() === key.toLowerCase());
248
+ return resolvedKey ? env[resolvedKey] : undefined;
249
+ }
250
+
251
+ function getPathEnvKey(env: NodeJS.ProcessEnv): string {
252
+ return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
253
+ }
254
+
255
255
  function prioritizeUserNpmGlobalBin(env: NodeJS.ProcessEnv): { key: string; value: string | undefined } {
256
- const pathKey = getPathEnvKey(env);
257
- const currentPath = env[pathKey];
258
- if (!currentPath) {
259
- return { key: pathKey, value: currentPath };
260
- }
261
-
262
- const delimiter = path.delimiter;
263
- const pathEntries = currentPath.split(delimiter).filter(Boolean);
264
- const npmPrefix = readEnvValue(env, 'npm_config_prefix');
265
- const appData = readEnvValue(env, 'APPDATA');
266
- const candidates = [
267
- npmPrefix || '',
268
- npmPrefix ? path.join(npmPrefix, 'bin') : '',
269
- appData ? path.join(appData, 'npm') : '',
270
- path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
271
- path.join(os.homedir(), '.npm-global', 'bin'),
272
- ].filter(Boolean);
273
-
274
- const normalizedPathEntries = pathEntries.map((entry) => os.platform() === 'win32' ? entry.toLowerCase() : entry);
275
- const preferredEntries = candidates.filter((candidate, index) => {
276
- const normalizedCandidate = os.platform() === 'win32' ? candidate.toLowerCase() : candidate;
277
- return (
278
- candidates.indexOf(candidate) === index &&
279
- normalizedPathEntries.includes(normalizedCandidate)
280
- );
281
- });
282
-
283
- if (preferredEntries.length === 0) {
284
- return { key: pathKey, value: currentPath };
285
- }
286
-
287
- const normalizedPreferredEntries = preferredEntries.map((entry) =>
288
- os.platform() === 'win32' ? entry.toLowerCase() : entry
289
- );
290
-
291
- const value = [
292
- ...preferredEntries,
293
- ...pathEntries.filter((entry) => {
294
- const normalizedEntry = os.platform() === 'win32' ? entry.toLowerCase() : entry;
295
- return !normalizedPreferredEntries.includes(normalizedEntry);
296
- }),
297
- ].join(delimiter);
298
-
256
+ const pathKey = getPathEnvKey(env);
257
+ const currentPath = env[pathKey];
258
+ if (!currentPath) {
259
+ return { key: pathKey, value: currentPath };
260
+ }
261
+
262
+ const delimiter = path.delimiter;
263
+ const pathEntries = currentPath.split(delimiter).filter(Boolean);
264
+ const npmPrefix = readEnvValue(env, 'npm_config_prefix');
265
+ const appData = readEnvValue(env, 'APPDATA');
266
+ const candidates = [
267
+ npmPrefix || '',
268
+ npmPrefix ? path.join(npmPrefix, 'bin') : '',
269
+ appData ? path.join(appData, 'npm') : '',
270
+ path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
271
+ path.join(os.homedir(), '.npm-global', 'bin'),
272
+ ].filter(Boolean);
273
+
274
+ const normalizedPathEntries = pathEntries.map((entry) => os.platform() === 'win32' ? entry.toLowerCase() : entry);
275
+ const preferredEntries = candidates.filter((candidate, index) => {
276
+ const normalizedCandidate = os.platform() === 'win32' ? candidate.toLowerCase() : candidate;
277
+ return (
278
+ candidates.indexOf(candidate) === index &&
279
+ normalizedPathEntries.includes(normalizedCandidate)
280
+ );
281
+ });
282
+
283
+ if (preferredEntries.length === 0) {
284
+ return { key: pathKey, value: currentPath };
285
+ }
286
+
287
+ const normalizedPreferredEntries = preferredEntries.map((entry) =>
288
+ os.platform() === 'win32' ? entry.toLowerCase() : entry
289
+ );
290
+
291
+ const value = [
292
+ ...preferredEntries,
293
+ ...pathEntries.filter((entry) => {
294
+ const normalizedEntry = os.platform() === 'win32' ? entry.toLowerCase() : entry;
295
+ return !normalizedPreferredEntries.includes(normalizedEntry);
296
+ }),
297
+ ].join(delimiter);
298
+
299
299
  return { key: pathKey, value };
300
300
  }
301
301
 
@@ -376,53 +376,53 @@ function buildPtySessionKey({
376
376
  });
377
377
  return `pty:${crypto.createHash('sha256').update(identity).digest('hex')}`;
378
378
  }
379
-
380
- /**
381
- * Used by this module's websocket gateway to connect the standalone Shell UI
382
- * to a retained PTY while keeping process lifecycle ownership on the server.
383
- */
384
- export function handleShellConnection(
385
- ws: WebSocket,
386
- dependencies: ShellWebSocketDependencies
387
- ): void {
388
- console.log('[INFO] Shell websocket connected');
389
-
379
+
380
+ /**
381
+ * Used by this module's websocket gateway to connect the standalone Shell UI
382
+ * to a retained PTY while keeping process lifecycle ownership on the server.
383
+ */
384
+ export function handleShellConnection(
385
+ ws: WebSocket,
386
+ dependencies: ShellWebSocketDependencies
387
+ ): void {
388
+ console.log('[INFO] Shell websocket connected');
389
+
390
390
  let shellProcess: IPty | null = null;
391
391
  let ptySessionKey: string | null = null;
392
392
  let isHerdrConnection = false;
393
- let urlDetectionBuffer = '';
394
- const announcedAuthUrls = new Set<string>();
395
-
396
- ws.on('message', async (rawMessage) => {
397
- try {
398
- const data = parseShellMessage(rawMessage);
399
- if (!data?.type) {
400
- throw new Error('Invalid websocket payload');
401
- }
402
-
403
- if (data.type === 'init') {
404
- const projectPath = readString(data.projectPath, process.cwd());
405
- const sessionId = readString(data.sessionId) || null;
406
- const hasSession = readBoolean(data.hasSession);
407
- const provider = readString(data.provider, 'claude');
408
- const initialCommand = readString(data.initialCommand);
393
+ let urlDetectionBuffer = '';
394
+ const announcedAuthUrls = new Set<string>();
395
+
396
+ ws.on('message', async (rawMessage) => {
397
+ try {
398
+ const data = parseShellMessage(rawMessage);
399
+ if (!data?.type) {
400
+ throw new Error('Invalid websocket payload');
401
+ }
402
+
403
+ if (data.type === 'init') {
404
+ const projectPath = readString(data.projectPath, process.cwd());
405
+ const sessionId = readString(data.sessionId) || null;
406
+ const hasSession = readBoolean(data.hasSession);
407
+ const provider = readString(data.provider, 'claude');
408
+ const initialCommand = readString(data.initialCommand);
409
409
  const forceRestart = readBoolean(data.forceRestart);
410
410
  const isHerdrMode = readString(data.shellMode) === 'herdr';
411
411
  isHerdrConnection = isHerdrMode;
412
412
  const isPlainShell =
413
- readBoolean(data.isPlainShell) ||
414
- (!!initialCommand && !hasSession) ||
415
- provider === 'plain-shell';
416
-
417
- urlDetectionBuffer = '';
418
- announcedAuthUrls.clear();
419
-
420
- const isLoginCommand =
421
- !!initialCommand &&
422
- (initialCommand.includes('setup-token') ||
423
- initialCommand.includes('cursor-agent login') ||
424
- initialCommand.includes('auth login'));
425
-
413
+ readBoolean(data.isPlainShell) ||
414
+ (!!initialCommand && !hasSession) ||
415
+ provider === 'plain-shell';
416
+
417
+ urlDetectionBuffer = '';
418
+ announcedAuthUrls.clear();
419
+
420
+ const isLoginCommand =
421
+ !!initialCommand &&
422
+ (initialCommand.includes('setup-token') ||
423
+ initialCommand.includes('cursor-agent login') ||
424
+ initialCommand.includes('auth login'));
425
+
426
426
  const resolvedProjectPath = path.resolve(projectPath);
427
427
  try {
428
428
  const stats = fs.statSync(resolvedProjectPath);
@@ -446,25 +446,25 @@ export function handleShellConnection(
446
446
  shellMode: readString(data.shellMode),
447
447
  initialCommand: isPlainShell ? initialCommand : '',
448
448
  });
449
-
450
- if (isLoginCommand || forceRestart) {
451
- const oldSession = ptySessionsMap.get(ptySessionKey);
452
- if (oldSession) {
453
- if (oldSession.timeoutId) {
454
- clearTimeout(oldSession.timeoutId);
449
+
450
+ if (isLoginCommand || forceRestart) {
451
+ const oldSession = ptySessionsMap.get(ptySessionKey);
452
+ if (oldSession) {
453
+ if (oldSession.timeoutId) {
454
+ clearTimeout(oldSession.timeoutId);
455
455
  }
456
456
  terminalReplayStore.delete(ptySessionKey);
457
457
  oldSession.pty.kill();
458
- ptySessionsMap.delete(ptySessionKey);
459
- }
460
- }
461
-
462
- const existingSession =
463
- isLoginCommand || forceRestart ? null : ptySessionsMap.get(ptySessionKey);
458
+ ptySessionsMap.delete(ptySessionKey);
459
+ }
460
+ }
461
+
462
+ const existingSession =
463
+ isLoginCommand || forceRestart ? null : ptySessionsMap.get(ptySessionKey);
464
464
  if (existingSession) {
465
465
  shellProcess = existingSession.pty;
466
- if (existingSession.timeoutId) {
467
- clearTimeout(existingSession.timeoutId);
466
+ if (existingSession.timeoutId) {
467
+ clearTimeout(existingSession.timeoutId);
468
468
  existingSession.timeoutId = null;
469
469
  }
470
470
 
@@ -487,31 +487,31 @@ export function handleShellConnection(
487
487
  }
488
488
 
489
489
  ws.send(
490
- JSON.stringify({
491
- type: 'output',
492
- data: '\x1b[36m[Reconnected to existing session]\x1b[0m\r\n',
493
- })
494
- );
495
-
490
+ JSON.stringify({
491
+ type: 'output',
492
+ data: '\x1b[36m[Reconnected to existing session]\x1b[0m\r\n',
493
+ })
494
+ );
495
+
496
496
  const replay = terminalReplayStore.snapshot(ptySessionKey);
497
497
  if (replay.truncated) {
498
498
  ws.send(JSON.stringify({ type: 'output', data: TERMINAL_REPLAY_TRUNCATED_MESSAGE }));
499
499
  }
500
500
  if (replay.chunks.length > 0) {
501
501
  replay.chunks.forEach((bufferedData) => {
502
- ws.send(
503
- JSON.stringify({
504
- type: 'output',
505
- data: bufferedData,
506
- })
507
- );
508
- });
509
- }
510
-
502
+ ws.send(
503
+ JSON.stringify({
504
+ type: 'output',
505
+ data: bufferedData,
506
+ })
507
+ );
508
+ });
509
+ }
510
+
511
511
  existingSession.pty.resize(reconnectCols, reconnectRows);
512
512
  return;
513
513
  }
514
-
514
+
515
515
  const shellCommand = buildShellCommand(data, dependencies);
516
516
  const resumeSessionId = resolveResumeSessionId(data, dependencies);
517
517
  const platform = dependencies.platform?.() ?? os.platform();
@@ -528,153 +528,153 @@ export function handleShellConnection(
528
528
  : platform === 'win32'
529
529
  ? ['-Command', shellCommand]
530
530
  : ['-c', shellCommand];
531
- const termCols = readNumber(data.cols, 80);
532
- const termRows = readNumber(data.rows, 24);
531
+ const termCols = readNumber(data.cols, 80);
532
+ const termRows = readNumber(data.rows, 24);
533
533
  const shellEnvironment = buildShellEnvironment(data, dependencies);
534
-
535
- shellProcess = (dependencies.spawnPty ?? pty.spawn)(shell, shellArgs, {
536
- name: 'xterm-256color',
537
- cols: termCols,
538
- rows: termRows,
539
- cwd: resolvedProjectPath,
534
+
535
+ shellProcess = (dependencies.spawnPty ?? pty.spawn)(shell, shellArgs, {
536
+ name: 'xterm-256color',
537
+ cols: termCols,
538
+ rows: termRows,
539
+ cwd: resolvedProjectPath,
540
540
  env: shellEnvironment,
541
- });
542
-
541
+ });
542
+
543
543
  ptySessionsMap.set(ptySessionKey, {
544
544
  pty: shellProcess,
545
545
  ws,
546
546
  timeoutId: null,
547
- projectPath,
548
- sessionId,
549
- });
550
-
551
- shellProcess.onData((chunk) => {
552
- if (!ptySessionKey) {
553
- return;
554
- }
555
-
556
- const session = ptySessionsMap.get(ptySessionKey);
557
- if (!session) {
558
- return;
559
- }
560
-
547
+ projectPath,
548
+ sessionId,
549
+ });
550
+
551
+ shellProcess.onData((chunk) => {
552
+ if (!ptySessionKey) {
553
+ return;
554
+ }
555
+
556
+ const session = ptySessionsMap.get(ptySessionKey);
557
+ if (!session) {
558
+ return;
559
+ }
560
+
561
561
  if (!isHerdrMode) {
562
562
  terminalReplayStore.append(ptySessionKey, chunk);
563
- }
564
-
565
- if (session.ws && session.ws.readyState === WebSocket.OPEN) {
566
- let outputData = chunk;
567
- const cleanChunk = stripAnsiSequences(chunk);
568
- urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
569
-
570
- outputData = outputData.replace(
571
- /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
572
- '[INFO] Opening in browser: $1'
573
- );
574
-
575
- const emitAuthUrl = (detectedUrl: string, autoOpen = false) => {
576
- const normalizedUrl = normalizeDetectedUrl(detectedUrl);
577
- if (!normalizedUrl) {
578
- return;
579
- }
580
-
581
- const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
582
- if (isNewUrl) {
583
- announcedAuthUrls.add(normalizedUrl);
584
- session.ws?.send(
585
- JSON.stringify({
586
- type: 'auth_url',
587
- url: normalizedUrl,
588
- autoOpen,
589
- })
590
- );
591
- }
592
- };
593
-
594
- const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
595
- .map((url) => normalizeDetectedUrl(url))
596
- .filter((url): url is string => Boolean(url));
597
-
598
- const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter(
599
- (url, _, urls) =>
600
- !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
601
- );
602
-
603
- dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
604
-
605
- if (
606
- shouldAutoOpenUrlFromOutput(cleanChunk) &&
607
- dedupedDetectedUrls.length > 0
608
- ) {
609
- const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
610
- current.length > longest.length ? current : longest
611
- );
612
- emitAuthUrl(bestUrl, true);
613
- }
614
-
615
- session.ws.send(
616
- JSON.stringify({
617
- type: 'output',
618
- data: outputData,
619
- })
620
- );
621
- }
622
- });
623
-
624
- shellProcess.onExit((exitCode) => {
625
- if (!ptySessionKey) {
626
- return;
627
- }
628
-
629
- const session = ptySessionsMap.get(ptySessionKey);
630
- if (session && session.pty !== shellProcess) {
631
- return;
632
- }
633
-
634
- if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
635
- session.ws.send(
636
- JSON.stringify({
637
- type: 'output',
638
- data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${
639
- exitCode.signal != null ? ` (${exitCode.signal})` : ''
640
- }\x1b[0m\r\n`,
641
- })
642
- );
643
- }
644
-
563
+ }
564
+
565
+ if (session.ws && session.ws.readyState === WebSocket.OPEN) {
566
+ let outputData = chunk;
567
+ const cleanChunk = stripAnsiSequences(chunk);
568
+ urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
569
+
570
+ outputData = outputData.replace(
571
+ /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
572
+ '[INFO] Opening in browser: $1'
573
+ );
574
+
575
+ const emitAuthUrl = (detectedUrl: string, autoOpen = false) => {
576
+ const normalizedUrl = normalizeDetectedUrl(detectedUrl);
577
+ if (!normalizedUrl) {
578
+ return;
579
+ }
580
+
581
+ const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
582
+ if (isNewUrl) {
583
+ announcedAuthUrls.add(normalizedUrl);
584
+ session.ws?.send(
585
+ JSON.stringify({
586
+ type: 'auth_url',
587
+ url: normalizedUrl,
588
+ autoOpen,
589
+ })
590
+ );
591
+ }
592
+ };
593
+
594
+ const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
595
+ .map((url) => normalizeDetectedUrl(url))
596
+ .filter((url): url is string => Boolean(url));
597
+
598
+ const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter(
599
+ (url, _, urls) =>
600
+ !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
601
+ );
602
+
603
+ dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
604
+
605
+ if (
606
+ shouldAutoOpenUrlFromOutput(cleanChunk) &&
607
+ dedupedDetectedUrls.length > 0
608
+ ) {
609
+ const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
610
+ current.length > longest.length ? current : longest
611
+ );
612
+ emitAuthUrl(bestUrl, true);
613
+ }
614
+
615
+ session.ws.send(
616
+ JSON.stringify({
617
+ type: 'output',
618
+ data: outputData,
619
+ })
620
+ );
621
+ }
622
+ });
623
+
624
+ shellProcess.onExit((exitCode) => {
625
+ if (!ptySessionKey) {
626
+ return;
627
+ }
628
+
629
+ const session = ptySessionsMap.get(ptySessionKey);
630
+ if (session && session.pty !== shellProcess) {
631
+ return;
632
+ }
633
+
634
+ if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
635
+ session.ws.send(
636
+ JSON.stringify({
637
+ type: 'output',
638
+ data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${
639
+ exitCode.signal != null ? ` (${exitCode.signal})` : ''
640
+ }\x1b[0m\r\n`,
641
+ })
642
+ );
643
+ }
644
+
645
645
  if (session?.timeoutId) {
646
646
  clearTimeout(session.timeoutId);
647
647
  }
648
648
 
649
649
  terminalReplayStore.delete(ptySessionKey);
650
650
  ptySessionsMap.delete(ptySessionKey);
651
- shellProcess = null;
652
- });
653
-
654
- let welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
655
- if (!isPlainShell) {
656
- const providerName =
657
- provider === 'cursor'
658
- ? 'Cursor'
659
- : provider === 'codex'
660
- ? 'Codex'
661
- : provider === 'opencode'
662
- ? 'OpenCode'
663
- : 'Claude';
664
- welcomeMsg = hasSession && resumeSessionId
665
- ? `\x1b[36mResuming ${providerName} session ${resumeSessionId} in: ${projectPath}\x1b[0m\r\n`
666
- : `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
667
- }
668
-
669
- ws.send(
670
- JSON.stringify({
671
- type: 'output',
672
- data: welcomeMsg,
673
- })
674
- );
675
- return;
676
- }
677
-
651
+ shellProcess = null;
652
+ });
653
+
654
+ let welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
655
+ if (!isPlainShell) {
656
+ const providerName =
657
+ provider === 'cursor'
658
+ ? 'Cursor'
659
+ : provider === 'codex'
660
+ ? 'Codex'
661
+ : provider === 'opencode'
662
+ ? 'OpenCode'
663
+ : 'Claude';
664
+ welcomeMsg = hasSession && resumeSessionId
665
+ ? `\x1b[36mResuming ${providerName} session ${resumeSessionId} in: ${projectPath}\x1b[0m\r\n`
666
+ : `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
667
+ }
668
+
669
+ ws.send(
670
+ JSON.stringify({
671
+ type: 'output',
672
+ data: welcomeMsg,
673
+ })
674
+ );
675
+ return;
676
+ }
677
+
678
678
  if (data.type === 'input') {
679
679
  const activeSession = ptySessionKey ? ptySessionsMap.get(ptySessionKey) : null;
680
680
  if (shellProcess && activeSession?.pty === shellProcess && activeSession.ws === ws) {
@@ -723,54 +723,54 @@ export function handleShellConnection(
723
723
  );
724
724
  }
725
725
  }
726
- } catch (error) {
727
- const message = error instanceof Error ? error.message : String(error);
728
- console.error('[ERROR] Shell WebSocket error:', message);
729
- if (ws.readyState === WebSocket.OPEN) {
730
- ws.send(
731
- JSON.stringify({
732
- type: 'output',
733
- data: `\r\n\x1b[31mError: ${message}\x1b[0m\r\n`,
734
- })
735
- );
736
- }
737
- }
738
- });
739
-
740
- ws.on('close', () => {
741
- if (!ptySessionKey) {
742
- return;
743
- }
744
-
745
- const session = ptySessionsMap.get(ptySessionKey);
746
- if (!session) {
747
- return;
748
- }
749
-
750
- // Mobile networks can deliver an old socket's close after its replacement
751
- // has attached. Only the socket that currently owns the PTY may detach it.
752
- if (session.ws !== ws) {
753
- return;
754
- }
755
-
756
- session.ws = null;
757
- if (session.timeoutId) {
758
- clearTimeout(session.timeoutId);
759
- }
760
- session.timeoutId = setTimeout(() => {
761
- // A reconnect may win just as this timer becomes runnable. Re-check the
762
- // active socket so a queued cleanup can never kill a reattached PTY.
763
- if (ptySessionsMap.get(ptySessionKey as string) !== session || session.ws !== null) {
764
- return;
765
- }
766
-
726
+ } catch (error) {
727
+ const message = error instanceof Error ? error.message : String(error);
728
+ console.error('[ERROR] Shell WebSocket error:', message);
729
+ if (ws.readyState === WebSocket.OPEN) {
730
+ ws.send(
731
+ JSON.stringify({
732
+ type: 'output',
733
+ data: `\r\n\x1b[31mError: ${message}\x1b[0m\r\n`,
734
+ })
735
+ );
736
+ }
737
+ }
738
+ });
739
+
740
+ ws.on('close', () => {
741
+ if (!ptySessionKey) {
742
+ return;
743
+ }
744
+
745
+ const session = ptySessionsMap.get(ptySessionKey);
746
+ if (!session) {
747
+ return;
748
+ }
749
+
750
+ // Mobile networks can deliver an old socket's close after its replacement
751
+ // has attached. Only the socket that currently owns the PTY may detach it.
752
+ if (session.ws !== ws) {
753
+ return;
754
+ }
755
+
756
+ session.ws = null;
757
+ if (session.timeoutId) {
758
+ clearTimeout(session.timeoutId);
759
+ }
760
+ session.timeoutId = setTimeout(() => {
761
+ // A reconnect may win just as this timer becomes runnable. Re-check the
762
+ // active socket so a queued cleanup can never kill a reattached PTY.
763
+ if (ptySessionsMap.get(ptySessionKey as string) !== session || session.ws !== null) {
764
+ return;
765
+ }
766
+
767
767
  session.pty.kill();
768
768
  terminalReplayStore.delete(ptySessionKey as string);
769
769
  ptySessionsMap.delete(ptySessionKey as string);
770
- }, PTY_SESSION_TIMEOUT);
771
- });
772
-
773
- ws.on('error', (error) => {
774
- console.error('[ERROR] Shell WebSocket error:', error);
775
- });
776
- }
770
+ }, PTY_SESSION_TIMEOUT);
771
+ });
772
+
773
+ ws.on('error', (error) => {
774
+ console.error('[ERROR] Shell WebSocket error:', error);
775
+ });
776
+ }