@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.10

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 (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +993 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +79 -39
  25. package/lib/types/client/locales.d.ts +32 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +42 -22
  28. package/lib/types/client/mobile-adapt.d.ts +49 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +1002 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +82 -39
  91. package/src/client/locales.ts +42 -22
  92. package/src/client/mobile-adapt.ts +954 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17658
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -149
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -561
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -63
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -821
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -685
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -268
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1342
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
@@ -1,22 +0,0 @@
1
- /**
2
- * Mobile-surface theme: light by default, dark via an explicit persisted
3
- * toggle. The standalone page boots without the shell, so there is no theme
4
- * system to inherit — the choice is stored in localStorage and applied as a
5
- * `data-theme` attribute on <html>; the stylesheet defines both palettes
6
- * under `:root` (light) / `:root[data-theme='dark']` (dark).
7
- *
8
- * A tiny module store (subscribe/get) keeps the toggle button and the boot
9
- * path in sync without threading props through the three view levels.
10
- */
11
- export type MobileTheme = 'light' | 'dark';
12
- /** Current theme (light unless the user explicitly toggled to dark). */
13
- export declare function getMobileTheme(): MobileTheme;
14
- /** Subscribe to theme changes; returns the unsubscribe function. */
15
- export declare function subscribeMobileTheme(listener: () => void): () => void;
16
- /** Set the theme explicitly (persisted + applied to the document). */
17
- export declare function setMobileTheme(theme: MobileTheme): void;
18
- /** Flip light/dark and return the new theme. */
19
- export declare function toggleMobileTheme(): MobileTheme;
20
- /** Apply the persisted (or default) theme once at boot, before first paint. */
21
- export declare function initMobileTheme(): void;
22
- //# sourceMappingURL=mobile-theme.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mobile-theme.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAA;AAmC1C,wEAAwE;AACxE,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAGrE;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAMvD;AAED,gDAAgD;AAChD,wBAAgB,iBAAiB,IAAI,WAAW,CAG/C;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
@@ -1,70 +0,0 @@
1
- /**
2
- * Mobile-surface theme: light by default, dark via an explicit persisted
3
- * toggle. The standalone page boots without the shell, so there is no theme
4
- * system to inherit — the choice is stored in localStorage and applied as a
5
- * `data-theme` attribute on <html>; the stylesheet defines both palettes
6
- * under `:root` (light) / `:root[data-theme='dark']` (dark).
7
- *
8
- * A tiny module store (subscribe/get) keeps the toggle button and the boot
9
- * path in sync without threading props through the three view levels.
10
- */
11
- const STORAGE_KEY = 'dsh.remote.theme';
12
- const THEME_COLOR_META_NAME = 'theme-color';
13
- const LIGHT_THEME_COLOR = '#f3f5f9';
14
- const DARK_THEME_COLOR = '#111418';
15
- let current = readStored() ?? 'light';
16
- const listeners = new Set();
17
- function readStored() {
18
- try {
19
- const value = localStorage.getItem(STORAGE_KEY);
20
- return value === 'dark' || value === 'light' ? value : undefined;
21
- }
22
- catch {
23
- return undefined;
24
- }
25
- }
26
- function persist(theme) {
27
- try {
28
- localStorage.setItem(STORAGE_KEY, theme);
29
- }
30
- catch {
31
- // Private-mode storage failures are non-fatal; the session keeps the theme.
32
- }
33
- }
34
- /** Apply the theme to the document: data-theme attribute + browser chrome color. */
35
- function applyToDocument(theme) {
36
- if (typeof document === 'undefined')
37
- return;
38
- document.documentElement.dataset.theme = theme;
39
- const meta = document.querySelector(`meta[name="${THEME_COLOR_META_NAME}"]`);
40
- if (meta !== null)
41
- meta.content = theme === 'dark' ? DARK_THEME_COLOR : LIGHT_THEME_COLOR;
42
- }
43
- /** Current theme (light unless the user explicitly toggled to dark). */
44
- export function getMobileTheme() {
45
- return current;
46
- }
47
- /** Subscribe to theme changes; returns the unsubscribe function. */
48
- export function subscribeMobileTheme(listener) {
49
- listeners.add(listener);
50
- return () => { listeners.delete(listener); };
51
- }
52
- /** Set the theme explicitly (persisted + applied to the document). */
53
- export function setMobileTheme(theme) {
54
- if (theme === current)
55
- return;
56
- current = theme;
57
- persist(theme);
58
- applyToDocument(theme);
59
- for (const listener of [...listeners])
60
- listener();
61
- }
62
- /** Flip light/dark and return the new theme. */
63
- export function toggleMobileTheme() {
64
- setMobileTheme(current === 'light' ? 'dark' : 'light');
65
- return current;
66
- }
67
- /** Apply the persisted (or default) theme once at boot, before first paint. */
68
- export function initMobileTheme() {
69
- applyToDocument(current);
70
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=mobile-theme.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mobile-theme.test.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-theme.test.ts"],"names":[],"mappings":""}
@@ -1,71 +0,0 @@
1
- // @vitest-environment jsdom
2
- /** mobile-theme: light default, persisted dark toggle, document wiring. */
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { getMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme, } from "./mobile-theme.js";
5
- const STORAGE_KEY = 'dsh.remote.theme';
6
- /** jsdom in this setup ships a bare localStorage object; install a real fake. */
7
- function makeStorage() {
8
- const map = new Map();
9
- return {
10
- getItem: (key) => map.get(key) ?? null,
11
- setItem: (key, value) => { map.set(key, value); },
12
- removeItem: (key) => { map.delete(key); },
13
- clear: () => { map.clear(); },
14
- key: (index) => [...map.keys()][index] ?? null,
15
- get length() { return map.size; },
16
- };
17
- }
18
- let storage;
19
- beforeEach(() => {
20
- storage = makeStorage();
21
- vi.stubGlobal('localStorage', storage);
22
- setMobileTheme('light');
23
- document.documentElement.removeAttribute('data-theme');
24
- });
25
- afterEach(() => {
26
- vi.unstubAllGlobals();
27
- });
28
- describe('mobile-theme', () => {
29
- it('defaults to light with nothing stored', () => {
30
- expect(getMobileTheme()).toBe('light');
31
- });
32
- it('toggles to dark, persists it, and wires the document', () => {
33
- expect(toggleMobileTheme()).toBe('dark');
34
- expect(getMobileTheme()).toBe('dark');
35
- expect(document.documentElement.dataset.theme).toBe('dark');
36
- expect(storage.getItem(STORAGE_KEY)).toBe('dark');
37
- });
38
- it('toggles back to light', () => {
39
- toggleMobileTheme();
40
- expect(toggleMobileTheme()).toBe('light');
41
- expect(document.documentElement.dataset.theme).toBe('light');
42
- expect(storage.getItem(STORAGE_KEY)).toBe('light');
43
- });
44
- it('updates the browser chrome color along with the theme', () => {
45
- const meta = document.createElement('meta');
46
- meta.name = 'theme-color';
47
- document.head.appendChild(meta);
48
- setMobileTheme('dark');
49
- expect(meta.content).toBe('#111418');
50
- setMobileTheme('light');
51
- expect(meta.content).toBe('#f3f5f9');
52
- meta.remove();
53
- });
54
- it('boot reads the stored theme and initMobileTheme applies it', async () => {
55
- storage.setItem(STORAGE_KEY, 'dark');
56
- vi.resetModules();
57
- const fresh = await import("./mobile-theme.js");
58
- expect(fresh.getMobileTheme()).toBe('dark');
59
- fresh.initMobileTheme();
60
- expect(document.documentElement.dataset.theme).toBe('dark');
61
- });
62
- it('notifies subscribers on change', () => {
63
- const seen = [];
64
- const unsubscribe = subscribeMobileTheme(() => { seen.push(getMobileTheme()); });
65
- setMobileTheme('dark');
66
- setMobileTheme('light');
67
- unsubscribe();
68
- setMobileTheme('dark');
69
- expect(seen).toEqual(['dark', 'light']);
70
- });
71
- });
@@ -1,128 +0,0 @@
1
- /**
2
- * Mobile-surface live-event client: the plugin's `/m/api/events.mux` SSE
3
- * channel (Server-Sent Events — the host bridges the mux stream onto it, so
4
- * no WebSocket handshake or framing is needed on this side). The host
5
- * pushes mux frames (subscribed baselines, session events, approvals,
6
- * questions, queue snapshots, tasks, projections) as soon as the stream
7
- * opens — no subscription handshake is needed. Frames arrive as
8
- * server-request envelopes whose payload is the mux frame; unknown frame
9
- * types are dropped so a newer host never breaks this client.
10
- *
11
- * EventSource reconnects automatically — but only over a tunnel that
12
- * actually forwards frames. Public quick tunnels (Cloudflare quick tunnel /
13
- * Tailscale Serve) do not transparently pass Server-Sent Events: ordinary
14
- * HTTP works, yet the SSE connection stays open or reconnects with zero
15
- * bytes, so no live frame ever arrives. That is a transport-layer limit of
16
- * the tunnel, not something the host can fix. This client therefore
17
- * degrades gracefully: once the SSE channel has silently stalled (no frame
18
- * for {@link MuxClientOptions.stallThresholdMs}, or the EventSource reports
19
- * an error), it starts polling the open session's history over plain HTTP
20
- * (the `/m/api/session.history` RPC — unaffected by the SSE limitation),
21
- * and re-emits freshly appended events as `session/event` frames through
22
- * the same subscriber contract, so listeners (and the message fold) behave
23
- * exactly as if the frames had arrived over SSE. When the SSE channel
24
- * delivers again, fallback polling stops and the live stream takes over.
25
- */
26
- import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api/events';
27
- import { type HistoryPage } from './api.ts';
28
- /** Injectable seams for tests. */
29
- export interface MuxClientOptions {
30
- /** EventSource factory (defaults to the browser EventSource). */
31
- sourceFactory?: (url: string) => EventSourceLike;
32
- /**
33
- * Fetch one history page (tail) for a session — the polling fallback's
34
- * data source. Defaults to the mobile `session.history` RPC, which rides
35
- * the ordinary HTTP channel (unaffected by SSE-impairing tunnels).
36
- */
37
- pollLatest?: (sessionId: string) => Promise<HistoryPage>;
38
- /** Initial poll cadence while SSE is stalled (default 3000 ms). Empty polls back off to 60000 ms. */
39
- pollIntervalMs?: number;
40
- /** Initial SSE stall window (default 12000 ms); a previously-live stream gets three windows before fallback. */
41
- stallThresholdMs?: number;
42
- /** Clock seam for tests (defaults to Date.now). */
43
- now?: () => number;
44
- }
45
- /** The EventSource subset this client uses (browser EventSource fits). */
46
- export interface EventSourceLike {
47
- onmessage: ((event: {
48
- data: string;
49
- }) => void) | null;
50
- onerror: ((event: unknown) => void) | null;
51
- close(): void;
52
- }
53
- /**
54
- * Keep one SSE subscription open, fanning validated frames out to
55
- * subscribers. EventSource owns reconnection (with its own backoff); this
56
- * class only manages the subscription lifecycle, plus a polling fallback
57
- * that keeps the open session live when the SSE channel cannot deliver.
58
- */
59
- export declare class MuxClient {
60
- private readonly sourceFactory;
61
- private readonly pollLatest;
62
- private readonly pollIntervalMs;
63
- private readonly stallThresholdMs;
64
- private readonly now;
65
- private readonly listeners;
66
- private source;
67
- private stopped;
68
- private readonly url;
69
- /** The session to keep live via fallback polling (undefined = none). */
70
- private observeSessionId;
71
- /** Last epoch ms the SSE channel produced a frame (or the stream opened). */
72
- private lastDataAt;
73
- /**
74
- * Whether the SSE channel has ever delivered a frame in this stream (a
75
- * delivered frame proves the tunnel can forward SSE; sustained silence may
76
- * still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
77
- */
78
- private sseAlive;
79
- /** Per-session highest event seq already emitted, for poll dedup. */
80
- private readonly pollWatermark;
81
- /** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
82
- private tickTimer;
83
- private polling;
84
- /** Epoch ms of the next due poll while polling (kept on the same tick timer). */
85
- private nextPollAt;
86
- /** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
87
- private pollDelayMs;
88
- /**
89
- * @param url - the mobile events endpoint (browser-relative).
90
- * @param options - seams.
91
- */
92
- constructor(url?: string, options?: MuxClientOptions);
93
- /** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
94
- start(): void;
95
- /** Close for good. */
96
- stop(): void;
97
- /** Subscribe to validated frames; returns an unsubscribe function. */
98
- onFrame(listener: (frame: MuxFrame) => void): () => void;
99
- /**
100
- * Point the fallback at one open session (or `undefined` to stop it).
101
- * While the SSE channel is stalled this client polls that session's
102
- * history and re-emits new events as `session/event` frames.
103
- */
104
- observe(sessionId: string | undefined): void;
105
- private connect;
106
- /**
107
- * The single scheduler tick is the only interval this client owns. One
108
- * timer (at the finer of the stall-check and poll cadences) both arms the
109
- * polling fallback once the stall threshold passes and drives each poll at
110
- * {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
111
- */
112
- private startTick;
113
- private stopTick;
114
- private tick;
115
- private isSseStalled;
116
- private startPolling;
117
- private stopPolling;
118
- /**
119
- * Fetch the latest history page for the observed session and re-emit any
120
- * event above the per-session watermark as a `session/event` frame.
121
- * Idempotent by seq: listeners (and the fold) never see a duplicate.
122
- */
123
- private pollTick;
124
- private handleMessage;
125
- private emit;
126
- private closeSource;
127
- }
128
- //# sourceMappingURL=mux.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,qGAAqG;IACrG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gHAAgH;IAChH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAyBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IACtB,2GAA2G;IAC3G,OAAO,CAAC,WAAW,CAAQ;IAE3B;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IAUrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IASZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAU5C,OAAO,CAAC,OAAO;IAqBf;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,IAAI;IAgBZ,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,WAAW;IAMnB;;;;OAIG;YACW,QAAQ;IAsCtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
@@ -1,305 +0,0 @@
1
- /**
2
- * Mobile-surface live-event client: the plugin's `/m/api/events.mux` SSE
3
- * channel (Server-Sent Events — the host bridges the mux stream onto it, so
4
- * no WebSocket handshake or framing is needed on this side). The host
5
- * pushes mux frames (subscribed baselines, session events, approvals,
6
- * questions, queue snapshots, tasks, projections) as soon as the stream
7
- * opens — no subscription handshake is needed. Frames arrive as
8
- * server-request envelopes whose payload is the mux frame; unknown frame
9
- * types are dropped so a newer host never breaks this client.
10
- *
11
- * EventSource reconnects automatically — but only over a tunnel that
12
- * actually forwards frames. Public quick tunnels (Cloudflare quick tunnel /
13
- * Tailscale Serve) do not transparently pass Server-Sent Events: ordinary
14
- * HTTP works, yet the SSE connection stays open or reconnects with zero
15
- * bytes, so no live frame ever arrives. That is a transport-layer limit of
16
- * the tunnel, not something the host can fix. This client therefore
17
- * degrades gracefully: once the SSE channel has silently stalled (no frame
18
- * for {@link MuxClientOptions.stallThresholdMs}, or the EventSource reports
19
- * an error), it starts polling the open session's history over plain HTTP
20
- * (the `/m/api/session.history` RPC — unaffected by the SSE limitation),
21
- * and re-emits freshly appended events as `session/event` frames through
22
- * the same subscriber contract, so listeners (and the message fold) behave
23
- * exactly as if the frames had arrived over SSE. When the SSE channel
24
- * delivers again, fallback polling stops and the live stream takes over.
25
- */
26
- import { muxFrameSchema } from '@deepseek-ai/dsh-host-apiproxy/api/events.schema';
27
- import { serverRequestSchema } from '@deepseek-ai/dsh-host-apiproxy/api/rpc.schema';
28
- import { history as fetchHistory } from "./api.js";
29
- /** Browser default source factory. */
30
- function browserSource(url) {
31
- // The DOM EventSource is structurally compatible; the `this`-typed handler
32
- // signatures differ, so the narrow face takes it through an adapter cast.
33
- return new EventSource(url);
34
- }
35
- const DEFAULT_POLL_INTERVAL_MS = 3000;
36
- const DEFAULT_STALL_THRESHOLD_MS = 12000;
37
- const LIVE_SSE_STALL_MULTIPLIER = 3;
38
- const MAX_POLL_BACKOFF_MS = 60000;
39
- /**
40
- * Stall-check granularity: the single scheduler tick runs at least this
41
- * often so fallback arms within a second of the stall threshold passing,
42
- * while the poll cadence itself stays {@link MuxClientOptions.pollIntervalMs}.
43
- */
44
- const STALL_CHECK_MS = 1000;
45
- /** Poll window: enough recent events to cover a few seconds of agent output. */
46
- const DEFAULT_POLL_PAGE_SIZE = 50;
47
- /**
48
- * Keep one SSE subscription open, fanning validated frames out to
49
- * subscribers. EventSource owns reconnection (with its own backoff); this
50
- * class only manages the subscription lifecycle, plus a polling fallback
51
- * that keeps the open session live when the SSE channel cannot deliver.
52
- */
53
- export class MuxClient {
54
- sourceFactory;
55
- pollLatest;
56
- pollIntervalMs;
57
- stallThresholdMs;
58
- now;
59
- listeners = new Set();
60
- source;
61
- stopped = false;
62
- url;
63
- /** The session to keep live via fallback polling (undefined = none). */
64
- observeSessionId;
65
- /** Last epoch ms the SSE channel produced a frame (or the stream opened). */
66
- lastDataAt = 0;
67
- /**
68
- * Whether the SSE channel has ever delivered a frame in this stream (a
69
- * delivered frame proves the tunnel can forward SSE; sustained silence may
70
- * still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
71
- */
72
- sseAlive = false;
73
- /** Per-session highest event seq already emitted, for poll dedup. */
74
- pollWatermark = new Map();
75
- /** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
76
- tickTimer;
77
- polling = false;
78
- /** Epoch ms of the next due poll while polling (kept on the same tick timer). */
79
- nextPollAt = 0;
80
- /** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
81
- pollDelayMs;
82
- /**
83
- * @param url - the mobile events endpoint (browser-relative).
84
- * @param options - seams.
85
- */
86
- constructor(url = '/m/api/events.mux', options = {}) {
87
- this.url = url;
88
- this.sourceFactory = options.sourceFactory ?? browserSource;
89
- this.pollLatest = options.pollLatest ?? ((sessionId) => fetchHistory(sessionId, undefined, DEFAULT_POLL_PAGE_SIZE));
90
- this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
91
- this.pollDelayMs = this.pollIntervalMs;
92
- this.stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
93
- this.now = options.now ?? (() => Date.now());
94
- }
95
- /** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
96
- start() {
97
- this.stopped = false;
98
- this.lastDataAt = this.now();
99
- if (this.source === undefined)
100
- this.connect();
101
- this.startTick();
102
- }
103
- /** Close for good. */
104
- stop() {
105
- this.stopped = true;
106
- this.stopTick();
107
- this.stopPolling();
108
- this.closeSource();
109
- this.observeSessionId = undefined;
110
- this.nextPollAt = 0;
111
- }
112
- /** Subscribe to validated frames; returns an unsubscribe function. */
113
- onFrame(listener) {
114
- this.listeners.add(listener);
115
- return () => { this.listeners.delete(listener); };
116
- }
117
- /**
118
- * Point the fallback at one open session (or `undefined` to stop it).
119
- * While the SSE channel is stalled this client polls that session's
120
- * history and re-emits new events as `session/event` frames.
121
- */
122
- observe(sessionId) {
123
- this.observeSessionId = sessionId;
124
- if (sessionId === undefined) {
125
- this.stopPolling();
126
- return;
127
- }
128
- // If SSE is already stalled for this session, start patching right away.
129
- if (!this.polling && !this.stopped && this.isSseStalled())
130
- this.startPolling();
131
- }
132
- connect() {
133
- // A fresh stream starts unknown; only a delivered frame proves it works.
134
- this.sseAlive = false;
135
- const source = this.sourceFactory(this.url);
136
- this.source = source;
137
- source.onmessage = (event) => {
138
- this.handleMessage(event.data);
139
- };
140
- source.onerror = () => {
141
- // EventSource reconnects by itself; when we are closing, detach first
142
- // so the native reconnect cannot outlive stop(). Otherwise an error is
143
- // a strong signal the transport is not delivering — degrade to polling.
144
- if (this.stopped && this.source === source) {
145
- this.closeSource();
146
- return;
147
- }
148
- this.sseAlive = false;
149
- if (this.observeSessionId !== undefined)
150
- this.startPolling();
151
- };
152
- }
153
- /**
154
- * The single scheduler tick is the only interval this client owns. One
155
- * timer (at the finer of the stall-check and poll cadences) both arms the
156
- * polling fallback once the stall threshold passes and drives each poll at
157
- * {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
158
- */
159
- startTick() {
160
- if (this.tickTimer !== undefined)
161
- return;
162
- const cadence = Math.min(this.pollIntervalMs, STALL_CHECK_MS);
163
- this.tickTimer = setInterval(() => { this.tick(); }, cadence);
164
- }
165
- stopTick() {
166
- if (this.tickTimer !== undefined) {
167
- clearInterval(this.tickTimer);
168
- this.tickTimer = undefined;
169
- }
170
- }
171
- tick() {
172
- if (this.stopped)
173
- return;
174
- if (this.observeSessionId === undefined)
175
- return;
176
- if (this.polling) {
177
- // The stall phase has ended; the same tick now paces the adaptive polls.
178
- if (this.now() >= this.nextPollAt) {
179
- // pollTick schedules the next run after it settles, so slow requests
180
- // cannot overlap with another scheduler tick.
181
- this.nextPollAt = Number.POSITIVE_INFINITY;
182
- void this.pollTick();
183
- }
184
- return;
185
- }
186
- if (this.isSseStalled())
187
- this.startPolling();
188
- }
189
- isSseStalled() {
190
- const windowMs = this.sseAlive
191
- ? this.stallThresholdMs * LIVE_SSE_STALL_MULTIPLIER
192
- : this.stallThresholdMs;
193
- return (this.now() - this.lastDataAt) > windowMs;
194
- }
195
- startPolling() {
196
- if (this.polling || this.stopped)
197
- return;
198
- this.polling = true;
199
- this.pollDelayMs = this.pollIntervalMs;
200
- this.nextPollAt = Number.POSITIVE_INFINITY;
201
- void this.pollTick();
202
- }
203
- stopPolling() {
204
- this.polling = false;
205
- this.pollDelayMs = this.pollIntervalMs;
206
- this.nextPollAt = 0;
207
- }
208
- /**
209
- * Fetch the latest history page for the observed session and re-emit any
210
- * event above the per-session watermark as a `session/event` frame.
211
- * Idempotent by seq: listeners (and the fold) never see a duplicate.
212
- */
213
- async pollTick() {
214
- const sessionId = this.observeSessionId;
215
- if (sessionId === undefined) {
216
- this.stopPolling();
217
- return;
218
- }
219
- let emitted = 0;
220
- try {
221
- const page = await this.pollLatest(sessionId);
222
- let maxSeq = this.pollWatermark.get(sessionId) ?? -1;
223
- const ordered = [...page.events].sort((left, right) => {
224
- const leftSeq = typeof left.event?.seq === 'number' ? left.event.seq : -1;
225
- const rightSeq = typeof right.event?.seq === 'number' ? right.event.seq : -1;
226
- return leftSeq - rightSeq;
227
- });
228
- for (const entry of ordered) {
229
- const event = entry.event;
230
- const seq = typeof event?.seq === 'number' ? event.seq : -1;
231
- if (seq <= maxSeq)
232
- continue;
233
- maxSeq = seq;
234
- emitted += 1;
235
- this.emit({ type: 'session/event', sessionId: sessionId, event });
236
- }
237
- this.pollWatermark.set(sessionId, maxSeq);
238
- }
239
- catch {
240
- // Transient (network, pairing, history paging); retry with backoff.
241
- }
242
- finally {
243
- if (emitted > 0) {
244
- this.pollDelayMs = this.pollIntervalMs;
245
- }
246
- else {
247
- this.pollDelayMs = Math.min(MAX_POLL_BACKOFF_MS, this.pollDelayMs + this.pollIntervalMs);
248
- }
249
- if (this.polling && this.observeSessionId === sessionId) {
250
- this.nextPollAt = this.now() + this.pollDelayMs;
251
- }
252
- }
253
- }
254
- handleMessage(data) {
255
- if (typeof data !== 'string' || data === '')
256
- return;
257
- let parsed;
258
- try {
259
- parsed = JSON.parse(data);
260
- }
261
- catch {
262
- return;
263
- }
264
- // The SSE channel carries server-request envelopes whose payload is the
265
- // mux frame (same wire shape as the desktop mux channel).
266
- const envelope = serverRequestSchema.safeParse(parsed);
267
- if (!envelope.success)
268
- return;
269
- const frame = muxFrameSchema.safeParse(envelope.data.payload);
270
- if (!frame.success)
271
- return;
272
- // A delivered frame proves the SSE channel is live (the tunnel forwards
273
- // it) and delivers again — drop any fallback polling so the live stream
274
- // takes over without double delivery.
275
- this.sseAlive = true;
276
- this.lastDataAt = this.now();
277
- if (this.polling)
278
- this.stopPolling();
279
- this.emit(frame.data);
280
- }
281
- emit(frame) {
282
- for (const listener of this.listeners) {
283
- try {
284
- listener(frame);
285
- }
286
- catch {
287
- // A throwing subscriber must not break the emit loop.
288
- }
289
- }
290
- }
291
- closeSource() {
292
- const source = this.source;
293
- this.source = undefined;
294
- if (source !== undefined) {
295
- source.onmessage = null;
296
- source.onerror = null;
297
- try {
298
- source.close();
299
- }
300
- catch {
301
- // Already closed.
302
- }
303
- }
304
- }
305
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=mux.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mux.test.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.test.ts"],"names":[],"mappings":""}