dsh-zen-remote 1.1.16 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/AGENTS.md +54 -9
  2. package/README.md +249 -190
  3. package/README.zh-CN.md +247 -201
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +12 -11
  10. package/cordis.patch.yml.example +24 -9
  11. package/docs/interface.md +57 -2
  12. package/docs/remote-access.en.md +99 -58
  13. package/docs/remote-access.md +99 -42
  14. package/dsh-push.mjs +67 -54
  15. package/lan-gate.mjs +88 -20
  16. package/lib/activity.js +168 -0
  17. package/lib/activity.js.map +1 -0
  18. package/lib/admin-routes.js +526 -0
  19. package/lib/admin-routes.js.map +1 -0
  20. package/lib/client-pairing.js +224 -0
  21. package/lib/client-pairing.js.map +1 -0
  22. package/lib/client-routes.js +674 -0
  23. package/lib/client-routes.js.map +1 -0
  24. package/lib/client.js +10051 -4777
  25. package/lib/config.js +346 -0
  26. package/lib/config.js.map +1 -0
  27. package/lib/fetch-route-intercept.js +418 -0
  28. package/lib/fetch-route-intercept.js.map +1 -0
  29. package/lib/fingerprint.js +363 -0
  30. package/lib/fingerprint.js.map +1 -0
  31. package/lib/http.js +69 -0
  32. package/lib/http.js.map +1 -0
  33. package/lib/index.js +675 -54
  34. package/lib/index.js.map +1 -1
  35. package/lib/intercept-shape.js +81 -0
  36. package/lib/intercept-shape.js.map +1 -0
  37. package/lib/intercept.js +3040 -0
  38. package/lib/intercept.js.map +1 -0
  39. package/lib/lan-gate-server.cjs +532 -121
  40. package/lib/merge-streams.js +1165 -0
  41. package/lib/merge-streams.js.map +1 -0
  42. package/lib/relay-access.js +496 -0
  43. package/lib/relay-access.js.map +1 -0
  44. package/lib/relay-client.js +1303 -0
  45. package/lib/relay-client.js.map +1 -0
  46. package/lib/relay-filter.js +282 -0
  47. package/lib/relay-filter.js.map +1 -0
  48. package/lib/relay-server.js +1792 -0
  49. package/lib/relay-server.js.map +1 -0
  50. package/lib/restart-fields.js +34 -0
  51. package/lib/restart-fields.js.map +1 -0
  52. package/lib/restart-watcher.js +106 -0
  53. package/lib/restart-watcher.js.map +1 -0
  54. package/lib/session-reference.js +157 -0
  55. package/lib/session-reference.js.map +1 -0
  56. package/lib/share-export.js +16 -1
  57. package/lib/share-export.js.map +1 -1
  58. package/lib/share-ops.js +86 -0
  59. package/lib/share-ops.js.map +1 -0
  60. package/lib/share-store.js +294 -0
  61. package/lib/share-store.js.map +1 -0
  62. package/lib/types/activity.d.ts +99 -0
  63. package/lib/types/activity.d.ts.map +1 -0
  64. package/lib/types/admin-routes.d.ts +129 -0
  65. package/lib/types/admin-routes.d.ts.map +1 -0
  66. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -1
  67. package/lib/types/client/MobileHome.d.ts +1 -1
  68. package/lib/types/client/MobileHome.d.ts.map +1 -1
  69. package/lib/types/client/MobileHomeChips.d.ts +1 -1
  70. package/lib/types/client/MobileHomeChips.d.ts.map +1 -1
  71. package/lib/types/client/MobileSessionHeader.d.ts +13 -2
  72. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  73. package/lib/types/client/MobileSessionInfo.d.ts +20 -3
  74. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  75. package/lib/types/client/RemoteComposerBanner.d.ts +42 -0
  76. package/lib/types/client/RemoteComposerBanner.d.ts.map +1 -0
  77. package/lib/types/client/RemoteHeaderIcon.d.ts +37 -0
  78. package/lib/types/client/RemoteHeaderIcon.d.ts.map +1 -0
  79. package/lib/types/client/RemoteShareMenu.d.ts +28 -0
  80. package/lib/types/client/RemoteShareMenu.d.ts.map +1 -0
  81. package/lib/types/client/RemoteStatusIcon.d.ts +36 -0
  82. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -0
  83. package/lib/types/client/compat/desktop.d.ts +21 -0
  84. package/lib/types/client/compat/desktop.d.ts.map +1 -0
  85. package/lib/types/client/compat/types.d.ts +57 -0
  86. package/lib/types/client/compat/types.d.ts.map +1 -1
  87. package/lib/types/client/current-session-report.d.ts +37 -0
  88. package/lib/types/client/current-session-report.d.ts.map +1 -0
  89. package/lib/types/client/effects/header-status.d.ts.map +1 -1
  90. package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
  91. package/lib/types/client/effects/permission-sheet.d.ts +39 -0
  92. package/lib/types/client/effects/permission-sheet.d.ts.map +1 -0
  93. package/lib/types/client/effects/remote-session.d.ts +25 -0
  94. package/lib/types/client/effects/remote-session.d.ts.map +1 -0
  95. package/lib/types/client/effects/turn-fold.d.ts +9 -0
  96. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  97. package/lib/types/client/index.d.ts.map +1 -1
  98. package/lib/types/client/locales.d.ts +187 -2
  99. package/lib/types/client/locales.d.ts.map +1 -1
  100. package/lib/types/client/remote-fetch.d.ts +51 -0
  101. package/lib/types/client/remote-fetch.d.ts.map +1 -0
  102. package/lib/types/client/remote-group-expansion.d.ts +41 -0
  103. package/lib/types/client/remote-group-expansion.d.ts.map +1 -0
  104. package/lib/types/client/remote-share-css.d.ts +19 -0
  105. package/lib/types/client/remote-share-css.d.ts.map +1 -0
  106. package/lib/types/client/remote-share-register.d.ts +113 -0
  107. package/lib/types/client/remote-share-register.d.ts.map +1 -0
  108. package/lib/types/client/remote-status-css.d.ts +15 -0
  109. package/lib/types/client/remote-status-css.d.ts.map +1 -0
  110. package/lib/types/client/remote-status-register.d.ts +46 -0
  111. package/lib/types/client/remote-status-register.d.ts.map +1 -0
  112. package/lib/types/client/settings/SettingsSection.d.ts +46 -0
  113. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -0
  114. package/lib/types/client/settings/register-settings.d.ts +58 -0
  115. package/lib/types/client/settings/register-settings.d.ts.map +1 -0
  116. package/lib/types/client/settings/settings-css.d.ts +10 -0
  117. package/lib/types/client/settings/settings-css.d.ts.map +1 -0
  118. package/lib/types/client/share/share-preview.d.ts.map +1 -1
  119. package/lib/types/client/styles/composer.css.d.ts +1 -1
  120. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  121. package/lib/types/client/styles/header.css.d.ts +1 -1
  122. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  123. package/lib/types/client/styles/info.css.d.ts +1 -1
  124. package/lib/types/client/styles/info.css.d.ts.map +1 -1
  125. package/lib/types/client/styles/remote-session.css.d.ts +2 -0
  126. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -0
  127. package/lib/types/client-data/current-session.d.ts +41 -0
  128. package/lib/types/client-data/current-session.d.ts.map +1 -0
  129. package/lib/types/client-data/remote-group-expansion.d.ts +76 -0
  130. package/lib/types/client-data/remote-group-expansion.d.ts.map +1 -0
  131. package/lib/types/client-data/remote-status.d.ts +148 -0
  132. package/lib/types/client-data/remote-status.d.ts.map +1 -0
  133. package/lib/types/client-data/settings-form.d.ts +774 -0
  134. package/lib/types/client-data/settings-form.d.ts.map +1 -0
  135. package/lib/types/client-data/shares.d.ts +211 -0
  136. package/lib/types/client-data/shares.d.ts.map +1 -0
  137. package/lib/types/client-pairing.d.ts +114 -0
  138. package/lib/types/client-pairing.d.ts.map +1 -0
  139. package/lib/types/client-routes.d.ts +112 -0
  140. package/lib/types/client-routes.d.ts.map +1 -0
  141. package/lib/types/config.d.ts +200 -0
  142. package/lib/types/config.d.ts.map +1 -0
  143. package/lib/types/fetch-route-intercept.d.ts +156 -0
  144. package/lib/types/fetch-route-intercept.d.ts.map +1 -0
  145. package/lib/types/fingerprint.d.ts +164 -0
  146. package/lib/types/fingerprint.d.ts.map +1 -0
  147. package/lib/types/http.d.ts +41 -0
  148. package/lib/types/http.d.ts.map +1 -0
  149. package/lib/types/index.d.ts +56 -24
  150. package/lib/types/index.d.ts.map +1 -1
  151. package/lib/types/intercept-shape.d.ts +32 -0
  152. package/lib/types/intercept-shape.d.ts.map +1 -0
  153. package/lib/types/intercept.d.ts +432 -0
  154. package/lib/types/intercept.d.ts.map +1 -0
  155. package/lib/types/merge-streams.d.ts +393 -0
  156. package/lib/types/merge-streams.d.ts.map +1 -0
  157. package/lib/types/relay-access.d.ts +310 -0
  158. package/lib/types/relay-access.d.ts.map +1 -0
  159. package/lib/types/relay-client.d.ts +274 -0
  160. package/lib/types/relay-client.d.ts.map +1 -0
  161. package/lib/types/relay-filter.d.ts +103 -0
  162. package/lib/types/relay-filter.d.ts.map +1 -0
  163. package/lib/types/relay-server.d.ts +208 -0
  164. package/lib/types/relay-server.d.ts.map +1 -0
  165. package/lib/types/restart-fields.d.ts +22 -0
  166. package/lib/types/restart-fields.d.ts.map +1 -0
  167. package/lib/types/restart-watcher.d.ts +83 -0
  168. package/lib/types/restart-watcher.d.ts.map +1 -0
  169. package/lib/types/session-reference.d.ts +66 -0
  170. package/lib/types/session-reference.d.ts.map +1 -0
  171. package/lib/types/share-export.d.ts +14 -1
  172. package/lib/types/share-export.d.ts.map +1 -1
  173. package/lib/types/share-ops.d.ts +79 -0
  174. package/lib/types/share-ops.d.ts.map +1 -0
  175. package/lib/types/share-store.d.ts +87 -0
  176. package/lib/types/share-store.d.ts.map +1 -0
  177. package/lib/types/virtual-id.d.ts +38 -0
  178. package/lib/types/virtual-id.d.ts.map +1 -0
  179. package/lib/virtual-id.js +52 -0
  180. package/lib/virtual-id.js.map +1 -0
  181. package/llms.txt +17 -14
  182. package/package.json +60 -50
  183. package/scripts/build-client.mjs +5 -3
  184. package/scripts/check-host-shadow.mjs +19 -2
  185. package/scripts/check-settings-form.mjs +933 -0
  186. package/scripts/check-shares.mjs +397 -0
  187. package/scripts/check-upload-endpoint.mjs +59 -3
  188. package/scripts/sync-doc-version.mjs +8 -15
  189. package/scripts/verify-fingerprint-live.mjs +111 -0
  190. package/src/activity.ts +218 -0
  191. package/src/admin-routes.ts +627 -0
  192. package/src/client/MobileAttachButton.tsx +2 -2
  193. package/src/client/MobileAttachChips.tsx +3 -3
  194. package/src/client/MobileDrawerFooter.tsx +7 -4
  195. package/src/client/MobileHome.tsx +20 -16
  196. package/src/client/MobileHomeChips.tsx +16 -16
  197. package/src/client/MobileNavOverlay.tsx +2 -2
  198. package/src/client/MobileNavToggle.tsx +3 -3
  199. package/src/client/MobileSessionHeader.tsx +63 -42
  200. package/src/client/MobileSessionInfo.tsx +118 -17
  201. package/src/client/RemoteComposerBanner.tsx +76 -0
  202. package/src/client/RemoteHeaderIcon.tsx +103 -0
  203. package/src/client/RemoteShareMenu.tsx +57 -0
  204. package/src/client/RemoteStatusIcon.tsx +91 -0
  205. package/src/client/compat/desktop.ts +22 -0
  206. package/src/client/compat/types.ts +62 -0
  207. package/src/client/current-session-report.ts +164 -0
  208. package/src/client/effects/header-status.ts +21 -17
  209. package/src/client/effects/keyboard-guard.ts +65 -1
  210. package/src/client/effects/permission-sheet.ts +149 -0
  211. package/src/client/effects/remote-session.ts +68 -0
  212. package/src/client/effects/turn-fold.ts +52 -0
  213. package/src/client/index.tsx +151 -36
  214. package/src/client/locales.ts +396 -2
  215. package/src/client/remote-fetch.ts +125 -0
  216. package/src/client/remote-group-expansion.ts +190 -0
  217. package/src/client/remote-share-css.ts +54 -0
  218. package/src/client/remote-share-register.ts +244 -0
  219. package/src/client/remote-status-css.ts +64 -0
  220. package/src/client/remote-status-register.ts +107 -0
  221. package/src/client/settings/SettingsSection.tsx +1229 -0
  222. package/src/client/settings/register-settings.ts +116 -0
  223. package/src/client/settings/settings-css.ts +236 -0
  224. package/src/client/share/share-preview.tsx +6 -3
  225. package/src/client/share/share-sheet.tsx +3 -3
  226. package/src/client/styles/composer.css.ts +119 -26
  227. package/src/client/styles/header.css.ts +7 -7
  228. package/src/client/styles/info.css.ts +74 -0
  229. package/src/client/styles/remote-session.css.ts +251 -0
  230. package/src/client-data/current-session.ts +57 -0
  231. package/src/client-data/remote-group-expansion.ts +145 -0
  232. package/src/client-data/remote-status.ts +374 -0
  233. package/src/client-data/settings-form.ts +1371 -0
  234. package/src/client-data/shares.ts +467 -0
  235. package/src/client-pairing.ts +244 -0
  236. package/src/client-routes.ts +732 -0
  237. package/src/config.ts +447 -0
  238. package/src/fetch-route-intercept.ts +510 -0
  239. package/src/fingerprint.ts +408 -0
  240. package/src/http.ts +68 -0
  241. package/src/index.ts +719 -62
  242. package/src/intercept-shape.ts +87 -0
  243. package/src/intercept.ts +3362 -0
  244. package/src/merge-streams.ts +1272 -0
  245. package/src/relay-access.ts +669 -0
  246. package/src/relay-client.ts +1514 -0
  247. package/src/relay-filter.ts +291 -0
  248. package/src/relay-server.ts +2025 -0
  249. package/src/restart-fields.ts +36 -0
  250. package/src/restart-watcher.ts +140 -0
  251. package/src/session-reference.ts +156 -0
  252. package/src/share-export.ts +26 -1
  253. package/src/share-ops.ts +114 -0
  254. package/src/share-store.ts +373 -0
  255. package/src/virtual-id.ts +59 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Pure mergers for the three GLOBAL calls (T23b-2): `workspace/follow` and
