dsh-smooth-stream 0.1.0 → 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
@@ -8,41 +8,42 @@ window.__ModuleLoader__.load({
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let _deepseek_ai_dsh_client_ui_attachment = require("@deepseek-ai/dsh-client-ui-attachment");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
11
12
  //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
12
- const css = ".I17U7q_root{min-width:0;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:16px;line-height:28px;display:flex}.I17U7q_body{flex-direction:column;gap:16px;min-width:0;display:flex}.I17U7q_think{flex-direction:column;display:flex}.I17U7q_thinkRow{position:relative;overflow:hidden}.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{content:\"\";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite I17U7q_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes I17U7q_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.I17U7q_thinkLeading{flex-shrink:0}.I17U7q_thinkChevron{color:var(--dsw-alias-label-secondary)}.I17U7q_thinkTitle{font-weight:400}.I17U7q_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.I17U7q_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.I17U7q_thinkSummary[data-follow-end]{text-overflow:clip}.I17U7q_thinkBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.I17U7q_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.I17U7q_disclosureRow{cursor:pointer;align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.I17U7q_disclosureLeading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.I17U7q_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.I17U7q_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureIconIdle{opacity:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureChevronHover{opacity:1}.I17U7q_disclosureTitle{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.I17U7q_disclosureContent{visibility:visible;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s;grid-template-rows:1fr;display:grid}.I17U7q_disclosureContent[data-collapsed]{visibility:hidden;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s var(--ds-transition-duration,.2s);grid-template-rows:0fr}.I17U7q_disclosureContent>*{min-height:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{animation:none}.I17U7q_disclosureContent,.I17U7q_disclosureContent[data-collapsed]{transition:none}}.I17U7q_stopped{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:6px;align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}.I17U7q_visuallyHidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.I17U7q_follow{min-width:0}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,li,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
13
- const tagId = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
13
+ const css$1 = ".I17U7q_root{min-width:0;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:16px;line-height:28px;display:flex}.I17U7q_body{flex-direction:column;gap:16px;min-width:0;display:flex}.I17U7q_think{flex-direction:column;display:flex}.I17U7q_thinkRow{position:relative;overflow:hidden}.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{content:\"\";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite I17U7q_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes I17U7q_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.I17U7q_thinkLeading{flex-shrink:0}.I17U7q_thinkChevron{color:var(--dsw-alias-label-secondary)}.I17U7q_thinkTitle{font-weight:400}.I17U7q_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.I17U7q_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.I17U7q_thinkSummary[data-follow-end]{text-overflow:clip}.I17U7q_thinkBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.I17U7q_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.I17U7q_disclosureRow{cursor:pointer;align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.I17U7q_disclosureLeading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.I17U7q_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.I17U7q_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureIconIdle{opacity:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureChevronHover{opacity:1}.I17U7q_disclosureTitle{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.I17U7q_disclosureContent{visibility:visible;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s;grid-template-rows:1fr;display:grid}.I17U7q_disclosureContent[data-collapsed]{visibility:hidden;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s var(--ds-transition-duration,.2s);grid-template-rows:0fr}.I17U7q_disclosureContent>*{min-height:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{animation:none}.I17U7q_disclosureContent,.I17U7q_disclosureContent[data-collapsed]{transition:none}}.I17U7q_stopped{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:6px;align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}.I17U7q_visuallyHidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.I17U7q_follow{min-width:0}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,li,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
14
+ const tagId$1 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
14
15
  if (typeof document !== "undefined") {
15
- let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
16
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
16
17
  if (tag === null) {
17
18
  tag = document.createElement("style");
18
19
  tag.dataset.plugin = "dsh-smooth-stream";
19
- tag.dataset.pluginCss = tagId;
20
+ tag.dataset.pluginCss = tagId$1;
20
21
  document.head.appendChild(tag);
21
22
  }
22
- tag.textContent = css;
23
+ tag.textContent = css$1;
23
24
  }
24
25
  var TypewriterAssistantNodeView_module_css_default = {
25
- "disclosureChevronHover": "I17U7q_disclosureChevronHover",
26
- "think": "I17U7q_think",
27
- "thinkSeparator": "I17U7q_thinkSeparator",
28
- "disclosureTitle": "I17U7q_disclosureTitle",
29
- "thinkSummary": "I17U7q_thinkSummary",
30
- "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
31
- "disclosureRoot": "I17U7q_disclosureRoot",
26
+ "body": "I17U7q_body",
27
+ "disclosureRow": "I17U7q_disclosureRow",
32
28
  "disclosureIconIdle": "I17U7q_disclosureIconIdle",
33
- "disclosureContent": "I17U7q_disclosureContent",
34
- "thinkLeading": "I17U7q_thinkLeading",
29
+ "stopped": "I17U7q_stopped",
30
+ "root": "I17U7q_root",
31
+ "disclosureRoot": "I17U7q_disclosureRoot",
35
32
  "thinkRow": "I17U7q_thinkRow",
36
- "thinkChevron": "I17U7q_thinkChevron",
37
- "disclosureRow": "I17U7q_disclosureRow",
38
- "disclosureLeading": "I17U7q_disclosureLeading",
33
+ "thinkLeading": "I17U7q_thinkLeading",
34
+ "follow": "I17U7q_follow",
35
+ "disclosureTitle": "I17U7q_disclosureTitle",
36
+ "visuallyHidden": "I17U7q_visuallyHidden",
37
+ "thinkSeparator": "I17U7q_thinkSeparator",
39
38
  "thinkTitle": "I17U7q_thinkTitle",
40
- "stopped": "I17U7q_stopped",
39
+ "disclosureContent": "I17U7q_disclosureContent",
40
+ "think": "I17U7q_think",
41
41
  "thinkBody": "I17U7q_thinkBody",
42
- "visuallyHidden": "I17U7q_visuallyHidden",
43
- "body": "I17U7q_body",
44
- "follow": "I17U7q_follow",
45
- "root": "I17U7q_root"
42
+ "disclosureChevronHover": "I17U7q_disclosureChevronHover",
43
+ "thinkChevron": "I17U7q_thinkChevron",
44
+ "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
45
+ "disclosureLeading": "I17U7q_disclosureLeading",
46
+ "thinkSummary": "I17U7q_thinkSummary"
46
47
  };
47
48
  //#endregion
48
49
  //#region src/client/AnimatedDisclosure.tsx
@@ -505,17 +506,15 @@ window.__ModuleLoader__.load({
505
506
  * - while the port has scroll room, writes the interpolated lag as
506
507
  * `translate3d` on `[data-chat-transcript]` (message rows only), so the
507
508
  * turn-status chrome stays pinned at the floor;
508
- * - before the port has scroll room (content shorter than the viewport), the
509
- * same lag is a content-height lag written as a negative translate on
510
- * `[data-chat-turn-status]`, so the status label descends smoothly with
511
- * 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.
512
513
  *
513
- * Handing the port back to the reader (unpin, or the owner going inactive)
514
- * first writes the effective visual top (`engine - lag`) into `scrollTop`
515
- * before clearing the transform, so the frame stays continuous. That write
516
- * also lands in ChatView's observed-top ledger as reader input beyond
517
- * `FOLLOW_THRESHOLD`, releasing its native snap-follow instead of fighting
518
- * 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.
519
518
  *
520
519
  * Unpin is a real gesture (wheel / touch / pointer / key) that leaves the
521
520
  * floor; a `scrollTop` delta from our own write must not release the pin.
@@ -586,33 +585,48 @@ window.__ModuleLoader__.load({
586
585
  function hasShiftSurface(port) {
587
586
  return shiftRootOf(port) !== null || shiftRowsOf(port).length > 0;
588
587
  }
589
- function setShift(element, px, negative = false) {
590
- const value = negative ? -px : px;
591
- if (Math.abs(value) > .01) {
592
- 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)`;
593
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)` : "";
594
610
  } else {
595
611
  element.style.transform = "";
596
612
  element.style.willChange = "";
613
+ element.style.clipPath = "";
597
614
  }
598
615
  }
599
616
  /**
600
- * The running-turn label. Before the port scrolls (content shorter than the
601
- * viewport) the content-height lag has no `scrollTop` room to ride, so it is
602
- * expressed as a negative translate on this sibling: the label descends
603
- * 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.
604
619
  */
605
- function chromeShiftOf(port) {
606
- return port.querySelector("[data-chat-turn-status]");
620
+ function statusChromeOf(port) {
621
+ return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
607
622
  }
608
623
  /**
609
- * Render the single smoothed extent `animatedH`. Before the port scrolls
610
- * (content shorter than the viewport) `animatedH` is clamped to the content
611
- * height and the content-height lag rides a negative translate on the
612
- * turn-status label; once the port has scroll room the engine is pinned at
613
- * the floor and the lag rides `[data-chat-transcript]` (or each message row
614
- * when the host has no transcript box) so the chrome stays put. Without any
615
- * 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.
616
630
  */
617
631
  function applyVisual(port, animatedH) {
618
632
  const contentHeight = Math.max(0, port.scrollHeight);
@@ -621,19 +635,19 @@ window.__ModuleLoader__.load({
621
635
  const lag = contentHeight - extent;
622
636
  port.style.overflowAnchor = "none";
623
637
  port.style.scrollBehavior = "auto";
638
+ const chrome = statusChromeOf(port);
639
+ if (chrome !== null) setShift(chrome, 0);
624
640
  const shiftRoot = shiftRootOf(port);
625
641
  if (shiftRoot !== null) {
626
642
  if (floor <= 0) {
627
643
  port.scrollTop = 0;
628
644
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
629
- const chrome = chromeShiftOf(port);
630
- if (chrome !== null) setShift(chrome, lag, true);
631
645
  setShift(shiftRoot, 0);
632
646
  return;
633
647
  }
634
648
  port.scrollTop = floor;
635
649
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
636
- setShift(shiftRoot, lag);
650
+ setShift(shiftRoot, lag, chrome === null ? void 0 : statusGapBelow(shiftRoot, chrome));
637
651
  return;
638
652
  }
639
653
  const rows = shiftRowsOf(port);
@@ -641,14 +655,13 @@ window.__ModuleLoader__.load({
641
655
  if (floor <= 0) {
642
656
  port.scrollTop = 0;
643
657
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
644
- const chrome = chromeShiftOf(port);
645
- if (chrome !== null) setShift(chrome, lag, true);
646
658
  for (const row of rows) setShift(row, 0);
647
659
  return;
648
660
  }
649
661
  port.scrollTop = floor;
650
662
  port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
651
- 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);
652
665
  return;
653
666
  }
654
667
  port.scrollTop = Math.min(floor, Math.max(0, extent));
@@ -660,18 +673,27 @@ window.__ModuleLoader__.load({
660
673
  port.style.scrollBehavior = "";
661
674
  const shiftRoot = shiftRootOf(port);
662
675
  if (shiftRoot !== null) {
676
+ statusGapCache.delete(shiftRoot);
663
677
  shiftRoot.style.transform = "";
664
678
  shiftRoot.style.willChange = "";
679
+ shiftRoot.style.clipPath = "";
665
680
  } else for (const row of shiftRowsOf(port)) {
681
+ statusGapCache.delete(row);
666
682
  row.style.transform = "";
667
683
  row.style.willChange = "";
684
+ row.style.clipPath = "";
668
685
  }
669
- const chrome = chromeShiftOf(port);
686
+ const chrome = statusChromeOf(port);
670
687
  if (chrome !== null) {
671
688
  chrome.style.transform = "";
672
689
  chrome.style.willChange = "";
690
+ chrome.style.clipPath = "";
673
691
  }
674
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
+ }
675
697
  /** Live follow hosts per conversation port so one unmount does not clear another. */
676
698
  const followOwners = /* @__PURE__ */ new WeakMap();
677
699
  function acquireFollow(port) {
@@ -821,10 +843,15 @@ window.__ModuleLoader__.load({
821
843
  if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
822
844
  const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
823
845
  if (host === null) return;
824
- if (following) handBackVisual(host);
825
846
  const remaining = holding === host ? releaseFollow(host) : followOwners.get(host) ?? 0;
826
847
  holding = null;
827
- 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
+ }
828
855
  };
829
856
  }, [
830
857
  active,
@@ -864,6 +891,10 @@ window.__ModuleLoader__.load({
864
891
  */
865
892
  const STREAM_BOOT_GLOBAL = "__DSH_SMOOTH_STREAM_CONFIG__";
866
893
  //#endregion
894
+ //#region src/settings.ts
895
+ /** Defaults shared by the Host schema and the client-side fallback. */
896
+ const DEFAULT_STREAM_SETTINGS = { thinkAutoExpand: true };
897
+ //#endregion
867
898
  //#region src/client/TypewriterAssistantNodeView.tsx
868
899
  function usePrefersReducedMotion() {
869
900
  const [reduced, setReduced] = (0, react.useState)(() => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true);
@@ -957,30 +988,28 @@ 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, shouldHoldBack, t }) {
991
+ function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, t }) {
961
992
  const reduced = usePrefersReducedMotion();
962
- const [expanded, setExpanded] = (0, react.useState)(running);
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);
973
1003
  (0, react.useEffect)(() => {
974
- setExpanded(running);
975
- }, [running]);
1004
+ if (thinkAutoExpand) setExpanded(running);
1005
+ }, [running, thinkAutoExpand]);
976
1006
  (0, react.useEffect)(() => {
977
1007
  const element = summaryRef.current;
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
- 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, node, useTurnData, openFile, loadImage, fileMentions, t }) {
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)(() => {
@@ -1084,7 +1117,9 @@ window.__ModuleLoader__.load({
1084
1117
  text: block.text,
1085
1118
  running: streaming && index === last,
1086
1119
  preset,
1120
+ thinkAutoExpand,
1087
1121
  shouldHoldBack,
1122
+ followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
1088
1123
  t
1089
1124
  }, index));
1090
1125
  break;
@@ -1172,8 +1207,436 @@ window.__ModuleLoader__.load({
1172
1207
  };
1173
1208
  }
1174
1209
  //#endregion
1210
+ //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
1211
+ const css = ".XNGeRG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.XNGeRG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.XNGeRG_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.XNGeRG_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.XNGeRG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.XNGeRG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.XNGeRG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.XNGeRG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.XNGeRG_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.XNGeRG_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.XNGeRG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.XNGeRG_chevronOpen{transform:rotate(180deg)}.XNGeRG_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.XNGeRG_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.XNGeRG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.XNGeRG_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.XNGeRG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.XNGeRG_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_toggle:disabled{cursor:default}.XNGeRG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.XNGeRG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.XNGeRG_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.XNGeRG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.XNGeRG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.XNGeRG_save,.XNGeRG_discard,.XNGeRG_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.XNGeRG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.XNGeRG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.XNGeRG_discard:hover:not(:disabled),.XNGeRG_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.XNGeRG_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.XNGeRG_discard:disabled,.XNGeRG_save:disabled,.XNGeRG_update:disabled{opacity:.4;cursor:default}.XNGeRG_discard:focus-visible,.XNGeRG_save:focus-visible,.XNGeRG_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
1212
+ const tagId = "dsh-smooth-stream/SmoothStreamCard.module.css";
1213
+ if (typeof document !== "undefined") {
1214
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
1215
+ if (tag === null) {
1216
+ tag = document.createElement("style");
1217
+ tag.dataset.plugin = "dsh-smooth-stream";
1218
+ tag.dataset.pluginCss = tagId;
1219
+ document.head.appendChild(tag);
1220
+ }
1221
+ tag.textContent = css;
1222
+ }
1223
+ var SmoothStreamCard_module_css_default = {
1224
+ "name": "XNGeRG_name",
1225
+ "failure": "XNGeRG_failure",
1226
+ "save": "XNGeRG_save",
1227
+ "footer": "XNGeRG_footer",
1228
+ "update": "XNGeRG_update",
1229
+ "headText": "XNGeRG_headText",
1230
+ "fieldHead": "XNGeRG_fieldHead",
1231
+ "chevronOpen": "XNGeRG_chevronOpen",
1232
+ "cardOpen": "XNGeRG_cardOpen",
1233
+ "updateCopy": "XNGeRG_updateCopy",
1234
+ "readOnly": "XNGeRG_readOnly",
1235
+ "pending": "XNGeRG_pending",
1236
+ "body": "XNGeRG_body",
1237
+ "label": "XNGeRG_label",
1238
+ "updateRow": "XNGeRG_updateRow",
1239
+ "failed": "XNGeRG_failed",
1240
+ "toggle": "XNGeRG_toggle",
1241
+ "version": "XNGeRG_version",
1242
+ "hint": "XNGeRG_hint",
1243
+ "chevron": "XNGeRG_chevron",
1244
+ "description": "XNGeRG_description",
1245
+ "header": "XNGeRG_header",
1246
+ "discard": "XNGeRG_discard",
1247
+ "field": "XNGeRG_field",
1248
+ "card": "XNGeRG_card"
1249
+ };
1250
+ //#endregion
1251
+ //#region src/client/SmoothStreamCard.tsx
1252
+ /**
1253
+ * The smooth-stream plugin configuration card, rendered inside the Web
1254
+ * Settings "plugin configuration" page. One toggle, staged until the user
1255
+ * saves — the same shape as the Host-shipped cards, hand-drawn because the
1256
+ * Host cards' chrome is not exported for reuse.
1257
+ */
1258
+ /** Render the smooth-stream card independently of the core settings namespace allowlist. */
1259
+ function SmoothStreamCard(props) {
1260
+ const { t } = props;
1261
+ const [open, setOpen] = (0, react.useState)(false);
1262
+ const state = props.useSmoothStreamCard((snapshot) => snapshot);
1263
+ const blocked = !state.dirty || state.saving || state.status !== "ready";
1264
+ const versionLabel = state.version === void 0 ? null : t(state.installation === "development" ? "developmentVersion" : "version").replace("{version}", state.version);
1265
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1266
+ className: open ? `${SmoothStreamCard_module_css_default.card} ${SmoothStreamCard_module_css_default.cardOpen}` : SmoothStreamCard_module_css_default.card,
1267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1268
+ type: "button",
1269
+ className: SmoothStreamCard_module_css_default.header,
1270
+ "aria-expanded": open,
1271
+ onClick: () => {
1272
+ setOpen(!open);
1273
+ },
1274
+ children: [
1275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1276
+ className: SmoothStreamCard_module_css_default.headText,
1277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1278
+ className: SmoothStreamCard_module_css_default.name,
1279
+ children: t("title")
1280
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1281
+ className: SmoothStreamCard_module_css_default.description,
1282
+ children: t("description")
1283
+ })]
1284
+ }),
1285
+ versionLabel === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1286
+ className: SmoothStreamCard_module_css_default.version,
1287
+ children: versionLabel
1288
+ }),
1289
+ state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1290
+ className: SmoothStreamCard_module_css_default.pending,
1291
+ children: t("unsaved")
1292
+ }) : null,
1293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? `${SmoothStreamCard_module_css_default.chevron} ${SmoothStreamCard_module_css_default.chevronOpen}` : SmoothStreamCard_module_css_default.chevron })
1294
+ ]
1295
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1296
+ className: SmoothStreamCard_module_css_default.body,
1297
+ children: [
1298
+ state.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1299
+ className: SmoothStreamCard_module_css_default.readOnly,
1300
+ role: "status",
1301
+ children: t("loading")
1302
+ }) : null,
1303
+ state.status === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1304
+ className: SmoothStreamCard_module_css_default.failure,
1305
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1306
+ className: SmoothStreamCard_module_css_default.readOnly,
1307
+ role: "status",
1308
+ children: t("unavailable")
1309
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1310
+ type: "button",
1311
+ className: SmoothStreamCard_module_css_default.discard,
1312
+ onClick: props.reload,
1313
+ children: t("retry")
1314
+ })]
1315
+ }) : null,
1316
+ state.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1317
+ !state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1318
+ className: SmoothStreamCard_module_css_default.readOnly,
1319
+ role: "status",
1320
+ children: t("readOnly")
1321
+ }) : null,
1322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1323
+ className: SmoothStreamCard_module_css_default.field,
1324
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1325
+ className: SmoothStreamCard_module_css_default.fieldHead,
1326
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1327
+ className: SmoothStreamCard_module_css_default.label,
1328
+ children: t("thinkAutoExpand")
1329
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1330
+ type: "checkbox",
1331
+ className: SmoothStreamCard_module_css_default.toggle,
1332
+ checked: state.thinkAutoExpand,
1333
+ disabled: !state.writable,
1334
+ onChange: (event) => {
1335
+ props.edit(event.target.checked);
1336
+ }
1337
+ })]
1338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1339
+ className: SmoothStreamCard_module_css_default.hint,
1340
+ children: t("thinkAutoExpandHint")
1341
+ })]
1342
+ }),
1343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1344
+ className: SmoothStreamCard_module_css_default.updateRow,
1345
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1346
+ className: SmoothStreamCard_module_css_default.updateCopy,
1347
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1348
+ className: SmoothStreamCard_module_css_default.label,
1349
+ children: t("updates")
1350
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1351
+ className: SmoothStreamCard_module_css_default.hint,
1352
+ children: state.restartRequired ? t("restartRequired") : state.installation === "npm" ? t("updateHint") : state.installation === "development" ? t("developmentBuild") : t("updateUnavailable")
1353
+ })]
1354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1355
+ type: "button",
1356
+ className: SmoothStreamCard_module_css_default.update,
1357
+ disabled: !state.canUpgrade || state.upgrading || state.restartRequired,
1358
+ title: state.canUpgrade ? void 0 : t("updateUnavailable"),
1359
+ onClick: props.upgrade,
1360
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1361
+ "aria-hidden": "true",
1362
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
1363
+ }), t(state.upgrading ? "updating" : "update")]
1364
+ })]
1365
+ }),
1366
+ state.upgradeFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1367
+ className: SmoothStreamCard_module_css_default.failed,
1368
+ role: "status",
1369
+ children: t("updateFailed")
1370
+ }) : null,
1371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1372
+ className: SmoothStreamCard_module_css_default.footer,
1373
+ children: [
1374
+ state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1375
+ className: SmoothStreamCard_module_css_default.failed,
1376
+ role: "status",
1377
+ children: t("saveFailed")
1378
+ }) : null,
1379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1380
+ type: "button",
1381
+ className: SmoothStreamCard_module_css_default.discard,
1382
+ disabled: !state.dirty || state.saving,
1383
+ onClick: props.discard,
1384
+ children: t("discard")
1385
+ }),
1386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1387
+ type: "button",
1388
+ className: SmoothStreamCard_module_css_default.save,
1389
+ disabled: blocked,
1390
+ onClick: props.save,
1391
+ children: t(state.saving ? "saving" : "save")
1392
+ })
1393
+ ]
1394
+ })
1395
+ ] }) : null
1396
+ ]
1397
+ }) : null]
1398
+ });
1399
+ }
1400
+ //#endregion
1401
+ //#region src/client/smooth-stream-card-controller.ts
1402
+ /** Staged form state for the plugin-owned smooth-stream settings RPC. */
1403
+ /** Bridges the plugin's protected Host interface onto a staged settings form. */
1404
+ var SmoothStreamCardController = class {
1405
+ api;
1406
+ store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.projection());
1407
+ loaded;
1408
+ staged;
1409
+ saving = false;
1410
+ failed = false;
1411
+ upgrading = false;
1412
+ upgradeFailed = false;
1413
+ restartRequired = false;
1414
+ loadGeneration = 0;
1415
+ loadStatus = "loading";
1416
+ constructor(api) {
1417
+ this.api = api;
1418
+ }
1419
+ /** Begin the background read after the card has been registered. */
1420
+ start() {
1421
+ this.load();
1422
+ }
1423
+ /** Ignore a late response after the surrounding optional services unload. */
1424
+ stop() {
1425
+ this.loadGeneration += 1;
1426
+ }
1427
+ /** Current card snapshot, also consumed by the streaming preference cell. */
1428
+ getSnapshot() {
1429
+ return this.store.getSnapshot();
1430
+ }
1431
+ /** Subscribe to state changes. */
1432
+ subscribe(listener) {
1433
+ return this.store.subscribe(listener);
1434
+ }
1435
+ /** Build the face consumed by the settings slot renderer. */
1436
+ inject() {
1437
+ return {
1438
+ hooks: { smoothStreamCard: this.store },
1439
+ edit: (value) => {
1440
+ this.staged = value;
1441
+ this.failed = false;
1442
+ this.publish();
1443
+ },
1444
+ save: () => {
1445
+ this.save();
1446
+ },
1447
+ discard: () => {
1448
+ if (this.staged === void 0 && !this.failed) return;
1449
+ this.staged = void 0;
1450
+ this.failed = false;
1451
+ this.publish();
1452
+ },
1453
+ reload: () => {
1454
+ this.load();
1455
+ },
1456
+ upgrade: () => {
1457
+ this.upgrade();
1458
+ }
1459
+ };
1460
+ }
1461
+ projection() {
1462
+ return {
1463
+ status: this.loadStatus,
1464
+ writable: this.loaded?.writable ?? false,
1465
+ dirty: this.staged !== void 0,
1466
+ saving: this.saving,
1467
+ failed: this.failed,
1468
+ thinkAutoExpand: this.staged ?? this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
1469
+ version: this.loaded?.version,
1470
+ installation: this.loaded?.installation ?? "unmanaged",
1471
+ canUpgrade: this.loaded?.canUpgrade ?? false,
1472
+ upgrading: this.upgrading,
1473
+ upgradeFailed: this.upgradeFailed,
1474
+ restartRequired: this.restartRequired
1475
+ };
1476
+ }
1477
+ async load() {
1478
+ const generation = ++this.loadGeneration;
1479
+ this.loadStatus = "loading";
1480
+ this.loaded = void 0;
1481
+ this.publish();
1482
+ try {
1483
+ const view = await this.api.read();
1484
+ if (generation !== this.loadGeneration) return;
1485
+ this.loaded = view;
1486
+ this.loadStatus = "ready";
1487
+ } catch {
1488
+ if (generation !== this.loadGeneration) return;
1489
+ this.loaded = void 0;
1490
+ this.loadStatus = "unavailable";
1491
+ }
1492
+ this.publish();
1493
+ }
1494
+ async save() {
1495
+ if (this.staged === void 0 || this.saving || this.loaded?.writable !== true) return;
1496
+ const value = this.staged;
1497
+ this.saving = true;
1498
+ this.failed = false;
1499
+ this.publish();
1500
+ try {
1501
+ this.loaded = await this.api.write(value);
1502
+ this.staged = void 0;
1503
+ } catch {
1504
+ this.failed = true;
1505
+ }
1506
+ this.saving = false;
1507
+ this.publish();
1508
+ }
1509
+ async upgrade() {
1510
+ if (this.loaded?.canUpgrade !== true || this.upgrading) return;
1511
+ this.upgrading = true;
1512
+ this.upgradeFailed = false;
1513
+ this.restartRequired = false;
1514
+ this.publish();
1515
+ try {
1516
+ const result = await this.api.upgrade();
1517
+ this.restartRequired = result.restartRequired;
1518
+ } catch {
1519
+ this.upgradeFailed = true;
1520
+ }
1521
+ this.upgrading = false;
1522
+ this.publish();
1523
+ }
1524
+ publish() {
1525
+ this.store.set(this.projection());
1526
+ }
1527
+ };
1528
+ //#endregion
1529
+ //#region src/settings-api.ts
1530
+ /** Shared wire vocabulary for the plugin-owned settings RPC channel. */
1531
+ /** Dedicated, loopback-only RPC channel registered by the Host half. */
1532
+ const STREAM_SETTINGS_RPC_CHANNEL = "/smooth-stream";
1533
+ /** Endpoints accepted by {@link STREAM_SETTINGS_RPC_CHANNEL}. */
1534
+ const STREAM_SETTINGS_RPC = {
1535
+ read: "settings.read",
1536
+ write: "settings.write",
1537
+ upgrade: "plugin.upgrade"
1538
+ };
1539
+ //#endregion
1540
+ //#region src/client/smooth-stream-settings-api.ts
1541
+ function record(value) {
1542
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1543
+ }
1544
+ function settingsView(value) {
1545
+ const data = record(value);
1546
+ if (data === void 0 || typeof data.version !== "string" || ![
1547
+ "npm",
1548
+ "development",
1549
+ "unmanaged"
1550
+ ].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.thinkAutoExpand !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
1551
+ return data;
1552
+ }
1553
+ function upgradeView(value) {
1554
+ if (record(value)?.restartRequired !== true) throw new Error("dsh-smooth-stream: malformed update response");
1555
+ return { restartRequired: true };
1556
+ }
1557
+ function accepted(result) {
1558
+ if (!result.ok) throw new Error(result.error.message);
1559
+ return result.value;
1560
+ }
1561
+ /** Build the typed facade over the generic Connection RPC service. */
1562
+ function createSmoothStreamSettingsApi(connection) {
1563
+ return {
1564
+ async read() {
1565
+ return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.read, {})));
1566
+ },
1567
+ async write(thinkAutoExpand) {
1568
+ return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, { thinkAutoExpand })));
1569
+ },
1570
+ async upgrade() {
1571
+ return upgradeView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.upgrade, {})));
1572
+ }
1573
+ };
1574
+ }
1575
+ //#endregion
1576
+ //#region src/client/locales.ts
1577
+ /** Locale bundles for the smooth-stream plugin configuration card. */
1578
+ /** Dictionary namespace owned by this plugin's settings card. */
1579
+ const NS = "settings.smoothStream";
1580
+ /** English copy. */
1581
+ const en = {
1582
+ title: "Smooth stream",
1583
+ description: "How replies are revealed while they stream.",
1584
+ thinkAutoExpand: "Auto-expand thinking",
1585
+ thinkAutoExpandHint: "Open the thinking block while it streams. Turn off to keep it collapsed.",
1586
+ readOnly: "This deployment stores settings read-only.",
1587
+ loading: "Loading plugin settings…",
1588
+ unavailable: "Plugin settings are unavailable in this connection.",
1589
+ retry: "Retry",
1590
+ version: "Version {version}",
1591
+ developmentVersion: "Development version {version}",
1592
+ updates: "Updates",
1593
+ updateHint: "Install the newest npm version, then restart Harness.",
1594
+ developmentBuild: "Linked source; updates are managed in the checkout.",
1595
+ updateUnavailable: "Updates are available only for an npm profile installation.",
1596
+ update: "Update",
1597
+ updating: "Updating…",
1598
+ restartRequired: "Updated. Restart Harness to load the new version.",
1599
+ updateFailed: "The package update failed; your current version is unchanged.",
1600
+ save: "Save",
1601
+ saving: "Saving…",
1602
+ discard: "Discard",
1603
+ unsaved: "Unsaved",
1604
+ saveFailed: "The deployment did not accept these values; they were left for you to correct."
1605
+ };
1606
+ /** Simplified Chinese copy. */
1607
+ const zh = {
1608
+ title: "丝滑流式",
1609
+ description: "回复在流式输出时如何逐字展现。",
1610
+ thinkAutoExpand: "自动展开思考",
1611
+ thinkAutoExpandHint: "思考块在流式时自动展开;关闭后保持折叠,可手动展开。",
1612
+ readOnly: "本部署的设置为只读。",
1613
+ loading: "正在加载插件设置…",
1614
+ unavailable: "当前连接无法访问插件设置。",
1615
+ retry: "重试",
1616
+ version: "版本 {version}",
1617
+ developmentVersion: "开发版本 {version}",
1618
+ updates: "更新",
1619
+ updateHint: "安装最新 npm 版本后重启 Harness。",
1620
+ developmentBuild: "当前为本地链接版本,请在源码目录管理更新。",
1621
+ updateUnavailable: "只有 profile 使用 npm 包时才能更新。",
1622
+ update: "更新",
1623
+ updating: "更新中…",
1624
+ restartRequired: "已更新;重启 Harness 后加载新版本。",
1625
+ updateFailed: "包更新失败,当前版本未改变。",
1626
+ save: "保存",
1627
+ saving: "保存中…",
1628
+ discard: "放弃修改",
1629
+ unsaved: "未保存",
1630
+ saveFailed: "本部署没有接受这些值,已保留供你修改。"
1631
+ };
1632
+ //#endregion
1175
1633
  //#region src/client/index.ts
