@khorsheed/dsh-client-message-tools 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.en.md +16 -12
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +35 -31
  5. package/lib/client.js +295 -135
  6. package/lib/index.js +63 -67
  7. package/lib/typert.host.js +3 -3
  8. package/lib/typert.remote-client.js +3 -3
  9. package/lib/types/client/RestoredMessageView.d.ts +1 -1
  10. package/lib/types/client/RestoredMessageView.js +20 -1
  11. package/lib/types/client/UserMessageView.d.ts +1 -1
  12. package/lib/types/client/UserMessageView.js +16 -10
  13. package/lib/types/client/WithdrawnDividerView.d.ts +1 -1
  14. package/lib/types/client/WithdrawnDividerView.js +13 -9
  15. package/lib/types/client/chat-hook.d.ts +36 -0
  16. package/lib/types/client/chat-hook.js +51 -0
  17. package/lib/types/client/dom-hider.d.ts +3 -2
  18. package/lib/types/client/dom-hider.js +20 -13
  19. package/lib/types/client/edit-in-place.d.ts +2 -1
  20. package/lib/types/client/index.d.ts +6 -4
  21. package/lib/types/client/index.js +42 -8
  22. package/lib/types/client/locales.d.ts +8 -0
  23. package/lib/types/client/locales.js +8 -0
  24. package/lib/types/client/withdrawn-node.d.ts +3 -2
  25. package/lib/types/client/withdrawn-node.js +45 -54
  26. package/lib/types/index.js +10 -8
  27. package/lib/types/marker.d.ts +2 -2
  28. package/lib/types/withdraw.js +76 -80
  29. package/package.json +19 -18
  30. package/lib/client.js.map +0 -1
  31. package/lib/tsconfig.client.tsbuildinfo +0 -1
  32. package/lib/tsconfig.host.tsbuildinfo +0 -1
  33. package/lib/typert.remote-client.d.ts.map +0 -1
  34. package/lib/types/client/ModelChip.d.ts.map +0 -1
  35. package/lib/types/client/RestoredMessageView.d.ts.map +0 -1
  36. package/lib/types/client/UserMessageView.d.ts.map +0 -1
  37. package/lib/types/client/WithdrawnDividerView.d.ts.map +0 -1
  38. package/lib/types/client/backfill.d.ts.map +0 -1
  39. package/lib/types/client/dom-hider.d.ts.map +0 -1
  40. package/lib/types/client/edit-in-place.d.ts.map +0 -1
  41. package/lib/types/client/icons.d.ts.map +0 -1
  42. package/lib/types/client/index.d.ts.map +0 -1
  43. package/lib/types/client/locales.d.ts.map +0 -1
  44. package/lib/types/client/slots.d.ts.map +0 -1
  45. package/lib/types/client/withdraw-backfill.d.ts.map +0 -1
  46. package/lib/types/client/withdrawn-node.d.ts.map +0 -1
  47. package/lib/types/index.d.ts.map +0 -1
  48. package/lib/types/invariant.d.ts.map +0 -1
  49. package/lib/types/marker.d.ts.map +0 -1
  50. package/lib/types/types.d.ts.map +0 -1
  51. package/lib/types/withdraw.d.ts.map +0 -1
