@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.
- package/CHANGELOG.md +16 -0
- package/README.en.md +16 -12
- package/README.i18n.yaml +2 -2
- package/README.md +35 -31
- package/lib/client.js +295 -135
- package/lib/index.js +63 -67
- package/lib/typert.host.js +3 -3
- package/lib/typert.remote-client.js +3 -3
- package/lib/types/client/RestoredMessageView.d.ts +1 -1
- package/lib/types/client/RestoredMessageView.js +20 -1
- package/lib/types/client/UserMessageView.d.ts +1 -1
- package/lib/types/client/UserMessageView.js +16 -10
- package/lib/types/client/WithdrawnDividerView.d.ts +1 -1
- package/lib/types/client/WithdrawnDividerView.js +13 -9
- package/lib/types/client/chat-hook.d.ts +36 -0
- package/lib/types/client/chat-hook.js +51 -0
- package/lib/types/client/dom-hider.d.ts +3 -2
- package/lib/types/client/dom-hider.js +20 -13
- package/lib/types/client/edit-in-place.d.ts +2 -1
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/index.js +42 -8
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.js +8 -0
- package/lib/types/client/withdrawn-node.d.ts +3 -2
- package/lib/types/client/withdrawn-node.js +45 -54
- package/lib/types/index.js +10 -8
- package/lib/types/marker.d.ts +2 -2
- package/lib/types/withdraw.js +76 -80
- package/package.json +19 -18
- package/lib/client.js.map +0 -1
- package/lib/tsconfig.client.tsbuildinfo +0 -1
- package/lib/tsconfig.host.tsbuildinfo +0 -1
- package/lib/typert.remote-client.d.ts.map +0 -1
- package/lib/types/client/ModelChip.d.ts.map +0 -1
- package/lib/types/client/RestoredMessageView.d.ts.map +0 -1
- package/lib/types/client/UserMessageView.d.ts.map +0 -1
- package/lib/types/client/WithdrawnDividerView.d.ts.map +0 -1
- package/lib/types/client/backfill.d.ts.map +0 -1
- package/lib/types/client/dom-hider.d.ts.map +0 -1
- package/lib/types/client/edit-in-place.d.ts.map +0 -1
- package/lib/types/client/icons.d.ts.map +0 -1
- package/lib/types/client/index.d.ts.map +0 -1
- package/lib/types/client/locales.d.ts.map +0 -1
- package/lib/types/client/slots.d.ts.map +0 -1
- package/lib/types/client/withdraw-backfill.d.ts.map +0 -1
- package/lib/types/client/withdrawn-node.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/invariant.d.ts.map +0 -1
- package/lib/types/marker.d.ts.map +0 -1
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/withdraw.d.ts.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
1
2
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
3
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
4
|
import { isAppendSurfaceEvent, isReplacementSurfaceEvent } from "@deepseek-ai/dsh-session/surface";
|
|
@@ -225,20 +226,20 @@ function joinAssistantMessageText(blocks) {
|
|
|
225
226
|
* (the message left the surface through another producer, e.g. compaction).
|
|
226
227
|
*
|
|
227
228
|
* `sourceEventSeqs` is authoritative for the shadowed *surface* nodes, but an
|
|
228
|
-
* interrupted assistant step may exist only as `assistant/
|
|
229
|
-
* `assistant/message` ever landed).
|
|
230
|
-
* they are absent from `sourceEventSeqs`. The replacement's own `
|
|
231
|
-
* `seq` delimit the whole withdrawn log interval — `[
|
|
229
|
+
* interrupted assistant step may exist only as `assistant/attempt` events (no
|
|
230
|
+
* `assistant/message` ever landed). Attempts are not surface nodes, so
|
|
231
|
+
* they are absent from `sourceEventSeqs`. The replacement's own `startSeq` and
|
|
232
|
+
* `seq` delimit the whole withdrawn log interval — `[startSeq, seq)` — and are
|
|
232
233
|
* still read from the replacement itself, never guessed from another producer.
|
|
233
234
|
*/
|
|
234
235
|
function findWithdrawnSpan(events, targetSeq) {
|
|
235
236
|
for (let index = events.length - 1; index > targetSeq; index--) {
|
|
236
237
|
const event = events[index];
|
|
237
|
-
if (event
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
if (event === void 0 || !isMessageToolsReplacement(event)) continue;
|
|
239
|
+
if (event.sourceEventSeqs?.includes(targetSeq) !== true) continue;
|
|
240
|
+
const span = [];
|
|
241
|
+
for (let seq = event.surfaceOp.startSeq; seq < event.seq; seq++) span.push(seq);
|
|
242
|
+
return span;
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
/**
|
|
@@ -248,50 +249,23 @@ function findWithdrawnSpan(events, targetSeq) {
|
|
|
248
249
|
* restore-assistant replays already carry the frame). Skipped: withdrawal
|
|
249
250
|
* placeholders, edit triggers, other plugin context, and tool calls/results.
|
|
250
251
|
*
|
|
251
|
-
* Assistant text normally comes from `assistant/message` surface events
|
|
252
|
-
* turn
|
|
253
|
-
* `
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
252
|
+
* Assistant text normally comes from `assistant/message` surface events (a
|
|
253
|
+
* cancelled turn finalizes its delivered prefix there with `interrupted:
|
|
254
|
+
* true`). An attempt that committed no surface message — failed, retried, or
|
|
255
|
+
* stream-error — settles as an `assistant/attempt` log event carrying its
|
|
256
|
+
* exact compact stream. Each such attempt folds into one assistant replay
|
|
257
|
+
* when no final `assistant/message` exists for that step, so
|
|
258
|
+
* withdrawing/restoring an interrupted reply does not lose the partial
|
|
259
|
+
* assistant content. A retried step's earlier attempts are superseded by the
|
|
260
|
+
* step's final message and do not replay.
|
|
257
261
|
*/
|
|
258
262
|
function replayEntries(events, spanSeqs) {
|
|
259
263
|
const entries = [];
|
|
260
|
-
let pending;
|
|
261
264
|
const finalizedSteps = /* @__PURE__ */ new Set();
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
const reasoning = [...pending.reasoning.values()].join("");
|
|
267
|
-
const replay = text !== "" ? text : reasoning;
|
|
268
|
-
if (replay !== "") entries.push({
|
|
269
|
-
role: "assistant",
|
|
270
|
-
text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
|
|
271
|
-
sourceSeq: pending.sourceSeq
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
pending = void 0;
|
|
275
|
-
};
|
|
276
|
-
const pushChunk = (event) => {
|
|
277
|
-
if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`)) return;
|
|
278
|
-
if (pending === void 0 || pending.turn !== event.data.turn || pending.step !== event.data.step) {
|
|
279
|
-
flushPending();
|
|
280
|
-
pending = {
|
|
281
|
-
turn: event.data.turn,
|
|
282
|
-
step: event.data.step,
|
|
283
|
-
sourceSeq: event.seq,
|
|
284
|
-
text: /* @__PURE__ */ new Map(),
|
|
285
|
-
reasoning: /* @__PURE__ */ new Map(),
|
|
286
|
-
finalized: false
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
const chunk = event.data.chunk;
|
|
290
|
-
if (chunk.type === "text-delta") pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? "") + chunk.text);
|
|
291
|
-
else if (chunk.type === "block-end" && chunk.block.type === "text") pending.text.set(chunk.index, chunk.block.text);
|
|
292
|
-
else if (chunk.type === "reasoning-delta") pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? "") + chunk.text);
|
|
293
|
-
else if (chunk.type === "block-end" && chunk.block.type === "reasoning") pending.reasoning.set(chunk.index, chunk.block.text);
|
|
294
|
-
};
|
|
265
|
+
for (const seq of spanSeqs) {
|
|
266
|
+
const event = events[seq];
|
|
267
|
+
if (event?.type === "assistant/message") finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
|
|
268
|
+
}
|
|
295
269
|
const pushAssistantText = (text, sourceSeq) => {
|
|
296
270
|
if (text !== "") entries.push({
|
|
297
271
|
role: "assistant",
|
|
@@ -303,7 +277,6 @@ function replayEntries(events, spanSeqs) {
|
|
|
303
277
|
const event = events[seq];
|
|
304
278
|
if (event === void 0) continue;
|
|
305
279
|
if (isMessageToolsEdit(event)) {
|
|
306
|
-
flushPending();
|
|
307
280
|
entries.push({
|
|
308
281
|
role: "user",
|
|
309
282
|
content: event.data.content,
|
|
@@ -311,12 +284,12 @@ function replayEntries(events, spanSeqs) {
|
|
|
311
284
|
});
|
|
312
285
|
continue;
|
|
313
286
|
}
|
|
314
|
-
if (event.type === "assistant/
|
|
315
|
-
|
|
287
|
+
if (event.type === "assistant/attempt") {
|
|
288
|
+
if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`)) continue;
|
|
289
|
+
pushAssistantText(joinAttemptStreamText(event.data.stream), seq);
|
|
316
290
|
continue;
|
|
317
291
|
}
|
|
318
292
|
if (event.type === "user/message") {
|
|
319
|
-
flushPending();
|
|
320
293
|
if (event.surfaceOp !== "append") continue;
|
|
321
294
|
if (event.data.source.kind === "user" || isMessageToolsRestore(event)) entries.push({
|
|
322
295
|
role: "user",
|
|
@@ -331,17 +304,40 @@ function replayEntries(events, spanSeqs) {
|
|
|
331
304
|
continue;
|
|
332
305
|
}
|
|
333
306
|
if (event.type === "assistant/message") {
|
|
334
|
-
finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
|
|
335
|
-
if (pending !== void 0 && pending.turn === event.data.turn && pending.step === event.data.step) pending.finalized = true;
|
|
336
|
-
flushPending();
|
|
337
307
|
pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
|
|
338
308
|
continue;
|
|
339
309
|
}
|
|
340
310
|
}
|
|
341
|
-
flushPending();
|
|
342
311
|
return entries;
|
|
343
312
|
}
|
|
344
313
|
/**
|
|
314
|
+
* Join one attempt's compact stream into its visible text. Text deltas win;
|
|
315
|
+
* reasoning is the fallback so an attempt that streamed only reasoning does
|
|
316
|
+
* not lose the only assistant content the user saw (the same preference
|
|
317
|
+
* {@link joinAssistantMessageText} applies to finalized messages). Packed
|
|
318
|
+
* delta runs append per block index; a raw `block-end` record carries the
|
|
319
|
+
* assembled block and replaces its index's accumulated deltas. Tool-call
|
|
320
|
+
* runs never replay (their side effects are not replayable).
|
|
321
|
+
*/
|
|
322
|
+
function joinAttemptStreamText(stream) {
|
|
323
|
+
const text = /* @__PURE__ */ new Map();
|
|
324
|
+
const reasoning = /* @__PURE__ */ new Map();
|
|
325
|
+
const append = (map, index, fragment) => {
|
|
326
|
+
map.set(index, (map.get(index) ?? "") + fragment);
|
|
327
|
+
};
|
|
328
|
+
for (const record of stream) if (record.type === "text-chunks") append(text, record.index, record.texts.join(""));
|
|
329
|
+
else if (record.type === "reasoning-chunks") append(reasoning, record.index, record.texts.join(""));
|
|
330
|
+
else if (record.type === "chunk") {
|
|
331
|
+
const chunk = record.chunk;
|
|
332
|
+
if (chunk.type === "text-delta") append(text, chunk.index, chunk.text);
|
|
333
|
+
else if (chunk.type === "reasoning-delta") append(reasoning, chunk.index, chunk.text);
|
|
334
|
+
else if (chunk.type === "block-end" && chunk.block.type === "text") text.set(chunk.index, chunk.block.text);
|
|
335
|
+
else if (chunk.type === "block-end" && chunk.block.type === "reasoning") reasoning.set(chunk.index, chunk.block.text);
|
|
336
|
+
}
|
|
337
|
+
const joined = [...text.values()].join("");
|
|
338
|
+
return joined !== "" ? joined : [...reasoning.values()].join("");
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
345
341
|
* Plan a restore: the target must be an editable user message (the same
|
|
346
342
|
* vocabulary as withdrawal — original, restore replay, or edit replacement)
|
|
347
343
|
* that is no longer on the live surface (i.e. withdrawn). Restoring an edit
|
|
@@ -502,7 +498,7 @@ let MessageToolsService = (() => {
|
|
|
502
498
|
ok: false,
|
|
503
499
|
error: { code: "session-not-found" }
|
|
504
500
|
};
|
|
505
|
-
const planned = planWithdrawal(session.
|
|
501
|
+
const planned = planWithdrawal(session.snapshotEvents(), session.surface.nodes, request.targetSeq);
|
|
506
502
|
if (!planned.ok) return {
|
|
507
503
|
ok: false,
|
|
508
504
|
error: { code: planned.code }
|
|
@@ -519,10 +515,10 @@ let MessageToolsService = (() => {
|
|
|
519
515
|
}), {
|
|
520
516
|
surfaceOp: {
|
|
521
517
|
op: "replace",
|
|
522
|
-
|
|
523
|
-
|
|
518
|
+
startSeq: SessionSeq(planned.plan.start),
|
|
519
|
+
endSeq: SessionSeq(planned.plan.end)
|
|
524
520
|
},
|
|
525
|
-
sourceEventSeqs:
|
|
521
|
+
sourceEventSeqs: planned.plan.sourceEventSeqs.map((seq) => SessionSeq(seq))
|
|
526
522
|
});
|
|
527
523
|
await this.ctx.sessions.flush(session);
|
|
528
524
|
return {
|
|
@@ -553,7 +549,7 @@ let MessageToolsService = (() => {
|
|
|
553
549
|
ok: false,
|
|
554
550
|
error: { code: "session-not-found" }
|
|
555
551
|
};
|
|
556
|
-
const planned = planRestore(session.
|
|
552
|
+
const planned = planRestore(session.snapshotEvents(), session.surface.nodes, request.targetSeq);
|
|
557
553
|
if (!planned.ok) return {
|
|
558
554
|
ok: false,
|
|
559
555
|
error: { code: planned.code }
|
|
@@ -575,7 +571,7 @@ let MessageToolsService = (() => {
|
|
|
575
571
|
});
|
|
576
572
|
appendedSeqs.push(session.append("user/message", message, {
|
|
577
573
|
surfaceOp: "append",
|
|
578
|
-
sourceEventSeqs: [entry.sourceSeq]
|
|
574
|
+
sourceEventSeqs: [SessionSeq(entry.sourceSeq)]
|
|
579
575
|
}).seq);
|
|
580
576
|
}
|
|
581
577
|
await this.ctx.sessions.flush(session);
|
|
@@ -602,7 +598,7 @@ let MessageToolsService = (() => {
|
|
|
602
598
|
ok: false,
|
|
603
599
|
error: { code: "session-not-found" }
|
|
604
600
|
};
|
|
605
|
-
const planned = planEdit(session.
|
|
601
|
+
const planned = planEdit(session.snapshotEvents(), session.surface.nodes, request.targetSeq, request.text);
|
|
606
602
|
if (!planned.ok) return {
|
|
607
603
|
ok: false,
|
|
608
604
|
error: { code: planned.code }
|
|
@@ -616,10 +612,10 @@ let MessageToolsService = (() => {
|
|
|
616
612
|
}), {
|
|
617
613
|
surfaceOp: {
|
|
618
614
|
op: "replace",
|
|
619
|
-
|
|
620
|
-
|
|
615
|
+
startSeq: SessionSeq(planned.plan.start),
|
|
616
|
+
endSeq: SessionSeq(planned.plan.end)
|
|
621
617
|
},
|
|
622
|
-
sourceEventSeqs:
|
|
618
|
+
sourceEventSeqs: planned.plan.sourceEventSeqs.map((seq) => SessionSeq(seq))
|
|
623
619
|
});
|
|
624
620
|
await this.ctx.sessions.flush(session);
|
|
625
621
|
const agent = this.ctx.agents.get(request.sessionId);
|
package/lib/typert.host.js
CHANGED
|
@@ -99,7 +99,7 @@ export const TYPERT = {
|
|
|
99
99
|
typeSymbol: '@khorsheed/dsh-client-message-tools/types#MessageToolsEditResult',
|
|
100
100
|
schema: _khorsheed_dsh_client_message_tools_messageTools_edit_result$schema,
|
|
101
101
|
},
|
|
102
|
-
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":
|
|
102
|
+
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":135,"column":9},
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
id: '@khorsheed/dsh-client-message-tools#messageTools/restore',
|
|
@@ -124,7 +124,7 @@ export const TYPERT = {
|
|
|
124
124
|
typeSymbol: '@khorsheed/dsh-client-message-tools/types#MessageToolsRestoreResult',
|
|
125
125
|
schema: _khorsheed_dsh_client_message_tools_messageTools_restore_result$schema,
|
|
126
126
|
},
|
|
127
|
-
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":
|
|
127
|
+
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":97,"column":9},
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
id: '@khorsheed/dsh-client-message-tools#messageTools/withdraw',
|
|
@@ -149,7 +149,7 @@ export const TYPERT = {
|
|
|
149
149
|
typeSymbol: '@khorsheed/dsh-client-message-tools/types#MessageToolsWithdrawResult',
|
|
150
150
|
schema: _khorsheed_dsh_client_message_tools_messageTools_withdraw_result$schema,
|
|
151
151
|
},
|
|
152
|
-
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":
|
|
152
|
+
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":63,"column":9},
|
|
153
153
|
},
|
|
154
154
|
],
|
|
155
155
|
model: {
|
|
@@ -96,7 +96,7 @@ export const TYPERT_REMOTE = {
|
|
|
96
96
|
typeSymbol: '@khorsheed/dsh-client-message-tools/types#MessageToolsEditResult',
|
|
97
97
|
schema: _khorsheed_dsh_client_message_tools_messageTools_edit_result$schema,
|
|
98
98
|
},
|
|
99
|
-
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":
|
|
99
|
+
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":135,"column":9},
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
id: '@khorsheed/dsh-client-message-tools#messageTools/restore',
|
|
@@ -121,7 +121,7 @@ export const TYPERT_REMOTE = {
|
|
|
121
121
|
typeSymbol: '@khorsheed/dsh-client-message-tools/types#MessageToolsRestoreResult',
|
|
122
122
|
schema: _khorsheed_dsh_client_message_tools_messageTools_restore_result$schema,
|
|
123
123
|
},
|
|
124
|
-
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":
|
|
124
|
+
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":97,"column":9},
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
id: '@khorsheed/dsh-client-message-tools#messageTools/withdraw',
|
|
@@ -146,7 +146,7 @@ export const TYPERT_REMOTE = {
|
|
|
146
146
|
typeSymbol: '@khorsheed/dsh-client-message-tools/types#MessageToolsWithdrawResult',
|
|
147
147
|
schema: _khorsheed_dsh_client_message_tools_messageTools_withdraw_result$schema,
|
|
148
148
|
},
|
|
149
|
-
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":
|
|
149
|
+
sourceLocation: {"file":"packages/message-tools/src/index.ts","line":63,"column":9},
|
|
150
150
|
},
|
|
151
151
|
],
|
|
152
152
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* marker. The model-facing frame text is not displayed (stripped in the
|
|
9
9
|
* Definition). No action row: the group's user bubbles already carry copy.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { type ReactNode } from 'react';
|
|
12
12
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
13
13
|
/** Full props of the restored-assistant renderer. */
|
|
14
14
|
export type RestoredMessageViewProps = PropsRuntime<'conversation.chat.node', 'message-tools-restored-assistant'> & PropsLocale<'message-tools'>;
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Restored-assistant line ('message-tools-restored-assistant' keyed
|
|
4
|
+
* renderer): one withdrawn assistant reply's text replayed at the
|
|
5
|
+
* conversation tail by a restore. The body renders through the official
|
|
6
|
+
* `MarkdownText` (the same public renderer behind ui-conversation's
|
|
7
|
+
* AssistantMarkdown), so typography, code blocks, and lists match a native
|
|
8
|
+
* assistant reply; the「已恢复 · 助手回复」caption stays as a small tertiary
|
|
9
|
+
* marker. The model-facing frame text is not displayed (stripped in the
|
|
10
|
+
* Definition). No action row: the group's user bubbles already carry copy.
|
|
11
|
+
*/
|
|
12
|
+
import { useMemo } from 'react';
|
|
2
13
|
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
3
14
|
import { IconUndoOutline16 } from './icons.js';
|
|
4
15
|
import css from './RestoredMessageView.module.css';
|
|
16
|
+
/** Localized Markdown chrome (code-fence copy buttons, footnotes) over the message-tools namespace. */
|
|
17
|
+
function markdownLabels(t) {
|
|
18
|
+
return {
|
|
19
|
+
code: { copyLabel: t('markdown.copy'), copiedLabel: t('markdown.copied') },
|
|
20
|
+
footnotes: t('markdown.footnotes'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
5
23
|
/** The restored assistant line: caption plus the reply body in official markdown chrome. */
|
|
6
24
|
export function RestoredMessageView({ node, t }) {
|
|
7
25
|
const data = node.data;
|
|
8
|
-
|
|
26
|
+
const labels = useMemo(() => markdownLabels(t), [t]);
|
|
27
|
+
return (_jsxs("div", { className: css.restoredAssistantRow, children: [_jsxs("div", { className: css.restoredLabel, children: [_jsx(IconUndoOutline16, {}), _jsx("span", { children: t('restored.assistant') })] }), _jsx(MarkdownText, { text: data.text, labels: labels })] }));
|
|
9
28
|
}
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
import { type ReactNode } from 'react';
|
|
13
13
|
import type { UserMessageViewProps } from './slots.ts';
|
|
14
14
|
/** The shadowed user-message view: bubble plus copy/edit/withdraw actions. */
|
|
15
|
-
export declare const UserMessageView: import("react").MemoExoticComponent<({ node, renderMessageImages, t,
|
|
15
|
+
export declare const UserMessageView: import("react").MemoExoticComponent<({ node, renderMessageImages, t, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }: UserMessageViewProps) => ReactNode>;
|
|
16
16
|
//# sourceMappingURL=UserMessageView.d.ts.map
|
|
@@ -11,16 +11,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* marks the spot.
|
|
12
12
|
*/
|
|
13
13
|
import { memo, useEffect, useRef, useState } from 'react';
|
|
14
|
-
import { shallowEqual } from '@deepseek-ai/dsh-client-
|
|
15
|
-
import { Button, IconCheckOutline16, IconCopyOutline16, IconEditOutline16, JsonBlock,
|
|
14
|
+
import { shallowEqual } from '@deepseek-ai/dsh-client-store';
|
|
15
|
+
import { Button, IconCheckOutline16, IconCopyOutline16, IconEditOutline16, JsonBlock, RiskConfirmation, Tooltip, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
16
16
|
import { IconUndoOutline16 } from './icons.js';
|
|
17
17
|
import { foldHiddenRanges, isSeqHidden } from './withdrawn-node.js';
|
|
18
|
+
import { chatHookOf } from './chat-hook.js';
|
|
18
19
|
import { withdrawAndBackfill } from './withdraw-backfill.js';
|
|
19
20
|
import { ModelChip } from './ModelChip.js';
|
|
20
21
|
import css from './UserMessageView.module.css';
|
|
21
22
|
/** Selector identity kept module-level so uSES memoization holds. */
|
|
22
|
-
function selectHiddenRanges(
|
|
23
|
-
return foldHiddenRanges(
|
|
23
|
+
function selectHiddenRanges(chat) {
|
|
24
|
+
return foldHiddenRanges(chat.nodes.values());
|
|
24
25
|
}
|
|
25
26
|
/** Split user content into joined text, images, and leftover blocks. */
|
|
26
27
|
function contentParts(content) {
|
|
@@ -50,15 +51,18 @@ function projectUserText(text) {
|
|
|
50
51
|
const tokenStart = m.index + (m[1]?.length ?? 0);
|
|
51
52
|
/* v8 ignore next -- the token group always participates in a match, so m[2] is never undefined. */
|
|
52
53
|
const label = m[2] ?? '';
|
|
54
|
+
// Plain runs render through the local text-run block (the official
|
|
55
|
+
// `MessageText` primitive was retired in host 0.1.5; `.textRun` keeps its
|
|
56
|
+
// exact metrics).
|
|
53
57
|
if (tokenStart > cursor)
|
|
54
|
-
parts.push(_jsx(
|
|
58
|
+
parts.push(_jsx("div", { className: css.textRun, children: text.slice(cursor, tokenStart) }, cursor));
|
|
55
59
|
parts.push(_jsx("span", { className: css.refChip, "data-ref-chip": label.startsWith('@') ? 'subagent' : 'skill', children: label }, tokenStart));
|
|
56
60
|
cursor = tokenStart + label.length;
|
|
57
61
|
}
|
|
58
62
|
if (parts.length === 0)
|
|
59
|
-
return _jsx(
|
|
63
|
+
return _jsx("div", { className: css.textRun, children: text });
|
|
60
64
|
if (cursor < text.length)
|
|
61
|
-
parts.push(_jsx(
|
|
65
|
+
parts.push(_jsx("div", { className: css.textRun, children: text.slice(cursor) }, cursor));
|
|
62
66
|
return _jsx(_Fragment, { children: parts });
|
|
63
67
|
}
|
|
64
68
|
/** Copy action: the copy icon swaps to a short-lived check after a successful write. */
|
|
@@ -129,8 +133,10 @@ function InlineEditor({ initial, busy, t, onSave, onCancel, models }) {
|
|
|
129
133
|
} }), _jsxs("div", { className: css.editorActions, children: [_jsx("div", { className: css.editorTrailing, children: models !== undefined && _jsx(ModelChip, { ...models }) }), _jsx(Button, { variant: "outline", disabled: busy, onClick: onCancel, children: t('cancel') }), _jsx(Button, { variant: "primary", disabled: busy || draft.trim() === '', onClick: save, children: t('editor.save') })] })] }));
|
|
130
134
|
}
|
|
131
135
|
/** The shadowed user-message view: bubble plus copy/edit/withdraw actions. */
|
|
132
|
-
export const UserMessageView = memo(function UserMessageView({ node, renderMessageImages, t,
|
|
133
|
-
|
|
136
|
+
export const UserMessageView = memo(function UserMessageView({ node, renderMessageImages, t, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }) {
|
|
137
|
+
// Chat data lives in the `useChat` session standard prop on host 0.1.2
|
|
138
|
+
// (the helper degrades to the frozen empty snapshot without it).
|
|
139
|
+
const ranges = chatHookOf(standard)(selectHiddenRanges, shallowEqual);
|
|
134
140
|
const [editing, setEditing] = useState(false);
|
|
135
141
|
const [confirming, setConfirming] = useState(false);
|
|
136
142
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
@@ -168,5 +174,5 @@ export const UserMessageView = memo(function UserMessageView({ node, renderMessa
|
|
|
168
174
|
? { useModelDirectory, loadModels, selectModel, t }
|
|
169
175
|
: undefined }), failed === 'edit' && _jsx("div", { className: css.actionError, role: "status", children: t('error.edit') })] }) }));
|
|
170
176
|
}
|
|
171
|
-
return (_jsxs("div", { className: css.userRow, "data-time-hover-root": true, children: [_jsxs("div", { className: css.userStack, children: [node.kind === 'message-tools-edited' && (_jsx("div", { className: css.editedLabel, children: t('edited.badge') })), node.kind === 'message-tools-restored' && (_jsx("div", { className: css.editedLabel, children: t('withdrawn.restored') })), images.length > 0 ? renderMessageImages({ images, align: 'end' }) : null, showBubble && (_jsxs("div", { className: css.bubble, children: [projectUserText(text), rest.map((block, index) => (_jsx(JsonBlock, { label: t('message.extraBlock'), payload: block, truncatedLabel: truncated }, index)))] }))] }), _jsxs("div", { className: css.actions, children: [_jsx(CopyButton, { text: text, t: t }), _jsx(Tooltip, { label: t('edit'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('edit'), onClick: () => { setFailed(null); setEditing(true); }, children: _jsx(IconEditOutline16, {}) }) }), _jsx(Tooltip, { label: t('action.withdraw'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('action.withdraw'), onClick: () => { setFailed(null); setAcknowledged(false); setConfirming(true); }, children: _jsx(IconUndoOutline16, {}) }) })] }), failed === 'withdraw' && _jsx("div", { className: css.actionError, role: "status", children: t('error.withdraw') }), confirming && (_jsx(RiskConfirmation, { open: true, title: t('withdraw.title'), description: t('withdraw.description'), acknowledgeLabel: t('withdraw.acknowledge'), cancelLabel: t('cancel'), confirmLabel: t('withdraw.confirm'), acknowledged: acknowledged, disabled: busy, onAcknowledgedChange: setAcknowledged, onCancel: () => { setConfirming(false); setAcknowledged(false); }, onConfirm: confirmWithdraw }))] }));
|
|
177
|
+
return (_jsxs("div", { className: css.userRow, "data-time-hover-root": true, children: [_jsxs("div", { className: css.userStack, children: [node.kind === 'message-tools-edited' && (_jsx("div", { className: css.editedLabel, children: t('edited.badge') })), node.kind === 'message-tools-restored' && (_jsx("div", { className: css.editedLabel, children: t('withdrawn.restored') })), images.length > 0 ? renderMessageImages({ images, align: 'end' }) : null, showBubble && (_jsxs("div", { className: css.bubble, children: [projectUserText(text), rest.map((block, index) => (_jsx(JsonBlock, { label: t('message.extraBlock'), payload: block, truncatedLabel: truncated }, index)))] }))] }), _jsxs("div", { className: css.actions, children: [_jsx(CopyButton, { text: text, t: t }), _jsx(Tooltip, { label: t('edit'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('edit'), onClick: () => { setFailed(null); setEditing(true); }, children: _jsx(IconEditOutline16, {}) }) }), _jsx(Tooltip, { label: t('action.withdraw'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('action.withdraw'), onClick: () => { setFailed(null); setAcknowledged(false); setConfirming(true); }, children: _jsx(IconUndoOutline16, {}) }) })] }), failed === 'withdraw' && _jsx("div", { className: css.actionError, role: "status", children: t('error.withdraw') }), confirming && (_jsx(RiskConfirmation, { open: true, title: t('withdraw.title'), description: t('withdraw.description'), acknowledgeLabel: t('withdraw.acknowledge'), cancelLabel: t('cancel'), closeLabel: t('close'), confirmLabel: t('withdraw.confirm'), acknowledged: acknowledged, disabled: busy, onAcknowledgedChange: setAcknowledged, onCancel: () => { setConfirming(false); setAcknowledged(false); }, onConfirm: confirmWithdraw }))] }));
|
|
172
178
|
});
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
import { type ReactNode } from 'react';
|
|
14
14
|
import type { WithdrawnDividerViewProps } from './slots.ts';
|
|
15
15
|
/** The withdrawal divider: collapsed marker row plus the expandable replay. */
|
|
16
|
-
export declare function WithdrawnDividerView({ node, t,
|
|
16
|
+
export declare function WithdrawnDividerView({ node, t, restoreMessage, ...standard }: WithdrawnDividerViewProps): ReactNode;
|
|
17
17
|
//# sourceMappingURL=WithdrawnDividerView.d.ts.map
|
|
@@ -12,32 +12,36 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* once withdrawals backfill the draft automatically.
|
|
13
13
|
*/
|
|
14
14
|
import { useState } from 'react';
|
|
15
|
-
import { Button, IconChevronDownOutline14, IconChevronRightOutline14
|
|
15
|
+
import { Button, IconChevronDownOutline14, IconChevronRightOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
16
16
|
import { IconUndoOutline16 } from './icons.js';
|
|
17
17
|
import { collectWithdrawnEntries, countHiddenInSpan, foldHiddenRanges, hasRestoreForSpan, isRestoreSuperseded, } from './withdrawn-node.js';
|
|
18
|
+
import { chatHookOf } from './chat-hook.js';
|
|
18
19
|
import css from './WithdrawnDividerView.module.css';
|
|
19
20
|
/** The withdrawal divider: collapsed marker row plus the expandable replay. */
|
|
20
|
-
export function WithdrawnDividerView({ node, t,
|
|
21
|
+
export function WithdrawnDividerView({ node, t, restoreMessage, ...standard }) {
|
|
21
22
|
const data = node.data;
|
|
22
23
|
const [expanded, setExpanded] = useState(false);
|
|
23
24
|
const [entries, setEntries] = useState(null);
|
|
24
25
|
const [busy, setBusy] = useState(false);
|
|
25
26
|
const [failed, setFailed] = useState(false);
|
|
27
|
+
// Chat data lives in the `useChat` session standard prop on host 0.1.2
|
|
28
|
+
// (the helper degrades to the frozen empty snapshot without it).
|
|
29
|
+
const useChatSlice = chatHookOf(standard);
|
|
26
30
|
// The node store is a stable live reader: the count/badge selectors read
|
|
27
31
|
// through it, and the expand click folds the replay from the live snapshot
|
|
28
32
|
// (event handlers may read live snapshots; render code subscribes).
|
|
29
|
-
const nodesStore =
|
|
30
|
-
const count =
|
|
33
|
+
const nodesStore = useChatSlice(chat => chat.nodes);
|
|
34
|
+
const count = useChatSlice(chat => countHiddenInSpan(chat.nodes.values(), data.hiddenStartSeq, data.seq));
|
|
31
35
|
// The badge tracks a LIVE restore row: withdrawing the restored rows again
|
|
32
36
|
// clears it. A superseded divider still shows its historical marker, but its
|
|
33
37
|
// restore action is disabled because the later re-withdrawal divider is now
|
|
34
38
|
// the active restore point for the same logical content.
|
|
35
|
-
const restored =
|
|
36
|
-
const nodes =
|
|
39
|
+
const restored = useChatSlice((chat) => {
|
|
40
|
+
const nodes = chat.nodes.values();
|
|
37
41
|
return hasRestoreForSpan(nodes, data.hiddenStartSeq, foldHiddenRanges(nodes));
|
|
38
42
|
});
|
|
39
|
-
const superseded =
|
|
40
|
-
const nodes =
|
|
43
|
+
const superseded = useChatSlice((chat) => {
|
|
44
|
+
const nodes = chat.nodes.values();
|
|
41
45
|
return isRestoreSuperseded(nodes, data.hiddenStartSeq, foldHiddenRanges(nodes));
|
|
42
46
|
});
|
|
43
47
|
const toggle = () => {
|
|
@@ -55,5 +59,5 @@ export function WithdrawnDividerView({ node, t, useSession, restoreMessage, }) {
|
|
|
55
59
|
.catch(() => { setFailed(true); })
|
|
56
60
|
.finally(() => { setBusy(false); });
|
|
57
61
|
};
|
|
58
|
-
return (_jsxs("div", { className: css.row, children: [_jsxs("button", { type: "button", className: css.marker, "aria-expanded": expanded, "aria-label": t('withdrawn.expand'), onClick: toggle, children: [_jsx("span", { className: css.leading, "aria-hidden": true, children: expanded ? _jsx(IconChevronDownOutline14, {}) : _jsx(IconChevronRightOutline14, {}) }), _jsx("span", { className: css.title, children: t('withdrawn.divider', { count }) }), restored && (_jsxs("span", { className: css.badge, children: [_jsx(IconUndoOutline16, {}), t('withdrawn.restored')] }))] }), expanded && (_jsxs("div", { className: css.body, children: [entries === null || entries.length === 0 ? (_jsx("div", { className: css.empty, children: t('withdrawn.empty') })) : (entries.map((entry, index) => (_jsxs("div", { className: entry.kind === 'user' ? css.entryUser : css.entryAssistant, children: [_jsx("div", { className: css.entryKind, children: entry.kind === 'user' ? t('withdrawn.entryUser') : t('withdrawn.entryAssistant') }), _jsx(
|
|
62
|
+
return (_jsxs("div", { className: css.row, children: [_jsxs("button", { type: "button", className: css.marker, "aria-expanded": expanded, "aria-label": t('withdrawn.expand'), onClick: toggle, children: [_jsx("span", { className: css.leading, "aria-hidden": true, children: expanded ? _jsx(IconChevronDownOutline14, {}) : _jsx(IconChevronRightOutline14, {}) }), _jsx("span", { className: css.title, children: t('withdrawn.divider', { count }) }), restored && (_jsxs("span", { className: css.badge, children: [_jsx(IconUndoOutline16, {}), t('withdrawn.restored')] }))] }), expanded && (_jsxs("div", { className: css.body, children: [entries === null || entries.length === 0 ? (_jsx("div", { className: css.empty, children: t('withdrawn.empty') })) : (entries.map((entry, index) => (_jsxs("div", { className: entry.kind === 'user' ? css.entryUser : css.entryAssistant, children: [_jsx("div", { className: css.entryKind, children: entry.kind === 'user' ? t('withdrawn.entryUser') : t('withdrawn.entryAssistant') }), _jsx("div", { className: css.textRun, children: entry.text })] }, index)))), _jsxs("div", { className: css.restoreRow, children: [_jsx(Button, { variant: "outline", disabled: busy || restored || superseded, onClick: restore, children: t('withdrawn.restore') }), failed && _jsx("span", { className: css.restoreFailed, role: "status", children: t('withdrawn.restoreFailed') })] })] }))] }));
|
|
59
63
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat snapshot access on host 0.1.2: the chat snapshot lives OUTSIDE the
|
|
3
|
+
* Session snapshot — components read it through the `useChat` session
|
|
4
|
+
* standard prop (ui-chat), the apply world through the conversation binding's
|
|
5
|
+
* chat target (`ctx.uiConversation.binding(sessionId).target('chat')`). The
|
|
6
|
+
* target snapshot is `undefined` until the first subscriber activates the
|
|
7
|
+
* chat view; callers treat that as "no chat yet", never as an error.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
11
|
+
import type { ChatSnapshot, UseChat } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
12
|
+
import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
13
|
+
/** The chat slice the plugin's selectors read. */
|
|
14
|
+
export type ChatSlice = ChatSnapshot;
|
|
15
|
+
/** Selector hook over the chat slice. */
|
|
16
|
+
export type ChatSliceHook = SnapshotSelectorHook<ChatSlice>;
|
|
17
|
+
/**
|
|
18
|
+
* Pick the live chat hook from the entry's composed props (`useChat` is a
|
|
19
|
+
* session standard prop on 0.1.2), degrading to the frozen empty snapshot
|
|
20
|
+
* when the framework composed none.
|
|
21
|
+
* @param props - the entry's composed props.
|
|
22
|
+
* @returns the chat selector hook.
|
|
23
|
+
*/
|
|
24
|
+
export declare function chatHookOf(props: {
|
|
25
|
+
useChat?: UseChat;
|
|
26
|
+
}): ChatSliceHook;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the apply-world chat source for one session.
|
|
29
|
+
* @param ctx - client root context.
|
|
30
|
+
* @param sessionId - owning session.
|
|
31
|
+
* @returns the conversation binding's chat target (its snapshot is undefined
|
|
32
|
+
* until the first subscription activates the view), or undefined when the
|
|
33
|
+
* uiConversation service or the session binding is absent.
|
|
34
|
+
*/
|
|
35
|
+
export declare function chatSourceOf(ctx: Context, sessionId: SessionId): HostObservable<ChatSlice | undefined> | undefined;
|
|
36
|
+
//# sourceMappingURL=chat-hook.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen empty chat snapshot for the degraded render path: a composition
|
|
3
|
+
* without ui-chat never mounts the `conversation.chat.node` slot at all, so
|
|
4
|
+
* this only guards a stray render — hook call order stays stable and every
|
|
5
|
+
* selector reads the empty state.
|
|
6
|
+
*/
|
|
7
|
+
const EMPTY_CHAT = {
|
|
8
|
+
order: [],
|
|
9
|
+
nodes: {
|
|
10
|
+
get: () => undefined,
|
|
11
|
+
source: () => ({ getSnapshot: () => undefined, subscribe: () => () => { } }),
|
|
12
|
+
processSource: () => ({ getSnapshot: () => undefined, subscribe: () => () => { } }),
|
|
13
|
+
values: () => [],
|
|
14
|
+
},
|
|
15
|
+
locations: { getTurn: () => [], getStep: () => [] },
|
|
16
|
+
navigation: { items: () => [] },
|
|
17
|
+
timeline: { turnOrder: [], turns: new Map() },
|
|
18
|
+
legacy: { nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [] },
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Pick the live chat hook from the entry's composed props (`useChat` is a
|
|
22
|
+
* session standard prop on 0.1.2), degrading to the frozen empty snapshot
|
|
23
|
+
* when the framework composed none.
|
|
24
|
+
* @param props - the entry's composed props.
|
|
25
|
+
* @returns the chat selector hook.
|
|
26
|
+
*/
|
|
27
|
+
export function chatHookOf(props) {
|
|
28
|
+
const direct = props.useChat;
|
|
29
|
+
if (direct !== undefined)
|
|
30
|
+
return direct;
|
|
31
|
+
return selector => selector(EMPTY_CHAT);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the apply-world chat source for one session.
|
|
35
|
+
* @param ctx - client root context.
|
|
36
|
+
* @param sessionId - owning session.
|
|
37
|
+
* @returns the conversation binding's chat target (its snapshot is undefined
|
|
38
|
+
* until the first subscription activates the view), or undefined when the
|
|
39
|
+
* uiConversation service or the session binding is absent.
|
|
40
|
+
*/
|
|
41
|
+
export function chatSourceOf(ctx, sessionId) {
|
|
42
|
+
const uiConversation = ctx.get('uiConversation');
|
|
43
|
+
if (uiConversation === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
return uiConversation.binding(sessionId).target('chat');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined; // unknown session — the caller's undefined branch
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* session only: flow keys are session-scoped (seq-like ids collide across
|
|
19
19
|
* sessions), and only one chat view is mounted at a time.
|
|
20
20
|
*/
|
|
21
|
-
import type {
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
22
|
+
import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
22
23
|
/**
|
|
23
24
|
* Flow keys of every chat node inside a hidden span. The withdrawal divider
|
|
24
25
|
* itself (`message-tools-withdrawn`, anchored at the span's exclusive end) is
|
|
@@ -57,5 +58,5 @@ export interface DomHiderOptions {
|
|
|
57
58
|
* @param options - probe retry tuning.
|
|
58
59
|
* @returns disposer removing the stylesheet and every subscription.
|
|
59
60
|
*/
|
|
60
|
-
export declare function installDomHider(ctx:
|
|
61
|
+
export declare function installDomHider(ctx: Context, options?: DomHiderOptions): () => void;
|
|
61
62
|
//# sourceMappingURL=dom-hider.d.ts.map
|