@jingyi0605/codingns4dsh 0.2.0-rc.2 → 0.2.0-rc3

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 (355) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +15 -6
  3. package/assets/provider-icons/antigravity.svg +39 -0
  4. package/assets/provider-icons/codebuddy.svg +37 -0
  5. package/assets/provider-icons/cursor-cli.svg +1 -0
  6. package/assets/provider-icons/kiro-cli.svg +11 -0
  7. package/assets/provider-icons/qoder-cn.svg +5 -0
  8. package/assets/provider-icons/qoder.svg +5 -0
  9. package/assets/provider-icons/workbuddy.svg +29 -0
  10. package/data/build/dist/bootstrap/dsh-peer-host-preboot-shim.d.ts.map +1 -1
  11. package/data/build/dist/bootstrap/dsh-peer-host-preboot-shim.js +78 -1
  12. package/data/build/dist/bootstrap/dsh-peer-host-preboot-shim.js.map +1 -1
  13. package/data/build/dist/client/bundle.js +3864 -507
  14. package/data/build/dist/client/bundle.js.map +1 -1
  15. package/data/build/dist/client/cli-catalog.d.ts +1 -1
  16. package/data/build/dist/client/cli-catalog.d.ts.map +1 -1
  17. package/data/build/dist/client/cli-catalog.js +11 -5
  18. package/data/build/dist/client/cli-catalog.js.map +1 -1
  19. package/data/build/dist/client/cli-slots.d.ts.map +1 -1
  20. package/data/build/dist/client/cli-slots.js +111 -13
  21. package/data/build/dist/client/cli-slots.js.map +1 -1
  22. package/data/build/dist/client/debug/ui.d.ts.map +1 -1
  23. package/data/build/dist/client/debug/ui.js +2 -1
  24. package/data/build/dist/client/debug/ui.js.map +1 -1
  25. package/data/build/dist/client/delegate-command.d.ts +1 -0
  26. package/data/build/dist/client/delegate-command.d.ts.map +1 -1
  27. package/data/build/dist/client/delegate-command.js +486 -27
  28. package/data/build/dist/client/delegate-command.js.map +1 -1
  29. package/data/build/dist/client/delegate-plan.d.ts +13 -4
  30. package/data/build/dist/client/delegate-plan.d.ts.map +1 -1
  31. package/data/build/dist/client/delegate-plan.js +10 -6
  32. package/data/build/dist/client/delegate-plan.js.map +1 -1
  33. package/data/build/dist/client/delegate-ui-dom.d.ts +46 -0
  34. package/data/build/dist/client/delegate-ui-dom.d.ts.map +1 -1
  35. package/data/build/dist/client/delegate-ui-dom.js +291 -7
  36. package/data/build/dist/client/delegate-ui-dom.js.map +1 -1
  37. package/data/build/dist/client/features/cli-adapters.d.ts.map +1 -1
  38. package/data/build/dist/client/features/cli-adapters.js +117 -8
  39. package/data/build/dist/client/features/cli-adapters.js.map +1 -1
  40. package/data/build/dist/client/features/mobile-access-panel.d.ts.map +1 -1
  41. package/data/build/dist/client/features/mobile-access-panel.js +8 -7
  42. package/data/build/dist/client/features/mobile-access-panel.js.map +1 -1
  43. package/data/build/dist/client/features/peer-host.d.ts +6 -4
  44. package/data/build/dist/client/features/peer-host.d.ts.map +1 -1
  45. package/data/build/dist/client/features/peer-host.js +346 -18
  46. package/data/build/dist/client/features/peer-host.js.map +1 -1
  47. package/data/build/dist/client/git-history-graph.d.ts +2 -2
  48. package/data/build/dist/client/git-history-graph.d.ts.map +1 -1
  49. package/data/build/dist/client/git-history-graph.js +2 -2
  50. package/data/build/dist/client/git-history-graph.js.map +1 -1
  51. package/data/build/dist/client/git-management.d.ts.map +1 -1
  52. package/data/build/dist/client/git-management.js +21 -1
  53. package/data/build/dist/client/git-management.js.map +1 -1
  54. package/data/build/dist/client/locale.d.ts.map +1 -1
  55. package/data/build/dist/client/locale.js +60 -6
  56. package/data/build/dist/client/locale.js.map +1 -1
  57. package/data/build/dist/client/locales/terminalAuth.d.ts.map +1 -1
  58. package/data/build/dist/client/locales/terminalAuth.js +4 -0
  59. package/data/build/dist/client/locales/terminalAuth.js.map +1 -1
  60. package/data/build/dist/client/locales/usage.d.ts.map +1 -1
  61. package/data/build/dist/client/locales/usage.js +20 -0
  62. package/data/build/dist/client/locales/usage.js.map +1 -1
  63. package/data/build/dist/client/mobile-session-interaction.d.ts.map +1 -1
  64. package/data/build/dist/client/mobile-session-interaction.js +24 -7
  65. package/data/build/dist/client/mobile-session-interaction.js.map +1 -1
  66. package/data/build/dist/client/mobile-sidebar-gestures.d.ts +55 -7
  67. package/data/build/dist/client/mobile-sidebar-gestures.d.ts.map +1 -1
  68. package/data/build/dist/client/mobile-sidebar-gestures.js +339 -40
  69. package/data/build/dist/client/mobile-sidebar-gestures.js.map +1 -1
  70. package/data/build/dist/client/model-catalog-cache.d.ts +5 -0
  71. package/data/build/dist/client/model-catalog-cache.d.ts.map +1 -0
  72. package/data/build/dist/client/model-catalog-cache.js +58 -0
  73. package/data/build/dist/client/model-catalog-cache.js.map +1 -0
  74. package/data/build/dist/client/peer-host-aggregate-refresh.d.ts +7 -0
  75. package/data/build/dist/client/peer-host-aggregate-refresh.d.ts.map +1 -1
  76. package/data/build/dist/client/peer-host-aggregate-refresh.js +13 -1
  77. package/data/build/dist/client/peer-host-aggregate-refresh.js.map +1 -1
  78. package/data/build/dist/client/peer-host-native-projection.d.ts +16 -1
  79. package/data/build/dist/client/peer-host-native-projection.d.ts.map +1 -1
  80. package/data/build/dist/client/peer-host-native-projection.js +49 -8
  81. package/data/build/dist/client/peer-host-native-projection.js.map +1 -1
  82. package/data/build/dist/client/peer-host-native-store-projection.d.ts +6 -2
  83. package/data/build/dist/client/peer-host-native-store-projection.d.ts.map +1 -1
  84. package/data/build/dist/client/peer-host-native-store-projection.js +308 -10
  85. package/data/build/dist/client/peer-host-native-store-projection.js.map +1 -1
  86. package/data/build/dist/client/provider-icon-assets.js +14 -0
  87. package/data/build/dist/client/provider-icon-assets.js.map +1 -1
  88. package/data/build/dist/client/provider-icons.d.ts.map +1 -1
  89. package/data/build/dist/client/provider-icons.js +15 -3
  90. package/data/build/dist/client/provider-icons.js.map +1 -1
  91. package/data/build/dist/client/remote-web-context.js +10 -6
  92. package/data/build/dist/client/remote-web-context.js.map +1 -1
  93. package/data/build/dist/client/service-tier.d.ts +54 -0
  94. package/data/build/dist/client/service-tier.d.ts.map +1 -0
  95. package/data/build/dist/client/service-tier.js +78 -0
  96. package/data/build/dist/client/service-tier.js.map +1 -0
  97. package/data/build/dist/client/settings-promo-styles.d.ts +28 -0
  98. package/data/build/dist/client/settings-promo-styles.d.ts.map +1 -0
  99. package/data/build/dist/client/settings-promo-styles.js +58 -0
  100. package/data/build/dist/client/settings-promo-styles.js.map +1 -0
  101. package/data/build/dist/client/settings-promo.d.ts +14 -0
  102. package/data/build/dist/client/settings-promo.d.ts.map +1 -0
  103. package/data/build/dist/client/settings-promo.js +68 -0
  104. package/data/build/dist/client/settings-promo.js.map +1 -0
  105. package/data/build/dist/client/settings-section.d.ts +2 -1
  106. package/data/build/dist/client/settings-section.d.ts.map +1 -1
  107. package/data/build/dist/client/settings-section.js +7 -4
  108. package/data/build/dist/client/settings-section.js.map +1 -1
  109. package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
  110. package/data/build/dist/client/subscription-slot.js +181 -16
  111. package/data/build/dist/client/subscription-slot.js.map +1 -1
  112. package/data/build/dist/client/terminal/model.d.ts +82 -3
  113. package/data/build/dist/client/terminal/model.d.ts.map +1 -1
  114. package/data/build/dist/client/terminal/model.js +368 -29
  115. package/data/build/dist/client/terminal/model.js.map +1 -1
  116. package/data/build/dist/client/terminal/recovery.d.ts +16 -1
  117. package/data/build/dist/client/terminal/recovery.d.ts.map +1 -1
  118. package/data/build/dist/client/terminal/recovery.js +101 -39
  119. package/data/build/dist/client/terminal/recovery.js.map +1 -1
  120. package/data/build/dist/client/terminal/styles.d.ts +10 -0
  121. package/data/build/dist/client/terminal/styles.d.ts.map +1 -1
  122. package/data/build/dist/client/terminal/styles.js +27 -3
  123. package/data/build/dist/client/terminal/styles.js.map +1 -1
  124. package/data/build/dist/client/terminal/ui.d.ts +3 -2
  125. package/data/build/dist/client/terminal/ui.d.ts.map +1 -1
  126. package/data/build/dist/client/terminal/ui.js +215 -104
  127. package/data/build/dist/client/terminal/ui.js.map +1 -1
  128. package/data/build/dist/client/terminal/xterm-view.d.ts +5 -1
  129. package/data/build/dist/client/terminal/xterm-view.d.ts.map +1 -1
  130. package/data/build/dist/client/terminal/xterm-view.js +378 -20
  131. package/data/build/dist/client/terminal/xterm-view.js.map +1 -1
  132. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts +58 -0
  133. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts.map +1 -0
  134. package/data/build/dist/host/cli-adapters/acp-cli-driver.js +271 -0
  135. package/data/build/dist/host/cli-adapters/acp-cli-driver.js.map +1 -0
  136. package/data/build/dist/host/cli-adapters/antigravity-driver.d.ts +30 -0
  137. package/data/build/dist/host/cli-adapters/antigravity-driver.d.ts.map +1 -0
  138. package/data/build/dist/host/cli-adapters/antigravity-driver.js +210 -0
  139. package/data/build/dist/host/cli-adapters/antigravity-driver.js.map +1 -0
  140. package/data/build/dist/host/cli-adapters/claude-driver.js +1 -1
  141. package/data/build/dist/host/cli-adapters/claude-driver.js.map +1 -1
  142. package/data/build/dist/host/cli-adapters/codebuddy-driver.d.ts +139 -0
  143. package/data/build/dist/host/cli-adapters/codebuddy-driver.d.ts.map +1 -0
  144. package/data/build/dist/host/cli-adapters/codebuddy-driver.js +1721 -0
  145. package/data/build/dist/host/cli-adapters/codebuddy-driver.js.map +1 -0
  146. package/data/build/dist/host/cli-adapters/codebuddy-subscription.d.ts +53 -0
  147. package/data/build/dist/host/cli-adapters/codebuddy-subscription.d.ts.map +1 -0
  148. package/data/build/dist/host/cli-adapters/codebuddy-subscription.js +778 -0
  149. package/data/build/dist/host/cli-adapters/codebuddy-subscription.js.map +1 -0
  150. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +12 -0
  151. package/data/build/dist/host/cli-adapters/codex-driver.d.ts.map +1 -1
  152. package/data/build/dist/host/cli-adapters/codex-driver.js +153 -6
  153. package/data/build/dist/host/cli-adapters/codex-driver.js.map +1 -1
  154. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +43 -3
  155. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts.map +1 -1
  156. package/data/build/dist/host/cli-adapters/command-code-driver.js +637 -72
  157. package/data/build/dist/host/cli-adapters/command-code-driver.js.map +1 -1
  158. package/data/build/dist/host/cli-adapters/command-code-history.d.ts +147 -0
  159. package/data/build/dist/host/cli-adapters/command-code-history.d.ts.map +1 -0
  160. package/data/build/dist/host/cli-adapters/command-code-history.js +659 -0
  161. package/data/build/dist/host/cli-adapters/command-code-history.js.map +1 -0
  162. package/data/build/dist/host/cli-adapters/cursor-driver.d.ts +12 -0
  163. package/data/build/dist/host/cli-adapters/cursor-driver.d.ts.map +1 -0
  164. package/data/build/dist/host/cli-adapters/cursor-driver.js +20 -0
  165. package/data/build/dist/host/cli-adapters/cursor-driver.js.map +1 -0
  166. package/data/build/dist/host/cli-adapters/custom-upstream-classifier.d.ts +67 -0
  167. package/data/build/dist/host/cli-adapters/custom-upstream-classifier.d.ts.map +1 -0
  168. package/data/build/dist/host/cli-adapters/custom-upstream-classifier.js +169 -0
  169. package/data/build/dist/host/cli-adapters/custom-upstream-classifier.js.map +1 -0
  170. package/data/build/dist/host/cli-adapters/delegate-dispatch.d.ts +2 -1
  171. package/data/build/dist/host/cli-adapters/delegate-dispatch.d.ts.map +1 -1
  172. package/data/build/dist/host/cli-adapters/delegate-dispatch.js +14 -61
  173. package/data/build/dist/host/cli-adapters/delegate-dispatch.js.map +1 -1
  174. package/data/build/dist/host/cli-adapters/delegation-authorization.d.ts +19 -0
  175. package/data/build/dist/host/cli-adapters/delegation-authorization.d.ts.map +1 -0
  176. package/data/build/dist/host/cli-adapters/delegation-authorization.js +49 -0
  177. package/data/build/dist/host/cli-adapters/delegation-authorization.js.map +1 -0
  178. package/data/build/dist/host/cli-adapters/delegation-mention-rewrite.d.ts +34 -0
  179. package/data/build/dist/host/cli-adapters/delegation-mention-rewrite.d.ts.map +1 -0
  180. package/data/build/dist/host/cli-adapters/delegation-mention-rewrite.js +121 -0
  181. package/data/build/dist/host/cli-adapters/delegation-mention-rewrite.js.map +1 -0
  182. package/data/build/dist/host/cli-adapters/driver.d.ts +6 -0
  183. package/data/build/dist/host/cli-adapters/driver.d.ts.map +1 -1
  184. package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
  185. package/data/build/dist/host/cli-adapters/feature.js +269 -18
  186. package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
  187. package/data/build/dist/host/cli-adapters/kiro-driver.d.ts +12 -0
  188. package/data/build/dist/host/cli-adapters/kiro-driver.d.ts.map +1 -0
  189. package/data/build/dist/host/cli-adapters/kiro-driver.js +18 -0
  190. package/data/build/dist/host/cli-adapters/kiro-driver.js.map +1 -0
  191. package/data/build/dist/host/cli-adapters/legacy-session-adapter.d.ts.map +1 -1
  192. package/data/build/dist/host/cli-adapters/legacy-session-adapter.js +11 -0
  193. package/data/build/dist/host/cli-adapters/legacy-session-adapter.js.map +1 -1
  194. package/data/build/dist/host/cli-adapters/model-catalog.d.ts +5 -0
  195. package/data/build/dist/host/cli-adapters/model-catalog.d.ts.map +1 -1
  196. package/data/build/dist/host/cli-adapters/model-catalog.js +130 -0
  197. package/data/build/dist/host/cli-adapters/model-catalog.js.map +1 -1
  198. package/data/build/dist/host/cli-adapters/native-subagent-dispatch.d.ts +33 -3
  199. package/data/build/dist/host/cli-adapters/native-subagent-dispatch.d.ts.map +1 -1
  200. package/data/build/dist/host/cli-adapters/native-subagent-dispatch.js +115 -9
  201. package/data/build/dist/host/cli-adapters/native-subagent-dispatch.js.map +1 -1
  202. package/data/build/dist/host/cli-adapters/native-team-subagent.d.ts +14 -2
  203. package/data/build/dist/host/cli-adapters/native-team-subagent.d.ts.map +1 -1
  204. package/data/build/dist/host/cli-adapters/native-team-subagent.js +23 -4
  205. package/data/build/dist/host/cli-adapters/native-team-subagent.js.map +1 -1
  206. package/data/build/dist/host/cli-adapters/new-api-subscription.d.ts +75 -0
  207. package/data/build/dist/host/cli-adapters/new-api-subscription.d.ts.map +1 -0
  208. package/data/build/dist/host/cli-adapters/new-api-subscription.js +959 -0
  209. package/data/build/dist/host/cli-adapters/new-api-subscription.js.map +1 -0
  210. package/data/build/dist/host/cli-adapters/opencode-driver.d.ts.map +1 -1
  211. package/data/build/dist/host/cli-adapters/opencode-driver.js +9 -7
  212. package/data/build/dist/host/cli-adapters/opencode-driver.js.map +1 -1
  213. package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +33 -3
  214. package/data/build/dist/host/cli-adapters/provider-subscription.d.ts.map +1 -1
  215. package/data/build/dist/host/cli-adapters/provider-subscription.js +158 -40
  216. package/data/build/dist/host/cli-adapters/provider-subscription.js.map +1 -1
  217. package/data/build/dist/host/cli-adapters/qoder-driver.d.ts +71 -0
  218. package/data/build/dist/host/cli-adapters/qoder-driver.d.ts.map +1 -0
  219. package/data/build/dist/host/cli-adapters/qoder-driver.js +553 -0
  220. package/data/build/dist/host/cli-adapters/qoder-driver.js.map +1 -0
  221. package/data/build/dist/host/cli-adapters/qoder-subscription.d.ts +31 -0
  222. package/data/build/dist/host/cli-adapters/qoder-subscription.d.ts.map +1 -0
  223. package/data/build/dist/host/cli-adapters/qoder-subscription.js +178 -0
  224. package/data/build/dist/host/cli-adapters/qoder-subscription.js.map +1 -0
  225. package/data/build/dist/host/cli-adapters/registry.d.ts +41 -2
  226. package/data/build/dist/host/cli-adapters/registry.d.ts.map +1 -1
  227. package/data/build/dist/host/cli-adapters/registry.js +187 -18
  228. package/data/build/dist/host/cli-adapters/registry.js.map +1 -1
  229. package/data/build/dist/host/cli-adapters/session-store.d.ts +4 -0
  230. package/data/build/dist/host/cli-adapters/session-store.d.ts.map +1 -1
  231. package/data/build/dist/host/cli-adapters/session-store.js +18 -2
  232. package/data/build/dist/host/cli-adapters/session-store.js.map +1 -1
  233. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +14 -1
  234. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts.map +1 -1
  235. package/data/build/dist/host/cli-adapters/standard-stream-driver.js +121 -23
  236. package/data/build/dist/host/cli-adapters/standard-stream-driver.js.map +1 -1
  237. package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts +10 -1
  238. package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts.map +1 -1
  239. package/data/build/dist/host/cli-adapters/stream-normalizer.js +23 -0
  240. package/data/build/dist/host/cli-adapters/stream-normalizer.js.map +1 -1
  241. package/data/build/dist/host/cli-adapters/subagent-tool.d.ts.map +1 -1
  242. package/data/build/dist/host/cli-adapters/subagent-tool.js +53 -7
  243. package/data/build/dist/host/cli-adapters/subagent-tool.js.map +1 -1
  244. package/data/build/dist/host/cli-bridge/bridge-server.d.ts +5 -0
  245. package/data/build/dist/host/cli-bridge/bridge-server.d.ts.map +1 -1
  246. package/data/build/dist/host/cli-bridge/bridge-server.js +7 -2
  247. package/data/build/dist/host/cli-bridge/bridge-server.js.map +1 -1
  248. package/data/build/dist/host/cli-bridge/command-code-mod.d.ts +5 -1
  249. package/data/build/dist/host/cli-bridge/command-code-mod.d.ts.map +1 -1
  250. package/data/build/dist/host/cli-bridge/command-code-mod.js +43 -12
  251. package/data/build/dist/host/cli-bridge/command-code-mod.js.map +1 -1
  252. package/data/build/dist/host/cli-bridge/dispatch.d.ts.map +1 -1
  253. package/data/build/dist/host/cli-bridge/dispatch.js +43 -5
  254. package/data/build/dist/host/cli-bridge/dispatch.js.map +1 -1
  255. package/data/build/dist/host/cli-bridge/mcp-stdio-entry.js +24 -5
  256. package/data/build/dist/host/cli-bridge/mcp-stdio-entry.js.map +1 -1
  257. package/data/build/dist/host/features/peer-host.d.ts +21 -1
  258. package/data/build/dist/host/features/peer-host.d.ts.map +1 -1
  259. package/data/build/dist/host/features/peer-host.js +174 -30
  260. package/data/build/dist/host/features/peer-host.js.map +1 -1
  261. package/data/build/dist/host/features/types.d.ts +2 -0
  262. package/data/build/dist/host/features/types.d.ts.map +1 -1
  263. package/data/build/dist/host/index.d.ts +11 -1
  264. package/data/build/dist/host/index.d.ts.map +1 -1
  265. package/data/build/dist/host/index.js +12 -2
  266. package/data/build/dist/host/index.js.map +1 -1
  267. package/data/build/dist/host/modules/peer-host/peer-host-native-protocol.js +6 -1
  268. package/data/build/dist/host/modules/peer-host/peer-host-native-protocol.js.map +1 -1
  269. package/data/build/dist/host/modules/peer-host/peer-host-virtual-registry.d.ts +2 -0
  270. package/data/build/dist/host/modules/peer-host/peer-host-virtual-registry.d.ts.map +1 -1
  271. package/data/build/dist/host/modules/peer-host/peer-host-virtual-registry.js +26 -7
  272. package/data/build/dist/host/modules/peer-host/peer-host-virtual-registry.js.map +1 -1
  273. package/data/build/dist/host/native-session-bridge.d.ts.map +1 -1
  274. package/data/build/dist/host/native-session-bridge.js +36 -7
  275. package/data/build/dist/host/native-session-bridge.js.map +1 -1
  276. package/data/build/dist/host/rpc.js +5 -2
  277. package/data/build/dist/host/rpc.js.map +1 -1
  278. package/data/build/dist/host/settings.d.ts.map +1 -1
  279. package/data/build/dist/host/settings.js +12 -5
  280. package/data/build/dist/host/settings.js.map +1 -1
  281. package/data/build/dist/host/terminal/backends/local-pty-backend.d.ts.map +1 -1
  282. package/data/build/dist/host/terminal/backends/local-pty-backend.js +14 -0
  283. package/data/build/dist/host/terminal/backends/local-pty-backend.js.map +1 -1
  284. package/data/build/dist/host/terminal/backends/tmux-backend.d.ts +40 -1
  285. package/data/build/dist/host/terminal/backends/tmux-backend.d.ts.map +1 -1
  286. package/data/build/dist/host/terminal/backends/tmux-backend.js +291 -18
  287. package/data/build/dist/host/terminal/backends/tmux-backend.js.map +1 -1
  288. package/data/build/dist/host/terminal/runtime-adapter.d.ts +32 -0
  289. package/data/build/dist/host/terminal/runtime-adapter.d.ts.map +1 -1
  290. package/data/build/dist/host/terminal/runtime-adapter.js +8 -0
  291. package/data/build/dist/host/terminal/runtime-adapter.js.map +1 -1
  292. package/data/build/dist/host/terminal/runtime-manager.d.ts +3 -0
  293. package/data/build/dist/host/terminal/runtime-manager.d.ts.map +1 -1
  294. package/data/build/dist/host/terminal/runtime-manager.js +92 -4
  295. package/data/build/dist/host/terminal/runtime-manager.js.map +1 -1
  296. package/data/build/dist/host/terminal/terminal-controller.d.ts.map +1 -1
  297. package/data/build/dist/host/terminal/terminal-controller.js +58 -6
  298. package/data/build/dist/host/terminal/terminal-controller.js.map +1 -1
  299. package/data/build/dist/host/terminal/terminal-process-service.d.ts.map +1 -1
  300. package/data/build/dist/host/terminal/terminal-process-service.js +12 -2
  301. package/data/build/dist/host/terminal/terminal-process-service.js.map +1 -1
  302. package/data/build/dist/host/terminal/terminal-service.d.ts +41 -4
  303. package/data/build/dist/host/terminal/terminal-service.d.ts.map +1 -1
  304. package/data/build/dist/host/terminal/terminal-service.js +551 -61
  305. package/data/build/dist/host/terminal/terminal-service.js.map +1 -1
  306. package/data/build/dist/index.d.ts +2 -2
  307. package/data/build/dist/index.d.ts.map +1 -1
  308. package/data/build/dist/index.js +2 -2
  309. package/data/build/dist/index.js.map +1 -1
  310. package/data/build/dist/shared/contracts/cli-adapter.d.ts +64 -2
  311. package/data/build/dist/shared/contracts/cli-adapter.d.ts.map +1 -1
  312. package/data/build/dist/shared/contracts/config.d.ts +54 -11
  313. package/data/build/dist/shared/contracts/config.d.ts.map +1 -1
  314. package/data/build/dist/shared/contracts/config.js +52 -8
  315. package/data/build/dist/shared/contracts/config.js.map +1 -1
  316. package/data/build/dist/shared/contracts/subscription.d.ts +5 -1
  317. package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
  318. package/data/build/dist/shared/contracts/subscription.js.map +1 -1
  319. package/data/build/dist/shared/contracts/terminal.d.ts +8 -1
  320. package/data/build/dist/shared/contracts/terminal.d.ts.map +1 -1
  321. package/data/build/dist/shared/contracts/version.d.ts +1 -1
  322. package/data/build/dist/shared/contracts/version.d.ts.map +1 -1
  323. package/data/build/dist/shared/contracts/version.js +1 -1
  324. package/data/build/dist/shared/contracts/version.js.map +1 -1
  325. package/data/build/dist/shared/debug.d.ts +8 -0
  326. package/data/build/dist/shared/debug.d.ts.map +1 -1
  327. package/data/build/dist/shared/debug.js +43 -7
  328. package/data/build/dist/shared/debug.js.map +1 -1
  329. package/data/build/dist/shared/delegation-carrier.d.ts +24 -0
  330. package/data/build/dist/shared/delegation-carrier.d.ts.map +1 -0
  331. package/data/build/dist/shared/delegation-carrier.js +111 -0
  332. package/data/build/dist/shared/delegation-carrier.js.map +1 -0
  333. package/data/build/dist/shared/index.d.ts +2 -2
  334. package/data/build/dist/shared/index.d.ts.map +1 -1
  335. package/data/build/dist/shared/index.js +2 -2
  336. package/data/build/dist/shared/index.js.map +1 -1
  337. package/data/build/dist/shared/terminal-input.d.ts +8 -0
  338. package/data/build/dist/shared/terminal-input.d.ts.map +1 -0
  339. package/data/build/dist/shared/terminal-input.js +10 -0
  340. package/data/build/dist/shared/terminal-input.js.map +1 -0
  341. package/data/build/dist/transport/debug.d.ts +7 -0
  342. package/data/build/dist/transport/debug.d.ts.map +1 -1
  343. package/data/build/dist/transport/debug.js +33 -5
  344. package/data/build/dist/transport/debug.js.map +1 -1
  345. package/data/build/dist/transport/index.d.ts +1 -1
  346. package/data/build/dist/transport/index.d.ts.map +1 -1
  347. package/data/build/dist/transport/index.js +1 -1
  348. package/data/build/dist/transport/index.js.map +1 -1
  349. package/data/build/dist/typert.host.js +1 -1
  350. package/data/build/dist/typert.host.js.map +1 -1
  351. package/package.json +1 -1
  352. package/profile/README.md +2 -2
  353. package/profile/package.json +2 -2
  354. package/profile/version.json +1 -1
  355. package/version.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { debugInfo, debugWarn } from '../../shared/debug.js';
