@linxin666/dsh-remote-web-ui 0.1.1 → 0.1.2
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.
- package/README.md +4 -1
- package/lib/client.js.map +1 -0
- package/lib/mobile.js +15467 -0
- package/lib/mobile.js.map +1 -0
- package/lib/types/client/PairFailedNotice.js +24 -0
- package/lib/types/client/PairFailedNotice.js.map +1 -0
- package/lib/types/client/PhoneIcon.js +9 -0
- package/lib/types/client/PhoneIcon.js.map +1 -0
- package/lib/types/client/PluginSettingsCard.js +39 -0
- package/lib/types/client/PluginSettingsCard.js.map +1 -0
- package/lib/types/client/RemoteEntry.js +162 -0
- package/lib/types/client/RemoteEntry.js.map +1 -0
- package/lib/types/client/RemotePanel.js +34 -0
- package/lib/types/client/RemotePanel.js.map +1 -0
- package/lib/types/client/RemoteSettingsCard.js +59 -0
- package/lib/types/client/RemoteSettingsCard.js.map +1 -0
- package/lib/types/client/deep-link.js +118 -0
- package/lib/types/client/deep-link.js.map +1 -0
- package/lib/types/client/index.js +111 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/locales.js +141 -0
- package/lib/types/client/locales.js.map +1 -0
- package/lib/types/client/pair-api.js +123 -0
- package/lib/types/client/pair-api.js.map +1 -0
- package/lib/types/client/settings-form.js +211 -0
- package/lib/types/client/settings-form.js.map +1 -0
- package/lib/types/gate.js +121 -0
- package/lib/types/gate.js.map +1 -0
- package/lib/types/index.js +227 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/invariant.js.map +1 -0
- package/lib/types/lan.js +19 -0
- package/lib/types/lan.js.map +1 -0
- package/lib/types/mobile/api.js +60 -0
- package/lib/types/mobile/api.js.map +1 -0
- package/lib/types/mobile/index.js +25 -0
- package/lib/types/mobile/index.js.map +1 -0
- package/lib/types/mobile/messages.js +449 -0
- package/lib/types/mobile/messages.js.map +1 -0
- package/lib/types/mobile/messages.test.js +189 -0
- package/lib/types/mobile/messages.test.js.map +1 -0
- package/lib/types/mobile/mobile-styles.js +853 -0
- package/lib/types/mobile/mobile-styles.js.map +1 -0
- package/lib/types/mobile/mobile-theme.js +70 -0
- package/lib/types/mobile/mobile-theme.test.js +71 -0
- package/lib/types/mobile/mux.js +110 -0
- package/lib/types/mobile/mux.js.map +1 -0
- package/lib/types/mobile/rpc.js +83 -0
- package/lib/types/mobile/rpc.js.map +1 -0
- package/lib/types/mobile/theme-toggle.js +26 -0
- package/lib/types/mobile/views/App.js +102 -0
- package/lib/types/mobile/views/App.js.map +1 -0
- package/lib/types/mobile/views/ChatView.js +348 -0
- package/lib/types/mobile/views/ChatView.js.map +1 -0
- package/lib/types/mobile/views/ChatView.test.js +215 -0
- package/lib/types/mobile/views/SessionListView.js +108 -0
- package/lib/types/mobile/views/SessionListView.js.map +1 -0
- package/lib/types/mobile/views/SessionListView.test.js +110 -0
- package/lib/types/mobile/views/WorkspaceView.js +43 -0
- package/lib/types/mobile/views/WorkspaceView.js.map +1 -0
- package/lib/types/mobile-api.js +263 -0
- package/lib/types/mobile-api.js.map +1 -0
- package/lib/types/mobile-routes.js +71 -0
- package/lib/types/mobile-routes.js.map +1 -0
- package/lib/types/pairing.js +285 -0
- package/lib/types/pairing.js.map +1 -0
- package/lib/types/routes.js +327 -0
- package/lib/types/routes.js.map +1 -0
- package/lib/types/tunnel.js +222 -0
- package/lib/types/tunnel.js.map +1 -0
- package/package.json +9 -6
|
@@ -0,0 +1,449 @@
|
|
|
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
|
+
/** Max message seq across an existing list, or -1 for an empty list. */
|
|
87
|
+
function watermarkOf(existing) {
|
|
88
|
+
if (existing === undefined)
|
|
89
|
+
return -1;
|
|
90
|
+
let maxSeq = -1;
|
|
91
|
+
for (const message of existing) {
|
|
92
|
+
if (message.seq > maxSeq)
|
|
93
|
+
maxSeq = message.seq;
|
|
94
|
+
}
|
|
95
|
+
return maxSeq;
|
|
96
|
+
}
|
|
97
|
+
function createState(existing) {
|
|
98
|
+
const messages = existing === undefined ? [] : [...existing];
|
|
99
|
+
const state = {
|
|
100
|
+
messages,
|
|
101
|
+
byId: new Map(),
|
|
102
|
+
pendingByTurnStep: new Map(),
|
|
103
|
+
turnStepMessage: new Map(),
|
|
104
|
+
messageTurn: new Map(),
|
|
105
|
+
toolNames: new Map(),
|
|
106
|
+
};
|
|
107
|
+
for (const message of messages) {
|
|
108
|
+
state.byId.set(message.id, message);
|
|
109
|
+
if (message.kind !== 'assistant')
|
|
110
|
+
continue;
|
|
111
|
+
// Rebuild the (turn, step) and turn index maps lost when `existing` was
|
|
112
|
+
// handed back to us as plain rows.
|
|
113
|
+
const decoded = decodePendingTurnStep(message.id);
|
|
114
|
+
const key = decoded === undefined ? undefined : tsKey(decoded.turn, decoded.step);
|
|
115
|
+
if (message.pending === true && key !== undefined) {
|
|
116
|
+
state.pendingByTurnStep.set(key, message);
|
|
117
|
+
state.turnStepMessage.set(key, message);
|
|
118
|
+
}
|
|
119
|
+
if (decoded !== undefined) {
|
|
120
|
+
state.messageTurn.set(message.id, decoded.turn);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
function tsKey(turn, step) {
|
|
126
|
+
return turn === undefined || step === undefined ? undefined : `${turn}.${step}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Recover the `(turn, step)` a pending assistant message was created under from
|
|
130
|
+
* its synthetic id (`assistant,<turn>.<step>#<seq>`), so an incremental fold
|
|
131
|
+
* over an `existing` list can re-attach index maps that were lost across calls.
|
|
132
|
+
*/
|
|
133
|
+
function decodePendingTurnStep(id) {
|
|
134
|
+
if (!id.startsWith('assistant,'))
|
|
135
|
+
return undefined;
|
|
136
|
+
const rest = id.slice('assistant,'.length);
|
|
137
|
+
const hash = rest.indexOf('#');
|
|
138
|
+
const tsPart = hash === -1 ? rest : rest.slice(0, hash);
|
|
139
|
+
const dot = tsPart.indexOf('.');
|
|
140
|
+
if (dot <= 0 || dot === tsPart.length - 1)
|
|
141
|
+
return undefined;
|
|
142
|
+
const turn = Number(tsPart.slice(0, dot));
|
|
143
|
+
const step = Number(tsPart.slice(dot + 1));
|
|
144
|
+
if (!Number.isInteger(turn) || !Number.isInteger(step))
|
|
145
|
+
return undefined;
|
|
146
|
+
return { turn, step };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Swap in a replacement message object at the old message's position and
|
|
150
|
+
* re-index it. Immutable: `next` is a fresh object; the old one is untouched.
|
|
151
|
+
*/
|
|
152
|
+
function replaceMessage(state, oldMessage, next) {
|
|
153
|
+
const index = state.messages.indexOf(oldMessage);
|
|
154
|
+
if (index !== -1)
|
|
155
|
+
state.messages[index] = next;
|
|
156
|
+
state.byId.delete(oldMessage.id);
|
|
157
|
+
state.byId.set(next.id, next);
|
|
158
|
+
}
|
|
159
|
+
/** Bundle the maps keyed per `(turn, step)` over to a newly swapped message. */
|
|
160
|
+
function retargetTurnStep(state, key, oldMessage, next) {
|
|
161
|
+
if (key === undefined)
|
|
162
|
+
return;
|
|
163
|
+
if (state.pendingByTurnStep.get(key) === oldMessage)
|
|
164
|
+
state.pendingByTurnStep.set(key, next);
|
|
165
|
+
if (state.turnStepMessage.get(key) === oldMessage)
|
|
166
|
+
state.turnStepMessage.set(key, next);
|
|
167
|
+
}
|
|
168
|
+
/** Fold one event into the working state. Assumes the event passes the watermark. */
|
|
169
|
+
function applyEvent(state, event) {
|
|
170
|
+
switch (event.type) {
|
|
171
|
+
case 'user/message':
|
|
172
|
+
applyUserMessage(state, event);
|
|
173
|
+
break;
|
|
174
|
+
case 'assistant/message':
|
|
175
|
+
applyAssistantMessage(state, event);
|
|
176
|
+
break;
|
|
177
|
+
case 'assistant/chunk':
|
|
178
|
+
case 'message/chunk':
|
|
179
|
+
applyChunk(state, event);
|
|
180
|
+
break;
|
|
181
|
+
case 'message/update':
|
|
182
|
+
applyUpdate(state, event);
|
|
183
|
+
break;
|
|
184
|
+
case 'message/delete':
|
|
185
|
+
applyDelete(state, event);
|
|
186
|
+
break;
|
|
187
|
+
case 'turn/end':
|
|
188
|
+
applyTurnEnd(state, event);
|
|
189
|
+
break;
|
|
190
|
+
case 'tool/call':
|
|
191
|
+
applyToolCall(state, event);
|
|
192
|
+
break;
|
|
193
|
+
// turn/start, session/end-seed, and every other/unknown type render nothing.
|
|
194
|
+
default:
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function applyUserMessage(state, event) {
|
|
199
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
200
|
+
const id = pickString(data['id']) ?? syntheticId('user', event.seq);
|
|
201
|
+
const text = textFromContent(data['content']);
|
|
202
|
+
const existing = state.byId.get(id);
|
|
203
|
+
if (existing !== undefined) {
|
|
204
|
+
// Idempotent replace (replayed events update in place, never duplicate).
|
|
205
|
+
replaceMessage(state, existing, { ...existing, text, seq: event.seq, time: event.time });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const message = { id, kind: 'user', text, seq: event.seq, time: event.time };
|
|
209
|
+
state.messages.push(message);
|
|
210
|
+
state.byId.set(id, message);
|
|
211
|
+
}
|
|
212
|
+
function applyAssistantMessage(state, event) {
|
|
213
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
214
|
+
const messageData = isRecord(data['message']) ? data['message'] : data;
|
|
215
|
+
const id = pickString(messageData['id']) ?? pickString(data['id']) ?? syntheticId('assistant', event.seq);
|
|
216
|
+
const turn = pickNumber(data['turn']);
|
|
217
|
+
const step = pickNumber(data['step']);
|
|
218
|
+
const finalText = textFromContent(messageData['content']);
|
|
219
|
+
const finalReasoning = reasoningFromContent(messageData['content']);
|
|
220
|
+
const key = tsKey(turn, step);
|
|
221
|
+
// Finalize the matching assistant message (by id, or by turn/step for the
|
|
222
|
+
// streaming partial that chunks built before the final event arrived).
|
|
223
|
+
let target = state.byId.get(id);
|
|
224
|
+
if (target === undefined && key !== undefined)
|
|
225
|
+
target = state.pendingByTurnStep.get(key);
|
|
226
|
+
if (target !== undefined) {
|
|
227
|
+
const next = {
|
|
228
|
+
...target,
|
|
229
|
+
id,
|
|
230
|
+
text: finalText,
|
|
231
|
+
// The final content block list is authoritative; an adapter that omits
|
|
232
|
+
// reasoning from the final message keeps the streamed reasoning text.
|
|
233
|
+
...(finalReasoning !== '' ? { reasoning: finalReasoning } : {}),
|
|
234
|
+
seq: event.seq,
|
|
235
|
+
time: event.time,
|
|
236
|
+
pending: false,
|
|
237
|
+
};
|
|
238
|
+
replaceMessage(state, target, next);
|
|
239
|
+
retargetTurnStep(state, key, target, next);
|
|
240
|
+
if (turn !== undefined)
|
|
241
|
+
state.messageTurn.set(next.id, turn);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const message = {
|
|
245
|
+
id,
|
|
246
|
+
kind: 'assistant',
|
|
247
|
+
text: finalText,
|
|
248
|
+
...(finalReasoning !== '' ? { reasoning: finalReasoning } : {}),
|
|
249
|
+
seq: event.seq,
|
|
250
|
+
time: event.time,
|
|
251
|
+
};
|
|
252
|
+
state.messages.push(message);
|
|
253
|
+
state.byId.set(id, message);
|
|
254
|
+
if (key !== undefined) {
|
|
255
|
+
state.pendingByTurnStep.delete(key);
|
|
256
|
+
state.turnStepMessage.set(key, message);
|
|
257
|
+
}
|
|
258
|
+
if (turn !== undefined)
|
|
259
|
+
state.messageTurn.set(id, turn);
|
|
260
|
+
}
|
|
261
|
+
function applyChunk(state, event) {
|
|
262
|
+
const target = chunkTarget(event.data);
|
|
263
|
+
if (target === null)
|
|
264
|
+
return;
|
|
265
|
+
const key = tsKey(target.turn, target.step);
|
|
266
|
+
let message;
|
|
267
|
+
if (target.id !== undefined) {
|
|
268
|
+
message = state.byId.get(target.id);
|
|
269
|
+
}
|
|
270
|
+
else if (key !== undefined) {
|
|
271
|
+
message = state.pendingByTurnStep.get(key) ?? state.turnStepMessage.get(key);
|
|
272
|
+
}
|
|
273
|
+
if (message !== undefined && message.kind === 'assistant') {
|
|
274
|
+
const next = target.kind === 'reasoning'
|
|
275
|
+
? { ...message, reasoning: (message.reasoning ?? '') + target.text, seq: event.seq, time: event.time }
|
|
276
|
+
: { ...message, text: message.text + target.text, seq: event.seq, time: event.time };
|
|
277
|
+
replaceMessage(state, message, next);
|
|
278
|
+
retargetTurnStep(state, key, message, next);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const id = target.id
|
|
282
|
+
?? (key !== undefined ? syntheticId(`assistant,${key}`, event.seq) : syntheticId('assistant', event.seq));
|
|
283
|
+
const created = target.kind === 'reasoning'
|
|
284
|
+
? { id, kind: 'assistant', text: '', reasoning: target.text, seq: event.seq, time: event.time, pending: true }
|
|
285
|
+
: { id, kind: 'assistant', text: target.text, seq: event.seq, time: event.time, pending: true };
|
|
286
|
+
state.messages.push(created);
|
|
287
|
+
state.byId.set(id, created);
|
|
288
|
+
if (key !== undefined) {
|
|
289
|
+
state.pendingByTurnStep.set(key, created);
|
|
290
|
+
state.turnStepMessage.set(key, created);
|
|
291
|
+
}
|
|
292
|
+
if (target.turn !== undefined)
|
|
293
|
+
state.messageTurn.set(id, target.turn);
|
|
294
|
+
}
|
|
295
|
+
function findByIdOrSeq(state, event) {
|
|
296
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
297
|
+
const id = pickString(data['id']);
|
|
298
|
+
if (id !== undefined) {
|
|
299
|
+
const byId = state.byId.get(id);
|
|
300
|
+
if (byId !== undefined)
|
|
301
|
+
return byId;
|
|
302
|
+
}
|
|
303
|
+
const seq = pickNumber(data['seq'] ?? data['messageSeq']);
|
|
304
|
+
if (seq !== undefined) {
|
|
305
|
+
return state.messages.find(message => message.seq === seq);
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
function applyUpdate(state, event) {
|
|
310
|
+
const message = findByIdOrSeq(state, event);
|
|
311
|
+
if (message === undefined)
|
|
312
|
+
return;
|
|
313
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
314
|
+
const text = pickString(data['text']);
|
|
315
|
+
const next = {
|
|
316
|
+
...message,
|
|
317
|
+
...(text !== undefined ? { text } : {}),
|
|
318
|
+
seq: event.seq,
|
|
319
|
+
time: event.time,
|
|
320
|
+
};
|
|
321
|
+
replaceMessage(state, message, next);
|
|
322
|
+
}
|
|
323
|
+
function removeMessage(state, message) {
|
|
324
|
+
const index = state.messages.indexOf(message);
|
|
325
|
+
if (index !== -1)
|
|
326
|
+
state.messages.splice(index, 1);
|
|
327
|
+
state.byId.delete(message.id);
|
|
328
|
+
state.messageTurn.delete(message.id);
|
|
329
|
+
state.toolNames.delete(message.id);
|
|
330
|
+
for (const [key, candidate] of state.turnStepMessage) {
|
|
331
|
+
if (candidate === message)
|
|
332
|
+
state.turnStepMessage.delete(key);
|
|
333
|
+
}
|
|
334
|
+
for (const [key, candidate] of state.pendingByTurnStep) {
|
|
335
|
+
if (candidate === message)
|
|
336
|
+
state.pendingByTurnStep.delete(key);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function applyDelete(state, event) {
|
|
340
|
+
const message = findByIdOrSeq(state, event);
|
|
341
|
+
if (message === undefined)
|
|
342
|
+
return;
|
|
343
|
+
removeMessage(state, message);
|
|
344
|
+
}
|
|
345
|
+
function applyToolCall(state, event) {
|
|
346
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
347
|
+
const name = pickString(data['name']);
|
|
348
|
+
if (name === undefined)
|
|
349
|
+
return;
|
|
350
|
+
const turn = pickNumber(data['turn']);
|
|
351
|
+
const step = pickNumber(data['step']);
|
|
352
|
+
const key = tsKey(turn, step);
|
|
353
|
+
let target = key === undefined ? undefined : state.turnStepMessage.get(key);
|
|
354
|
+
if (target === undefined && turn !== undefined) {
|
|
355
|
+
for (let i = state.messages.length - 1; i >= 0; i--) {
|
|
356
|
+
const candidate = state.messages[i];
|
|
357
|
+
if (candidate !== undefined && candidate.kind === 'assistant' && state.messageTurn.get(candidate.id) === turn) {
|
|
358
|
+
target = candidate;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (target === undefined) {
|
|
364
|
+
for (let i = state.messages.length - 1; i >= 0; i--) {
|
|
365
|
+
const candidate = state.messages[i];
|
|
366
|
+
if (candidate !== undefined && candidate.kind === 'assistant') {
|
|
367
|
+
target = candidate;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (target === undefined)
|
|
373
|
+
return;
|
|
374
|
+
const names = state.toolNames.get(target.id) ?? new Set();
|
|
375
|
+
const isNewName = !names.has(name);
|
|
376
|
+
if (isNewName) {
|
|
377
|
+
names.add(name);
|
|
378
|
+
state.toolNames.set(target.id, names);
|
|
379
|
+
}
|
|
380
|
+
const callId = pickString(data['callId']) ?? `${name}#${String(event.seq)}`;
|
|
381
|
+
const args = pickString(data['arguments']);
|
|
382
|
+
const tools = target.tools ?? [];
|
|
383
|
+
const existingIndex = tools.findIndex(tool => tool.callId === callId);
|
|
384
|
+
const isNewCall = existingIndex === -1;
|
|
385
|
+
const nextTools = isNewCall
|
|
386
|
+
? [...tools, { callId, name, ...(args !== undefined ? { arguments: args } : {}) }]
|
|
387
|
+
: tools.map((tool, index) => index === existingIndex
|
|
388
|
+
? { ...tool, ...(args !== undefined ? { arguments: args } : {}) }
|
|
389
|
+
: tool);
|
|
390
|
+
const next = {
|
|
391
|
+
...target,
|
|
392
|
+
...(isNewName ? { toolSummary: `使用 ${[...names].join(' / ')}` } : {}),
|
|
393
|
+
...(isNewCall || args !== undefined ? { tools: nextTools } : {}),
|
|
394
|
+
seq: event.seq,
|
|
395
|
+
time: event.time,
|
|
396
|
+
};
|
|
397
|
+
replaceMessage(state, target, next);
|
|
398
|
+
retargetTurnStep(state, key, target, next);
|
|
399
|
+
}
|
|
400
|
+
function applyTurnEnd(state, event) {
|
|
401
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
402
|
+
const turn = pickNumber(data['turn']);
|
|
403
|
+
const reason = isRecord(data['reason']) ? data['reason'] : {};
|
|
404
|
+
const failed = reason['kind'] === 'error';
|
|
405
|
+
let targets;
|
|
406
|
+
if (turn !== undefined) {
|
|
407
|
+
targets = state.messages.filter(message => message.kind === 'assistant' && state.messageTurn.get(message.id) === turn);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
targets = state.messages.filter(message => message.kind === 'assistant');
|
|
411
|
+
}
|
|
412
|
+
if (targets.length === 0) {
|
|
413
|
+
for (let i = state.messages.length - 1; i >= 0; i--) {
|
|
414
|
+
const candidate = state.messages[i];
|
|
415
|
+
if (candidate !== undefined && candidate.kind === 'assistant') {
|
|
416
|
+
targets = [candidate];
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
for (const message of targets) {
|
|
422
|
+
const wasPending = message.pending === true;
|
|
423
|
+
replaceMessage(state, message, {
|
|
424
|
+
...message,
|
|
425
|
+
...(wasPending ? { pending: false } : {}),
|
|
426
|
+
...(failed ? { failed: true } : {}),
|
|
427
|
+
seq: Math.max(message.seq, event.seq),
|
|
428
|
+
time: event.time,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Fold a batch of session events into a renderable message list.
|
|
434
|
+
*
|
|
435
|
+
* @param events - events to apply, in any order (folded by ascending seq).
|
|
436
|
+
* @param existing - the previously rendered list (live-stream incremental tail).
|
|
437
|
+
* @returns messages sorted by seq.
|
|
438
|
+
*/
|
|
439
|
+
export function foldEvents(events, existing) {
|
|
440
|
+
const sorted = [...events].sort((a, b) => a.seq - b.seq);
|
|
441
|
+
const watermark = watermarkOf(existing);
|
|
442
|
+
const state = createState(existing);
|
|
443
|
+
for (const event of sorted) {
|
|
444
|
+
if (event.seq <= watermark)
|
|
445
|
+
continue;
|
|
446
|
+
applyEvent(state, event);
|
|
447
|
+
}
|
|
448
|
+
return [...state.messages].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
|
|
449
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/** foldEvents: message-list folding from a session event stream. */
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { foldEvents } from "./messages.js";
|
|
4
|
+
/** Assemble one event with an auto-incrementing seq / time. */
|
|
5
|
+
function makeEvent(type, data, seq, time = seq * 1_000) {
|
|
6
|
+
return { type, seq, time, data };
|
|
7
|
+
}
|
|
8
|
+
/** A DSH-shaped user message payload (content: text blocks). */
|
|
9
|
+
function userMessageData(id, text) {
|
|
10
|
+
return { id, role: 'user', content: [{ type: 'text', text }], source: { kind: 'user' } };
|
|
11
|
+
}
|
|
12
|
+
/** A DSH-shaped assistant message payload for one step. */
|
|
13
|
+
function assistantMessageData(id, turn, step, text) {
|
|
14
|
+
return {
|
|
15
|
+
turn,
|
|
16
|
+
step,
|
|
17
|
+
message: {
|
|
18
|
+
id,
|
|
19
|
+
role: 'assistant',
|
|
20
|
+
content: [{ type: 'text', text }],
|
|
21
|
+
source: { kind: 'model', provider: 'fx', model: 'fx-1' },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** A DSH-shaped text-delta chunk for one step. */
|
|
26
|
+
function textChunk(turn, step, text, seq) {
|
|
27
|
+
return makeEvent('assistant/chunk', { turn, step, chunk: { type: 'text-delta', index: 0, text } }, seq);
|
|
28
|
+
}
|
|
29
|
+
describe('foldEvents', () => {
|
|
30
|
+
it('folds one full turn: user message, streamed chunks, final assistant message', () => {
|
|
31
|
+
const events = [
|
|
32
|
+
makeEvent('user/message', userMessageData('u-1', '查一下天气'), 0),
|
|
33
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '今天' } }, 1),
|
|
34
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '晴天' } }, 2),
|
|
35
|
+
makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '今天晴天'), 3),
|
|
36
|
+
];
|
|
37
|
+
const result = foldEvents(events);
|
|
38
|
+
expect(result).toHaveLength(2);
|
|
39
|
+
expect(result[0]).toMatchObject({ kind: 'user', id: 'u-1', text: '查一下天气', seq: 0 });
|
|
40
|
+
expect(result[1]).toMatchObject({
|
|
41
|
+
kind: 'assistant',
|
|
42
|
+
id: 'a-1',
|
|
43
|
+
text: '今天晴天',
|
|
44
|
+
seq: 3,
|
|
45
|
+
pending: false,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('keeps the streamed pending assistant alive while chunks arrive, then finalizes', () => {
|
|
49
|
+
const first = foldEvents([
|
|
50
|
+
makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
|
|
51
|
+
textChunk(0, 0, '你', 1),
|
|
52
|
+
textChunk(0, 0, '好', 2),
|
|
53
|
+
]);
|
|
54
|
+
const pending = first.find(message => message.kind === 'assistant');
|
|
55
|
+
expect(pending).toMatchObject({ text: '你好', pending: true, seq: 2 });
|
|
56
|
+
const finalized = foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '你好!'), 3)], first);
|
|
57
|
+
const assistant = finalized.find(message => message.kind === 'assistant');
|
|
58
|
+
expect(assistant).toMatchObject({ id: 'a-1', text: '你好!', pending: false, seq: 3 });
|
|
59
|
+
// No duplicate assistant message appears.
|
|
60
|
+
expect(finalized.filter(message => message.kind === 'assistant')).toHaveLength(1);
|
|
61
|
+
});
|
|
62
|
+
it('message/update replaces text and message/delete removes the row', () => {
|
|
63
|
+
const events = [
|
|
64
|
+
makeEvent('user/message', userMessageData('u-1', '旧文本'), 0),
|
|
65
|
+
makeEvent('user/message', userMessageData('u-2', '要删掉'), 1),
|
|
66
|
+
makeEvent('message/update', { id: 'u-1', text: '新文本' }, 2),
|
|
67
|
+
makeEvent('message/delete', { id: 'u-2' }, 3),
|
|
68
|
+
];
|
|
69
|
+
const result = foldEvents(events);
|
|
70
|
+
expect(result.map(message => message.id)).toEqual(['u-1']);
|
|
71
|
+
expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user', text: '新文本', seq: 2 });
|
|
72
|
+
});
|
|
73
|
+
it('accumulates toolSummary on the assistant message owning the tool calls', () => {
|
|
74
|
+
const events = [
|
|
75
|
+
makeEvent('user/message', userMessageData('u-1', '改文件'), 0),
|
|
76
|
+
textChunk(0, 0, '正在处理', 1),
|
|
77
|
+
makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 2),
|
|
78
|
+
makeEvent('tool/call', { turn: 0, step: 0, callId: 'c2', name: 'read', arguments: '{"path":"a.txt"}' }, 3),
|
|
79
|
+
makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '已完成'), 4),
|
|
80
|
+
];
|
|
81
|
+
const result = foldEvents(events);
|
|
82
|
+
const assistant = result.find(message => message.kind === 'assistant');
|
|
83
|
+
expect(assistant?.toolSummary).toBe('使用 bash / read');
|
|
84
|
+
expect(assistant?.tools).toEqual([
|
|
85
|
+
{ callId: 'c1', name: 'bash', arguments: '{}' },
|
|
86
|
+
{ callId: 'c2', name: 'read', arguments: '{"path":"a.txt"}' },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
it('keeps reasoning text apart from the message body and folds reasoning-delta chunks', () => {
|
|
90
|
+
const events = [
|
|
91
|
+
makeEvent('user/message', userMessageData('u-1', '复杂问题'), 0),
|
|
92
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '先分析' } }, 1),
|
|
93
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '再行动' } }, 2),
|
|
94
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 1, text: '结论' } }, 3),
|
|
95
|
+
makeEvent('assistant/message', {
|
|
96
|
+
turn: 0,
|
|
97
|
+
step: 0,
|
|
98
|
+
message: {
|
|
99
|
+
id: 'a-1',
|
|
100
|
+
role: 'assistant',
|
|
101
|
+
content: [
|
|
102
|
+
{ type: 'reasoning', text: '先分析再行动' },
|
|
103
|
+
{ type: 'text', text: '结论' },
|
|
104
|
+
],
|
|
105
|
+
source: { kind: 'model', provider: 'fx', model: 'fx-1' },
|
|
106
|
+
},
|
|
107
|
+
}, 4),
|
|
108
|
+
];
|
|
109
|
+
const result = foldEvents(events);
|
|
110
|
+
const assistant = result.find(message => message.kind === 'assistant');
|
|
111
|
+
expect(assistant).toMatchObject({ text: '结论', reasoning: '先分析再行动', pending: false });
|
|
112
|
+
});
|
|
113
|
+
it('keeps streamed reasoning when the final assistant message omits the reasoning block', () => {
|
|
114
|
+
const streamed = foldEvents([
|
|
115
|
+
makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
|
|
116
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '思考过程' } }, 1),
|
|
117
|
+
]);
|
|
118
|
+
const finalized = foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '回答'), 2)], streamed);
|
|
119
|
+
const assistant = finalized.find(message => message.kind === 'assistant');
|
|
120
|
+
expect(assistant).toMatchObject({ text: '回答', reasoning: '思考过程', pending: false });
|
|
121
|
+
});
|
|
122
|
+
it('dedupes repeated tool/call events by callId', () => {
|
|
123
|
+
const events = [
|
|
124
|
+
makeEvent('user/message', userMessageData('u-1', '跑测试'), 0),
|
|
125
|
+
textChunk(0, 0, '开始', 1),
|
|
126
|
+
makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }, 2),
|
|
127
|
+
makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }, 3),
|
|
128
|
+
makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'done'), 4),
|
|
129
|
+
];
|
|
130
|
+
const result = foldEvents(events);
|
|
131
|
+
const assistant = result.find(message => message.kind === 'assistant');
|
|
132
|
+
expect(assistant?.tools).toEqual([{ callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }]);
|
|
133
|
+
});
|
|
134
|
+
it('marks the assistant message failed when turn/end ends in an error', () => {
|
|
135
|
+
const events = [
|
|
136
|
+
makeEvent('user/message', userMessageData('u-1', 'hello'), 0),
|
|
137
|
+
makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '部分' } }, 1),
|
|
138
|
+
makeEvent('turn/end', { turn: 0, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } }, 2),
|
|
139
|
+
];
|
|
140
|
+
const result = foldEvents(events);
|
|
141
|
+
const assistant = result.find(message => message.kind === 'assistant');
|
|
142
|
+
expect(assistant).toMatchObject({ failed: true, pending: false, kind: 'assistant' });
|
|
143
|
+
});
|
|
144
|
+
it('does not flag failed for a completed turn', () => {
|
|
145
|
+
const events = [
|
|
146
|
+
makeEvent('user/message', userMessageData('u-1', 'hello'), 0),
|
|
147
|
+
makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'ok'), 1),
|
|
148
|
+
makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 2),
|
|
149
|
+
];
|
|
150
|
+
const result = foldEvents(events);
|
|
151
|
+
const assistant = result.find(message => message.kind === 'assistant');
|
|
152
|
+
expect(assistant?.failed).toBeFalsy();
|
|
153
|
+
expect(assistant?.pending).toBeFalsy();
|
|
154
|
+
});
|
|
155
|
+
it('is idempotent: applying the same batch twice yields an identical list', () => {
|
|
156
|
+
const events = [
|
|
157
|
+
makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
|
|
158
|
+
textChunk(0, 0, '一', 1),
|
|
159
|
+
textChunk(0, 0, '二', 2),
|
|
160
|
+
makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 3),
|
|
161
|
+
makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '一二'), 4),
|
|
162
|
+
makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 5),
|
|
163
|
+
];
|
|
164
|
+
const once = foldEvents(events);
|
|
165
|
+
const twice = foldEvents(events, once);
|
|
166
|
+
expect(twice).toEqual(once);
|
|
167
|
+
expect(twice.filter(message => message.kind === 'assistant')).toHaveLength(1);
|
|
168
|
+
// Text must not have doubled from re-aggregating the streamed chunks.
|
|
169
|
+
expect(twice.find(message => message.kind === 'assistant')?.text).toBe('一二');
|
|
170
|
+
});
|
|
171
|
+
it('skips unknown / unsupported event types safely', () => {
|
|
172
|
+
const events = [
|
|
173
|
+
makeEvent('session/end-seed', {}, 0),
|
|
174
|
+
makeEvent('turn/start', { turn: 0 }, 1),
|
|
175
|
+
makeEvent('user/message', userMessageData('u-1', 'hello'), 2),
|
|
176
|
+
makeEvent('some/future-plugin', { whatever: true }, 3),
|
|
177
|
+
makeEvent('goal/change', { objective: 'x' }, 4),
|
|
178
|
+
];
|
|
179
|
+
const result = foldEvents(events);
|
|
180
|
+
expect(result).toHaveLength(1);
|
|
181
|
+
expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user' });
|
|
182
|
+
});
|
|
183
|
+
it('does not mutate the caller-supplied existing list', () => {
|
|
184
|
+
const first = foldEvents([makeEvent('user/message', userMessageData('u-1', 'hello'), 0)]);
|
|
185
|
+
const snapshot = JSON.stringify(first);
|
|
186
|
+
foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'world'), 1)], first);
|
|
187
|
+
expect(JSON.stringify(first)).toBe(snapshot);
|
|
188
|
+
});
|
|
189
|
+
});
|