1176
- /** Cordis services required by the browser half. */
1634
+ /**
1635
+ * Cordis services required by the browser half. Only `slots` is load-bearing
1636
+ * for the stream itself; locale and Connection power the configuration card
1637
+ * and are wired through `ctx.inject` below so a deployment without them still
1638
+ * streams with defaults.
1639
+ */
1177
1640
  const inject = ["slots"];
1178
1641
  const STREAM_MODES = ["typewriter", "teleprompter"];
1179
1642
  const STREAM_PRESETS = [
@@ -1244,23 +1707,93 @@ window.__ModuleLoader__.load({
1244
1707
  };
1245
1708
  }
1246
1709
  /**
1710
+ * A live preference cell read by `useSyncExternalStore`. It starts on the
1711
+ * shared default and, once the plugin-owned settings controller is attached,
1712
+ * tracks the resolved `thinkAutoExpand` value. A composition (or test) that
1713
+ * ships no settings surface keeps the cell on the default.
1714
+ */
1715
+ var PreferenceCell = class {
1716
+ listeners = /* @__PURE__ */ new Set();
1717
+ card;
1718
+ value = DEFAULT_STREAM_SETTINGS.thinkAutoExpand;
1719
+ /** Re-point the cell at the plugin-owned settings controller. */
1720
+ attach(card) {
1721
+ this.card = card;
1722
+ this.refresh();
1723
+ const unsubscribe = card.subscribe(() => {
1724
+ this.refresh();
1725
+ });
1726
+ return () => {
1727
+ unsubscribe();
1728
+ if (this.card !== card) return;
1729
+ this.card = void 0;
1730
+ this.refresh();
1731
+ };
1732
+ }
1733
+ read() {
1734
+ return this.card?.getSnapshot().thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand;
1735
+ }
1736
+ refresh() {
1737
+ const next = this.read();
1738
+ if (next === this.value) return;
1739
+ this.value = next;
1740
+ for (const listener of this.listeners) listener();
1741
+ }
1742
+ getSnapshot = () => this.value;
1743
+ subscribe = (listener) => {
1744
+ this.listeners.add(listener);
1745
+ return () => {
1746
+ this.listeners.delete(listener);
1747
+ };
1748
+ };
1749
+ };
1750
+ /**
1247
1751
  * Register the typewriter renderer after the conversation package declares the
1248
1752
  * keyed Chat node seat. A lower priority shadows the built-in assistant row;
1249
1753
  * every other growing row is wrapped in place so Tool cards, retries, and
1250
1754
  * workflow runs share conversation follow. The Host-bridged configuration
1251
- * selects the render direction, smoothing preset, and glide speed.
1755
+ * selects the render direction, smoothing preset, and glide speed; the
1756
+ * plugin-owned settings RPC supplies the live auto-expand preference when the
1757
+ * settings surface is composed.
1252
1758
  * @param ctx - Browser context carrying the shared slot registry.
1253
1759
  */
1254
1760
  function apply(ctx) {
1255
1761
  const config = readBootConfig();
1762
+ const preference = new PreferenceCell();
1763
+ ctx.inject([
1764
+ "slots",
1765
+ "locale",
1766
+ "connection"
1767
+ ], (settingsCtx) => {
1768
+ const card = new SmoothStreamCardController(createSmoothStreamSettingsApi(settingsCtx.get("connection")));
1769
+ const detachPreference = preference.attach(card);
1770
+ card.start();
1771
+ settingsCtx.effect(() => settingsCtx.locale.register(NS, {
1772
+ zh,
1773
+ en
1774
+ }), "dsh-smooth-stream: settings dictionaries");
1775
+ settingsCtx.slots.inject("settings.plugin.item", () => settingsCtx.slots.register({
1776
+ name: "settings.plugin.item",
1777
+ id: "smooth-stream",
1778
+ order: 30,
1779
+ locale: NS,
1780
+ inject: () => card.inject()
1781
+ }, SmoothStreamCard));
1782
+ return () => {
1783
+ card.stop();
1784
+ detachPreference();
1785
+ };
1786
+ });
1256
1787
  const configured = function StreamConfiguredView(props) {
1788
+ const thinkAutoExpand = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot, preference.getSnapshot);
1257
1789
  return (0, react.createElement)(TypewriterAssistantNodeView, {
1258
1790
  ...props,
1259
1791
  mode: config.mode,
1260
1792
  preset: config.preset,
1261
1793
  revealCharsPerSec: config.revealCharsPerSec,
1262
1794
  scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
1263
- maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec
1795
+ maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
1796
+ thinkAutoExpand
1264
1797
  });
1265
1798
  };
1266
1799
  ctx.slots.inject("conversation.chat.node", () => {