@khorsheed/dsh-client-message-tools 0.2.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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.en.md +4 -4
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +4 -4
  5. package/lib/client.js +51 -36
  6. package/lib/index.js +52 -57
  7. package/lib/types/client/UserMessageView.js +7 -4
  8. package/lib/types/client/WithdrawnDividerView.js +2 -2
  9. package/lib/types/client/withdrawn-node.js +3 -3
  10. package/lib/types/index.js +2 -2
  11. package/lib/types/marker.d.ts +2 -2
  12. package/lib/types/withdraw.js +67 -75
  13. package/package.json +19 -19
  14. package/lib/client.js.map +0 -1
  15. package/lib/tsconfig.client.tsbuildinfo +0 -1
  16. package/lib/tsconfig.host.tsbuildinfo +0 -1
  17. package/lib/typert.remote-client.d.ts.map +0 -1
  18. package/lib/types/client/ModelChip.d.ts.map +0 -1
  19. package/lib/types/client/RestoredMessageView.d.ts.map +0 -1
  20. package/lib/types/client/UserMessageView.d.ts.map +0 -1
  21. package/lib/types/client/WithdrawnDividerView.d.ts.map +0 -1
  22. package/lib/types/client/backfill.d.ts.map +0 -1
  23. package/lib/types/client/chat-hook.d.ts.map +0 -1
  24. package/lib/types/client/dom-hider.d.ts.map +0 -1
  25. package/lib/types/client/edit-in-place.d.ts.map +0 -1
  26. package/lib/types/client/icons.d.ts.map +0 -1
  27. package/lib/types/client/index.d.ts.map +0 -1
  28. package/lib/types/client/locales.d.ts.map +0 -1
  29. package/lib/types/client/slots.d.ts.map +0 -1
  30. package/lib/types/client/withdraw-backfill.d.ts.map +0 -1
  31. package/lib/types/client/withdrawn-node.d.ts.map +0 -1
  32. package/lib/types/index.d.ts.map +0 -1
  33. package/lib/types/invariant.d.ts.map +0 -1
  34. package/lib/types/marker.d.ts.map +0 -1
  35. package/lib/types/types.d.ts.map +0 -1
  36. package/lib/types/withdraw.d.ts.map +0 -1