3
+ * `session/control` are STREAMS whose frames carry no session-locating
4
+ * argument, and `session/list` is an unscoped invoke — the sidebar reads all
5
+ * three to render its tree, so a sub-client must see the server's shared
6
+ * sessions merged INTO the local answer instead of in place of it.
7
+ *
8
+ * Frame and result shapes are the generated 0.2.0 codecs, field by field:
9
+ * `workspace_follow_result` (baseline / upsert / remove / order / archived /
10
+ * pinned), `session_control_result` (exactly two frame types: `baseline`
11
+ * nesting `value.projections`, and one-key `projection` updates), and
12
+ * `session_list_result` (`{ items }` — no cursor field). The same shapes the
13
+ * server filters by (relay-filter.ts); remote frames arriving here have
14
+ * ALREADY been narrowed to the shared sessions and carry ORIGINAL ids.
15
+ *
16
+ * The rules, mirroring tasks/T23b2.md as corrected by tasks/T23b2-fix.md:
17
+ * - local frames pass through, except that `baseline` / `order` get the
18
+ * remote workspaces APPENDED to their order (remote groups sort after
19
+ * local) and the `archived` / `pinned` lists merge local + remote;
20
+ * - a remote baseline NEVER travels as a second baseline — it would wipe the
21
+ * local data. It becomes one virtualized `upsert` per remote workspace,
22
+ * then the merged `order` / `archived` / `pinned`;
23
+ * - remote frames that arrive before the local baseline are CACHED as state
24
+ * only — the UI has seen nothing yet, so emitting them would order
25
+ * workspaces the local baseline would immediately erase;
26
+ * - a remote stream that died (or a merely offline relay) emits NOTHING and
27
+ * keeps the shown state ({@link WorkspaceMerger.onRemoteDown}): the UI's
28
+ * `ClientWorkspaceModel.remove()` records ids in a never-cleared blacklist
29
+ * and both `upsert` and baseline replacement drop blacklisted ids, so one
30
+ * `remove` for a virtual id would make that group un-revivable on
31
+ * reconnect — while the UI itself keeps the last complete projection
32
+ * visible across carrier loss (its `handleCarrierFailure`);
33
+ * - a NEW remote baseline on the same server is DIFFED against the shown
34
+ * set: still present → fresh `upsert` (content from the new baseline),
35
+ * absent (deleted server-side / no longer shared) → `remove`, then the
36
+ * merged `order` / `archived` / `pinned`;
37
+ * - a server RENAME (same serverId) re-upserts the shown groups under the
38
+ * new title — no removals ({@link WorkspaceMerger.onServerRenamed});
39
+ * - only a serverId CHANGE removes the whole group
40
+ * ({@link WorkspaceMerger.onRemoteGone}): the new prefix is fresh, so it
41
+ * can never collide with the UI's blacklist. The relay entering
42
+ * `unpaired` / `revoked` is NOT this case (T23b2-fix3): the server
43
+ * persists its serverId (relay-server.ts loadServerId), so a re-pair to
44
+ * the same server reuses the prefix and is handled as a remote death
45
+ * ({@link WorkspaceMerger.onRemoteDown}) — the reconnect diff revives
46
+ * the group;
47
+ * - a relay status that is not serving normally (T34: offline, unpaired,
48
+ * revoked, interface mismatch) only ANNOTATES TITLES: setStatus +
49
+ * onStatusChanged re-upsert the shown groups under the annotated titles,
50
+ * and the reconnecting baseline (which always upserts) restores them.
51
+ * - a session the SERVER stopped serving (remote closed / idle-slept / its
52
+ * fork parent closed → the filtered workspace upsert drops it from
53
+ * `sessionIds`) keeps a TOMBSTONE in the group it was last seen in (CP4,
54
+ * reworked by CP4-client-fix2): the merger remembers the workspace each
55
+ * session id was last carried by, and the forwarded record for THAT
56
+ * workspace still lists the id (appended at the end of `sessionIds`). The
57
+ * UI's session store keeps the merged projection/list entry and nothing
58
+ * ever removes it, so without this the sidebar would park the session in
59
+ * 「未分组」 forever — and hiding it in the merged `archived` frame instead
60
+ * (what CP4-client first did) makes the RT navigation guard
61
+ * `clearArchivedCurrent` (dsh-client-ui-workspace `watchNavigation`) kick
62
+ * the OPEN session page back to the home page before its 「远程已关闭」
63
+ * banner can show. A re-share (the id back in some workspace's
64
+ * `sessionIds`) renders it live again and clears the tombstone; the
65
+ * tombstone dies with its workspace (a remove) and with the identity
66
+ * (onRemoteGone).
67
+ * - T58 reworks the tombstone's VISIBILITY, reshaped by T65: a closed-remote
68
+ * session is HIDDEN — moved into the merged `archived` set (the host's
69
+ * `sessionVisible` hides archived ids under the default filter) — while
70
+ * STAYING in its group's forwarded `sessionIds`: under「显示已归档」it
71
+ * renders in place, and it can never resurface as a 「未分组」 stray. The
72
+ * one exception is the session the user currently has OPEN: it is not
73
+ * hidden at all (the page must keep its 「远程已关闭」 banner), and once
74
+ * the caller reports a different current session
75
+ * ({@link WorkspaceMerger.setCurrentSession}) it moves into `archived`
76
+ * too. `archived` is exactly what the host's navigation guard
77
+ * `clearArchivedCurrent` acts on, so the CURRENT session may never enter
78
+ * it — and an unreadable current-session signal falls back to the
79
+ * conservative tombstone behavior (nothing is hidden). Because the
80
+ * browser's report can lag the user's switch by up to a poll interval, a
81
+ * JUST-created tombstone is not hidden for a short grace (T65, injectable
82
+ * clock): the 1s poll re-judges with the fresh report afterwards — the
83
+ * session the user really switched to keeps its tombstone, anything else
84
+ * hides. Re-sharing the session takes it back out of `archived`
85
+ * (live-carried ids never count as hidden). Since hiding moves only the
86
+ * archived set, hide/reveal updates are exactly one merged archived frame
87
+ * — no group content changes, no ordering dance; the current session
88
+ * never enters the set by construction. The hidden ids are sticky: they
89
+ * survive even the death of their home workspace until a re-share or the
90
+ * identity ends.
91
+ * - a workspace with NOTHING to show is not shown at all (T56): the server
92
+ * keeps every workspace and only narrows `sessionIds` (relay-filter.ts),
93
+ * so a workspace where nothing is shared would arrive as an empty group
94
+ * and render as a bare 「服务端名 · 工作区名」 heading. A workspace the UI
95
+ * has never seen is therefore held back — no baseline item, no upsert, no
96
+ * order entry — until its forwarded `sessionIds` (live sessions or
97
+ * tombstones it must carry) first gains content. From that first forward
98
+ * on it counts as SHOWN and stays shown even when the sessions leave
99
+ * again: a `remove` would blacklist the id in the UI's ClientWorkspaceModel
100
+ * and make the group un-revivable, so an emptied group keeps its (empty)
101
+ * display until the page reload rebuilds the model and this rule hides it
102
+ * again. Every removal path — the reconnecting baseline diff, an explicit
103
+ * server remove, {@link WorkspaceMerger.onRemoteGone} — emits a `remove`
104
+ * only for shown workspaces; never-shown ones leave the state silently.
105
+ *
106
+ * Two KNOWN LIMITATIONS, both rooted in the UI's `removedIds` blacklist
107
+ * never clearing during a page's life (a reload rebuilds the model from
108
+ * scratch, which is why a reload fixes both):
109
+ * - switch to a DIFFERENT server and later back to the SAME old one: the
110
+ * switch-away removed the old prefix, so the returning group's upserts
111
+ * are dropped by the blacklist and it stays invisible until the page is
112
+ * reloaded;
113
+ * - the server deletes a workspace and later shares a NEW workspace under
114
+ * the SAME id: the deletion's remove blacklists the id, and the new
115
+ * workspace's upserts are dropped the same way.
116
+ *
117
+ * Everything here is a pure state machine: no DSH imports, no network, no
118
+ * clocks — the virtual-id arithmetic is the one dependency (virtual-id.ts).
119
+ * Shapes are read defensively like relay-filter.ts does: a malformed frame is
120
+ * dropped (diagnosed through the optional hook), never forwarded.
121
+ *
122
+ * T52 adds two RESULT-side pure helpers on the same principles:
123
+ * {@link mergeModelCatalogs} folds the relay's `session/modelCatalog` answer
124
+ * into the local one (the catalog is the model-selection dropdown's data), and
125
+ * {@link virtualizeModelSelectionValue} rewrites a `modelSelection` projection
126
+ * value's provider ids to virtual group ids so the UI can find the current
127
+ * model's display name in the merged catalog.
128
+ *
129
+ * T52-fix2 made that rewrite CONDITIONAL on the server catalog listing the
130
+ * provider; T52-fix3 reverts it to UNCONDITIONAL, and the revert is final:
131
+ * the host's projection store is FIRST-WRITE-WINS per sequence number
132
+ * (dsh-api-session-controller lib/client.js:986-994 —
133
+ * `if (row?.kind === "sequenced" && seq <= row.seq) return`, and the
134
+ * seed / list blocks take the same road). A projection rewritten ORIGINAL
135
+ * while the catalog had not landed yet occupies its seq; when the catalog
136
+ * arrives, the same seq's VIRTUAL value is dropped as stale — the server
137
+ * group never gets its check mark, and resubmitting a reasoning effort
138
+ * echoes the ORIGINAL provider back, which `session/selectModel`'s routing
139
+ * refuses ("请选择「服务端 · …」分组里的模型"). The rewrite of ONE provider
140
+ * must therefore not depend on when the catalog happened to arrive. The
141
+ * accepted cost: a provider absent from the server's OWN catalog renders the
142
+ * `zr~<serverId>~provider/model` fallback string (the pre-fix2 display).
143
+ */
144
+ /** The (serverId, serverName) pair a merger virtualizes with. A re-handshake
145
+ * with a different server retargets the SAME merger ({@link retarget}) so the
146
+ * locally observed baseline/order state survives the swap. */
147
+ export interface MergerIdentity {
148
+ serverId: string;
149
+ serverName: string;
150
+ }
151
+ /**
152
+ * The status annotation (T34) appended to every shown remote group's title
153
+ * while the relay is not serving normally. The CALLER (intercept.ts) maps the
154
+ * relay state onto one annotation — `revoked` / `unpaired` outrank `offline`,
155
+ * which outranks `mismatch` — and the merger only renders the suffix it is
156
+ * told to. The suffixes are the CHINESE copy, deliberately hardcoded here:
157
+ * this module is background code with no access to the UI language (the
158
+ * English forms live in src/client/locales.ts, `remoteGroup*` keys).
159
+ */
160
+ export type MergerAnnotation = 'none' | 'offline' | 'revoked' | 'unpaired' | 'mismatch';
161
+ export interface WorkspaceMergerOptions extends MergerIdentity {
162
+ /** Diagnostics for frames this merger dropped (unknown type or malformed).
163
+ * Optional: without it the drop is silent. */
164
+ onDiagnostic?: (message: string) => void;
165
+ /** T65: the clock the tombstone hide-grace runs on. Default Date.now;
166
+ * tests advance an injectable fake to cross the grace. */
167
+ now?: () => number;
168
+ }
169
+ export interface WorkspaceMerger {
170
+ readonly serverId: string;
171
+ readonly serverName: string;
172
+ /** Feed one frame of the LOCAL workspace/follow stream; returns the frames
173
+ * the UI should see (usually exactly the input, order-merged when remote
174
+ * state exists). */
175
+ onLocal(frame: unknown): unknown[];
176
+ /** Feed one server-filtered frame of the REMOTE stream; returns the frames
177
+ * the UI should see (possibly none — state-only while the local baseline
178
+ * has not passed). A later baseline is DIFFED against what was shown. */
179
+ onRemote(frame: unknown): unknown[];
180
+ /** The remote stream died or the relay went offline (the same server is
181
+ * expected back): emits NOTHING and keeps the shown state — the UI keeps
182
+ * the last projection visible across carrier loss, and a `remove` here
183
+ * would blacklist the virtual ids against revival. */
184
+ onRemoteDown(): unknown[];
185
+ /** The remote side is gone FOR GOOD under THIS identity: a serverId
186
+ * change. (Since T23b2-fix3 an `unpaired` / `revoked` relay is NOT this
187
+ * case — the same serverId comes back on re-pair, so the caller uses
188
+ * {@link onRemoteDown} there.) Removes everything shown from the remote
189
+ * side, resets the remote state. Idempotent — a second call with no
190
+ * remote state returns []. */
191
+ onRemoteGone(): unknown[];
192
+ /** The server was renamed (same serverId — call after {@link retarget}):
193
+ * re-upserts every shown workspace under the new title, nothing else. */
194
+ onServerRenamed(): unknown[];
195
+ /** Record the status annotation (T34) future virtualized titles carry. No
196
+ * frames on its own — pair it with {@link onStatusChanged} to re-upsert the
197
+ * shown groups under the new annotation, or let the next natural upserts
198
+ * (a reconnecting baseline) carry it. */
199
+ setStatus(annotation: MergerAnnotation): void;
200
+ /** Re-upsert every SHOWN workspace under the CURRENT annotation: the whole
201
+ * update when the relay's serving status changed without any remote frame
202
+ * (offline, revoked, unpaired, version mismatch). [] while nothing is
203
+ * shown (no local baseline yet, or no remote state). */
204
+ onStatusChanged(): unknown[];
205
+ /** Record the session id the UI currently has OPEN (T58) — the raw id as
206
+ * the UI knows it (a virtual id for a remote session, a local id
207
+ * otherwise); `undefined` means a READABLE signal says no session is
208
+ * open. An UNREADABLE signal is expressed by not calling this at all —
209
+ * the merger then keeps every tombstone (the conservative fallback).
210
+ * Returns the frames that apply the resulting hide transitions ([] when
211
+ * nothing changed). */
212
+ setCurrentSession(sessionId: string | undefined): unknown[];
213
+ /** Whether some closed-remote session is being kept visible — it is the
214
+ * current session (waiting for the user to navigate away), or the current
215
+ * session was never readable (conservative fallback). While true the
216
+ * caller should keep polling {@link setCurrentSession} at ≥1s intervals. */
217
+ readonly hasPendingHide: boolean;
218
+ /** Point the merger at a (possibly different) server. Local state survives;
219
+ * for a serverId change the remote state must be gone first (onRemoteGone
220
+ * first); for a rename it may stay. */
221
+ retarget(identity: MergerIdentity): void;
222
+ }
223
+ /**
224
+ * The workspace/follow merger. Remote state is a Map keyed by the ORIGINAL
225
+ * workspace id whose INSERTION ORDER tracks the remote order (baseline sets
226
+ * it, upsert appends new keys, order frames reorder it) — that sequence is
227
+ * what gets appended to every local baseline/order that passes through.
228
+ */
229
+ export declare function createWorkspaceMerger(options: WorkspaceMergerOptions): WorkspaceMerger;
230
+ export interface ControlMergerOptions {
231
+ serverId: string;
232
+ serverName?: string;
233
+ onDiagnostic?: (message: string) => void;
234
+ }
235
+ export interface ControlMerger {
236
+ readonly serverId: string;
237
+ readonly serverName: string;
238
+ /** Local control frames pass through untouched; the local BASELINE also
239
+ * flushes anything the remote side buffered before it arrived. */
240
+ onLocal(frame: unknown): unknown[];
241
+ /** Remote baseline → per-session per-key `projection` frames; remote
242
+ * projection updates → virtualized. Unknown types are dropped. Frames
243
+ * arriving before the local baseline are buffered (the host's snapshot
244
+ * stream treats an update before the opening snapshot as a protocol
245
+ * violation and kills the stream). */
246
+ onRemote(frame: unknown): unknown[];
247
+ /** Always [] — a control stream has nothing to remove on a temporary
248
+ * remote end (the workspace merger owns the group). */
249
+ onRemoteDown(): unknown[];
250
+ /** Always [] — leftover projection state for vanished virtual sessions is
251
+ * harmless; the workspace merger removes the group. */
252
+ onRemoteGone(): unknown[];
253
+ /** Always [] — projection frames carry no display name. */
254
+ onServerRenamed(): unknown[];
255
+ /** Accepted and ignored: a status annotation is a TITLE concern, and
256
+ * control projections carry no titles. */
257
+ setStatus(annotation: MergerAnnotation): void;
258
+ /** Always [] — nothing shown here could carry an annotation. */
259
+ onStatusChanged(): unknown[];
260
+ /** Accepted and ignored; always answers []. */
261
+ setCurrentSession(sessionId: string | undefined): unknown[];
262
+ /** Always false — the control merger hides nothing. */
263
+ readonly hasPendingHide: boolean;
264
+ retarget(identity: MergerIdentity): void;
265
+ }
266
+ /**
267
+ * The session/control merger. The control stream is key-value projection
268
+ * traffic with no ordering constraints between sessions, so it needs no
269
+ * state beyond the pre-baseline buffer: local frames pass through, remote
270
+ * frames get their session id virtualized, and a remote baseline is EXPLODED
271
+ * into one `projection` frame per key (never a second baseline).
272
+ */
273
+ export declare function createControlMerger(options: ControlMergerOptions): ControlMerger;
274
+ /**
275
+ * Merge one `session/list` result pair (first page only — the caller skips
276
+ * paged requests). Local items keep their order and their pagination fields;
277
+ * the remote items are appended with virtualized session ids — `sessionId`
278
+ * AND `parentSessionId` (CP4): the fork link must point at the VIRTUAL parent
279
+ * id the UI knows, or the fork would sort beside a parent id that exists in
280
+ * no list the UI holds — and a T52-fix rewrite of the row's own projections
281
+ * block: a list row carries `{kind:'cached'|'sequenced', asOfSeq, values}`
282
+ * (RT dsh-api-session-controller lib/typert.remote-client.js:381-428) and the
283
+ * client face applies it PER SESSION (lib/client.js:2633 → applyListBlock
284
+ * :2842-2855) — a `sequenced` modelSelection lands under higher-seq-wins
285
+ * (lib/client.js:986-995, `seq <= row.seq` rejects), and the control stream's
286
+ * REWRITTEN frame rides that projection's OWN seq, so an original-provider
287
+ * value left in a row would poison the store forever: the remote session's
288
+ * dropdown would show the raw `provider/model` fallback with no check mark.
289
+ * The rewrite is UNCONDITIONAL (T52-fix3): first-write-wins per seq forbids
290
+ * a catalog gate — a row rewritten while the catalog was missing would
291
+ * occupy the seq forever. A missing or malformed remote result means "remote
292
+ * said nothing" — the local result passes back untouched.
293
+ */
294
+ export declare function mergeSessionList(localResult: unknown, remoteResult: unknown, serverId: string): unknown;
295
+ /**
296
+ * Fold the relay's `session/modelCatalog` answer into the local one (T52).
297
+ * The result shape is RT dsh-api-session-controller
298
+ * `session_modelCatalog_result$schema` (lib/typert.remote-client.js:489-518):
299
+ * `{default, routableProviders, groups, failures}` — provider group ids and
300
+ * display names only, no session data anywhere, so no output filter guards it
301
+ * server-side and nothing here needs narrowing.
302
+ *
303
+ * - server groups are APPENDED after the local ones (the dropdown sorts
304
+ * `deepseek-account` / `deepseek-official` first and keeps the rest in
305
+ * order, RT dsh-client-ui-model-selection lib/client.js:510 — appended
306
+ * groups render last); each group id becomes the virtual group id
307
+ * `zr~<serverId>~<original>` and its name is prefixed
308
+ * `${serverName} · ${name}` — the same workspace title discipline the
309
+ * merger uses;
310
+ * - `default` and `failures` stay LOCAL untouched: the default drives what a
311
+ * blank LOCAL session would run (a remote session's current model comes
312
+ * from its projection instead), and the UI renders every failure as a
313
+ * prominent warning row whose Retry reloads the WHOLE catalog
314
+ * (lib/client.js:816-827) — a server-side group failure is not retryable
315
+ * from here and would put a permanent alarm on every dropdown, so it does
316
+ * not travel (dsh-vision-router's settings read the same result and shows
317
+ * an error state exactly when groups are empty AND failures exist,
318
+ * lib/client.js:635-647 — dropping remote failures keeps that honest).
319
+ * KNOWN BOUNDARY (pre-T52 behavior, unchanged): a NEW server session's
320
+ * projection `next` is null until its first selection or turn, so the UI
321
+ * shows `catalog.value.default` — the LOCAL default — as that session's
322
+ * current model (RT dsh-client-ui-model-selection lib/client.js:316,
323
+ * `projected?.next ?? catalog.value?.default`), which may differ from what
324
+ * the server would actually run until its first selection lands.
325
+ * - `routableProviders` mirrors the group ids by construction host-side
326
+ * (RT dsh-api-session-controller lib/index.js:544 — literally
327
+ * `groups.map(g => g.id)`); no client UI reads it (the dropdown's
328
+ * routable verdict comes from the groups themselves, lib/client.js:334),
329
+ * so appending the virtual ids is a consistency move: the merged catalog
330
+ * keeps the invariant the Host maintains, whatever reads it next.
331
+ *
332
+ * A malformed local or remote value means "one side said nothing" — the
333
+ * local result passes back untouched, like {@link mergeSessionList}.
334
+ */
335
+ export declare function mergeModelCatalogs(localValue: unknown, remoteValue: unknown, identity: MergerIdentity): unknown;
336
+ /**
337
+ * Virtualize the provider ids inside one `modelSelection` projection value —
338
+ * `{lastUsed: {provider, model, reasoningEffort?} | null, next: …}` (RT
339
+ * dsh-api-session-controller lib/types/model-selection-projection.js:12-15,
340
+ * wire shapes at lib/typert.remote-client.js:83-94). The UI resolves the
341
+ * current model's display name by matching `(provider, model)` against the
342
+ * catalog groups (dsh-client-ui-model-selection lib/client.js:334, 520); for
343
+ * a remote session the provider is a SERVER group id, so it must become the
344
+ * virtual group id to be found in the merged catalog — and while the relay
345
+ * serves, the merged catalog always carries it, so the composer trigger
346
+ * shows the model NAME instead of the raw `provider/model` fallback string
347
+ * (lib/client.js:700).
348
+ *
349
+ * UNCONDITIONAL by decision (T52-fix3, reverting T52-fix2's catalog gate):
350
+ * the host's projection store is first-write-wins per sequence number
351
+ * (lib/client.js:986-994), so whether a value is rewritten may not depend on
352
+ * WHEN the catalog happened to arrive — an original written pre-catalog
353
+ * occupies the seq and the later virtual value is dropped, leaving the
354
+ * server group unchecked and the next reasoning-effort submit refused.
355
+ * Accepted cost: a provider the server's own catalog does not list renders
356
+ * the `zr~<serverId>~provider/model` fallback string. Nulls and malformed
357
+ * entries pass through untouched; `model` and `reasoningEffort` are
358
+ * group-agnostic ids and stay as they are.
359
+ */
360
+ export declare function virtualizeModelSelectionValue(value: unknown, serverId: string): unknown;
361
+ /**
362
+ * T63: the virtual session ids a set of FORWARDED workspace frames carries —
363
+ * the `sessionIds` of `upsert` records and of baseline `value.items`. The
364
+ * workspace route feeds these to the summary sync, which announces list rows
365
+ * for ids the UI has no summary for yet (the sidebar drops a group member
366
+ * without one — RT dsh-client-ui-workspace orderByRecency / groupByWorkspace).
367
+ * Reading the FORWARDED output (not the merger's raw state) is the point:
368
+ * T56 already withheld empty groups and T58 already dropped hidden sessions,
369
+ * so a hidden closed-remote session never reaches here and is never
370
+ * announced. Only virtual ids count — local baseline content rides these
371
+ * frames verbatim and its rows already have summaries.
372
+ */
373
+ export declare function virtualSessionIdsInWorkspaceFrames(frames: readonly unknown[]): string[];
374
+ /**
375
+ * T63: the synthesized `api-session/added` emit frames for one relay
376
+ * `session/list` answer — one `{type:'emit', event:'api-session/added',
377
+ * args:[row]}` frame per row. This is the host's own mechanism for adding a
378
+ * list row without a re-pull (RT dsh-api-session-controller: the client face
379
+ * subscribes `api-session/added` → handleSessionAdded → mergeSummary →
380
+ * applyMutation's upsert, which ADDS an unknown row and fills an existing
381
+ * one — idempotent), and the emit frame shape is the client face's
382
+ * exact-keys `{type, event, args}` with args a JSON array
383
+ * (dsh-api-gateway lib/client.js parseRemoteEventFrame) — the same shape the
384
+ * T52 catalog-refresh frame uses. The rows are virtualized by
385
+ * {@link mergeSessionList} itself, so a synthesized row carries exactly what
386
+ * the merged `session/list` route would have answered: the session id AND
387
+ * the fork parent virtualized, and the projections' modelSelection providers
388
+ * rewritten (a row's `sequenced` block must not poison the projection store
389
+ * against the control stream's rewritten frames — T52-fix3). A malformed
390
+ * remote result means "the server said nothing" — no frames.
391
+ */
392
+ export declare function sessionSummaryAddedFrames(remoteResult: unknown, serverId: string): unknown[];
393
+ //# sourceMappingURL=merge-streams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-streams.d.ts","sourceRoot":"","sources":["../../src/merge-streams.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8IG;AAIH;;8DAE8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAA;AAoDvF,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D;kDAC8C;IAC9C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC;8DAC0D;IAC1D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B;;wBAEoB;IACpB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAAA;IAClC;;6EAEyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAAA;IACnC;;;0DAGsD;IACtD,YAAY,IAAI,OAAO,EAAE,CAAA;IACzB;;;;;kCAK8B;IAC9B,YAAY,IAAI,OAAO,EAAE,CAAA;IACzB;6EACyE;IACzE,eAAe,IAAI,OAAO,EAAE,CAAA;IAC5B;;;6CAGyC;IACzC,SAAS,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC7C;;;4DAGwD;IACxD,eAAe,IAAI,OAAO,EAAE,CAAA;IAC5B;;;;;;2BAMuB;IACvB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,EAAE,CAAA;IAC3D;;;gFAG4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAA;IAChC;;2CAEuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;CACzC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAgkBtF;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B;sEACkE;IAClE,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAAA;IAClC;;;;0CAIsC;IACtC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAAA;IACnC;2DACuD;IACvD,YAAY,IAAI,OAAO,EAAE,CAAA;IACzB;2DACuD;IACvD,YAAY,IAAI,OAAO,EAAE,CAAA;IACzB,2DAA2D;IAC3D,eAAe,IAAI,OAAO,EAAE,CAAA;IAC5B;8CAC0C;IAC1C,SAAS,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC7C,gEAAgE;IAChE,eAAe,IAAI,OAAO,EAAE,CAAA;IAC5B,+CAA+C;IAC/C,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,EAAE,CAAA;IAC3D,uDAAuD;IACvD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAA;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;CACzC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CA2JhF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAqBvG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,GAAG,OAAO,CAsB/G;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAUvF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,CAiBvF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAc5F"}