@@ -111,21 +111,25 @@ function joinAssistantMessageText(blocks) {
111
111
  * (the message left the surface through another producer, e.g. compaction).
112
112
  *
113
113
  * `sourceEventSeqs` is authoritative for the shadowed *surface* nodes, but an
114
- * interrupted assistant step may exist only as `assistant/chunk` events (no
115
- * `assistant/message` ever landed). Those chunks are not surface nodes, so
116
- * they are absent from `sourceEventSeqs`. The replacement's own `start` and
117
- * `seq` delimit the whole withdrawn log interval — `[start, seq)` — and are
114
+ * interrupted assistant step may exist only as `assistant/attempt` events (no
115
+ * `assistant/message` ever landed). Attempts are not surface nodes, so
116
+ * they are absent from `sourceEventSeqs`. The replacement's own `startSeq` and
117
+ * `seq` delimit the whole withdrawn log interval — `[startSeq, seq)` — and are
118
118
  * still read from the replacement itself, never guessed from another producer.
119
119
  */
120
120
  function findWithdrawnSpan(events, targetSeq) {
121
121
  for (let index = events.length - 1; index > targetSeq; index--) {
122
122
  const event = events[index];
123
- if (event !== undefined && isMessageToolsReplacement(event) && event.sourceEventSeqs?.includes(targetSeq)) {
124
- const span = [];
125
- for (let seq = event.surfaceOp.start; seq < event.seq; seq++)
126
- span.push(seq);
127
- return span;
128
- }
123
+ if (event === undefined || !isMessageToolsReplacement(event))
124
+ continue;
125
+ // The brands are compile-time only; the fold addresses the log by plain number.
126
+ const sources = event.sourceEventSeqs;
127
+ if (sources?.includes(targetSeq) !== true)
128
+ continue;
129
+ const span = [];
130
+ for (let seq = event.surfaceOp.startSeq; seq < event.seq; seq++)
131
+ span.push(seq);
132
+ return span;
129
133
  }
130
134
  return undefined;
131
135
  }
@@ -136,64 +140,27 @@ function findWithdrawnSpan(events, targetSeq) {
136
140
  * restore-assistant replays already carry the frame). Skipped: withdrawal
137
141
  * placeholders, edit triggers, other plugin context, and tool calls/results.
138
142
  *
139
- * Assistant text normally comes from `assistant/message` surface events. A
140
- * turn that was interrupted before finalization may never emit
141
- * `assistant/message`; its visible content survives only as `assistant/chunk`
142
- * rows. Those chunks are folded into one assistant replay per step when no
143
- * final `assistant/message` exists for that step, so withdrawing/restoring an
144
- * interrupted reply does not lose the partial assistant content.
143
+ * Assistant text normally comes from `assistant/message` surface events (a
144
+ * cancelled turn finalizes its delivered prefix there with `interrupted:
145
+ * true`). An attempt that committed no surface message — failed, retried, or
146
+ * stream-error — settles as an `assistant/attempt` log event carrying its
147
+ * exact compact stream. Each such attempt folds into one assistant replay
148
+ * when no final `assistant/message` exists for that step, so
149
+ * withdrawing/restoring an interrupted reply does not lose the partial
150
+ * assistant content. A retried step's earlier attempts are superseded by the
151
+ * step's final message and do not replay.
145
152
  */
146
153
  function replayEntries(events, spanSeqs) {
147
154
  const entries = [];
148
- let pending;
155
+ // A step's final message supersedes its attempts; collect the finalized
156
+ // steps first so an attempt is skipped regardless of log order.
149
157
  const finalizedSteps = new Set();
150
- const flushPending = () => {
151
- if (pending === undefined)
152
- return;
153
- if (!pending.finalized) {
154
- const text = [...pending.text.values()].join('');
155
- const reasoning = [...pending.reasoning.values()].join('');
156
- const replay = text !== '' ? text : reasoning;
157
- if (replay !== '') {
158
- entries.push({
159
- role: 'assistant',
160
- text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
161
- sourceSeq: pending.sourceSeq,
162
- });
163
- }
164
- }
165
- pending = undefined;
166
- };
167
- const pushChunk = (event) => {
168
- if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`))
169
- return;
170
- if (pending === undefined
171
- || pending.turn !== event.data.turn
172
- || pending.step !== event.data.step) {
173
- flushPending();
174
- pending = {
175
- turn: event.data.turn,
176
- step: event.data.step,
177
- sourceSeq: event.seq,
178
- text: new Map(),
179
- reasoning: new Map(),
180
- finalized: false,
181
- };
182
- }
183
- const chunk = event.data.chunk;
184
- if (chunk.type === 'text-delta') {
185
- pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? '') + chunk.text);
186
- }
187
- else if (chunk.type === 'block-end' && chunk.block.type === 'text') {
188
- pending.text.set(chunk.index, chunk.block.text);
189
- }
190
- else if (chunk.type === 'reasoning-delta') {
191
- pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? '') + chunk.text);
192
- }
193
- else if (chunk.type === 'block-end' && chunk.block.type === 'reasoning') {
194
- pending.reasoning.set(chunk.index, chunk.block.text);
158
+ for (const seq of spanSeqs) {
159
+ const event = events[seq];
160
+ if (event?.type === 'assistant/message') {
161
+ finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
195
162
  }
196
- };
163
+ }
197
164
  const pushAssistantText = (text, sourceSeq) => {
198
165
  if (text !== '') {
199
166
  entries.push({ role: 'assistant', text: `${RESTORED_ASSISTANT_NOTICE}\n${text}`, sourceSeq });
@@ -204,16 +171,16 @@ function replayEntries(events, spanSeqs) {
204
171
  if (event === undefined)
205
172
  continue;
206
173
  if (isMessageToolsEdit(event)) {
207
- flushPending();
208
174
  entries.push({ role: 'user', content: event.data.content, sourceSeq: seq });
209
175
  continue;
210
176
  }
211
- if (event.type === 'assistant/chunk') {
212
- pushChunk(event);
177
+ if (event.type === 'assistant/attempt') {
178
+ if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`))
179
+ continue;
180
+ pushAssistantText(joinAttemptStreamText(event.data.stream), seq);
213
181
  continue;
214
182
  }
