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

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 +955 -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 +73 -39
  25. package/lib/types/client/locales.d.ts +26 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +36 -22
  28. package/lib/types/client/mobile-adapt.d.ts +40 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +967 -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 +77 -39
  91. package/src/client/locales.ts +36 -22
  92. package/src/client/mobile-adapt.ts +913 -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,561 +0,0 @@
1
- /** Runtime shape guard for the lossless-JSON `data` of a `WireEvent`. */
2
- function isRecord(value) {
3
- return typeof value === 'object' && value !== null && !Array.isArray(value);
4
- }
5
- function pickString(value) {
6
- return typeof value === 'string' ? value : undefined;
7
- }
8
- function pickNumber(value) {
9
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
10
- }
11
- /** Fallback message id for events without a stable wire id. */
12
- function syntheticId(prefix, seq) {
13
- return `${prefix}#${String(seq)}`;
14
- }
15
- /** Concatenate the plain text of every `text` content block. */
16
- function textFromContent(content) {
17
- return blocksOfType(content, 'text');
18
- }
19
- /** Concatenate the plain text of every `reasoning` content block. */
20
- function reasoningFromContent(content) {
21
- return blocksOfType(content, 'reasoning');
22
- }
23
- /** Concatenate the plain text of every content block of one type. */
24
- function blocksOfType(content, type) {
25
- if (!Array.isArray(content))
26
- return '';
27
- let out = '';
28
- for (const block of content) {
29
- if (!isRecord(block))
30
- continue;
31
- if (block['type'] !== type)
32
- continue;
33
- const text = pickString(block['text']);
34
- if (text !== undefined)
35
- out += text;
36
- }
37
- return out;
38
- }
39
- /**
40
- * Extract a text-chunk target from `assistant/chunk` or the mobile alias
41
- * `message/chunk`.
42
- *
43
- * - DSH shape: `data.chunk = { type: 'text-delta', text }` keyed by
44
- * `(turn, step)`; the pending message is created/aggregated on the owning
45
- * step and later finalized by the matching `assistant/message`.
46
- * - Mobile shape: `data.text` with an optional `messageId` binding the delta
47
- * to a specific assistant message.
48
- *
49
- * Returns null for non-text chunk variants (usage / finish / block-start).
50
- */
51
- function chunkTarget(data) {
52
- if (!isRecord(data))
53
- return null;
54
- let text;
55
- let kind = 'text';
56
- let idValue;
57
- let turn;
58
- let step;
59
- const chunk = data['chunk'];
60
- if (isRecord(chunk)) {
61
- if (chunk['type'] !== 'text-delta' && chunk['type'] !== 'reasoning-delta')
62
- return null;
63
- text = pickString(chunk['text']);
64
- kind = chunk['type'] === 'reasoning-delta' ? 'reasoning' : 'text';
65
- turn = pickNumber(data['turn']);
66
- step = pickNumber(data['step']);
67
- }
68
- else {
69
- text = pickString(data['text']);
70
- kind = pickString(data['kind']) === 'reasoning' ? 'reasoning' : 'text';
71
- idValue = pickString(data['messageId']) ?? pickString(data['id']);
72
- turn = pickNumber(data['turn']);
73
- step = pickNumber(data['step']);
74
- }
75
- if (text === undefined)
76
- return null;
77
- const result = { text, kind };
78
- if (idValue !== undefined)
79
- result.id = idValue;
80
- if (turn !== undefined)
81
- result.turn = turn;
82
- if (step !== undefined)
83
- result.step = step;
84
- return result;
85
- }
86
- function createState(existing) {
87
- const messages = existing === undefined ? [] : [...existing];
88
- const state = {
89
- messages,
90
- byId: new Map(),
91
- pendingByTurnStep: new Map(),
92
- turnStepMessage: new Map(),
93
- messageTurn: new Map(),
94
- toolNames: new Map(),
95
- maxSeq: -1,
96
- };
97
- for (const message of messages) {
98
- if (message.seq > state.maxSeq)
99
- state.maxSeq = message.seq;
100
- state.byId.set(message.id, message);
101
- if (message.kind !== 'assistant')
102
- continue;
103
- // Rebuild the (turn, step) and turn index maps lost when `existing` was
104
- // handed back to us as plain rows.
105
- const decoded = decodePendingTurnStep(message.id);
106
- const key = decoded === undefined ? undefined : tsKey(decoded.turn, decoded.step);
107
- if (message.pending === true && key !== undefined) {
108
- state.pendingByTurnStep.set(key, message);
109
- state.turnStepMessage.set(key, message);
110
- }
111
- if (decoded !== undefined) {
112
- state.messageTurn.set(message.id, decoded.turn);
113
- }
114
- }
115
- return state;
116
- }
117
- function tsKey(turn, step) {
118
- return turn === undefined || step === undefined ? undefined : `${turn}.${step}`;
119
- }
120
- /**
121
- * Recover the `(turn, step)` a pending assistant message was created under from
122
- * its synthetic id (`assistant,<turn>.<step>#<seq>`), so an incremental fold
123
- * over an `existing` list can re-attach index maps that were lost across calls.
124
- */
125
- function decodePendingTurnStep(id) {
126
- if (!id.startsWith('assistant,'))
127
- return undefined;
128
- const rest = id.slice('assistant,'.length);
129
- const hash = rest.indexOf('#');
130
- const tsPart = hash === -1 ? rest : rest.slice(0, hash);
131
- const dot = tsPart.indexOf('.');
132
- if (dot <= 0 || dot === tsPart.length - 1)
133
- return undefined;
134
- const turn = Number(tsPart.slice(0, dot));
135
- const step = Number(tsPart.slice(dot + 1));
136
- if (!Number.isInteger(turn) || !Number.isInteger(step))
137
- return undefined;
138
- return { turn, step };
139
- }
140
- /**
141
- * Swap in a replacement message object at the old message's position and
142
- * re-index it. Immutable: `next` is a fresh object; the old one is untouched.
143
- */
144
- function replaceMessage(state, oldMessage, next) {
145
- const index = state.messages.indexOf(oldMessage);
146
- if (index !== -1)
147
- state.messages[index] = next;
148
- state.byId.delete(oldMessage.id);
149
- state.byId.set(next.id, next);
150
- }
151
- /** Bundle the maps keyed per `(turn, step)` over to a newly swapped message. */
152
- function retargetTurnStep(state, key, oldMessage, next) {
153
- if (key === undefined)
154
- return;
155
- if (state.pendingByTurnStep.get(key) === oldMessage)
156
- state.pendingByTurnStep.set(key, next);
157
- if (state.turnStepMessage.get(key) === oldMessage)
158
- state.turnStepMessage.set(key, next);
159
- }
160
- /** Fold one event into the working state. Assumes the event passes the watermark. */
161
- function applyEvent(state, event) {
162
- if (event.seq > state.maxSeq)
163
- state.maxSeq = event.seq;
164
- switch (event.type) {
165
- case 'user/message':
166
- applyUserMessage(state, event);
167
- break;
168
- case 'assistant/message':
169
- applyAssistantMessage(state, event);
170
- break;
171
- case 'assistant/chunk':
172
- case 'message/chunk':
173
- applyChunk(state, event);
174
- break;
175
- case 'message/update':
176
- applyUpdate(state, event);
177
- break;
178
- case 'message/delete':
179
- applyDelete(state, event);
180
- break;
181
- case 'turn/end':
182
- applyTurnEnd(state, event);
183
- break;
184
- case 'tool/call':
185
- applyToolCall(state, event);
186
- break;
187
- case 'request/context': {
188
- // Wire shape: { provider, model, contextWindow? }. A present finite
189
- // contextWindow seeds every later assistant message that reports usage.
190
- const data = isRecord(event.data) ? event.data : {};
191
- const window = pickNumber(data['contextWindow']);
192
- if (window !== undefined)
193
- state.contextWindow = window;
194
- break;
195
- }
196
- // turn/start, session/end-seed, and every other/unknown type render nothing.
197
- default:
198
- break;
199
- }
200
- }
201
- function applyUserMessage(state, event) {
202
- const data = isRecord(event.data) ? event.data : {};
203
- const id = pickString(data['id']) ?? syntheticId('user', event.seq);
204
- const text = textFromContent(data['content']);
205
- const source = isRecord(data['source']) ? data['source'] : {};
206
- const sourceKind = pickString(source['kind']);
207
- const existing = state.byId.get(id);
208
- if (existing !== undefined) {
209
- // Idempotent replace (replayed events update in place, never duplicate).
210
- replaceMessage(state, existing, {
211
- ...existing,
212
- ...(sourceKind !== undefined ? { sourceKind } : {}),
213
- text,
214
- seq: event.seq,
215
- time: event.time,
216
- });
217
- return;
218
- }
219
- const message = {
220
- id,
221
- kind: 'user',
222
- text,
223
- ...(sourceKind !== undefined ? { sourceKind } : {}),
224
- seq: event.seq,
225
- time: event.time,
226
- };
227
- state.messages.push(message);
228
- state.byId.set(id, message);
229
- }
230
- function applyAssistantMessage(state, event) {
231
- const data = isRecord(event.data) ? event.data : {};
232
- const messageData = isRecord(data['message']) ? data['message'] : data;
233
- const id = pickString(messageData['id']) ?? pickString(data['id']) ?? syntheticId('assistant', event.seq);
234
- const turn = pickNumber(data['turn']);
235
- const step = pickNumber(data['step']);
236
- const finalText = textFromContent(messageData['content']);
237
- const finalReasoning = reasoningFromContent(messageData['content']);
238
- const key = tsKey(turn, step);
239
- const usage = usageFromData(data);
240
- const contextWindow = state.contextWindow;
241
- // Finalize the matching assistant message (by id, or by turn/step for the
242
- // streaming partial that chunks built before the final event arrived).
243
- let target = state.byId.get(id);
244
- if (target === undefined && key !== undefined)
245
- target = state.pendingByTurnStep.get(key);
246
- if (target !== undefined) {
247
- const next = {
248
- ...target,
249
- id,
250
- text: finalText,
251
- // The final content block list is authoritative; an adapter that omits
252
- // reasoning from the final message keeps the streamed reasoning text.
253
- ...(finalReasoning !== '' ? { reasoning: finalReasoning } : {}),
254
- ...(usage !== undefined ? { usage } : {}),
255
- ...(usage !== undefined && contextWindow !== undefined ? { contextWindow } : {}),
256
- seq: event.seq,
257
- time: event.time,
258
- pending: false,
259
- };
260
- replaceMessage(state, target, next);
261
- retargetTurnStep(state, key, target, next);
262
- if (turn !== undefined)
263
- state.messageTurn.set(next.id, turn);
264
- return;
265
- }
266
- const message = {
267
- id,
268
- kind: 'assistant',
269
- text: finalText,
270
- ...(finalReasoning !== '' ? { reasoning: finalReasoning } : {}),
271
- ...(usage !== undefined ? { usage } : {}),
272
- ...(usage !== undefined && contextWindow !== undefined ? { contextWindow } : {}),
273
- seq: event.seq,
274
- time: event.time,
275
- };
276
- state.messages.push(message);
277
- state.byId.set(id, message);
278
- if (key !== undefined) {
279
- state.pendingByTurnStep.delete(key);
280
- state.turnStepMessage.set(key, message);
281
- }
282
- if (turn !== undefined)
283
- state.messageTurn.set(id, turn);
284
- }
285
- /**
286
- * Extract token usage from an assistant event payload. Only attaches when the
287
- * wire carries finite `inputTokens` AND `outputTokens`; the cache fields are
288
- * included only for finite numbers.
289
- */
290
- function usageFromData(data) {
291
- const usageData = data['usage'];
292
- if (!isRecord(usageData))
293
- return undefined;
294
- const inputTokens = pickNumber(usageData['inputTokens']);
295
- const outputTokens = pickNumber(usageData['outputTokens']);
296
- if (inputTokens === undefined || outputTokens === undefined)
297
- return undefined;
298
- const usage = { inputTokens, outputTokens };
299
- const cacheReadTokens = pickNumber(usageData['cacheReadTokens']);
300
- const cacheWriteTokens = pickNumber(usageData['cacheWriteTokens']);
301
- if (cacheReadTokens !== undefined)
302
- usage.cacheReadTokens = cacheReadTokens;
303
- if (cacheWriteTokens !== undefined)
304
- usage.cacheWriteTokens = cacheWriteTokens;
305
- return usage;
306
- }
307
- function applyChunk(state, event) {
308
- const target = chunkTarget(event.data);
309
- if (target === null)
310
- return;
311
- const key = tsKey(target.turn, target.step);
312
- let message;
313
- if (target.id !== undefined) {
314
- message = state.byId.get(target.id);
315
- }
316
- else if (key !== undefined) {
317
- message = state.pendingByTurnStep.get(key) ?? state.turnStepMessage.get(key);
318
- }
319
- if (message !== undefined && message.kind === 'assistant') {
320
- const next = target.kind === 'reasoning'
321
- ? { ...message, reasoning: (message.reasoning ?? '') + target.text, seq: event.seq, time: event.time }
322
- : { ...message, text: message.text + target.text, seq: event.seq, time: event.time };
323
- replaceMessage(state, message, next);
324
- retargetTurnStep(state, key, message, next);
325
- return;
326
- }
327
- const id = target.id
328
- ?? (key !== undefined ? syntheticId(`assistant,${key}`, event.seq) : syntheticId('assistant', event.seq));
329
- const created = target.kind === 'reasoning'
330
- ? { id, kind: 'assistant', text: '', reasoning: target.text, seq: event.seq, time: event.time, pending: true }
331
- : { id, kind: 'assistant', text: target.text, seq: event.seq, time: event.time, pending: true };
332
- state.messages.push(created);
333
- state.byId.set(id, created);
334
- if (key !== undefined) {
335
- state.pendingByTurnStep.set(key, created);
336
- state.turnStepMessage.set(key, created);
337
- }
338
- if (target.turn !== undefined)
339
- state.messageTurn.set(id, target.turn);
340
- }
341
- function findByIdOrSeq(state, event) {
342
- const data = isRecord(event.data) ? event.data : {};
343
- const id = pickString(data['id']);
344
- if (id !== undefined) {
345
- const byId = state.byId.get(id);
346
- if (byId !== undefined)
347
- return byId;
348
- }
349
- const seq = pickNumber(data['seq'] ?? data['messageSeq']);
350
- if (seq !== undefined) {
351
- return state.messages.find(message => message.seq === seq);
352
- }
353
- return undefined;
354
- }
355
- function applyUpdate(state, event) {
356
- const message = findByIdOrSeq(state, event);
357
- if (message === undefined)
358
- return;
359
- const data = isRecord(event.data) ? event.data : {};
360
- const text = pickString(data['text']);
361
- const next = {
362
- ...message,
363
- ...(text !== undefined ? { text } : {}),
364
- seq: event.seq,
365
- time: event.time,
366
- };
367
- replaceMessage(state, message, next);
368
- }
369
- function removeMessage(state, message) {
370
- const index = state.messages.indexOf(message);
371
- if (index !== -1)
372
- state.messages.splice(index, 1);
373
- state.byId.delete(message.id);
374
- state.messageTurn.delete(message.id);
375
- state.toolNames.delete(message.id);
376
- for (const [key, candidate] of state.turnStepMessage) {
377
- if (candidate === message)
378
- state.turnStepMessage.delete(key);
379
- }
380
- for (const [key, candidate] of state.pendingByTurnStep) {
381
- if (candidate === message)
382
- state.pendingByTurnStep.delete(key);
383
- }
384
- }
385
- function applyDelete(state, event) {
386
- const message = findByIdOrSeq(state, event);
387
- if (message === undefined)
388
- return;
389
- removeMessage(state, message);
390
- }
391
- function applyToolCall(state, event) {
392
- const data = isRecord(event.data) ? event.data : {};
393
- const name = pickString(data['name']);
394
- if (name === undefined)
395
- return;
396
- const turn = pickNumber(data['turn']);
397
- const step = pickNumber(data['step']);
398
- const key = tsKey(turn, step);
399
- let target = key === undefined ? undefined : state.turnStepMessage.get(key);
400
- if (target === undefined && turn !== undefined) {
401
- for (let i = state.messages.length - 1; i >= 0; i--) {
402
- const candidate = state.messages[i];
403
- if (candidate !== undefined && candidate.kind === 'assistant' && state.messageTurn.get(candidate.id) === turn) {
404
- target = candidate;
405
- break;
406
- }
407
- }
408
- }
409
- if (target === undefined) {
410
- for (let i = state.messages.length - 1; i >= 0; i--) {
411
- const candidate = state.messages[i];
412
- if (candidate !== undefined && candidate.kind === 'assistant') {
413
- target = candidate;
414
- break;
415
- }
416
- }
417
- }
418
- if (target === undefined)
419
- return;
420
- const names = state.toolNames.get(target.id) ?? new Set();
421
- const isNewName = !names.has(name);
422
- if (isNewName) {
423
- names.add(name);
424
- state.toolNames.set(target.id, names);
425
- }
426
- const callId = pickString(data['callId']) ?? `${name}#${String(event.seq)}`;
427
- const args = pickString(data['arguments']);
428
- const tools = target.tools ?? [];
429
- const existingIndex = tools.findIndex(tool => tool.callId === callId);
430
- const isNewCall = existingIndex === -1;
431
- const nextTools = isNewCall
432
- ? [...tools, { callId, name, ...(args !== undefined ? { arguments: args } : {}) }]
433
- : tools.map((tool, index) => index === existingIndex
434
- ? { ...tool, ...(args !== undefined ? { arguments: args } : {}) }
435
- : tool);
436
- const next = {
437
- ...target,
438
- ...(isNewName ? { toolSummary: `使用 ${[...names].join(' / ')}` } : {}),
439
- ...(isNewCall || args !== undefined ? { tools: nextTools } : {}),
440
- seq: event.seq,
441
- time: event.time,
442
- };
443
- replaceMessage(state, target, next);
444
- retargetTurnStep(state, key, target, next);
445
- }
446
- function applyTurnEnd(state, event) {
447
- const data = isRecord(event.data) ? event.data : {};
448
- const turn = pickNumber(data['turn']);
449
- const reason = isRecord(data['reason']) ? data['reason'] : {};
450
- const failed = reason['kind'] === 'error';
451
- let targets;
452
- if (turn !== undefined) {
453
- targets = state.messages.filter(message => message.kind === 'assistant' && state.messageTurn.get(message.id) === turn);
454
- }
455
- else {
456
- targets = state.messages.filter(message => message.kind === 'assistant');
457
- }
458
- if (targets.length === 0) {
459
- for (let i = state.messages.length - 1; i >= 0; i--) {
460
- const candidate = state.messages[i];
461
- if (candidate !== undefined && candidate.kind === 'assistant') {
462
- targets = [candidate];
463
- break;
464
- }
465
- }
466
- }
467
- for (const message of targets) {
468
- const wasPending = message.pending === true;
469
- replaceMessage(state, message, {
470
- ...message,
471
- ...(wasPending ? { pending: false } : {}),
472
- ...(failed ? { failed: true } : {}),
473
- // Preserve each step's own final-event seq. Collapsing every message
474
- // onto turn/end makes same-turn ordering depend on arbitrary ids.
475
- time: event.time,
476
- });
477
- }
478
- }
479
- /**
480
- * Fold a batch of session events into a renderable message list.
481
- *
482
- * @param events - events to apply, in any order (folded by ascending seq).
483
- * @param existing - the previously rendered list (live-stream incremental tail).
484
- * @returns messages sorted by seq.
485
- */
486
- export function foldEvents(events, existing) {
487
- return new EventFolder(existing).fold(events);
488
- }
489
- /**
490
- * Incremental folder for one message stream. Live chat folds one event at a
491
- * time; rebuilding the five index maps by scanning every message per event
492
- * made that path O(n) per event (O(n * events) per turn). A folder keeps the
493
- * indexes alive across folds, applies each event in (near-)O(1) map
494
- * operations, and returns the previous snapshot identity unchanged when
495
- * nothing applied, so React skips the re-render entirely. Replayed events
496
- * are no-ops: the maxSeq watermark advanced by the first application skips
497
- * them, which also makes a double-invoked React state updater harmless.
498
- *
499
- * Cost contract: every fold that applies at least one event rebuilds the
500
- * snapshot as one fresh O(messages) array copy (React needs the new
501
- * identity), and replacement paths locate rows by an O(messages) identity
502
- * scan. Both are amortized by the consumer folding bursts as ONE batch on a
503
- * fixed cadence (see FOLD_FLUSH_MS in ChatView) instead of per event. An
504
- * id-to-index map was measured and deliberately rejected: at realistic
505
- * message counts the Map maintenance cost exceeds the identity-scan cost.
506
- */
507
- export class EventFolder {
508
- state;
509
- snapshotList;
510
- /** @param initial - seed rows (history tail load); omit for an empty stream. */
511
- constructor(initial) {
512
- this.state = createState(initial);
513
- }
514
- /** Fold one batch incrementally; returns the current snapshot list. */
515
- fold(events) {
516
- const sorted = [...events].sort((a, b) => a.seq - b.seq);
517
- let applied = false;
518
- for (const event of sorted) {
519
- if (event.seq <= this.state.maxSeq)
520
- continue;
521
- applyEvent(this.state, event);
522
- applied = true;
523
- }
524
- if (!applied && this.snapshotList !== undefined)
525
- return this.snapshotList;
526
- this.snapshotList = snapshotOf(this.state);
527
- return this.snapshotList;
528
- }
529
- /** Replace the whole stream (history reload / session switch). */
530
- seed(messages) {
531
- this.state = createState(messages);
532
- this.snapshotList = undefined;
533
- }
534
- /** Prepend an older history page (exact seam; no overlapping seqs). */
535
- prepend(older) {
536
- this.state = createState([...older, ...this.state.messages]);
537
- this.snapshotList = undefined;
538
- }
539
- /** Current snapshot list; a fresh copy whenever the folder changed. */
540
- snapshot() {
541
- if (this.snapshotList !== undefined)
542
- return this.snapshotList;
543
- this.snapshotList = snapshotOf(this.state);
544
- return this.snapshotList;
545
- }
546
- }
547
- /** Copy the folder's rows and keep them seq-ordered (skips re-sorting the common ordered case). */
548
- function snapshotOf(state) {
549
- const out = [...state.messages];
550
- let ordered = true;
551
- for (let index = 1; index < out.length; index += 1) {
552
- const prev = out[index - 1];
553
- const current = out[index];
554
- if (prev.seq > current.seq) {
555
- ordered = false;
556
- break;
557
- }
558
- }
559
- // Array.sort is stable: equal-seq rows keep their event insertion order.
560
- return ordered ? out : out.sort((a, b) => a.seq - b.seq);
561
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=messages.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"messages.test.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.test.ts"],"names":[],"mappings":""}