@@ -111,10 +111,10 @@ 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) {
@@ -127,7 +127,7 @@ function findWithdrawnSpan(events, targetSeq) {
127
127
  if (sources?.includes(targetSeq) !== true)
128
128
  continue;
129
129
  const span = [];
130
- for (let seq = event.surfaceOp.start; seq < event.seq; seq++)
130
+ for (let seq = event.surfaceOp.startSeq; seq < event.seq; seq++)
131
131
  span.push(seq);
132
132
  return span;
133
133
  }
@@ -140,64 +140,27 @@ function findWithdrawnSpan(events, targetSeq) {
140
140
  * restore-assistant replays already carry the frame). Skipped: withdrawal
141
141
  * placeholders, edit triggers, other plugin context, and tool calls/results.
142
142
  *
143
- * Assistant text normally comes from `assistant/message` surface events. A
144
- * turn that was interrupted before finalization may never emit
145
- * `assistant/message`; its visible content survives only as `assistant/chunk`
146
- * rows. Those chunks are folded into one assistant replay per step when no
147
- * final `assistant/message` exists for that step, so withdrawing/restoring an
148
- * 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.
149
152
  */
150
153
  function replayEntries(events, spanSeqs) {
151
154
  const entries = [];
152
- 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.
153
157
  const finalizedSteps = new Set();
154
- const flushPending = () => {
155
- if (pending === undefined)
156
- return;
157
- if (!pending.finalized) {
158
- const text = [...pending.text.values()].join('');
159
- const reasoning = [...pending.reasoning.values()].join('');
160
- const replay = text !== '' ? text : reasoning;
161
- if (replay !== '') {
162
- entries.push({
163
- role: 'assistant',
164
- text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
165
- sourceSeq: pending.sourceSeq,
166
- });
167
- }
168
- }
169
- pending = undefined;
170
- };
171
- const pushChunk = (event) => {
172
- if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`))
173
- return;
174
- if (pending === undefined
175
- || pending.turn !== event.data.turn
176
- || pending.step !== event.data.step) {
177
- flushPending();
178
- pending = {
179
- turn: event.data.turn,
180
- step: event.data.step,
181
- sourceSeq: event.seq,
182
- text: new Map(),
183
- reasoning: new Map(),
184
- finalized: false,
185
- };
186
- }
187
- const chunk = event.data.chunk;
188
- if (chunk.type === 'text-delta') {
189
- pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? '') + chunk.text);
190
- }
191
- else if (chunk.type === 'block-end' && chunk.block.type === 'text') {
192
- pending.text.set(chunk.index, chunk.block.text);
193
- }
194
- else if (chunk.type === 'reasoning-delta') {
195
- pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? '') + chunk.text);
196
- }
197
- else if (chunk.type === 'block-end' && chunk.block.type === 'reasoning') {
198
- 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}`);
199
162
  }
200
- };
163
+ }
201
164
  const pushAssistantText = (text, sourceSeq) => {
202
165
  if (text !== '') {
203
166
  entries.push({ role: 'assistant', text: `${RESTORED_ASSISTANT_NOTICE}\n${text}`, sourceSeq });
@@ -208,16 +171,16 @@ function replayEntries(events, spanSeqs) {
208
171
  if (event === undefined)
209
172
  continue;
210
173
  if (isMessageToolsEdit(event)) {
211
- flushPending();
212
174
  entries.push({ role: 'user', content: event.data.content, sourceSeq: seq });
213
175
  continue;
214
176
  }
215
- if (event.type === 'assistant/chunk') {
216
- 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);
217
181
  continue;
218
182
  }
219
183
  if (event.type === 'user/message') {
220
- flushPending();
221
184
  if (event.surfaceOp !== 'append')
222
185
  continue;
223
186
  if (event.data.source.kind === 'user' || isMessageToolsRestore(event)) {
@@ -231,25 +194,54 @@ function replayEntries(events, spanSeqs) {
231
194
  continue;
232
195
  }
233
196
  if (event.type === 'assistant/message') {
234
- // A finalized step supersedes any pending chunk accumulation for the
235
- // same step: flush without emitting the partial, then replay the final.
236
- finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
237
- if (pending !== undefined
238
- && pending.turn === event.data.turn
239
- && pending.step === event.data.step) {
240
- pending.finalized = true;
241
- }
242
- flushPending();
243
197
  pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
244
198
  continue;
245
199
  }
246
- // Log-only events (boundaries, headers, titles) do not split a chunk run;
247
- // the pending assistant step is flushed at the next replayable boundary or
248
- // when a different step starts.
200
+ // Log-only events (boundaries, headers, titles) contribute nothing.
249
201
  }
250
- flushPending();
251
202
  return entries;
252
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
+ }
253
245
  /**
254
246
  * Plan a restore: the target must be an editable user message (the same
255
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.2.0",
10
+ "version": "0.3.0",
11
11
  "type": "module",
12
12
  "main": "lib/index.js",
13
13
  "types": "lib/types/index.d.ts",
@@ -53,8 +53,8 @@
53
53
  "platform": "web"
54
54
  },
55
55
  "compat": {
56
- "minHost": "0.1.2-rc.1",
57
- "verifiedHost": "0.1.2-rc.1"
56
+ "minHost": "0.1.5-rc.1",
57
+ "verifiedHost": "0.1.5-rc.1"
58
58
  }
59
59
  },
60
60
  "license": "MIT",
@@ -74,22 +74,22 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@deepseek-ai/cordis": "^4.0.1",
77
- "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
78
- "@deepseek-ai/dsh-api-remotes": "^0.1.2-rc.1",
79
- "@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1",
80
- "@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
81
- "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
82
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.2-rc.1",
83
- "@deepseek-ai/dsh-client-ui-chat": "^0.1.2-rc.1",
84
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
85
- "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.2-rc.1",
86
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
87
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
88
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
89
- "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
90
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
91
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
92
- "@deepseek-ai/dsh-typert-protocol": "^0.1.2-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",
93
93
  "@testing-library/react": "^16.1.0",
94
94
  "@types/node": "^26.2.0",
95
95
  "@types/react": "~18.3.1",