dsh-smooth-stream 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -23,27 +23,27 @@ window.__ModuleLoader__.load({
23
23
  tag.textContent = css$1;
24
24
  }
25
25
  var TypewriterAssistantNodeView_module_css_default = {
26
- "root": "I17U7q_root",
27
- "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
28
- "disclosureLeading": "I17U7q_disclosureLeading",
29
- "thinkBody": "I17U7q_thinkBody",
30
- "disclosureChevronHover": "I17U7q_disclosureChevronHover",
31
- "disclosureContent": "I17U7q_disclosureContent",
26
+ "body": "I17U7q_body",
27
+ "disclosureRow": "I17U7q_disclosureRow",
28
+ "disclosureIconIdle": "I17U7q_disclosureIconIdle",
32
29
  "stopped": "I17U7q_stopped",
33
- "thinkTitle": "I17U7q_thinkTitle",
34
- "thinkSeparator": "I17U7q_thinkSeparator",
35
- "think": "I17U7q_think",
36
- "disclosureTitle": "I17U7q_disclosureTitle",
30
+ "root": "I17U7q_root",
37
31
  "disclosureRoot": "I17U7q_disclosureRoot",
32
+ "thinkRow": "I17U7q_thinkRow",
33
+ "thinkLeading": "I17U7q_thinkLeading",
34
+ "follow": "I17U7q_follow",
35
+ "disclosureTitle": "I17U7q_disclosureTitle",
38
36
  "visuallyHidden": "I17U7q_visuallyHidden",
37
+ "thinkSeparator": "I17U7q_thinkSeparator",
38
+ "thinkTitle": "I17U7q_thinkTitle",
39
+ "disclosureContent": "I17U7q_disclosureContent",
40
+ "think": "I17U7q_think",
41
+ "thinkBody": "I17U7q_thinkBody",
42
+ "disclosureChevronHover": "I17U7q_disclosureChevronHover",
39
43
  "thinkChevron": "I17U7q_thinkChevron",
40
- "follow": "I17U7q_follow",
41
- "disclosureIconIdle": "I17U7q_disclosureIconIdle",
42
- "body": "I17U7q_body",
43
- "thinkSummary": "I17U7q_thinkSummary",
44
- "disclosureRow": "I17U7q_disclosureRow",
45
- "thinkLeading": "I17U7q_thinkLeading",
46
- "thinkRow": "I17U7q_thinkRow"
44
+ "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
45
+ "disclosureLeading": "I17U7q_disclosureLeading",
46
+ "thinkSummary": "I17U7q_thinkSummary"
47
47
  };
48
48
  //#endregion
49
49
  //#region src/client/AnimatedDisclosure.tsx
@@ -202,7 +202,8 @@ window.__ModuleLoader__.load({
202
202
  const SETTLE_DRAIN_MULTIPLIER = 1.8;
203
203
  /** Pure per-frame reveal decision shared by the loop and its tests. */
204
204
  function computeRevealStep(config, input, dtSeconds) {
205
- const baseCps = clamp$1(Math.max(input.emaCps, input.arrivalCpsEma), config.minCps, config.maxFlushCps);
205
+ const trackedCps = Math.max(input.emaCps, input.arrivalCpsEma);
206
+ const baseCps = clamp$1(trackedCps, config.minCps, config.maxFlushCps);
206
207
  const targetLagChars = input.inputActive ? Math.max(2, Math.round(baseCps * config.targetBufferMs / 1e3)) : 0;
207
208
  let currentCps;
208
209
  if (input.steadyCps !== void 0) currentCps = input.inputActive || input.settling ? clamp$1(input.steadyCps * (input.inputActive ? 1 : SETTLE_DRAIN_MULTIPLIER), config.minCps, config.maxFlushCps) : 0;
@@ -211,7 +212,10 @@ window.__ModuleLoader__.load({
211
212
  const catchup = overflow > 0 ? overflow / CATCHUP_SECONDS : 0;
212
213
  currentCps = clamp$1(baseCps * 1.08 + catchup, config.minCps, config.maxFlushCps);
213
214
  } else if (input.settling) currentCps = computeSettleDrain(config, input);
214
- else currentCps = clamp$1(Math.max(config.flushCps, baseCps * 1.8, input.arrivalCpsEma * .8), config.flushCps, config.maxFlushCps);
215
+ else {
216
+ const idleFlushCps = Math.max(config.flushCps, baseCps * 1.8, input.arrivalCpsEma * .8);
217
+ currentCps = clamp$1(idleFlushCps, config.flushCps, config.maxFlushCps);
218
+ }
215
219
  const minRevealChars = input.inputActive ? 1 : 2;
216
220
  return {
217
221
  revealChars: Math.max(minRevealChars, Math.round(currentCps * dtSeconds)),
@@ -385,10 +389,11 @@ window.__ModuleLoader__.load({
385
389
  const deltaChars = targetCountRef.current - lastInputCountRef.current;
386
390
  const deltaMs = Math.max(1, now - lastInputTsRef.current);
387
391
  if (hadSample && deltaChars > 0) {
388
- const normalizedInstantCps = clamp$1(deltaChars * 1e3 / deltaMs, config.minCps, config.maxFlushCps * 3);
392
+ const instantCps = deltaChars * 1e3 / deltaMs;
393
+ const normalizedInstantCps = clamp$1(instantCps, config.minCps, config.maxFlushCps * 3);
389
394
  const chunkEmaAlpha = .45;
390
- chunkSizeEmaRef.current = chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;
391
- arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;
395
+ chunkSizeEmaRef.current = chunkSizeEmaRef.current * .55 + appendedCount * chunkEmaAlpha;
396
+ arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * .55 + normalizedInstantCps * chunkEmaAlpha;
392
397
  emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + normalizedInstantCps * config.emaAlpha;
393
398
  }
394
399
  lastInputTsRef.current = now;
@@ -501,17 +506,15 @@ window.__ModuleLoader__.load({
501
506
  * - while the port has scroll room, writes the interpolated lag as
502
507
  * `translate3d` on `[data-chat-transcript]` (message rows only), so the
503
508
  * turn-status chrome stays pinned at the floor;
504
- * - before the port has scroll room (content shorter than the viewport), the
505
- * same lag is a content-height lag written as a negative translate on
506
- * `[data-chat-turn-status]`, so the status label descends smoothly with
507
- * each wrap instead of hopping a line at a time.
509
+ * - clips only the part of the translated surface that crosses the
510
+ * turn-status line, preserving the normal gap above that line;
511
+ * - leaves `[data-chat-turn-status]` in normal flow at every height, so its
512
+ * layout gap cannot be consumed by an interpolated transform.
508
513
  *
509
- * Handing the port back to the reader (unpin, or the owner going inactive)
510
- * first writes the effective visual top (`engine - lag`) into `scrollTop`
511
- * before clearing the transform, so the frame stays continuous. That write
512
- * also lands in ChatView's observed-top ledger as reader input beyond
513
- * `FOLLOW_THRESHOLD`, releasing its native snap-follow instead of fighting
514
- * the gesture for the port.
514
+ * A real reader unpin first writes the effective visual top (`engine - lag`)
515
+ * into `scrollTop` before clearing the transform, so the frame stays
516
+ * continuous. Lifecycle completion is different: without a reader gesture,
517
+ * the last owner settles at the floor and preserves bottom-follow.
515
518
  *
516
519
  * Unpin is a real gesture (wheel / touch / pointer / key) that leaves the
517
520
  * floor; a `scrollTop` delta from our own write must not release the pin.
@@ -582,33 +585,48 @@ window.__ModuleLoader__.load({
582
585
  function hasShiftSurface(port) {
583
586
  return shiftRootOf(port) !== null || shiftRowsOf(port).length > 0;
584
587
  }
585
- function setShift(element, px, negative = false) {
586
- const value = negative ? -px : px;
587
- if (Math.abs(value) > .01) {
588
- element.style.transform = `translate3d(0, ${value}px, 0)`;
588
+ /** Cached natural gap from a shifted surface's bottom to the status line. */
589
+ const statusGapCache = /* @__PURE__ */ new WeakMap();
590
+ function currentShiftOf(element) {
591
+ return Number(/translate3d\(0, ([\d.]+)px, 0\)/.exec(element.style.transform)?.[1] ?? 0);
592
+ }
593
+ function statusGapBelow(surface, chrome) {
594
+ const cached = statusGapCache.get(surface);
595
+ if (cached?.chrome === chrome) return cached.px;
596
+ const naturalBottom = surface.getBoundingClientRect().bottom - currentShiftOf(surface);
597
+ const px = Math.max(0, chrome.getBoundingClientRect().top - naturalBottom);
598
+ statusGapCache.set(surface, {
599
+ chrome,
600
+ px
601
+ });
602
+ return px;
603
+ }
604
+ function setShift(element, px, paintAllowance) {
605
+ if (Math.abs(px) > .01) {
606
+ element.style.transform = `translate3d(0, ${px}px, 0)`;
589
607
  element.style.willChange = "transform";
608
+ const bottomClip = paintAllowance === void 0 ? 0 : Math.max(0, px - paintAllowance);
609
+ element.style.clipPath = bottomClip > .01 ? `inset(0 0 ${bottomClip}px 0)` : "";
590
610
  } else {
591
611
  element.style.transform = "";
592
612
  element.style.willChange = "";
613
+ element.style.clipPath = "";
593
614
  }
594
615
  }
595
616
  /**
596
- * The running-turn label. Before the port scrolls (content shorter than the
597
- * viewport) the content-height lag has no `scrollTop` room to ride, so it is
598
- * expressed as a negative translate on this sibling: the label descends
599
- * smoothly with each wrap instead of hopping down a line at a time.
617
+ * The running-turn label. It stays in normal flow so the host column's gap
618
+ * always separates it from the transcript.
600
619
  */
601
- function chromeShiftOf(port) {
602
- return port.querySelector("[data-chat-turn-status]");
620
+ function statusChromeOf(port) {
621
+ return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
603
622
  }
604
623
  /**
605
- * Render the single smoothed extent `animatedH`. Before the port scrolls
606
- * (content shorter than the viewport) `animatedH` is clamped to the content
607
- * height and the content-height lag rides a negative translate on the
608
- * turn-status label; once the port has scroll room the engine is pinned at
609
- * the floor and the lag rides `[data-chat-transcript]` (or each message row
610
- * when the host has no transcript box) so the chrome stays put. Without any
611
- * shift surface the engine itself carries the interpolated top.
624
+ * Render the single smoothed extent `animatedH`. Once the port has scroll
625
+ * room the engine is pinned at the floor and the lag rides
626
+ * `[data-chat-transcript]` (or each message row when the host has no
627
+ * transcript box). Before that, message rows and status chrome remain in
628
+ * normal flow. Without any shift surface the engine carries the interpolated
629
+ * top itself.
612
630
  */
613
631
  function applyVisual(port, animatedH) {
614
632
  const contentHeight = Math.max(0, port.scrollHeight);
@@ -617,19 +635,19 @@ window.__ModuleLoader__.load({
617
635
  const lag = contentHeight - extent;
618
636
  port.style.overflowAnchor = "none";
619
637
  port.style.scrollBehavior = "auto";
638
+ const chrome = statusChromeOf(port);
639
+ if (chrome !== null) setShift(chrome, 0);
620
640
  const shiftRoot = shiftRootOf(port);
621
641
  if (shiftRoot !== null) {
622
642
  if (floor <= 0) {
623
643
  port.scrollTop = 0;
624
644
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
625
- const chrome = chromeShiftOf(port);
626
- if (chrome !== null) setShift(chrome, lag, true);
627
645
  setShift(shiftRoot, 0);
628
646
  return;
629
647
  }
630
648
  port.scrollTop = floor;
631
649
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
632
- setShift(shiftRoot, lag);
650
+ setShift(shiftRoot, lag, chrome === null ? void 0 : statusGapBelow(shiftRoot, chrome));
633
651
  return;
634
652
  }
635
653
  const rows = shiftRowsOf(port);
@@ -637,14 +655,13 @@ window.__ModuleLoader__.load({
637
655
  if (floor <= 0) {
638
656
  port.scrollTop = 0;
639
657
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
640
- const chrome = chromeShiftOf(port);
641
- if (chrome !== null) setShift(chrome, lag, true);
642
658
  for (const row of rows) setShift(row, 0);
643
659
  return;
644
660
  }
645
661
  port.scrollTop = floor;
646
662
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
647
- for (const row of rows) setShift(row, lag);
663
+ const last = rows.length - 1;
664
+ for (const [index, row] of rows.entries()) setShift(row, lag, index === last && chrome !== null ? statusGapBelow(row, chrome) : void 0);
648
665
  return;
649
666
  }
650
667
  port.scrollTop = Math.min(floor, Math.max(0, extent));
@@ -656,18 +673,27 @@ window.__ModuleLoader__.load({
656
673
  port.style.scrollBehavior = "";
657
674
  const shiftRoot = shiftRootOf(port);
658
675
  if (shiftRoot !== null) {
676
+ statusGapCache.delete(shiftRoot);
659
677
  shiftRoot.style.transform = "";
660
678
  shiftRoot.style.willChange = "";
679
+ shiftRoot.style.clipPath = "";
661
680
  } else for (const row of shiftRowsOf(port)) {
681
+ statusGapCache.delete(row);
662
682
  row.style.transform = "";
663
683
  row.style.willChange = "";
684
+ row.style.clipPath = "";
664
685
  }
665
- const chrome = chromeShiftOf(port);
686
+ const chrome = statusChromeOf(port);
666
687
  if (chrome !== null) {
667
688
  chrome.style.transform = "";
668
689
  chrome.style.willChange = "";
690
+ chrome.style.clipPath = "";
669
691
  }
670
692
  }
693
+ function settleAtFloor(port) {
694
+ port.scrollTop = Math.max(0, port.scrollHeight - port.clientHeight);
695
+ port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
696
+ }
671
697
  /** Live follow hosts per conversation port so one unmount does not clear another. */
672
698
  const followOwners = /* @__PURE__ */ new WeakMap();
673
699
  function acquireFollow(port) {
@@ -817,10 +843,15 @@ window.__ModuleLoader__.load({
817
843
  if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
818
844
  const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
819
845
  if (host === null) return;
820
- if (following) handBackVisual(host);
821
846
  const remaining = holding === host ? releaseFollow(host) : followOwners.get(host) ?? 0;
822
847
  holding = null;
823
- if (remaining === 0) clearVisual(host);
848
+ if (remaining === 0) {
849
+ if (following) {
850
+ if (interacting && awayPx >= 8) handBackVisual(host);
851
+ else settleAtFloor(host);
852
+ }
853
+ clearVisual(host);
854
+ }
824
855
  };
825
856
  }, [
826
857
  active,
@@ -957,16 +988,15 @@ window.__ModuleLoader__.load({
957
988
  * or the assistant node settling — not when the rest of the reply is
958
989
  * still streaming.
959
990
  */
960
- function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, t }) {
991
+ function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, t }) {
961
992
  const reduced = usePrefersReducedMotion();
962
993
  const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
963
994
  const summaryRef = (0, react.useRef)(null);
964
- const speedCpsRef = (0, react.useRef)(35);
965
995
  const displayed = useSmoothStreamContent(text, {
966
996
  enabled: running && !reduced,
967
997
  preset,
968
998
  shouldHoldBack,
969
- speedCpsRef
999
+ speedCpsRef: followSpeedCpsRef
970
1000
  });
971
1001
  const shown = running && !reduced ? displayed : text;
972
1002
  const summary = running ? latestLine(shown) : firstLine(text);
@@ -978,9 +1008,8 @@ window.__ModuleLoader__.load({
978
1008
  if (element === null) return;
979
1009
  element.scrollLeft = running ? element.scrollWidth - element.clientWidth : 0;
980
1010
  }, [running, summary]);
981
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
982
- active: running,
983
- speedCpsRef,
1011
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1012
+ className: TypewriterAssistantNodeView_module_css_default.follow,
984
1013
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
985
1014
  className: TypewriterAssistantNodeView_module_css_default.think,
986
1015
  "data-variant": "think",
@@ -1021,16 +1050,20 @@ window.__ModuleLoader__.load({
1021
1050
  * streaming is revealed by the smoother through the Harness Markdown
1022
1051
  * renderer at a rate that tracks arrival. Reasoning blocks keep the
1023
1052
  * built-in Think disclosure and only receive a smoothed text feed; the
1024
- * last text or reasoning block owns conversation-port follow so wraps
1025
- * glide. The FPS guard holds offscreen
1026
- * reveals when the frame rate is degraded. Settled text renders with the
1027
- * full Markdown pipeline.
1053
+ * outer node owns conversation-port follow while streaming; the final text
1054
+ * block keeps ownership while its settled reveal queue drains. The FPS guard
1055
+ * holds offscreen reveals when the frame rate is degraded. Settled text
1056
+ * renders with the full Markdown pipeline.
1028
1057
  */
1029
1058
  const TypewriterAssistantNodeView = (0, react.memo)(function TypewriterAssistantNodeView({ mode: _mode = DEFAULT_STREAM_CONFIG.mode, preset = DEFAULT_STREAM_CONFIG.preset, revealCharsPerSec: _revealCharsPerSec = DEFAULT_STREAM_CONFIG.revealCharsPerSec, scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec, maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec, thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand, node, useTurnData, openFile, loadImage, fileMentions, t }) {
1030
1059
  const data = node.data;
1031
1060
  const streaming = data.status === "running";
1032
1061
  const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
1033
1062
  const rootSpeedRef = (0, react.useRef)(35);
1063
+ const reasoningOwnsSpeed = streaming && data.blocks[data.blocks.length - 1]?.kind === "reasoning";
1064
+ (0, react.useEffect)(() => {
1065
+ if (!reasoningOwnsSpeed) rootSpeedRef.current = 35;
1066
+ }, [reasoningOwnsSpeed]);
1034
1067
  const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
1035
1068
  const tail = useTurnData("turn-tail");
1036
1069
  const owner = (0, react.useMemo)(() => {
@@ -1086,6 +1119,7 @@ window.__ModuleLoader__.load({
1086
1119
  preset,
1087
1120
  thinkAutoExpand,
1088
1121
  shouldHoldBack,
1122
+ followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
1089
1123
  t
1090
1124
  }, index));
1091
1125
  break;
@@ -1107,13 +1141,11 @@ window.__ModuleLoader__.load({
1107
1141
  break;
1108
1142
  }
1109
1143
  case "tool-call": break;
1110
- case "other":
1111
- rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
1112
- label: t("message.unknownBlock"),
1113
- payload: block.block,
1114
- truncatedLabel: (total) => t("json.truncated", { total })
1115
- }, index));
1116
- break;
1144
+ case "other": rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
1145
+ label: t("message.unknownBlock"),
1146
+ payload: block.block,
1147
+ truncatedLabel: (total) => t("json.truncated", { total })
1148
+ }, index));
1117
1149
  }
1118
1150
  }
1119
1151
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1189,29 +1221,29 @@ window.__ModuleLoader__.load({
1189
1221
  tag.textContent = css;
1190
1222
  }
1191
1223
  var SmoothStreamCard_module_css_default = {
1192
- "description": "XNGeRG_description",
1193
- "header": "XNGeRG_header",
1194
- "fieldHead": "XNGeRG_fieldHead",
1195
- "updateCopy": "XNGeRG_updateCopy",
1196
- "footer": "XNGeRG_footer",
1197
- "discard": "XNGeRG_discard",
1198
- "readOnly": "XNGeRG_readOnly",
1224
+ "name": "XNGeRG_name",
1199
1225
  "failure": "XNGeRG_failure",
1200
- "chevronOpen": "XNGeRG_chevronOpen",
1201
- "cardOpen": "XNGeRG_cardOpen",
1202
- "update": "XNGeRG_update",
1203
1226
  "save": "XNGeRG_save",
1204
- "chevron": "XNGeRG_chevron",
1205
- "label": "XNGeRG_label",
1206
- "body": "XNGeRG_body",
1207
- "hint": "XNGeRG_hint",
1227
+ "footer": "XNGeRG_footer",
1228
+ "update": "XNGeRG_update",
1208
1229
  "headText": "XNGeRG_headText",
1230
+ "fieldHead": "XNGeRG_fieldHead",
1231
+ "chevronOpen": "XNGeRG_chevronOpen",
1232
+ "cardOpen": "XNGeRG_cardOpen",
1233
+ "updateCopy": "XNGeRG_updateCopy",
1234
+ "readOnly": "XNGeRG_readOnly",
1209
1235
  "pending": "XNGeRG_pending",
1210
- "name": "XNGeRG_name",
1211
- "toggle": "XNGeRG_toggle",
1236
+ "body": "XNGeRG_body",
1237
+ "label": "XNGeRG_label",
1212
1238
  "updateRow": "XNGeRG_updateRow",
1213
1239
  "failed": "XNGeRG_failed",
1240
+ "toggle": "XNGeRG_toggle",
1214
1241
  "version": "XNGeRG_version",
1242
+ "hint": "XNGeRG_hint",
1243
+ "chevron": "XNGeRG_chevron",
1244
+ "description": "XNGeRG_description",
1245
+ "header": "XNGeRG_header",
1246
+ "discard": "XNGeRG_discard",
1215
1247
  "field": "XNGeRG_field",
1216
1248
  "card": "XNGeRG_card"
1217
1249
  };
@@ -1613,7 +1645,7 @@ window.__ModuleLoader__.load({
1613
1645
  "silky"
1614
1646
  ];
1615
1647
  /** Human-authored rows stay on the built-in renderer; `assistant-step` is replaced. */
1616
- const SKIP_WRAP = new Set([
1648
+ const SKIP_WRAP = /* @__PURE__ */ new Set([
1617
1649
  "assistant-step",
1618
1650
  "user",
1619
1651
  "steering",