@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
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/chunk` events (no
229
- * `assistant/message` ever landed). Those chunks are not surface nodes, so
230
- * they are absent from `sourceEventSeqs`. The replacement's own `start` and
231
- * `seq` delimit the whole withdrawn log interval — `[start, seq)` — and are
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 !== void 0 && isMessageToolsReplacement(event) && event.sourceEventSeqs?.includes(targetSeq)) {
238
- const span = [];
239
- for (let seq = event.surfaceOp.start; seq < event.seq; seq++) span.push(seq);
240
- return span;
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. A
252
- * turn that was interrupted before finalization may never emit
253
- * `assistant/message`; its visible content survives only as `assistant/chunk`
254
- * rows. Those chunks are folded into one assistant replay per step when no
255
- * final `assistant/message` exists for that step, so withdrawing/restoring an
256
- * interrupted reply does not lose the partial assistant content.
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 flushPending = () => {
263
- if (pending === void 0) return;
264
- if (!pending.finalized) {
265
- const text = [...pending.text.values()].join("");
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/chunk") {
315
- pushChunk(event);
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.events, session.surface.nodes, request.targetSeq);
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
- start: planned.plan.start,
523
- end: planned.plan.end
518
+ startSeq: SessionSeq(planned.plan.start),
519
+ endSeq: SessionSeq(planned.plan.end)
524
520
  },
525
- sourceEventSeqs: [...planned.plan.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.events, session.surface.nodes, request.targetSeq);
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.events, session.surface.nodes, request.targetSeq, request.text);
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
- start: planned.plan.start,
620
- end: planned.plan.end
615
+ startSeq: SessionSeq(planned.plan.start),
616
+ endSeq: SessionSeq(planned.plan.end)
621
617
  },
622
- sourceEventSeqs: [...planned.plan.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);
@@ -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":133,"column":9},
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":95,"column":9},
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":61,"column":9},
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":133,"column":9},
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":95,"column":9},
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":61,"column":9},
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 { ReactNode } from 'react';
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
- 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 })] }));
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, useSession, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, }: UserMessageViewProps) => ReactNode>;
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-runtime/client';
15
- import { Button, IconCheckOutline16, IconCopyOutline16, IconEditOutline16, JsonBlock, MessageText, RiskConfirmation, Tooltip, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives';
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(snapshot) {
23
- return foldHiddenRanges(snapshot.chat.nodes.values());
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(MessageText, { text: text.slice(cursor, tokenStart) }, cursor));
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(MessageText, { text: text });
63
+ return _jsx("div", { className: css.textRun, children: text });
60
64
  if (cursor < text.length)
61
- parts.push(_jsx(MessageText, { text: text.slice(cursor) }, cursor));
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, useSession, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, }) {
133
- const ranges = useSession(selectHiddenRanges, shallowEqual);
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, useSession, restoreMessage, }: WithdrawnDividerViewProps): ReactNode;
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, MessageText } from '@deepseek-ai/dsh-client-ui-primitives';
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, useSession, restoreMessage, }) {
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 = useSession(snapshot => snapshot.chat.nodes);
30
- const count = useSession(snapshot => countHiddenInSpan(snapshot.chat.nodes.values(), data.hiddenStartSeq, data.seq));
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 = useSession((snapshot) => {
36
- const nodes = snapshot.chat.nodes.values();
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 = useSession((snapshot) => {
40
- const nodes = snapshot.chat.nodes.values();
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(MessageText, { text: 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') })] })] }))] }));
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 { ChatConversationViewNode, ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
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: ClientContext, options?: DomHiderOptions): () => void;
61
+ export declare function installDomHider(ctx: Context, options?: DomHiderOptions): () => void;
61
62
  //# sourceMappingURL=dom-hider.d.ts.map