2
3
  /**
3
4
  * 持久终端生命周期服务。
4
5
  *
@@ -10,6 +11,8 @@ export class CodingNsTerminalService {
10
11
  runtimes;
11
12
  now;
12
13
  controllers = new Map();
14
+ residents = new Map();
15
+ residentPromises = new Map();
13
16
  followers = new Map();
14
17
  operations = new Map();
15
18
  exitCallbacks = new Map();
@@ -24,6 +27,10 @@ export class CodingNsTerminalService {
24
27
  return;
25
28
  await this.store.load();
26
29
  this.initialized = true;
30
+ debugInfo('codingns4dsh: terminal service initialized', {
31
+ recordCount: this.store.list().length,
32
+ records: this.store.list().map(recordSummary),
33
+ });
27
34
  }
28
35
  supportsRuntimeType(runtimeType) {
29
36
  return this.runtimes.supports(runtimeType);
@@ -77,21 +84,47 @@ export class CodingNsTerminalService {
77
84
  validateTerminalId(input.terminalId);
78
85
  validateSize(input.cols, input.rows);
79
86
  const identity = { ...input.scope, terminalId: input.terminalId };
87
+ debugInfo('codingns4dsh: terminal service create start', {
88
+ identity,
89
+ runtimeType: input.runtimeType,
90
+ shellProfileId: input.shell.profileId,
91
+ cols: input.cols,
92
+ rows: input.rows,
93
+ });
80
94
  return this.enqueue(identity, async () => {
81
95
  const existing = this.store.get(identity);
96
+ debugInfo('codingns4dsh: terminal service create queued', {
97
+ identity,
98
+ existing: existing === undefined ? null : recordSummary(existing),
99
+ });
82
100
  if (existing !== undefined) {
83
101
  if (existing.state === 'closed' || existing.state === 'closing') {
84
102
  throw new TerminalServiceError('TERMINAL_UNAVAILABLE', '终端已关闭,不能复用相同标识');
85
103
  }
86
104
  const runtime = await this.runtimes.inspect(existing);
105
+ debugInfo('codingns4dsh: terminal service create existing inspect', {
106
+ identity,
107
+ record: recordSummary(existing),
108
+ runtime: runtimeSummary(runtime),
109
+ });
87
110
  if (runtime.alive) {
88
111
  const running = existing.state === 'running'
89
112
  ? existing
90
113
  : await this.update(existing, { state: 'running', error: '' });
114
+ try {
115
+ await this.ensureResident(running);
116
+ }
117
+ catch (error) {
118
+ debugWarn('codingns4dsh: terminal resident connection warmup failed', {
119
+ identity,
120
+ error: errorMessage(error),
121
+ });
122
+ }
91
123
  return this.info(running);
92
124
  }
93
- await this.update(existing, { state: 'lost', error: '持久终端运行时不存在' });
94
- throw new TerminalServiceError('TERMINAL_RUNTIME_LOST', '持久终端运行时不存在');
125
+ // 运行时确实没了:同一个 terminalId 允许重建,这样 Sidebar 标签、标题和
126
+ // 尺寸都能保留。旧实现只把记录标成 lost 再抛错,用户点"重建终端"永远失败。
127
+ return this.rebuild(existing, input);
95
128
  }
96
129
  const timestamp = this.now().toISOString();
97
130
  const record = {
@@ -117,20 +150,41 @@ export class CodingNsTerminalService {
117
150
  updatedAt: timestamp,
118
151
  };
119
152
  await this.store.put(record);
153
+ debugInfo('codingns4dsh: terminal service record starting', { record: recordSummary(record) });
120
154
  try {
121
155
  const runtime = await this.runtimes.create(record);
156
+ debugInfo('codingns4dsh: terminal service runtime created', {
157
+ identity,
158
+ runtime: runtimeSummary(runtime),
159
+ });
122
160
  if (!runtime.alive)
123
161
  throw new Error('backend 创建后未报告存活状态');
124
162
  if (input.onExit !== undefined)
125
163
  this.exitCallbacks.set(identityKey(identity), input.onExit);
126
164
  const running = await this.update(record, { state: 'running', error: '' });
127
- if (input.onExit !== undefined)
128
- await this.runtimes.monitor(running, (exitCode) => { void this.markExited(identity, exitCode); });
165
+ // 常驻连接同时承担自然退出监听。这样普通终端和启动项终端都使用同一
166
+ // 条 Host 内存连接,浏览器 follow 不再反复创建 backend attach。
167
+ try {
168
+ await this.ensureResident(running);
169
+ }
170
+ catch (error) {
171
+ debugWarn('codingns4dsh: terminal resident connection warmup failed', {
172
+ identity,
173
+ error: errorMessage(error),
174
+ });
175
+ }
176
+ debugInfo('codingns4dsh: terminal service create success', { record: recordSummary(running) });
129
177
  return this.info(running);
130
178
  }
131
179
  catch (error) {
132
180
  this.exitCallbacks.delete(identityKey(identity));
133
- await this.update(record, { state: 'error', error: errorMessage(error) });
181
+ const message = errorMessage(error);
182
+ debugWarn('codingns4dsh: terminal service create failed', {
183
+ identity,
184
+ record: recordSummary(record),
185
+ error: message,
186
+ });
187
+ await this.update(record, { state: 'error', error: message });
134
188
  throw error;
135
189
  }
136
190
  });
@@ -140,6 +194,72 @@ export class CodingNsTerminalService {
140
194
  this.requireInitialized();
141
195
  return this.store.get(identity);
142
196
  }
197
+ /**
198
+ * 用同一个 terminalId 重建已经丢失的运行时。
199
+ *
200
+ * 保留 cwd、标题、shell 与工作区归属,只换一个新的 runtimeSessionKey;旧的
201
+ * tmux 会话/PTY 已经不存在,先按幂等方式清理一次再创建。
202
+ */
203
+ async rebuild(existing, input) {
204
+ await this.update(existing, { state: 'lost', error: '持久终端运行时不存在' });
205
+ // 重建会更换 runtimeSessionKey;旧 resident 即使 backend 已经丢失,也不能
206
+ // 留在内存映射里,否则后面的 ensureResident 会把新终端绑定到旧订阅。
207
+ await this.releaseResident(identityOfRecord(existing));
208
+ try {
209
+ await this.runtimes.terminate(existing);
210
+ }
211
+ catch { /* 旧运行时已经不存在 */ }
212
+ await this.runtimes.detachTerminal(existing);
213
+ this.exitCallbacks.delete(identityKey(existing));
214
+ const identity = { ...input.scope, terminalId: existing.terminalId };
215
+ const timestamp = this.now().toISOString();
216
+ const record = {
217
+ ...existing,
218
+ ...input.scope,
219
+ runtimeSessionKey: randomUUID(),
220
+ runtimeType: input.runtimeType,
221
+ shellPath: input.shell.path,
222
+ shellProfileId: input.shell.profileId,
223
+ shellName: input.shell.name,
224
+ shellArgs: [...input.shell.args],
225
+ ...(input.commandPath === undefined ? {} : { commandPath: input.commandPath }),
226
+ ...(input.commandArgs === undefined ? {} : { commandArgs: [...input.commandArgs] }),
227
+ ...(input.commandEnv === undefined ? {} : { commandEnv: { ...input.commandEnv } }),
228
+ ...(input.launchProfileId === undefined ? {} : { launchProfileId: input.launchProfileId }),
229
+ cwd: input.cwd,
230
+ title: input.title?.trim() || existing.title,
231
+ cols: input.cols,
232
+ rows: input.rows,
233
+ state: 'starting',
234
+ exitCode: null,
235
+ error: '',
236
+ updatedAt: timestamp,
237
+ };
238
+ await this.store.put(record);
239
+ try {
240
+ const runtime = await this.runtimes.create(record);
241
+ if (!runtime.alive)
242
+ throw new Error('backend 重建后未报告存活状态');
243
+ if (input.onExit !== undefined)
244
+ this.exitCallbacks.set(identityKey(identity), input.onExit);
245
+ const running = await this.update(record, { state: 'running', error: '' });
246
+ try {
247
+ await this.ensureResident(running);
248
+ }
249
+ catch (error) {
250
+ debugWarn('codingns4dsh: terminal resident connection warmup failed', {
251
+ identity,
252
+ error: errorMessage(error),
253
+ });
254
+ }
255
+ return this.info(running);
256
+ }
257
+ catch (error) {
258
+ this.exitCallbacks.delete(identityKey(identity));
259
+ await this.update(record, { state: 'error', error: errorMessage(error) });
260
+ throw error;
261
+ }
262
+ }
143
263
  async inspect(identity) {
144
264
  const record = this.getRecord(identity);
145
265
  if (record === undefined)
@@ -156,6 +276,7 @@ export class CodingNsTerminalService {
156
276
  if (current === undefined || current.state === 'closed')
157
277
  return;
158
278
  if (current.state === 'closing') {
279
+ await this.releaseResident(identityOfRecord(current));
159
280
  await this.runtimes.terminate(current);
160
281
  await this.runtimes.detachTerminal(current);
161
282
  await this.update(current, { state: 'closed', exitCode: null, error: '' });
@@ -163,9 +284,25 @@ export class CodingNsTerminalService {
163
284
  }
164
285
  try {
165
286
  const identity = await this.runtimes.inspect(current);
166
- await this.update(current, identity.alive
167
- ? { state: 'running', error: '' }
168
- : { state: 'lost', error: identity.detail ?? '持久终端运行时不存在' });
287
+ if (identity.alive) {
288
+ await this.update(current, { state: 'running', error: '' });
289
+ // 恢复阶段提前建立常驻连接,用户第一次点击终端时直接收到已有状态。
290
+ try {
291
+ await this.ensureResident(this.store.get(current) ?? current);
292
+ }
293
+ catch (error) {
294
+ debugWarn('codingns4dsh: terminal resident connection restore failed', {
295
+ identity: identityOfRecord(current),
296
+ error: errorMessage(error),
297
+ });
298
+ }
299
+ return;
300
+ }
301
+ // 运行时不在了:有真实退出码才算 exited,否则只能报告 lost。
302
+ // 少了这一步,DSH 重启后会把"用户已经退出的终端"一直显示成丢失。
303
+ await this.update(current, identity.exitCode === undefined || identity.exitCode === null
304
+ ? { state: 'lost', exitCode: null, error: identity.detail ?? '持久终端运行时不存在' }
305
+ : { state: 'exited', exitCode: identity.exitCode, error: '' });
169
306
  }
170
307
  catch (error) {
171
308
  await this.update(current, { state: 'error', error: errorMessage(error) });
@@ -181,51 +318,84 @@ export class CodingNsTerminalService {
181
318
  }
182
319
  async *follow(input) {
183
320
  const record = this.requireAvailable(input.identity);
321
+ debugInfo('codingns4dsh: terminal service follow start', {
322
+ identity: input.identity,
323
+ attachmentId: input.attachmentId,
324
+ generation: input.generation,
325
+ record: recordSummary(record),
326
+ });
184
327
  const key = identityKey(input.identity);
328
+ // create/recover 通常已经建立 resident;旧记录或连接抖动时在这里补建一次。
329
+ await this.ensureResident(record);
330
+ const resident = this.residents.get(key);
331
+ if (resident === undefined)
332
+ throw new TerminalServiceError('TERMINAL_RUNTIME_LOST', '终端常驻连接不可用');
185
333
  const queue = new TerminalFrameQueue(2 * 1024 * 1024);
186
334
  const follower = { attachmentId: input.attachmentId, generation: input.generation, queue };
187
- this.addFollower(key, follower);
188
- // 无论 backend attach 是否同步吐出恢复画面,wire 的第一帧都必须是 snapshot。
189
- queue.pushSnapshot(this.info(record));
190
- let attachment;
335
+ // resident 已经存在时不再等待 backend attach;先从 tmux server 读取完整历史,
336
+ // 这样第一帧能真正填充 xterm scrollback,而不是只重放当前屏幕。
337
+ // 如果把它当作 output 增量发送,切换会话或 resident 重连时会把整屏再次追加,
338
+ // 造成 prompt 重复和 ANSI 内容错位。
339
+ let snapshot = resident.replay;
191
340
  try {
192
- attachment = await this.runtimes.attach(record, {
193
- identity: input.identity,
194
- generation: input.generation,
195
- streamId: input.attachmentId,
196
- requestedAttachmentId: input.attachmentId,
197
- cols: record.cols,
198
- rows: record.rows,
199
- onData: (data) => queue.pushOutput(data),
200
- onExit: (exitCode) => { void this.handleRuntimeExit(input.identity, exitCode, queue); },
201
- });
341
+ const captured = await this.runtimes.captureHistory(record, RESIDENT_HISTORY_LINES);
342
+ if (captured !== undefined) {
343
+ resident.replay = captured;
344
+ snapshot = captured;
345
+ debugInfo('codingns4dsh: terminal history snapshot ready', {
346
+ identity: input.identity,
347
+ source: 'runtime-capture',
348
+ historyLines: countLines(captured),
349
+ historyCharacters: captured.length,
350
+ });
351
+ }
352
+ else {
353
+ debugInfo('codingns4dsh: terminal history snapshot fallback', {
354
+ identity: input.identity,
355
+ source: 'resident-replay',
356
+ replayCharacters: snapshot.length,
357
+ });
358
+ }
202
359
  }
203
360
  catch (error) {
204
- this.removeFollower(key, follower);
205
- queue.finish();
206
- throw error;
361
+ debugWarn('codingns4dsh: terminal history capture failed, using resident replay', {
362
+ identity: input.identity,
363
+ error: errorMessage(error),
364
+ replayCharacters: snapshot.length,
365
+ });
207
366
  }
208
- this.controllers.set(key, {
367
+ // 历史读取期间不登记 follower,避免极短竞态把 output 排在 snapshot 之前。
368
+ snapshot = resident.replay;
369
+ this.addFollower(key, follower);
370
+ this.addController(key, input.attachmentId, {
209
371
  attachmentId: input.attachmentId,
210
- subscriptionId: attachment.subscriptionId,
372
+ subscriptionId: resident.subscriptionId,
211
373
  generation: input.generation,
212
374
  });
213
- this.broadcastState(input.identity);
214
- const abort = () => queue.finish();
215
- input.signal.addEventListener('abort', abort, { once: true });
216
- if (input.signal.aborted)
217
- abort();
375
+ queue.pushSnapshot(this.info(record, input.attachmentId), snapshot);
376
+ queue.pushState(this.info(record, input.attachmentId));
218
377
  try {
378
+ debugInfo('codingns4dsh: terminal service follow subscribed', {
379
+ identity: input.identity,
380
+ attachmentId: input.attachmentId,
381
+ residentSubscriptionId: resident.subscriptionId,
382
+ residentRuntimeAttachmentId: resident.runtimeAttachmentId,
383
+ });
384
+ const abort = () => queue.finish();
385
+ input.signal.addEventListener('abort', abort, { once: true });
386
+ if (input.signal.aborted)
387
+ abort();
219
388
  yield* queue.read();
389
+ input.signal.removeEventListener('abort', abort);
220
390
  }
221
391
  finally {
222
- input.signal.removeEventListener('abort', abort);
223
392
  this.removeFollower(key, follower);
224
- if (this.controllers.get(key)?.subscriptionId === attachment.subscriptionId) {
225
- this.controllers.delete(key);
226
- this.broadcastState(input.identity);
227
- }
228
- await this.runtimes.detach(attachment.subscriptionId);
393
+ this.removeController(key, input.attachmentId);
394
+ debugInfo('codingns4dsh: terminal service follow unsubscribed', {
395
+ identity: input.identity,
396
+ attachmentId: input.attachmentId,
397
+ residentSubscriptionId: resident.subscriptionId,
398
+ });
229
399
  }
230
400
  }
231
401
  async write(identity, attachmentId, data) {
@@ -239,6 +409,11 @@ export class CodingNsTerminalService {
239
409
  this.requireInitialized();
240
410
  await this.enqueue(identity, async () => {
241
411
  const record = this.requireAvailable(identity);
412
+ const resident = this.residents.get(identityKey(identity));
413
+ if (resident !== undefined) {
414
+ await this.runtimes.write(resident.subscriptionId, data);
415
+ return;
416
+ }
242
417
  await this.runtimes.writeSession(record, data);
243
418
  });
244
419
  }
@@ -263,29 +438,41 @@ export class CodingNsTerminalService {
263
438
  }
264
439
  async close(identity) {
265
440
  this.requireInitialized();
441
+ debugInfo('codingns4dsh: terminal service close start', { identity });
266
442
  await this.enqueue(identity, async () => {
267
443
  const record = this.store.get(identity);
268
- if (record === undefined || record.state === 'closed')
444
+ if (record === undefined || record.state === 'closed') {
445
+ debugInfo('codingns4dsh: terminal service close noop', { identity, record: record === undefined ? null : recordSummary(record) });
269
446
  return;
447
+ }
270
448
  const closing = record.state === 'closing' ? record : await this.update(record, { state: 'closing' });
271
449
  this.broadcastState(identity);
450
+ await this.releaseResident(identity);
272
451
  await this.runtimes.terminate(closing);
452
+ debugInfo('codingns4dsh: terminal service runtime terminated', { identity, record: recordSummary(closing) });
273
453
  await this.runtimes.detachTerminal(identity);
274
454
  this.controllers.delete(identityKey(identity));
275
455
  this.exitCallbacks.delete(identityKey(identity));
276
456
  const closed = await this.update(closing, { state: 'closed', exitCode: null, error: '' });
277
457
  this.broadcastState(identity, closed);
278
458
  this.finishFollowers(identityKey(identity));
459
+ debugInfo('codingns4dsh: terminal service close success', { identity, record: recordSummary(closed) });
279
460
  });
280
461
  }
281
462
  async detachGeneration(generation) {
463
+ // 浏览器 attach 已经只是 resident 的订阅,不再拥有 backend attachment;
464
+ // RuntimeManager 的 generation 清理仍保留,用于兼容旧的 monitor/调用方。
282
465
  await this.runtimes.detachGeneration(generation);
283
466
  const changed = new Set();
284
- for (const [key, binding] of this.controllers) {
285
- if (binding.generation !== generation)
286
- continue;
287
- this.controllers.delete(key);
288
- changed.add(key);
467
+ for (const [key, bindings] of this.controllers) {
468
+ for (const [attachmentId, binding] of bindings) {
469
+ if (binding.generation !== generation)
470
+ continue;
471
+ bindings.delete(attachmentId);
472
+ changed.add(key);
473
+ }
474
+ if (bindings.size === 0)
475
+ this.controllers.delete(key);
289
476
  }
290
477
  for (const [key, followers] of this.followers) {
291
478
  for (const follower of followers) {
@@ -305,13 +492,14 @@ export class CodingNsTerminalService {
305
492
  follower.queue.finish();
306
493
  this.followers.clear();
307
494
  this.controllers.clear();
495
+ this.residents.clear();
308
496
  this.exitCallbacks.clear();
309
497
  await this.runtimes.dispose();
310
498
  }
311
499
  requireController(identity, attachmentId) {
312
500
  this.requireAvailable(identity);
313
- const controller = this.controllers.get(identityKey(identity));
314
- if (controller?.attachmentId !== attachmentId) {
501
+ const controller = this.controllers.get(identityKey(identity))?.get(attachmentId);
502
+ if (controller === undefined) {
315
503
  throw new TerminalServiceError('TERMINAL_CONTROL_UNAVAILABLE', '当前 attach 没有终端输入控制权');
316
504
  }
317
505
  return controller;
@@ -340,34 +528,74 @@ export class CodingNsTerminalService {
340
528
  this.broadcastState(identity, failed);
341
529
  }
342
530
  }
343
- async handleRuntimeExit(identity, exitCode, queue) {
531
+ /**
532
+ * 浏览器 attach 客户端结束了。
533
+ *
534
+ * tmux 客户端在"服务器消失""会话被销毁""只是这次连接断了"时都会退出,退出码
535
+ * 本身不能证明 shell 结束。因此这里必须回查运行时的真实状态:只有确实拿到
536
+ * shell 的退出码才算 `exited`,否则只能标记 `lost`,让用户重新连接而不是看到
537
+ * 一个假的"进程已退出(1)"。
538
+ */
539
+ async handleAttachmentExit(identity, exitCode, queue) {
344
540
  try {
345
- await this.markExited(identity, exitCode);
541
+ await this.finishRuntime(identity, exitCode);
346
542
  }
347
543
  finally {
348
- // 先发 exited 元数据,再结束 Remote 流;否则官方 Client 会把正常退出误判为 attach 故障。
544
+ // 先发终态元数据,再结束 Remote 流;否则官方 Client 会把正常退出误判为 attach 故障。
349
545
  queue.finish();
350
546
  }
351
547
  }
352
- async markExited(identity, exitCode) {
548
+ /** 以运行时的实际状态收尾:能确认真实退出码才算 exited,否则是 lost。 */
549
+ async finishRuntime(identity, exitCode) {
550
+ debugInfo('codingns4dsh: terminal service runtime exit observed', { identity, callbackExitCode: exitCode });
353
551
  await this.enqueue(identity, async () => {
354
552
  const record = this.store.get(identity);
355
553
  if (record === undefined || record.state !== 'running')
356
554
  return;
357
- const exited = await this.update(record, { state: 'exited', exitCode, error: '' });
358
- this.broadcastState(identity, exited);
555
+ let runtime;
556
+ try {
557
+ runtime = await this.runtimes.inspect(record);
558
+ }
559
+ catch (error) {
560
+ const failed = await this.update(record, { state: 'error', error: errorMessage(error) });
561
+ this.broadcastState(identity, failed);
562
+ return;
563
+ }
564
+ debugInfo('codingns4dsh: terminal service runtime exit inspect', {
565
+ identity,
566
+ record: recordSummary(record),
567
+ runtime: runtimeSummary(runtime),
568
+ });
569
+ if (runtime.alive)
570
+ return;
571
+ const confirmed = runtime.exitCode ?? null;
572
+ const kind = runtime.exitCode === undefined || runtime.exitCode === null ? 'lost' : 'exited';
573
+ const finished = kind === 'exited'
574
+ ? await this.update(record, { state: 'exited', exitCode: confirmed, error: '' })
575
+ : await this.update(record, {
576
+ state: 'lost',
577
+ exitCode: null,
578
+ error: runtime.detail ?? `持久终端连接中断(客户端退出码 ${exitCode ?? '未知'})`,
579
+ });
580
+ this.broadcastState(identity, finished);
359
581
  const callback = this.exitCallbacks.get(identityKey(identity));
360
582
  this.exitCallbacks.delete(identityKey(identity));
361
- await callback?.(exitCode);
583
+ await callback?.(confirmed, kind);
584
+ debugInfo('codingns4dsh: terminal service runtime finalized', {
585
+ identity,
586
+ kind,
587
+ record: recordSummary(finished),
588
+ });
362
589
  });
363
590
  }
364
- info(record) {
365
- const controllerId = this.controllers.get(identityKey(record))?.attachmentId;
591
+ info(record, controllerId) {
366
592
  const state = record.state === 'running' || record.state === 'starting'
367
593
  ? 'running'
368
594
  : record.state === 'closed' || record.state === 'exited'
369
595
  ? 'exited'
370
- : 'failed';
596
+ : record.state === 'lost'
597
+ ? 'lost'
598
+ : 'failed';
371
599
  return {
372
600
  id: record.terminalId,
373
601
  title: record.title,
@@ -381,18 +609,210 @@ export class CodingNsTerminalService {
381
609
  ...(controllerId === undefined ? {} : { controllerId }),
382
610
  };
383
611
  }
612
+ /**
613
+ * 确保一个终端只有一条 Host backend attachment。
614
+ *
615
+ * 这个连接不绑定某个浏览器 generation,直到显式 close 或 Host dispose 才释放。
616
+ * 多个会话的 follow 都复用同一条连接,避免每次点击都重新启动 tmux client/PTY。
617
+ */
618
+ async ensureResident(record) {
619
+ const key = identityKey(record);
620
+ const existing = this.residents.get(key);
621
+ if (existing !== undefined)
622
+ return existing;
623
+ const pending = this.residentPromises.get(key);
624
+ if (pending !== undefined)
625
+ return pending;
626
+ const identity = identityOfRecord(record);
627
+ // 每次 resident 都使用独立 token。旧 backend attachment 的迟到回调不能
628
+ // 误删同一 terminalId 后面刚重建的新 resident。
629
+ const generation = `host-resident:${key}:${randomUUID()}`;
630
+ const requestedAttachmentId = `resident:${record.terminalId}`;
631
+ // resident 断线重连时,tmux attach 的首批输出是整屏重绘;它可能在
632
+ // backend.attach 返回后异步到达,不能只依赖 runtime-manager 的 earlyData。
633
+ // 在短暂启动窗口内先收集,随后以 snapshot 发给已有 follower。
634
+ const hadFollowers = (this.followers.get(key)?.size ?? 0) > 0;
635
+ // attach 尚未完成时先无限期收集 earlyData;真正的窗口从 attachment
636
+ // 建立后开始计算,避免慢机器上首屏在 100ms 后才到达而被误当增量。
637
+ let bootstrapDeadline = Number.POSITIVE_INFINITY;
638
+ let bootstrapReplay = '';
639
+ let replay = '';
640
+ let task;
641
+ task = this.runtimes.attach(record, {
642
+ identity,
643
+ generation,
644
+ streamId: requestedAttachmentId,
645
+ requestedAttachmentId,
646
+ cols: record.cols,
647
+ rows: record.rows,
648
+ onData: (data) => {
649
+ replay = boundedAppend(replay, data);
650
+ const resident = this.residents.get(key);
651
+ if (resident !== undefined)
652
+ resident.replay = replay;
653
+ if (hadFollowers && Date.now() < bootstrapDeadline) {
654
+ bootstrapReplay = boundedAppend(bootstrapReplay, data);
655
+ return;
656
+ }
657
+ this.broadcastOutput(key, data);
658
+ },
659
+ onExit: (exitCode) => { void this.handleResidentExit(identity, generation, exitCode); },
660
+ }).then(async (attachment) => {
661
+ const resident = {
662
+ subscriptionId: attachment.subscriptionId,
663
+ runtimeAttachmentId: attachment.runtimeAttachmentId,
664
+ generation,
665
+ replay,
666
+ };
667
+ this.residents.set(key, resident);
668
+ // tmux attach 的原始 ANSI 首屏不含 scrollback;把 server buffer 的纯文本历史
669
+ // 作为恢复快照,后续 output 仍由常驻连接增量推送。
670
+ try {
671
+ const captured = await this.runtimes.captureHistory(record, RESIDENT_HISTORY_LINES);
672
+ if (captured !== undefined) {
673
+ replay = captured;
674
+ resident.replay = captured;
675
+ debugInfo('codingns4dsh: terminal resident history captured', {
676
+ identity,
677
+ historyLines: countLines(captured),
678
+ historyCharacters: captured.length,
679
+ });
680
+ }
681
+ }
682
+ catch (error) {
683
+ debugWarn('codingns4dsh: terminal resident history capture failed', {
684
+ identity,
685
+ error: errorMessage(error),
686
+ });
687
+ }
688
+ bootstrapDeadline = Date.now() + RESIDENT_BOOTSTRAP_WINDOW_MS;
689
+ if (hadFollowers) {
690
+ // 等待 tmux attach 的初始整屏重绘收齐,再替换已有 xterm 画面。
691
+ // 新 follow 不走这里,它会直接收到上面的 snapshot。
692
+ setTimeout(() => {
693
+ const current = this.residents.get(key);
694
+ if (current?.generation !== generation || bootstrapReplay === '')
695
+ return;
696
+ const currentRecord = this.store.get(identity);
697
+ if (currentRecord === undefined || currentRecord.state !== 'running')
698
+ return;
699
+ for (const follower of this.followers.get(key) ?? []) {
700
+ follower.queue.pushSnapshot(this.info(currentRecord, follower.attachmentId), current.replay);
701
+ }
702
+ debugInfo('codingns4dsh: terminal resident bootstrap replay promoted to snapshot', {
703
+ identity,
704
+ generation,
705
+ replayCharacters: current.replay.length,
706
+ });
707
+ }, RESIDENT_BOOTSTRAP_WINDOW_MS);
708
+ }
709
+ debugInfo('codingns4dsh: terminal resident connection ready', {
710
+ identity,
711
+ subscriptionId: resident.subscriptionId,
712
+ runtimeAttachmentId: resident.runtimeAttachmentId,
713
+ generation,
714
+ });
715
+ return resident;
716
+ }).finally(() => {
717
+ if (this.residentPromises.get(key) === task)
718
+ this.residentPromises.delete(key);
719
+ });
720
+ this.residentPromises.set(key, task);
721
+ return task;
722
+ }
723
+ async releaseResident(identity) {
724
+ const key = identityKey(identity);
725
+ const pending = this.residentPromises.get(key);
726
+ if (pending !== undefined) {
727
+ try {
728
+ await pending;
729
+ }
730
+ catch { /* 创建失败时没有可释放的连接 */ }
731
+ }
732
+ const resident = this.residents.get(key);
733
+ if (resident === undefined)
734
+ return;
735
+ this.residents.delete(key);
736
+ await this.runtimes.detach(resident.subscriptionId);
737
+ debugInfo('codingns4dsh: terminal resident connection released', {
738
+ identity,
739
+ subscriptionId: resident.subscriptionId,
740
+ runtimeAttachmentId: resident.runtimeAttachmentId,
741
+ });
742
+ }
743
+ async handleResidentExit(identity, generation, exitCode) {
744
+ const key = identityKey(identity);
745
+ const resident = this.residents.get(key);
746
+ if (resident === undefined || resident.generation !== generation) {
747
+ debugInfo('codingns4dsh: terminal resident stale exit ignored', {
748
+ identity,
749
+ generation,
750
+ currentGeneration: resident?.generation ?? null,
751
+ exitCode,
752
+ });
753
+ return;
754
+ }
755
+ this.residents.delete(key);
756
+ // 这条 backend 连接已经不能继续向浏览器送数据。先撤销输入控制权;
757
+ // 是否结束订阅要等 finishRuntime 判断出“连接断开”还是“Shell 退出”之后决定。
758
+ this.controllers.delete(key);
759
+ try {
760
+ await this.runtimes.detach(resident.subscriptionId);
761
+ }
762
+ catch { /* backend 已经回收 attachment */ }
763
+ await this.finishRuntime(identity, exitCode);
764
+ const current = this.store.get(identity);
765
+ if (current?.state === 'running') {
766
+ // tmux client 断线不等于 Shell 结束。连接层恢复时保留已有 follower,
767
+ // 重新建立 resident;真正退出则由 finishRuntime 收敛并结束所有订阅。
768
+ try {
769
+ const nextResident = await this.ensureResident(current);
770
+ // resident 重建后 subscriptionId 已变化,原 follower 仍然有效,必须
771
+ // 重新绑定它们的输入与 resize 控制权。
772
+ for (const follower of this.followers.get(key) ?? []) {
773
+ this.addController(key, follower.attachmentId, {
774
+ attachmentId: follower.attachmentId,
775
+ subscriptionId: nextResident.subscriptionId,
776
+ generation: follower.generation,
777
+ });
778
+ }
779
+ debugInfo('codingns4dsh: terminal resident connection reattached', {
780
+ identity,
781
+ subscriptionId: nextResident.subscriptionId,
782
+ });
783
+ return;
784
+ }
785
+ catch (error) {
786
+ debugWarn('codingns4dsh: terminal resident connection reattach failed', {
787
+ identity,
788
+ error: errorMessage(error),
789
+ });
790
+ }
791
+ }
792
+ // finishRuntime 已经把 exited/lost 状态推入队列,队列此时才可结束,
793
+ // 保证 Client 先收到终态再看到流结束。
794
+ this.finishFollowers(key);
795
+ }
384
796
  async update(record, patch) {
385
797
  const next = await this.store.patch(record, { ...patch, updatedAt: this.now().toISOString() });
386
798
  if (next === undefined)
387
799
  throw new Error('终端记录在更新期间消失');
800
+ if (record.state !== next.state || patch.error !== undefined || patch.exitCode !== undefined) {
801
+ debugInfo('codingns4dsh: terminal service state update', {
802
+ identity: identityOfRecord(record),
803
+ before: recordSummary(record),
804
+ patch,
805
+ after: recordSummary(next),
806
+ });
807
+ }
388
808
  return next;
389
809
  }
390
810
  broadcastState(identity, record = this.store.get(identity)) {
391
811
  if (record === undefined)
392
812
  return;
393
- const info = this.info(record);
394
- for (const follower of this.followers.get(identityKey(identity)) ?? [])
395
- follower.queue.pushState(info);
813
+ for (const follower of this.followers.get(identityKey(identity)) ?? []) {
814
+ follower.queue.pushState(this.info(record, follower.attachmentId));
815
+ }
396
816
  }
397
817
  broadcastStateByKey(key) {
398
818
  const record = this.store.list().find((candidate) => identityKey(candidate) === key);
@@ -407,6 +827,26 @@ export class CodingNsTerminalService {
407
827
  }
408
828
  followers.add(follower);
409
829
  }
830
+ addController(key, attachmentId, binding) {
831
+ let bindings = this.controllers.get(key);
832
+ if (bindings === undefined) {
833
+ bindings = new Map();
834
+ this.controllers.set(key, bindings);
835
+ }
836
+ bindings.set(attachmentId, binding);
837
+ }
838
+ removeController(key, attachmentId) {
839
+ const bindings = this.controllers.get(key);
840
+ if (bindings === undefined)
841
+ return;
842
+ bindings.delete(attachmentId);
843
+ if (bindings.size === 0)
844
+ this.controllers.delete(key);
845
+ }
846
+ broadcastOutput(key, data) {
847
+ for (const follower of this.followers.get(key) ?? [])
848
+ follower.queue.pushOutput(data);
849
+ }
410
850
  removeFollower(key, follower) {
411
851
  const followers = this.followers.get(key);
412
852
  followers?.delete(follower);
@@ -453,11 +893,14 @@ class TerminalFrameQueue {
453
893
  constructor(maxBytes) {
454
894
  this.maxBytes = maxBytes;
455
895
  }
456
- pushSnapshot(info) {
457
- this.push({ type: 'snapshot', sequence: this.sequence, screen: '', info });
896
+ pushSnapshot(info, screen = '') {
897
+ // tmux capture-pane 返回纯文本 LF;xterm 的真实 PTY 模式不会自动把 LF
898
+ // 转成 CRLF,必须在 wire 边界补回 CR,否则历史行会逐行继承上一行的列位置。
899
+ this.push({ type: 'snapshot', sequence: this.sequence, screen: normalizeSnapshotLineEndings(screen), info });
458
900
  }
459
901
  pushOutput(data) {
460
902
  this.sequence += 1;
903
+ // 增量 PTY 输出保留原始 VT 语义;只有 capture-pane 的纯文本快照需要补 CR。
461
904
  this.push({ type: 'output', sequence: this.sequence, data });
462
905
  }
463
906
  pushState(info) {
@@ -515,6 +958,33 @@ function shellInfo(record) {
515
958
  function identityKey(identity) {
516
959
  return JSON.stringify([identity.hostId, identity.workspaceId, identity.terminalId]);
517
960
  }
961
+ function identityOfRecord(record) {
962
+ return {
963
+ hostId: record.hostId,
964
+ workspaceId: record.workspaceId,
965
+ terminalId: record.terminalId,
966
+ };
967
+ }
968
+ function recordSummary(record) {
969
+ return {
970
+ ...identityOfRecord(record),
971
+ dshSessionId: record.dshSessionId ?? null,
972
+ runtimeSessionKey: record.runtimeSessionKey,
973
+ runtimeType: record.runtimeType,
974
+ state: record.state,
975
+ exitCode: record.exitCode,
976
+ error: record.error || null,
977
+ };
978
+ }
979
+ function runtimeSummary(runtime) {
980
+ return {
981
+ alive: runtime.alive,
982
+ runtimePid: runtime.runtimePid ?? null,
983
+ shellPid: runtime.shellPid ?? null,
984
+ exitCode: runtime.exitCode ?? null,
985
+ detail: runtime.detail ?? null,
986
+ };
987
+ }
518
988
  function validateTerminalId(id) {
519
989
  if (!/^[\w-]{1,128}$/u.test(id))
520
990
  throw new TypeError('终端标识无效');
@@ -527,6 +997,26 @@ function validateSize(cols, rows) {
527
997
  function frameBytes(frame) {
528
998
  return new TextEncoder().encode(JSON.stringify(frame)).byteLength;
529
999
  }
1000
+ function normalizeSnapshotLineEndings(value) {
1001
+ return value.replace(/\r?\n/gu, '\r\n');
1002
+ }
1003
+ /** 常驻连接只保留有限的原始输出,避免长期运行终端无限增长。 */
1004
+ const MAX_RESIDENT_REPLAY_CHARACTERS = 1024 * 1024;
1005
+ /** tmux attach 的首屏通常在一个很短的窗口内分片送达。 */
1006
+ const RESIDENT_BOOTSTRAP_WINDOW_MS = 100;
1007
+ /** 初次恢复读取的 tmux 历史行数;xterm 会再按自身 scrollback 上限裁剪。 */
1008
+ const RESIDENT_HISTORY_LINES = 2000;
1009
+ function countLines(value) {
1010
+ if (value.length === 0)
1011
+ return 0;
1012
+ return value.split('\n').length;
1013
+ }
1014
+ function boundedAppend(current, data) {
1015
+ const combined = current + data;
1016
+ return combined.length <= MAX_RESIDENT_REPLAY_CHARACTERS
1017
+ ? combined
1018
+ : combined.slice(-MAX_RESIDENT_REPLAY_CHARACTERS);
1019
+ }
530
1020
  function untilAborted(signal) {
531
1021
  if (signal.aborted)
532
1022
  return Promise.resolve();