215
183
  if (event.type === 'user/message') {
216
- flushPending();
217
184
  if (event.surfaceOp !== 'append')
218
185
  continue;
219
186
  if (event.data.source.kind === 'user' || isMessageToolsRestore(event)) {
@@ -227,25 +194,54 @@ function replayEntries(events, spanSeqs) {
227
194
  continue;
228
195
  }
229
196
  if (event.type === 'assistant/message') {
230
- // A finalized step supersedes any pending chunk accumulation for the
231
- // same step: flush without emitting the partial, then replay the final.
232
- finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
233
- if (pending !== undefined
234
- && pending.turn === event.data.turn
235
- && pending.step === event.data.step) {
236
- pending.finalized = true;
237
- }
238
- flushPending();
239
197
  pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
240
198
  continue;
241
199
  }
242
- // Log-only events (boundaries, headers, titles) do not split a chunk run;
243
- // the pending assistant step is flushed at the next replayable boundary or
244
- // when a different step starts.
200
+ // Log-only events (boundaries, headers, titles) contribute nothing.
245
201
  }
246
- flushPending();
247
202
  return entries;
248
203
  }
204
+ /**
205
+ * Join one attempt's compact stream into its visible text. Text deltas win;
206
+ * reasoning is the fallback so an attempt that streamed only reasoning does
207
+ * not lose the only assistant content the user saw (the same preference
208
+ * {@link joinAssistantMessageText} applies to finalized messages). Packed
209
+ * delta runs append per block index; a raw `block-end` record carries the
210
+ * assembled block and replaces its index's accumulated deltas. Tool-call
211
+ * runs never replay (their side effects are not replayable).
212
+ */
213
+ function joinAttemptStreamText(stream) {
214
+ const text = new Map();
215
+ const reasoning = new Map();
216
+ const append = (map, index, fragment) => {
217
+ map.set(index, (map.get(index) ?? '') + fragment);
218
+ };
219
+ for (const record of stream) {
220
+ if (record.type === 'text-chunks') {
221
+ append(text, record.index, record.texts.join(''));
222
+ }
223
+ else if (record.type === 'reasoning-chunks') {
224
+ append(reasoning, record.index, record.texts.join(''));
225
+ }
226
+ else if (record.type === 'chunk') {
227
+ const chunk = record.chunk;
228
+ if (chunk.type === 'text-delta') {
229
+ append(text, chunk.index, chunk.text);
230
+ }
231
+ else if (chunk.type === 'reasoning-delta') {
232
+ append(reasoning, chunk.index, chunk.text);
233
+ }
234
+ else if (chunk.type === 'block-end' && chunk.block.type === 'text') {
235
+ text.set(chunk.index, chunk.block.text);
236
+ }
237
+ else if (chunk.type === 'block-end' && chunk.block.type === 'reasoning') {
238
+ reasoning.set(chunk.index, chunk.block.text);
239
+ }
240
+ }
241
+ }
242
+ const joined = [...text.values()].join('');
243
+ return joined !== '' ? joined : [...reasoning.values()].join('');
244
+ }
249
245
  /**
250
246
  * Plan a restore: the target must be an editable user message (the same
251
247
  * vocabulary as withdrawal — original, restore replay, or edit replacement)
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "cordis",
8
8
  "messages"
9
9
  ],
10
- "version": "0.1.0",
10
+ "version": "0.3.0",
11
11
  "type": "module",
12
12
  "main": "lib/index.js",
13
13
  "types": "lib/types/index.d.ts",
@@ -45,7 +45,6 @@
45
45
  },
46
46
  "client": {
47
47
  "inject": [
48
- "@deepseek-ai/dsh-client-runtime",
49
48
  "@deepseek-ai/dsh-api-remotes",
50
49
  "@deepseek-ai/dsh-client-locale",
51
50
  "@deepseek-ai/dsh-client-ui-conversation",
@@ -54,8 +53,8 @@
54
53
  "platform": "web"
55
54
  },
56
55
  "compat": {
57
- "minHost": "0.1.0-rc.8",
58
- "verifiedHost": "0.1.1-rc.2"
56
+ "minHost": "0.1.5-rc.1",
57
+ "verifiedHost": "0.1.5-rc.1"
59
58
  }
60
59
  },
61
60
  "license": "MIT",
@@ -63,7 +62,6 @@
63
62
  "@deepseek-ai/cordis": "^4.0.1",
64
63
  "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
65
64
  "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
66
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
67
65
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
68
66
  "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.0-rc.6",
69
67
  "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
@@ -76,19 +74,22 @@
76
74
  },
77
75
  "devDependencies": {
78
76
  "@deepseek-ai/cordis": "^4.0.1",
79
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
80
- "@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.1",
81
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
82
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
83
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.1-rc.1",
84
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
85
- "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.1-rc.1",
86
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.1",
87
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
88
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
89
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
90
- "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
91
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.1",
77
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
78
+ "@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.1",
79
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.1",
80
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
81
+ "@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
82
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.5-rc.1",
83
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.1",
84
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
85
+ "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.5-rc.1",
86
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
87
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
88
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
89
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
90
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
91
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
92
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.1",
92
93
  "@testing-library/react": "^16.1.0",
93
94
  "@types/node": "^26.2.0",
94
95
  "@types/react": "~18.3.1",