dsh-smooth-stream 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4,46 +4,85 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
7
29
  let react = require("react");
8
30
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
9
32
  let _deepseek_ai_dsh_client_ui_attachment = require("@deepseek-ai/dsh-client-ui-attachment");
10
33
  let react_jsx_runtime = require("react/jsx-runtime");
11
34
  let react_dom = require("react-dom");
12
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
13
- const css$3 = ".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[data-no-transition]{transition:none}.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{contain:layout style;min-width:0}.I17U7q_root :not(pre)>code{line-height:inherit;white-space:normal;overflow-wrap:anywhere;vertical-align:baseline;display:inline}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
14
- const tagId$3 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
35
+ //#region src/client/harnessIcons.ts
36
+ const icons = _deepseek_ai_dsh_client_ui_primitives;
37
+ function resolveIcon(currentName, legacyName) {
38
+ const icon = icons[currentName] ?? icons[legacyName];
39
+ if (icon === void 0) throw new Error(`dsh-smooth-stream: Harness icon export is missing (${currentName}, ${legacyName})`);
40
+ return icon;
41
+ }
42
+ const IconChevronDown = resolveIcon("IconChevronDownOutlineRegular", "IconChevronDownOutline14");
43
+ const IconClose = resolveIcon("IconCloseOutlineRegular", "IconCloseOutline16");
44
+ const IconCode = resolveIcon("IconCodeOutlineRegular", "IconCodeOutline16");
45
+ const IconCopy = resolveIcon("IconCopyOutlineRegular", "IconCopyOutline16");
46
+ const IconQuestion = resolveIcon("IconQuestionOutlineRegular", "IconQuestionOutline14");
47
+ const IconRefresh = resolveIcon("IconRefreshOutlineRegular", "IconRefreshOutline14");
48
+ const IconRefreshSmall = resolveIcon("IconRefreshOutlineRegular", "IconRefreshOutline16");
49
+ const IconThink = resolveIcon("IconThinkOutlineRegular", "IconThinkOutline14");
50
+ //#endregion
51
+ //#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
52
+ const css$4 = ".ydEAna_root{min-width:0;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size,16px);line-height:calc(24px + var(--dsh-content-font-delta,4px));flex-direction:column;display:flex}.ydEAna_body{flex-direction:column;gap:16px;min-width:0;display:flex}.ydEAna_body>[data-turn-process-inline][hidden]{margin-bottom:-16px}.ydEAna_think{flex-direction:column;display:flex}.ydEAna_thinkRow{position:relative;overflow:hidden}.ydEAna_think[data-state=running] .ydEAna_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 ydEAna_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes ydEAna_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.ydEAna_thinkLeading{flex-shrink:0}.ydEAna_thinkChevron{color:var(--dsw-alias-label-secondary)}.ydEAna_thinkTitle{font-weight:400}.ydEAna_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.ydEAna_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,14px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,4px));text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.ydEAna_thinkSummary[data-follow-end]{text-overflow:clip}.ydEAna_thinkBody{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,14px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,4px));white-space:pre-wrap;word-break:break-word;max-height:var(--dsh-thinking-max-height,300px);overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l4,#80808066) transparent;contain:layout;will-change:scroll-position;overflow-y:auto;transform:translateZ(0)}.ydEAna_thinkBody::-webkit-scrollbar{width:6px}.ydEAna_thinkBody::-webkit-scrollbar-track{background:0 0}.ydEAna_thinkBody::-webkit-scrollbar-thumb{background-color:var(--dsw-alias-border-l4,#80808066);border-radius:3px}.ydEAna_thinkBody::-webkit-scrollbar-thumb:hover{background-color:var(--dsw-alias-border-l3,#80808099)}.ydEAna_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.ydEAna_disclosureRow{height:calc(24px + var(--dsh-content-font-delta,0px));cursor:pointer;align-items:center;min-width:0;display:flex;position:relative;overflow:hidden}.ydEAna_disclosureLeading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.ydEAna_disclosureLeading svg:not([data-state]){width:calc(14px + var(--dsh-content-font-delta,0px));height:calc(14px + var(--dsh-content-font-delta,0px))}.ydEAna_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.ydEAna_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.ydEAna_disclosureRow:hover .ydEAna_disclosureIconIdle{opacity:0}.ydEAna_disclosureRow:hover .ydEAna_disclosureChevronHover{opacity:1}.ydEAna_disclosureTitle{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);flex:none}.ydEAna_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;overflow:hidden}.ydEAna_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}.ydEAna_disclosureContent[data-collapsed]>*{padding-top:0!important;padding-bottom:0!important}.ydEAna_disclosureContent[data-no-transition]{transition:none}.ydEAna_disclosureContent>*{min-height:0}.ydEAna_disclosureContent[data-collapsed]>*{overflow:hidden}@media (prefers-reduced-motion:reduce){.ydEAna_think[data-state=running] .ydEAna_thinkRow:after{animation:none}.ydEAna_disclosureContent,.ydEAna_disclosureContent[data-collapsed]{transition:none}}.ydEAna_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}.ydEAna_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}.ydEAna_follow{contain:layout style;min-width:0}.ydEAna_root :not(pre)>code{line-height:inherit;white-space:normal;overflow-wrap:anywhere;vertical-align:baseline;display:inline}@supports (text-box-trim:trim-both){.ydEAna_root :is(p,h1,h2,h3,h4,h5,h6,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
53
+ const tagId$4 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
15
54
  if (typeof document !== "undefined") {
16
- let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]");
55
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]");
17
56
  if (tag === null) {
18
57
  tag = document.createElement("style");
19
58
  tag.dataset.plugin = "dsh-smooth-stream";
20
- tag.dataset.pluginCss = tagId$3;
59
+ tag.dataset.pluginCss = tagId$4;
21
60
  document.head.appendChild(tag);
22
61
  }
23
- tag.textContent = css$3;
62
+ tag.textContent = css$4;
24
63
  }
25
64
  var TypewriterAssistantNodeView_module_css_default = {
26
- "stopped": "I17U7q_stopped",
27
- "thinkChevron": "I17U7q_thinkChevron",
28
- "thinkTitle": "I17U7q_thinkTitle",
29
- "thinkLeading": "I17U7q_thinkLeading",
30
- "thinkSeparator": "I17U7q_thinkSeparator",
31
- "body": "I17U7q_body",
32
- "thinkBody": "I17U7q_thinkBody",
33
- "disclosureContent": "I17U7q_disclosureContent",
34
- "visuallyHidden": "I17U7q_visuallyHidden",
35
- "disclosureTitle": "I17U7q_disclosureTitle",
36
- "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
37
- "disclosureIconIdle": "I17U7q_disclosureIconIdle",
38
- "follow": "I17U7q_follow",
39
- "think": "I17U7q_think",
40
- "thinkSummary": "I17U7q_thinkSummary",
41
- "disclosureRow": "I17U7q_disclosureRow",
42
- "root": "I17U7q_root",
43
- "disclosureLeading": "I17U7q_disclosureLeading",
44
- "thinkRow": "I17U7q_thinkRow",
45
- "disclosureChevronHover": "I17U7q_disclosureChevronHover",
46
- "disclosureRoot": "I17U7q_disclosureRoot"
65
+ "think": "ydEAna_think",
66
+ "thinkRow": "ydEAna_thinkRow",
67
+ "thinkSummary": "ydEAna_thinkSummary",
68
+ "disclosureLeading": "ydEAna_disclosureLeading",
69
+ "thinkSeparator": "ydEAna_thinkSeparator",
70
+ "root": "ydEAna_root",
71
+ "thinkLeading": "ydEAna_thinkLeading",
72
+ "dsh-smooth-stream-think-sweep": "ydEAna_dsh-smooth-stream-think-sweep",
73
+ "thinkTitle": "ydEAna_thinkTitle",
74
+ "disclosureTitle": "ydEAna_disclosureTitle",
75
+ "thinkChevron": "ydEAna_thinkChevron",
76
+ "disclosureContent": "ydEAna_disclosureContent",
77
+ "disclosureIconIdle": "ydEAna_disclosureIconIdle",
78
+ "thinkBody": "ydEAna_thinkBody",
79
+ "stopped": "ydEAna_stopped",
80
+ "disclosureRoot": "ydEAna_disclosureRoot",
81
+ "body": "ydEAna_body",
82
+ "disclosureChevronHover": "ydEAna_disclosureChevronHover",
83
+ "visuallyHidden": "ydEAna_visuallyHidden",
84
+ "follow": "ydEAna_follow",
85
+ "disclosureRow": "ydEAna_disclosureRow"
47
86
  };
48
87
  //#endregion
49
88
  //#region src/client/AnimatedDisclosure.tsx
@@ -78,10 +117,10 @@ window.__ModuleLoader__.load({
78
117
  children: [
79
118
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
80
119
  className: cx(TypewriterAssistantNodeView_module_css_default.disclosureLeading, leadingClassName),
81
- children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: chevronClassName }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
120
+ children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: chevronClassName }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
82
121
  className: TypewriterAssistantNodeView_module_css_default.disclosureIconIdle,
83
122
  children: icon
84
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: cx(chevronClassName, TypewriterAssistantNodeView_module_css_default.disclosureChevronHover) })] })
123
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: cx(chevronClassName, TypewriterAssistantNodeView_module_css_default.disclosureChevronHover) })] })
85
124
  }),
86
125
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
87
126
  className: cx(TypewriterAssistantNodeView_module_css_default.disclosureTitle, titleClassName),
@@ -126,8 +165,10 @@ window.__ModuleLoader__.load({
126
165
  const DEFAULT_STREAM_SETTINGS = {
127
166
  enabled: true,
128
167
  controlScroll: true,
168
+ preset: "silky",
129
169
  motionPreference: "auto",
130
170
  thinkAutoExpand: true,
171
+ logarithmicFade: true,
131
172
  debugEnabled: false,
132
173
  debugTuning: DEFAULT_STREAM_DEBUG_TUNING
133
174
  };
@@ -196,6 +237,21 @@ window.__ModuleLoader__.load({
196
237
  * the renderer and the chat-side panel: renderer loops publish measurements,
197
238
  * while panel edits are staged through the settings-card controller.
198
239
  */
240
+ /**
241
+ * Allocation-free read of the newest stream metric. The follower polls this
242
+ * every frame to learn whether the producer has stopped while reveal work
243
+ * remains, and `getSnapshot()` would allocate a fresh state object per frame
244
+ * just to answer one boolean.
245
+ */
246
+ function readNewestStreamMetric() {
247
+ let newest;
248
+ for (const candidate of streamMetrics.values()) if (newest === void 0 || candidate.updatedAt > newest.updatedAt) newest = candidate;
249
+ if (newest === void 0) return null;
250
+ return {
251
+ producerComplete: newest.producerComplete,
252
+ backlog: newest.backlog
253
+ };
254
+ }
199
255
  const EMPTY_METRICS = {
200
256
  fps: null,
201
257
  frameMs: null,
@@ -213,6 +269,12 @@ window.__ModuleLoader__.load({
213
269
  followRevealScale: 1,
214
270
  followFollowing: false,
215
271
  followConstrained: false,
272
+ followTerminalPhase: "live",
273
+ followRunwayPx: 0,
274
+ followTerminalBudgetPx: 0,
275
+ followBaselineShiftPx: 0,
276
+ followAnchorDeltaPx: null,
277
+ followRemainingRevealChars: 0,
216
278
  scrollTop: null,
217
279
  scrollHeight: null,
218
280
  clientHeight: null,
@@ -273,6 +335,12 @@ window.__ModuleLoader__.load({
273
335
  followRevealScale: follow?.revealScale ?? 1,
274
336
  followFollowing: follow?.following ?? false,
275
337
  followConstrained: follow?.constrained ?? false,
338
+ followTerminalPhase: follow?.terminalPhase ?? "live",
339
+ followRunwayPx: follow?.runwayPx ?? 0,
340
+ followTerminalBudgetPx: follow?.terminalBudgetPx ?? 0,
341
+ followBaselineShiftPx: follow?.baselineShiftPx ?? 0,
342
+ followAnchorDeltaPx: follow?.readingAnchorDeltaPx ?? null,
343
+ followRemainingRevealChars: follow?.remainingRevealChars ?? 0,
276
344
  scrollTop: follow?.scrollTop ?? null,
277
345
  scrollHeight: follow?.scrollHeight ?? null,
278
346
  clientHeight: follow?.clientHeight ?? null,
@@ -435,6 +503,123 @@ window.__ModuleLoader__.load({
435
503
  }
436
504
  };
437
505
  //#endregion
506
+ //#region src/client/FrameCoordinator.ts
507
+ var FrameCoordinator = class FrameCoordinator {
508
+ static coordinators = /* @__PURE__ */ new WeakMap();
509
+ tasks = /* @__PURE__ */ new Map();
510
+ rafId = null;
511
+ lastTs = null;
512
+ /**
513
+ * Owed layout read. Starts false: a fresh coordinator has no unread writes,
514
+ * and starting dirty would keep the clock armed forever after the first
515
+ * frame. Consumers declare reads via {@link markLayoutDirty}.
516
+ */
517
+ layoutDirty = false;
518
+ /** One id per consumer, so an unregister cannot evict a re-registered task. */
519
+ nextTaskId = 0;
520
+ doc;
521
+ constructor(doc) {
522
+ this.doc = doc;
523
+ }
524
+ static forDocument(doc = document) {
525
+ let coordinator = FrameCoordinator.coordinators.get(doc);
526
+ if (coordinator === void 0) {
527
+ coordinator = new FrameCoordinator(doc);
528
+ FrameCoordinator.coordinators.set(doc, coordinator);
529
+ }
530
+ return coordinator;
531
+ }
532
+ /**
533
+ * Mark that a DOM or geometry change requires a layout read next frame.
534
+ */
535
+ markLayoutDirty() {
536
+ this.layoutDirty = true;
537
+ this.ensureLoop();
538
+ }
539
+ /**
540
+ * Declare that geometry written since the last frame must be re-read at the
541
+ * top of the next one, and keep the clock running for it.
542
+ */
543
+ requestRead() {
544
+ this.markLayoutDirty();
545
+ }
546
+ /**
547
+ * Register or replace an animation/render task. Returns the task id to hand
548
+ * back to {@link unregisterTask}.
549
+ */
550
+ registerTask(task) {
551
+ const id = task.id ?? `frame-task-${this.nextTaskId++}`;
552
+ this.tasks.set(id, {
553
+ ...task,
554
+ id
555
+ });
556
+ this.ensureLoop();
557
+ return id;
558
+ }
559
+ /**
560
+ * Unregister a task when its stream completes or the element unmounts.
561
+ */
562
+ unregisterTask(id) {
563
+ if (id === null || id === void 0) return;
564
+ this.tasks.delete(id);
565
+ if (this.tasks.size === 0 && !this.layoutDirty) this.stopLoop();
566
+ }
567
+ ensureLoop() {
568
+ if (this.rafId !== null) return;
569
+ const win = this.doc.defaultView ?? (typeof window !== "undefined" ? window : null);
570
+ if (win === null) return;
571
+ const tick = (now) => {
572
+ this.rafId = null;
573
+ if (this.lastTs === null) {
574
+ this.lastTs = now;
575
+ this.ensureLoop();
576
+ return;
577
+ }
578
+ const frameIntervalMs = Math.max(0, now - this.lastTs);
579
+ const dtMs = Math.max(1, Math.min(frameIntervalMs, 100));
580
+ this.lastTs = now;
581
+ if (this.layoutDirty) {
582
+ this.layoutDirty = false;
583
+ for (const task of this.tasks.values()) task.onRead?.(now);
584
+ }
585
+ let anyActive = false;
586
+ for (const task of this.tasks.values()) if (task.onSimulate?.(dtMs, now) === true) anyActive = true;
587
+ for (const task of this.tasks.values()) task.onWrite?.(now);
588
+ if ((anyActive || this.layoutDirty) && this.rafId === null) this.ensureLoop();
589
+ else if (!anyActive && !this.layoutDirty) this.lastTs = null;
590
+ };
591
+ this.rafId = win.requestAnimationFrame(tick);
592
+ }
593
+ stopLoop() {
594
+ if (this.rafId !== null) {
595
+ (this.doc.defaultView ?? (typeof window !== "undefined" ? window : null))?.cancelAnimationFrame(this.rafId);
596
+ this.rafId = null;
597
+ }
598
+ this.lastTs = null;
599
+ }
600
+ /**
601
+ * Whether this document currently owns a live, still-needed animation
602
+ * frame. A frame that is merely draining an owed read with no registered
603
+ * task left is not considered active work.
604
+ */
605
+ get active() {
606
+ return this.rafId !== null && (this.tasks.size > 0 || this.layoutDirty);
607
+ }
608
+ /** Number of registered tasks; used by tests to pin the single-clock contract. */
609
+ get taskCount() {
610
+ return this.tasks.size;
611
+ }
612
+ /**
613
+ * Release every task, drop any owed read, and stop the clock. Used by tests
614
+ * for isolation; production consumers unregister their own task.
615
+ */
616
+ shutdown() {
617
+ this.tasks.clear();
618
+ this.layoutDirty = false;
619
+ this.stopLoop();
620
+ }
621
+ };
622
+ //#endregion
438
623
  //#region src/client/teleprompterGlide.ts
439
624
  /**
440
625
  * Conversation-port follow while an assistant reply streams.
@@ -673,7 +858,7 @@ window.__ModuleLoader__.load({
673
858
  setDirectShift(element, px);
674
859
  }
675
860
  function turnStatusOf(port) {
676
- return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
861
+ return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"], [data-chat-flow] > [data-chat-running]");
677
862
  }
678
863
  const FOLLOW_FLOW_FILL_SYMBOL = Symbol.for("dsh-smooth-stream.follow-flow-fill");
679
864
  const followFlowFillHost = globalThis;
@@ -686,6 +871,10 @@ window.__ModuleLoader__.load({
686
871
  function flowElementOf(port) {
687
872
  return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
688
873
  }
874
+ /** Keep completion padding on the stable flow wrapper across row replacement. */
875
+ function flowPadElementOf(port) {
876
+ return port.querySelector("[data-chat-flow]") ?? flowElementOf(port);
877
+ }
689
878
  function ensureFlowFillsPort(port) {
690
879
  const element = flowElementOf(port);
691
880
  const owned = followFlowFills.get(port);
@@ -745,6 +934,8 @@ window.__ModuleLoader__.load({
745
934
  followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
746
935
  const followPaintLimits = /* @__PURE__ */ new WeakMap();
747
936
  const followHadChrome = /* @__PURE__ */ new WeakSet();
937
+ /** A status row that existed during this turn makes its removal a host cascade. */
938
+ const followHadStatus = /* @__PURE__ */ new WeakSet();
748
939
  /** Last painted shift per port, to spread a wrap's one-line step over frames. */
749
940
  const followLastShiftPx = /* @__PURE__ */ new WeakMap();
750
941
  /** Last settled floor per port, to size the shift decay bound against extent collapse. */
@@ -858,7 +1049,8 @@ window.__ModuleLoader__.load({
858
1049
  });
859
1050
  return null;
860
1051
  }
861
- const delta = top - stored.top - (shift - stored.shift) + (pad - stored.pad);
1052
+ const scrollDelta = scrollTop - stored.scrollTop;
1053
+ const delta = top - stored.top + scrollDelta - (shift - stored.shift) + (pad - stored.pad);
862
1054
  if (stored.element === anchor) {
863
1055
  followGuardAnchors.set(port, {
864
1056
  element: anchor,
@@ -937,7 +1129,7 @@ window.__ModuleLoader__.load({
937
1129
  return false;
938
1130
  }
939
1131
  function setFlowPad(port, px) {
940
- const flow = flowElementOf(port);
1132
+ const flow = flowPadElementOf(port);
941
1133
  if (flow === null) return;
942
1134
  const existing = followSettlePads.get(port);
943
1135
  const original = existing?.original ?? flow.style.paddingBottom;
@@ -975,12 +1167,83 @@ window.__ModuleLoader__.load({
975
1167
  if (!traceActive()) return;
976
1168
  console.log(`[dsh-follow] ${event}`, JSON.stringify(detail));
977
1169
  }
1170
+ /**
1171
+ * Scroll length observed at the previous measured frame, per port. The
1172
+ * completion handoff uses the delta to seed the growth credit from geometry the
1173
+ * page actually committed, instead of assuming the full runway was earned.
1174
+ */
1175
+ const followObservedContentHeight = /* @__PURE__ */ new WeakMap();
1176
+ /** Growth this commit added, as last measured by `applyVisual`. */
1177
+ function environmentCommitGrowthPx(port) {
1178
+ const previous = followObservedContentHeight.get(port);
1179
+ if (previous === void 0) return 0;
1180
+ return Math.max(0, port.scrollHeight - previous);
1181
+ }
978
1182
  function hostShOf(port) {
979
1183
  return port.scrollHeight;
980
1184
  }
1185
+ /**
1186
+ * Reveal characters the smoother still owes, read from the stream ledger the
1187
+ * smoother publishes each frame. Only meaningful together with
1188
+ * `producerComplete`: a non-zero backlog while the producer runs is ordinary
1189
+ * streaming, the same backlog after it stops is the `terminal-drain` phase.
1190
+ */
1191
+ let followRemainingRevealChars = 0;
1192
+ function refreshTerminalRevealLedger() {
1193
+ const stream = readNewestStreamMetric();
1194
+ followRemainingRevealChars = stream?.backlog ?? 0;
1195
+ return {
1196
+ producerComplete: stream?.producerComplete ?? false,
1197
+ drain: (stream?.producerComplete ?? false) && (stream?.backlog ?? 0) > 0
1198
+ };
1199
+ }
1200
+ /**
1201
+ * Screen-space reading-anchor delta for diagnostics only. Gated on the debug
1202
+ * runtime so the production hot path pays nothing, and it reuses the shared
1203
+ * `measureReadingAnchor` ledger, which already excludes the engine's own glide
1204
+ * and only reports motion the follower did not author.
1205
+ */
1206
+ function measureAnchorDeltaForTelemetry(port) {
1207
+ if (!debugRuntime.isEnabled()) return null;
1208
+ return measureReadingAnchor(port)?.delta ?? null;
1209
+ }
981
1210
  function invalidatePaintLimit(port) {
982
1211
  followPaintLimits.delete(port);
983
1212
  }
1213
+ /**
1214
+ * Terminal-follow phase ledger. `terminal-drain` (producer stopped, reveal
1215
+ * queue still owes text) and `host-cascade` (host is swapping status rows,
1216
+ * collapsing Think, mounting the tail) demand opposite corrections: drain may
1217
+ * only continue upward toward the natural floor, while a cascade may need
1218
+ * temporary credit to hold the reading anchor. Keeping them in one readable
1219
+ * field is what lets the settle path stop treating ordinary completion as a
1220
+ * hostile host transaction.
1221
+ */
1222
+ const followTerminalPhases = /* @__PURE__ */ new WeakMap();
1223
+ /**
1224
+ * Measured terminal budget per port: the owned bottom space this port is
1225
+ * allowed to keep while the producer has stopped and reveal work is still
1226
+ * draining. It is seeded from a real measurement (the owned offset plus the
1227
+ * length the last commit actually added) and may only shrink afterwards — a
1228
+ * budget that grows re-opens speculative blank space below the reply, which is
1229
+ * exactly the "lift" the terminal phase exists to remove.
1230
+ */
1231
+ const followTerminalBudgets = /* @__PURE__ */ new WeakMap();
1232
+ /**
1233
+ * COMPLETION GROWTH CREDIT. Pixels of floor growth the reader actually
1234
+ * received in the completion window, which a shift rise may be funded from.
1235
+ *
1236
+ * This is a ledger rather than a per-frame clamp because growth and the
1237
+ * matching shift rise do not have to land on the same frame: a completion whose
1238
+ * final append covers several wraps commits its growth over two or three
1239
+ * frames, and a clamp that only ever compared against the PREVIOUS frame's
1240
+ * growth banked nothing across that gap. The uncovered remainder then had no
1241
+ * legal way to rise, so it survived as a real offset and was released later by
1242
+ * the pad retirement — the visible release-then-return this credit exists to
1243
+ * prevent. Credit is still bounded by growth the reader actually got, so a
1244
+ * rise can never manufacture motion that did not come from content.
1245
+ */
1246
+ const followCompletionGrowthCredit = /* @__PURE__ */ new WeakMap();
984
1247
  /** Logical position and velocity survive a React owner handoff and finish. */
985
1248
  const followMotionStates = /* @__PURE__ */ new WeakMap();
986
1249
  const followReaderHolds = /* @__PURE__ */ new WeakMap();
@@ -1129,6 +1392,7 @@ window.__ModuleLoader__.load({
1129
1392
  if (last === void 0) return 0;
1130
1393
  const status = turnStatusOf(port);
1131
1394
  const composer = port.querySelector("[data-composer-seat]");
1395
+ if (status !== null) followHadStatus.add(port);
1132
1396
  if (status !== null || composer !== null) followHadChrome.add(port);
1133
1397
  const cached = followPaintLimits.get(port);
1134
1398
  if (cached !== void 0 && performance.now() - cached.measuredAtMs <= 250 && cached.clientHeight === port.clientHeight && cached.surface === last && cached.status === status && cached.composer === composer) return cached.limit;
@@ -1203,6 +1467,35 @@ window.__ModuleLoader__.load({
1203
1467
  function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx, dtMs = 16.7, writeScrollTop = true) {
1204
1468
  const surfaces = shiftSurfacesOf(port);
1205
1469
  ensureFlowFillsPort(port);
1470
+ if (followTerminalPhases.get(port) === "terminal-drain" && turnStatusOf(port) === null && !followHadStatus.has(port)) {
1471
+ restoreRunway(port);
1472
+ setFlowPad(port, 0);
1473
+ const contentHeight = Math.max(0, port.scrollHeight);
1474
+ const floor = Math.max(0, contentHeight - port.clientHeight);
1475
+ if (port.style.overflowAnchor !== "none") port.style.overflowAnchor = "none";
1476
+ if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
1477
+ if (writeScrollTop) setFollowScrollTop(port, floor);
1478
+ followRunwayOffsetHistory.set(port, 0);
1479
+ followObservedContentHeight.set(port, contentHeight);
1480
+ followLastFloorPx.set(port, floor);
1481
+ followLastShiftPx.set(port, 0);
1482
+ followMotionStates.set(port, {
1483
+ capacityPx: Number.POSITIVE_INFINITY,
1484
+ constrained: false,
1485
+ extent: contentHeight,
1486
+ lagPx: 0,
1487
+ reservePx: 0,
1488
+ velocityPxPerSec: 0,
1489
+ terminalPhase: "terminal-drain",
1490
+ runwayPx: 0,
1491
+ baselineShiftPx: 0,
1492
+ anchorDeltaPx: measureAnchorDeltaForTelemetry(port),
1493
+ remainingRevealChars: followRemainingRevealChars
1494
+ });
1495
+ for (const surface of surfaces) setShift(surface, 0);
1496
+ FrameCoordinator.forDocument().markLayoutDirty();
1497
+ return contentHeight;
1498
+ }
1206
1499
  {
1207
1500
  const preFloor = Math.max(0, port.scrollHeight - port.clientHeight);
1208
1501
  const lastFloorSeen = followFloorHistory.get(port);
@@ -1214,7 +1507,7 @@ window.__ModuleLoader__.load({
1214
1507
  }
1215
1508
  if (preFloor !== lastFloorSeen) followFloorHistory.set(port, preFloor);
1216
1509
  }
1217
- ensureRunway(port, surfaces, reservePx);
1510
+ ensureRunway(port, surfaces, followTerminalPhases.has(port) ? Math.max(Math.min(72, followTerminalBudgets.get(port) ?? 72), reservePx) : 72);
1218
1511
  const contentHeight2 = Math.max(0, port.scrollHeight);
1219
1512
  const runwayOffset2 = runwayOffsetOf(port);
1220
1513
  const prevOffset2 = followRunwayOffsetHistory.get(port);
@@ -1222,6 +1515,7 @@ window.__ModuleLoader__.load({
1222
1515
  followRunwayOffsetHistory.set(port, runwayOffset2);
1223
1516
  const contentHeight = contentHeight2;
1224
1517
  const runwayOffset = runwayOffset2;
1518
+ followObservedContentHeight.set(port, contentHeight2);
1225
1519
  const targetHeight = Math.max(0, contentHeight - runwayOffset);
1226
1520
  const floor = Math.max(0, contentHeight - port.clientHeight);
1227
1521
  const extent = Math.min(targetHeight, Math.max(0, animatedH));
@@ -1260,7 +1554,15 @@ window.__ModuleLoader__.load({
1260
1554
  if (previousShift !== void 0 && shift < previousShift - maxDecayPx) shift = previousShift - maxDecayPx;
1261
1555
  if (followCompletionSettle.has(port) && previousShift !== void 0 && previousFloor !== void 0) {
1262
1556
  const confirmedGrowthPx = Math.max(0, floor - previousFloor);
1263
- if (shift > previousShift + confirmedGrowthPx) shift = previousShift + confirmedGrowthPx;
1557
+ let credit = (followCompletionGrowthCredit.get(port) ?? 0) + confirmedGrowthPx;
1558
+ if (shift > previousShift) {
1559
+ const rise = shift - previousShift;
1560
+ const spend = Math.min(Math.min(rise, 24), credit);
1561
+ const unfunded = rise - spend;
1562
+ if (unfunded > 0) shift -= unfunded;
1563
+ credit -= spend;
1564
+ }
1565
+ followCompletionGrowthCredit.set(port, credit);
1264
1566
  }
1265
1567
  followLastShiftPx.set(port, shift);
1266
1568
  const requestedShift = trajectoryShiftPx ?? baselineShift + requestedLag;
@@ -1275,11 +1577,17 @@ window.__ModuleLoader__.load({
1275
1577
  extent: effectiveExtent,
1276
1578
  lagPx: effectiveLag,
1277
1579
  reservePx: visibleReserve,
1278
- velocityPxPerSec
1580
+ velocityPxPerSec,
1581
+ terminalPhase: followTerminalPhases.get(port) ?? "live",
1582
+ runwayPx: runwayOffset,
1583
+ baselineShiftPx: baselineShift,
1584
+ anchorDeltaPx: followTerminalPhases.has(port) ? measureAnchorDeltaForTelemetry(port) : null,
1585
+ remainingRevealChars: followRemainingRevealChars
1279
1586
  });
1280
1587
  for (const surface of surfaces) setShift(surface, shift);
1281
1588
  const status = turnStatusOf(port);
1282
1589
  if (status !== null) setShift(status, 0);
1590
+ FrameCoordinator.forDocument().markLayoutDirty();
1283
1591
  return effectiveExtent;
1284
1592
  }
1285
1593
  function clearMotion(port) {
@@ -1305,8 +1613,10 @@ window.__ModuleLoader__.load({
1305
1613
  element.style.clipPath = "";
1306
1614
  }
1307
1615
  /** Remove equal offsets, land on the floor, then retire the compositor quietly. */
1308
- function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true) {
1616
+ function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true, deferCompositor = false) {
1309
1617
  followCompletionSettle.delete(port);
1618
+ followTerminalPhases.set(port, "natural");
1619
+ followCompletionGrowthCredit.delete(port);
1310
1620
  followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
1311
1621
  followTrace("finish-enter", {
1312
1622
  sh: hostShOf(port),
@@ -1329,6 +1639,10 @@ window.__ModuleLoader__.load({
1329
1639
  port.removeAttribute(FOLLOW_OWNED_ATTR);
1330
1640
  port.style.overflowAnchor = "";
1331
1641
  port.style.scrollBehavior = "";
1642
+ if (deferCompositor) {
1643
+ followMotionStates.delete(port);
1644
+ return;
1645
+ }
1332
1646
  for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
1333
1647
  else setShift(surface, 0);
1334
1648
  if (status !== null) {
@@ -1382,13 +1696,20 @@ window.__ModuleLoader__.load({
1382
1696
  const startedAsEntrance = entrance;
1383
1697
  const owner = {};
1384
1698
  const generation = ++followGeneration;
1385
- let rafId = 0;
1386
1699
  let last = performance.now();
1387
1700
  let following = true;
1388
1701
  let primed = false;
1389
1702
  let animatedH = 0;
1390
1703
  let reservePx = 0;
1391
1704
  let velocityPxPerSec = 0;
1705
+ /**
1706
+ * Scroll length the most recent observed commit actually added. This is the
1707
+ * honest measure of how much room the still-draining text needs, and it is
1708
+ * what seeds the terminal budget in place of the fixed 72px streaming
1709
+ * runway.
1710
+ */
1711
+ let lastCommitGrowthPx = 0;
1712
+ let lastObservedContentHeight = 0;
1392
1713
  let interacting = false;
1393
1714
  let readerGestureIntent = false;
1394
1715
  let readerReleased = false;
@@ -1412,7 +1733,28 @@ window.__ModuleLoader__.load({
1412
1733
  const revealPhase = new FollowRevealPhaseTracker();
1413
1734
  let holding = null;
1414
1735
  let entrancePending = entranceRef.current;
1736
+ /**
1737
+ * Tall-row entrance clamp. A surface shift can only mask lag up to the
1738
+ * real gap to status/composer chrome; a row that commits more than that
1739
+ * (an Edit call with its diff, a wide tool card) repaints the remainder
1740
+ * as an instant upward step — and the 0.2.x host's order-length tail
1741
+ * snap pins scrollTop to the new floor in the same task, so no
1742
+ * scroll-domain lag can be carried either. Reveal the excess through the
1743
+ * wrapper's REAL height instead: clamp it to 0 at prime and raise it at
1744
+ * the spring cadence, so the floor grows gradually and every bottom
1745
+ * writer (this engine and the host snap alike) glides the row in.
1746
+ */
1747
+ let entranceClamp = null;
1748
+ const releaseEntranceClamp = () => {
1749
+ if (entranceClamp === null) return;
1750
+ const clamp = entranceClamp;
1751
+ entranceClamp = null;
1752
+ clamp.element.style.maxHeight = clamp.maxHeight;
1753
+ clamp.element.style.overflow = clamp.overflow;
1754
+ };
1755
+ const entranceClampLagPx = () => entranceClamp === null ? 0 : Math.max(0, entranceClamp.extent - entranceClamp.revealedPx);
1415
1756
  const finishEntrance = () => {
1757
+ releaseEntranceClamp();
1416
1758
  if (!entrancePending) return;
1417
1759
  entrancePending = false;
1418
1760
  onEntranceSettledRef.current?.();
@@ -1435,6 +1777,8 @@ window.__ModuleLoader__.load({
1435
1777
  };
1436
1778
  const reportFollow = (next, isActive) => {
1437
1779
  const state = followMotionStates.get(next);
1780
+ const phase = followTerminalPhases.get(next) ?? (isActive ? "live" : "natural");
1781
+ const runwayPx = state?.runwayPx ?? runwayOffsetOf(next);
1438
1782
  debugRuntime.reportFollow(next, {
1439
1783
  lagPx: state ? state.lagPx : -1,
1440
1784
  velocityPxPerSec: state?.velocityPxPerSec ?? 0,
@@ -1446,7 +1790,13 @@ window.__ModuleLoader__.load({
1446
1790
  scrollTop: next.scrollTop,
1447
1791
  scrollHeight: next.scrollHeight,
1448
1792
  clientHeight: next.clientHeight,
1449
- active: isActive
1793
+ active: isActive,
1794
+ terminalPhase: phase,
1795
+ runwayPx,
1796
+ terminalBudgetPx: followTerminalBudgets.get(next) ?? runwayPx,
1797
+ baselineShiftPx: state?.baselineShiftPx ?? Math.max(0, runwayPx - (state?.reservePx ?? 0)),
1798
+ readingAnchorDeltaPx: state?.anchorDeltaPx ?? null,
1799
+ remainingRevealChars: followRemainingRevealChars
1450
1800
  });
1451
1801
  };
1452
1802
  const isLeader = (next) => followLeaders.get(next)?.owner === owner;
@@ -1679,23 +2029,29 @@ window.__ModuleLoader__.load({
1679
2029
  observedTail = tail;
1680
2030
  if (tail !== null) resize.observe(tail);
1681
2031
  };
2032
+ const coordinator = FrameCoordinator.forDocument();
2033
+ const frameTaskRef = { id: null };
2034
+ const stopFollowTask = () => {
2035
+ if (frameTaskRef.id === null) return;
2036
+ coordinator.unregisterTask(frameTaskRef.id);
2037
+ frameTaskRef.id = null;
2038
+ };
1682
2039
  const frame = (now) => {
1683
- rafId = requestAnimationFrame(frame);
1684
2040
  const elapsedMs = Math.max(.001, now - last);
1685
2041
  const dt = Math.min(32, elapsedMs);
1686
2042
  const tuning = debugRuntime.activeTuning();
1687
2043
  last = now;
1688
2044
  const root = rootRef.current;
1689
- if (root === null) return;
2045
+ if (root === null) return activeRef.current;
1690
2046
  const nextPort = root.closest("[data-conversation-scroll]");
1691
- if (nextPort === null) return;
2047
+ if (nextPort === null) return activeRef.current;
1692
2048
  bindPort(nextPort);
1693
2049
  observeTailSurface();
1694
2050
  resetHostScrollOwnershipForNewTurn(nextPort);
1695
2051
  hostOwnsScroll = followHostScrollPorts.has(nextPort);
1696
2052
  if (activeRef.current) followActivePorts.add(nextPort);
1697
2053
  else followActivePorts.delete(nextPort);
1698
- if (nextPort.clientHeight <= 0) return;
2054
+ if (nextPort.clientHeight <= 0) return true;
1699
2055
  const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
1700
2056
  const reportedLag = floor - nextPort.scrollTop;
1701
2057
  const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
@@ -1703,14 +2059,37 @@ window.__ModuleLoader__.load({
1703
2059
  if (completionSettleGuardsPort(nextPort)) {
1704
2060
  primed = true;
1705
2061
  following = false;
1706
- return;
2062
+ return activeRef.current;
2063
+ }
2064
+ if (activeRef.current) {
2065
+ followTerminalPhases.delete(nextPort);
2066
+ followTerminalBudgets.delete(nextPort);
2067
+ followCompletionGrowthCredit.delete(nextPort);
2068
+ followHadStatus.delete(nextPort);
1707
2069
  }
1708
2070
  const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
1709
2071
  if (inherited === void 0) {
1710
2072
  const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
1711
2073
  const predictGrowth = predictiveRef?.current ?? predictive;
1712
2074
  animatedH = entrancePending ? Math.max(0, nextPort.scrollHeight - entranceExtent) : nextPort.scrollHeight;
2075
+ if (entrancePending && entranceExtent > safeShiftLimit(nextPort, shiftSurfacesOf(nextPort)) + .25) {
2076
+ const fullPx = root.offsetHeight;
2077
+ if (fullPx > 0) {
2078
+ const basePx = Math.max(0, fullPx - entranceExtent);
2079
+ entranceClamp = {
2080
+ element: root,
2081
+ extent: entranceExtent,
2082
+ basePx,
2083
+ revealedPx: 0,
2084
+ maxHeight: root.style.maxHeight,
2085
+ overflow: root.style.overflow
2086
+ };
2087
+ root.style.overflow = "hidden";
2088
+ root.style.maxHeight = `${basePx}px`;
2089
+ }
2090
+ }
1713
2091
  const hasStatus = turnStatusOf(nextPort) !== null;
2092
+ if (hasStatus) followHadStatus.add(nextPort);
1714
2093
  reservePx = Math.max(ownedBottomSpaceOf(nextPort), predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0);
1715
2094
  if (!completionSettleGuardsPort(nextPort)) setFlowPad(nextPort, 0);
1716
2095
  statusWasPresent = hasStatus;
@@ -1745,11 +2124,11 @@ window.__ModuleLoader__.load({
1745
2124
  updateRevealScale(nextPort, elapsedMs);
1746
2125
  reportFollow(nextPort, activeRef.current);
1747
2126
  const runwayOffset = runwayOffsetOf(nextPort);
1748
- if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
2127
+ if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) + entranceClampLagPx() <= .25) finishEntrance();
1749
2128
  } else finishEntrance();
1750
2129
  } else finishEntrance();
1751
2130
  primed = true;
1752
- return;
2131
+ return activeRef.current;
1753
2132
  }
1754
2133
  if (!following && (!interacting || readerReleased && !readerGestureIntent && reportedLag <= 1) && reportedLag <= (readerReleased ? 1 : 25)) {
1755
2134
  following = true;
@@ -1775,22 +2154,23 @@ window.__ModuleLoader__.load({
1775
2154
  if (!activeRef.current || !following) {
1776
2155
  followScrollLedgers.set(nextPort, nextPort.scrollTop);
1777
2156
  reportFollow(nextPort, activeRef.current);
1778
- return;
2157
+ return activeRef.current;
1779
2158
  }
1780
2159
  detectHostScroll(nextPort, floor);
1781
2160
  if (hostOwnsScroll) {
1782
2161
  followScrollLedgers.set(nextPort, nextPort.scrollTop);
1783
2162
  reportFollow(nextPort, false);
1784
- return;
2163
+ return false;
1785
2164
  }
1786
2165
  hold(nextPort);
1787
2166
  if (!isLeader(nextPort)) {
1788
2167
  finishEntrance();
1789
- return;
2168
+ return true;
1790
2169
  }
1791
2170
  const predictGrowth = predictiveRef?.current ?? predictive;
1792
2171
  const statusElement = turnStatusOf(nextPort);
1793
2172
  const hasStatus = statusElement !== null;
2173
+ if (hasStatus) followHadStatus.add(nextPort);
1794
2174
  if (statusElement !== null) lastStatusHeightPx = statusElement.offsetHeight;
1795
2175
  const statusJustRemoved = predictGrowth && statusWasPresent === true && !hasStatus;
1796
2176
  if (statusJustRemoved) reservePx = Math.max(reservePx, tuning.runwayPx) + lastStatusHeightPx;
@@ -1800,10 +2180,25 @@ window.__ModuleLoader__.load({
1800
2180
  const effectiveReserveTarget = Math.max(reservePx, pressureReserveTarget);
1801
2181
  const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
1802
2182
  reservePx += (effectiveReserveTarget - reservePx) * reserveStep;
1803
- if (predictGrowth || runwayOffsetOf(nextPort) > .5) ensureRunway(nextPort, shiftSurfacesOf(nextPort), reservePx);
2183
+ if (refreshTerminalRevealLedger().drain) {
2184
+ if (!followTerminalPhases.has(nextPort)) {
2185
+ const seeded = Math.min(72, Math.max(runwayOffsetOf(nextPort), 0) + Math.max(0, lastCommitGrowthPx));
2186
+ followTerminalPhases.set(nextPort, "terminal-drain");
2187
+ followTerminalBudgets.set(nextPort, seeded);
2188
+ followTrace("terminal-drain", {
2189
+ budget: Math.round(seeded),
2190
+ reserve: Math.round(reservePx),
2191
+ backlog: followRemainingRevealChars
2192
+ });
2193
+ }
2194
+ }
2195
+ const terminalBudget = followTerminalPhases.has(nextPort) ? Math.max(Math.min(followTerminalBudgets.get(nextPort) ?? 72, 72), reservePx) : 72;
2196
+ if (predictGrowth || runwayOffsetOf(nextPort) > .5) ensureRunway(nextPort, shiftSurfacesOf(nextPort), terminalBudget);
1804
2197
  const runwayOffset = runwayOffsetOf(nextPort);
1805
2198
  const contentHeight = nextPort.scrollHeight;
1806
2199
  const floorNow = Math.max(0, contentHeight - nextPort.clientHeight);
2200
+ lastCommitGrowthPx = lastObservedContentHeight > 0 ? Math.max(0, contentHeight - lastObservedContentHeight) : 0;
2201
+ lastObservedContentHeight = contentHeight;
1807
2202
  const trajectoryActive = predictive && root.querySelector("[data-variant=\"think\"]") === null && runwayOffset > 0;
1808
2203
  let trajectoryShift;
1809
2204
  if (trajectoryActive) {
@@ -1861,18 +2256,38 @@ window.__ModuleLoader__.load({
1861
2256
  trajectoryPositionPx = null;
1862
2257
  trajectoryWasActive = false;
1863
2258
  }
1864
- const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
1865
- const step = computeFollowStep(dt, {
1866
- lag,
1867
- speedEma: speedCpsRef.current,
1868
- velocityPxPerSec
1869
- }, tuning);
1870
- if (lag <= .1) {
2259
+ if (entranceClamp !== null) {
2260
+ const clamp = entranceClamp;
2261
+ const remainingPx = Math.max(0, clamp.extent - clamp.revealedPx);
2262
+ const clampStep = computeFollowStep(dt, {
2263
+ lag: remainingPx,
2264
+ speedEma: speedCpsRef.current,
2265
+ velocityPxPerSec
2266
+ }, tuning);
2267
+ if (remainingPx <= .1) {
2268
+ clamp.revealedPx = clamp.extent;
2269
+ clamp.element.style.maxHeight = `${clamp.basePx + clamp.extent}px`;
2270
+ velocityPxPerSec = 0;
2271
+ } else {
2272
+ clamp.revealedPx = Math.min(clamp.extent, clamp.revealedPx + clampStep.advancePx);
2273
+ clamp.element.style.maxHeight = `${clamp.basePx + clamp.revealedPx}px`;
2274
+ velocityPxPerSec = clampStep.velocityPxPerSec;
2275
+ }
1871
2276
  animatedH = contentHeight - runwayOffset;
1872
- velocityPxPerSec = 0;
1873
2277
  } else {
1874
- animatedH = Math.min(contentHeight - runwayOffset, animatedH + step.advancePx);
1875
- velocityPxPerSec = step.velocityPxPerSec;
2278
+ const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
2279
+ const step = computeFollowStep(dt, {
2280
+ lag,
2281
+ speedEma: speedCpsRef.current,
2282
+ velocityPxPerSec
2283
+ }, tuning);
2284
+ if (lag <= .1) {
2285
+ animatedH = contentHeight - runwayOffset;
2286
+ velocityPxPerSec = 0;
2287
+ } else {
2288
+ animatedH = Math.min(contentHeight - runwayOffset, animatedH + step.advancePx);
2289
+ velocityPxPerSec = step.velocityPxPerSec;
2290
+ }
1876
2291
  }
1877
2292
  }
1878
2293
  animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift, elapsedMs, !hostOwnsScroll);
@@ -1883,12 +2298,15 @@ window.__ModuleLoader__.load({
1883
2298
  if (!activeRef.current) enforceReadingAnchor(nextPort);
1884
2299
  else measureReadingAnchor(nextPort);
1885
2300
  }
1886
- if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
2301
+ if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) + entranceClampLagPx() <= .25) finishEntrance();
2302
+ return true;
1887
2303
  };
2304
+ frameTaskRef.id = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
1888
2305
  frame(performance.now());
1889
2306
  return () => {
2307
+ releaseEntranceClamp();
1890
2308
  if (!controlScrollRef.current) {
1891
- cancelAnimationFrame(rafId);
2309
+ stopFollowTask();
1892
2310
  if (port !== null) followActivePorts.delete(port);
1893
2311
  unsubscribeCommit?.();
1894
2312
  resize?.disconnect();
@@ -1904,7 +2322,7 @@ window.__ModuleLoader__.load({
1904
2322
  releaseRevealScale();
1905
2323
  return;
1906
2324
  }
1907
- cancelAnimationFrame(rafId);
2325
+ stopFollowTask();
1908
2326
  if (port !== null) followActivePorts.delete(port);
1909
2327
  unsubscribeCommit?.();
1910
2328
  if (interactTimer !== null) clearTimeout(interactTimer);
@@ -1935,18 +2353,30 @@ window.__ModuleLoader__.load({
1935
2353
  }
1936
2354
  let settleQuietMs = 0;
1937
2355
  let settleSig = "";
1938
- const lagBeforeCompletionPaint = Math.max(0, host.scrollHeight - animatedH - runwayOffsetOf(host));
1939
- const currentRunway = runwayOffsetOf(host);
1940
- const currentShift = Math.abs(currentShiftOf(shiftSurfacesOf(host).at(-1) ?? host));
1941
- if (!activeRef.current && lagBeforeCompletionPaint <= 25 && currentRunway <= .25 && currentShift <= .25 && flowPadOf(host) <= .25) {
2356
+ if (!activeRef.current) {
1942
2357
  followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
1943
2358
  followTrace("fast-gate", {
1944
2359
  sh: host.scrollHeight,
1945
2360
  st: Math.round(host.scrollTop),
1946
2361
  pad: Math.round(flowPadOf(host))
1947
2362
  });
1948
- finishAtNaturalFloor(host, !startedAsEntrance);
2363
+ const stableTail = turnStatusOf(host) === null && followTerminalPhases.get(host) !== "host-cascade" && !followHadStatus.has(host);
2364
+ const ownedRunway = runwayOffsetOf(host);
2365
+ if (stableTail && (ownedRunway > .25 || flowPadOf(host) > .25)) {
2366
+ restoreRunway(host);
2367
+ setFlowPad(host, 0);
2368
+ finishAtNaturalFloor(host, !startedAsEntrance, true);
2369
+ followLeaders.delete(host);
2370
+ followCompletionSettle.delete(host);
2371
+ releaseRevealScale();
2372
+ debugRuntime.reportFollow(host, null);
2373
+ return;
2374
+ }
2375
+ restoreRunway(host);
2376
+ setFlowPad(host, 0);
2377
+ finishAtNaturalFloor(host, !startedAsEntrance, true);
1949
2378
  followLeaders.delete(host);
2379
+ followCompletionSettle.delete(host);
1950
2380
  releaseRevealScale();
1951
2381
  debugRuntime.reportFollow(host, null);
1952
2382
  return;
@@ -1963,6 +2393,9 @@ window.__ModuleLoader__.load({
1963
2393
  return;
1964
2394
  }
1965
2395
  followTraceUntilMs = performance.now() + 15e3;
2396
+ followTerminalPhases.set(host, statusWasPresent === true || turnStatusOf(host) !== null || followHadStatus.has(host) ? "host-cascade" : "terminal-drain");
2397
+ if (!followTerminalBudgets.has(host)) followTerminalBudgets.set(host, Math.min(72, Math.max(0, reservePx)));
2398
+ followCompletionGrowthCredit.set(host, Math.max(0, environmentCommitGrowthPx(host)));
1966
2399
  if (pruneDeadRunway(host)) {
1967
2400
  followTrace("cleanup-dead-margin", {
1968
2401
  sh: host.scrollHeight,
@@ -1989,7 +2422,14 @@ window.__ModuleLoader__.load({
1989
2422
  return;
1990
2423
  }
1991
2424
  for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
2425
+ const settleTaskRef = { id: null };
2426
+ const stopSettleTask = () => {
2427
+ if (settleTaskRef.id === null) return;
2428
+ coordinator.unregisterTask(settleTaskRef.id);
2429
+ settleTaskRef.id = null;
2430
+ };
1992
2431
  const stopSettleListeners = () => {
2432
+ stopSettleTask();
1993
2433
  for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
1994
2434
  resize?.disconnect();
1995
2435
  mutations?.disconnect();
@@ -2023,7 +2463,7 @@ window.__ModuleLoader__.load({
2023
2463
  const settleFrame = (now) => {
2024
2464
  if (!isLeader(host)) {
2025
2465
  stopSettleListeners();
2026
- return;
2466
+ return false;
2027
2467
  }
2028
2468
  if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
2029
2469
  readerGestureIntent = false;
@@ -2034,7 +2474,7 @@ window.__ModuleLoader__.load({
2034
2474
  releaseRevealScale();
2035
2475
  debugRuntime.reportFollow(host, null);
2036
2476
  stopSettleListeners();
2037
- return;
2477
+ return false;
2038
2478
  }
2039
2479
  const dt = Math.min(32, Math.max(0, now - settleLast));
2040
2480
  const tuning = debugRuntime.activeTuning();
@@ -2049,17 +2489,22 @@ window.__ModuleLoader__.load({
2049
2489
  releaseRevealScale();
2050
2490
  debugRuntime.reportFollow(host, null);
2051
2491
  stopSettleListeners();
2052
- return;
2492
+ return false;
2053
2493
  }
2054
2494
  const guardDelta = !settleRetiring && !followActivePorts.has(host) ? enforceReadingAnchor(host, true) : null;
2055
2495
  settleQuietMs = !settleRetiring && (guardDelta === null || Math.abs(guardDelta) <= .5) ? settleQuietMs + dt : 0;
2496
+ if (followTerminalPhases.get(host) !== "host-cascade") followTerminalPhases.set(host, turnStatusOf(host) === null ? "terminal-drain" : "host-cascade");
2056
2497
  const settleStatus = turnStatusOf(host);
2057
2498
  const ownedRunwayPx = runwayOffsetOf(host);
2058
2499
  if (ownedRunwayPx > .25) {
2500
+ const terminalDrain = followTerminalPhases.get(host) === "terminal-drain";
2501
+ const earnedBudgetPx = Math.min(72, Math.max(followTerminalBudgets.get(host) ?? 72, reservePx));
2059
2502
  const requestedTransferPx = settleStatus === null ? ownedRunwayPx : Math.min(reservePx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
2060
- const transferredPx = transferRunwayToFlowPad(host, requestedTransferPx);
2503
+ const cappedTransferPx = terminalDrain ? Math.min(requestedTransferPx, Math.max(0, ownedRunwayPx - earnedBudgetPx)) : requestedTransferPx;
2504
+ const transferredPx = transferRunwayToFlowPad(host, cappedTransferPx);
2061
2505
  if (transferredPx > 0) setFlowPad(host, Math.max(0, flowPadOf(host) - transferredPx));
2062
2506
  reservePx = Math.max(0, reservePx - transferredPx);
2507
+ followTerminalBudgets.set(host, Math.max(reservePx, Math.min(earnedBudgetPx, runwayOffsetOf(host))));
2063
2508
  animatedH += transferredPx;
2064
2509
  }
2065
2510
  const runwayOffset = runwayOffsetOf(host);
@@ -2093,7 +2538,7 @@ window.__ModuleLoader__.load({
2093
2538
  releaseRevealScale();
2094
2539
  debugRuntime.reportFollow(host, null);
2095
2540
  stopSettleListeners();
2096
- return;
2541
+ return false;
2097
2542
  }
2098
2543
  const step = computeFollowStep(dt, {
2099
2544
  lag,
@@ -2119,9 +2564,9 @@ window.__ModuleLoader__.load({
2119
2564
  }
2120
2565
  }
2121
2566
  reportFollow(host, false);
2122
- requestAnimationFrame(settleFrame);
2567
+ return true;
2123
2568
  };
2124
- requestAnimationFrame(settleFrame);
2569
+ settleTaskRef.id = coordinator.registerTask({ onSimulate: (_dtMs, now) => settleFrame(now) });
2125
2570
  };
2126
2571
  }, [
2127
2572
  active,
@@ -2432,7 +2877,8 @@ window.__ModuleLoader__.load({
2432
2877
  speedCps: revealSpeedCps,
2433
2878
  targetChars: targetCount,
2434
2879
  displayedChars: displayedCount,
2435
- active: !producerComplete
2880
+ active: !producerComplete || backlog > 0,
2881
+ producerComplete
2436
2882
  });
2437
2883
  if (holdBackRef.current?.() === true) {
2438
2884
  rafRef.current = requestAnimationFrame(tick);
@@ -2548,14 +2994,12 @@ window.__ModuleLoader__.load({
2548
2994
  const elementRef = (0, react.useRef)(null);
2549
2995
  (0, react.useEffect)(() => {
2550
2996
  if (!active) return;
2551
- let rafId = 0;
2552
2997
  let lastDebugReport = 0;
2553
2998
  const frame = (now) => {
2554
- rafId = requestAnimationFrame(frame);
2555
2999
  const fps = fpsRef.current;
2556
3000
  if (fps.lastMs === 0) {
2557
3001
  fps.lastMs = now;
2558
- return;
3002
+ return true;
2559
3003
  }
2560
3004
  const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs));
2561
3005
  fps.lastMs = now;
@@ -2572,10 +3016,12 @@ window.__ModuleLoader__.load({
2572
3016
  debugRuntime.reportFps(currentFps, fps.emaMs, fps.degraded);
2573
3017
  lastDebugReport = now;
2574
3018
  }
3019
+ return true;
2575
3020
  };
2576
- rafId = requestAnimationFrame(frame);
3021
+ const coordinator = FrameCoordinator.forDocument();
3022
+ const taskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
2577
3023
  return () => {
2578
- cancelAnimationFrame(rafId);
3024
+ coordinator.unregisterTask(taskId);
2579
3025
  fpsRef.current = {
2580
3026
  emaMs: 0,
2581
3027
  lastMs: 0,
@@ -2605,6 +3051,716 @@ window.__ModuleLoader__.load({
2605
3051
  };
2606
3052
  }
2607
3053
  //#endregion
3054
+ //#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/LogarithmicFade.module.css.mjs
3055
+ const css$3 = ".Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-0),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-0){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 0.0%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-1),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-1){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 3.22581%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-2),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-2){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 6.45161%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-3),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-3){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 9.67742%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-4),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-4){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 12.9032%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-5),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-5){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 16.129%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-6),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-6){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 19.3548%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-7),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-7){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 22.5807%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-8),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-8){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 25.8065%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-9),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-9){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 29.0323%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-10),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-10){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 32.2581%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-11),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-11){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 35.4839%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-12),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-12){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 38.7097%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-13),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-13){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 41.9355%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-14),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-14){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 45.1613%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-15),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-15){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 48.3871%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-16),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-16){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 51.6129%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-17),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-17){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 54.8387%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-18),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-18){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 58.0645%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-19),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-19){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 61.2903%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-20),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-20){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 64.5161%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-21),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-21){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 67.7419%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-22),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-22){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 70.9677%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-23),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-23){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 74.1936%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-24),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-24){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 77.4194%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-25),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-25){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 80.6452%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-26),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-26){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 83.871%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-27),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-27){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 87.0968%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-28),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-28){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 90.3226%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-29),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-29){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 93.5484%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-30),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-30){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 96.7742%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-31),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-31){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 100.0%, transparent)}";
3056
+ const tagId$3 = "dsh-smooth-stream/LogarithmicFade.module.css";
3057
+ if (typeof document !== "undefined") {
3058
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]");
3059
+ if (tag === null) {
3060
+ tag = document.createElement("style");
3061
+ tag.dataset.plugin = "dsh-smooth-stream";
3062
+ tag.dataset.pluginCss = tagId$3;
3063
+ document.head.appendChild(tag);
3064
+ }
3065
+ tag.textContent = css$3;
3066
+ }
3067
+ var LogarithmicFade_module_css_default = {
3068
+ "dsh-smooth-stream-log-fade-30": "Sv_6BW_dsh-smooth-stream-log-fade-30",
3069
+ "dsh-smooth-stream-log-fade-26": "Sv_6BW_dsh-smooth-stream-log-fade-26",
3070
+ "dsh-smooth-stream-log-fade-3": "Sv_6BW_dsh-smooth-stream-log-fade-3",
3071
+ "dsh-smooth-stream-log-fade-14": "Sv_6BW_dsh-smooth-stream-log-fade-14",
3072
+ "dsh-smooth-stream-log-fade-28": "Sv_6BW_dsh-smooth-stream-log-fade-28",
3073
+ "dsh-smooth-stream-log-fade-6": "Sv_6BW_dsh-smooth-stream-log-fade-6",
3074
+ "dsh-smooth-stream-log-fade-7": "Sv_6BW_dsh-smooth-stream-log-fade-7",
3075
+ "dsh-smooth-stream-log-fade-24": "Sv_6BW_dsh-smooth-stream-log-fade-24",
3076
+ "dsh-smooth-stream-log-fade-12": "Sv_6BW_dsh-smooth-stream-log-fade-12",
3077
+ "dsh-smooth-stream-log-fade-16": "Sv_6BW_dsh-smooth-stream-log-fade-16",
3078
+ "scope": "Sv_6BW_scope",
3079
+ "dsh-smooth-stream-log-fade-0": "Sv_6BW_dsh-smooth-stream-log-fade-0",
3080
+ "dsh-smooth-stream-log-fade-8": "Sv_6BW_dsh-smooth-stream-log-fade-8",
3081
+ "dsh-smooth-stream-log-fade-10": "Sv_6BW_dsh-smooth-stream-log-fade-10",
3082
+ "dsh-smooth-stream-log-fade-5": "Sv_6BW_dsh-smooth-stream-log-fade-5",
3083
+ "dsh-smooth-stream-log-fade-11": "Sv_6BW_dsh-smooth-stream-log-fade-11",
3084
+ "dsh-smooth-stream-log-fade-1": "Sv_6BW_dsh-smooth-stream-log-fade-1",
3085
+ "dsh-smooth-stream-log-fade-9": "Sv_6BW_dsh-smooth-stream-log-fade-9",
3086
+ "dsh-smooth-stream-log-fade-21": "Sv_6BW_dsh-smooth-stream-log-fade-21",
3087
+ "dsh-smooth-stream-log-fade-31": "Sv_6BW_dsh-smooth-stream-log-fade-31",
3088
+ "dsh-smooth-stream-log-fade-20": "Sv_6BW_dsh-smooth-stream-log-fade-20",
3089
+ "dsh-smooth-stream-log-fade-15": "Sv_6BW_dsh-smooth-stream-log-fade-15",
3090
+ "dsh-smooth-stream-log-fade-25": "Sv_6BW_dsh-smooth-stream-log-fade-25",
3091
+ "dsh-smooth-stream-log-fade-18": "Sv_6BW_dsh-smooth-stream-log-fade-18",
3092
+ "dsh-smooth-stream-log-fade-4": "Sv_6BW_dsh-smooth-stream-log-fade-4",
3093
+ "dsh-smooth-stream-log-fade-23": "Sv_6BW_dsh-smooth-stream-log-fade-23",
3094
+ "dsh-smooth-stream-log-fade-27": "Sv_6BW_dsh-smooth-stream-log-fade-27",
3095
+ "dsh-smooth-stream-log-fade-13": "Sv_6BW_dsh-smooth-stream-log-fade-13",
3096
+ "dsh-smooth-stream-log-fade-17": "Sv_6BW_dsh-smooth-stream-log-fade-17",
3097
+ "dsh-smooth-stream-log-fade-22": "Sv_6BW_dsh-smooth-stream-log-fade-22",
3098
+ "dsh-smooth-stream-log-fade-2": "Sv_6BW_dsh-smooth-stream-log-fade-2",
3099
+ "dsh-smooth-stream-log-fade-29": "Sv_6BW_dsh-smooth-stream-log-fade-29",
3100
+ "dsh-smooth-stream-log-fade-19": "Sv_6BW_dsh-smooth-stream-log-fade-19"
3101
+ };
3102
+ const FADE_STEPS = 32;
3103
+ const PREFIX = "dsh-smooth-stream-log-fade-";
3104
+ const COLOR_PROPERTY = "--dsh-smooth-stream-fade-color";
3105
+ const highlightName = (index) => LogarithmicFade_module_css_default[`${PREFIX}${index}`] ?? `${PREFIX}${index}`;
3106
+ const EXCLUDED = "pre,code,math,.katex,.katex-display,mjx-container,svg,script,style,textarea,input,button,select,[role=\"button\"],[contenteditable],[hidden],[aria-hidden=\"true\"],[aria-live]";
3107
+ const EXCLUDED_ATTRIBUTES = [
3108
+ "hidden",
3109
+ "aria-hidden",
3110
+ "role",
3111
+ "contenteditable",
3112
+ "class"
3113
+ ];
3114
+ const APPEARANCE_ATTRIBUTES = [
3115
+ "class",
3116
+ "style",
3117
+ "data-theme",
3118
+ "data-color-scheme",
3119
+ "data-appearance"
3120
+ ];
3121
+ function logarithmicOpacity(progress) {
3122
+ return 0 + 1 * (1 - Math.log1p(5 * (1 - (Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : 1))) / Math.log(6));
3123
+ }
3124
+ function fadeTailSize(speedCps) {
3125
+ return Math.min(160, Math.max(24, Math.ceil((Number.isFinite(speedCps) ? Math.max(0, speedCps) : 0) * 240 / 1e3)));
3126
+ }
3127
+ const schedulers = /* @__PURE__ */ new WeakMap();
3128
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
3129
+ /**
3130
+ * Hot-path counters for the streaming benchmark. Increments only — this is the
3131
+ * evidence that the per-frame pass count collapsed, not a control input.
3132
+ */
3133
+ const fadeHotPathStats = {
3134
+ /** Observer batches that reached reconcile(). */
3135
+ reconcileCalls: 0,
3136
+ /** Reconciles that did real work (the rest are dirty-check no-ops). */
3137
+ reconcilePasses: 0,
3138
+ /** Full text-node table rebuilds (structural DOM change). */
3139
+ nodeScans: 0,
3140
+ /** Forced style resolutions for newly faded elements. */
3141
+ styleReads: 0
3142
+ };
3143
+ function schedule(scheduler) {
3144
+ if (scheduler.taskId !== null || scheduler.pending.size === 0) return;
3145
+ scheduler.taskId = scheduler.coordinator.registerTask({ onSimulate: (_dtMs, now) => {
3146
+ for (const client of scheduler.pending) if (!client.paint(now)) scheduler.pending.delete(client);
3147
+ if (scheduler.pending.size === 0) {
3148
+ scheduler.taskId = null;
3149
+ return false;
3150
+ }
3151
+ return true;
3152
+ } });
3153
+ }
3154
+ /**
3155
+ * Whether an attribute record actually moved the attribute's value. jsdom (and
3156
+ * some engines) report a write even when the serialized value is unchanged, and
3157
+ * this controller writes the scope class on every enable/disable transition —
3158
+ * counting a no-op record as a DOM change would re-enter the observer forever.
3159
+ * Requires `attributeOldValue` on the observing call.
3160
+ * @param record - attribute mutation record from either observer.
3161
+ * @returns true only when the value differs from the recorded old value.
3162
+ */
3163
+ function attributeChanged(record) {
3164
+ const attribute = record.attributeName;
3165
+ if (attribute === null) return true;
3166
+ return record.oldValue !== record.target.getAttribute(attribute);
3167
+ }
3168
+ /**
3169
+ * One appearance watcher per document. A theme switch arrives either as an OS
3170
+ * media-query flip (the app's default `preference: system`) or as an attribute
3171
+ * write on <html>/<body>, so both are observed; the fade only ever writes
3172
+ * custom properties on its own root, never on those elements, so this watcher
3173
+ * cannot observe the controller's own output.
3174
+ * @param scheduler - document-scoped registry notified when the look changes.
3175
+ * @returns teardown for the last client's dispose.
3176
+ */
3177
+ function watchAppearance(scheduler) {
3178
+ const win = scheduler.window;
3179
+ const doc = win.document;
3180
+ const invalidate = () => {
3181
+ for (const client of scheduler.clients) client.invalidateColors();
3182
+ };
3183
+ const observer = new win.MutationObserver((records) => {
3184
+ if (records.some(attributeChanged)) invalidate();
3185
+ });
3186
+ const watched = doc.body === null ? [doc.documentElement] : [doc.documentElement, doc.body];
3187
+ for (const element of watched) observer.observe(element, {
3188
+ attributes: true,
3189
+ attributeFilter: APPEARANCE_ATTRIBUTES,
3190
+ attributeOldValue: true
3191
+ });
3192
+ const query = typeof win.matchMedia === "function" ? win.matchMedia("(prefers-color-scheme: dark)") : null;
3193
+ if (query !== null) query.addEventListener("change", invalidate);
3194
+ return () => {
3195
+ observer.disconnect();
3196
+ query?.removeEventListener("change", invalidate);
3197
+ };
3198
+ }
3199
+ function schedulerFor(root) {
3200
+ const doc = root.ownerDocument;
3201
+ const win = doc.defaultView;
3202
+ if (win === null) return null;
3203
+ const realm = win;
3204
+ if (typeof realm.Highlight !== "function" || !realm.CSS?.highlights || !realm.CSS.supports("color", "color-mix(in srgb, currentColor 15%, transparent)")) return null;
3205
+ let scheduler = schedulers.get(doc);
3206
+ if (scheduler === void 0) {
3207
+ const highlights = Array.from({ length: FADE_STEPS }, () => new realm.Highlight());
3208
+ for (const [index, highlight] of highlights.entries()) realm.CSS.highlights.set(highlightName(index), highlight);
3209
+ scheduler = {
3210
+ highlights,
3211
+ clients: /* @__PURE__ */ new Set(),
3212
+ pending: /* @__PURE__ */ new Set(),
3213
+ taskId: null,
3214
+ registry: realm.CSS.highlights,
3215
+ window: win,
3216
+ coordinator: FrameCoordinator.forDocument(doc),
3217
+ unwatchAppearance: () => {}
3218
+ };
3219
+ scheduler.unwatchAppearance = watchAppearance(scheduler);
3220
+ schedulers.set(doc, scheduler);
3221
+ }
3222
+ return scheduler;
3223
+ }
3224
+ /**
3225
+ * Owns ranges only: React retains ownership of every element and Text node.
3226
+ *
3227
+ * The controller used to re-derive its whole view of the subtree (textContent,
3228
+ * prefix walk, full TreeWalker, an O(tail x #nodes) filter) on *every* commit
3229
+ * AND on every observer batch for that same commit. It now keeps an
3230
+ * incremental text-node table, reconciles at most once per observed DOM
3231
+ * change, and preserves the per-element colour memo across passes.
3232
+ */
3233
+ var LogarithmicFadeController = class LogarithmicFadeController {
3234
+ root;
3235
+ scheduler;
3236
+ previous = "";
3237
+ characters = [];
3238
+ colors = /* @__PURE__ */ new Map();
3239
+ enabled = false;
3240
+ active = false;
3241
+ speedCps = 100;
3242
+ pausedAt = null;
3243
+ disposed = false;
3244
+ observer;
3245
+ /** Text nodes of the root with their source offsets; survives data-only edits. */
3246
+ nodes = [];
3247
+ /** Ineligible nodes in nodes[0..index): a span is fadeable when its bounds match. */
3248
+ ineligiblePrefix = [0];
3249
+ /**
3250
+ * `closest(EXCLUDED)` derived once per element instead of once per Text node:
3251
+ * exclusion is inherited from the ancestor chain, so a memo stays usable only
3252
+ * while the element keeps its own parent. Moving a subtree into `code` — or
3253
+ * into anything else that matches — invalidates exactly the moved chain the
3254
+ * next time it is read.
3255
+ */
3256
+ excludedElements = /* @__PURE__ */ new WeakMap();
3257
+ /** Own tag/attribute match per element; dropped when an exclusion attribute moves. */
3258
+ ownExcluded = /* @__PURE__ */ new WeakMap();
3259
+ /** Bumped by every observed mutation; reconcile() no-ops while it does not move. */
3260
+ domRevision = 0;
3261
+ /** Bumped only by structural (childList) mutations; the node table is rebuilt then. */
3262
+ structureRevision = 0;
3263
+ scannedStructureRevision = -1;
3264
+ reconciledRevision = -1;
3265
+ reconciledEnabled = false;
3266
+ reconciledActive = false;
3267
+ reconciledPaused = false;
3268
+ reconciledTailSize = 0;
3269
+ colorGeneration = 0;
3270
+ constructor(root, scheduler) {
3271
+ this.root = root;
3272
+ this.scheduler = scheduler;
3273
+ scheduler.clients.add(this);
3274
+ const win = root.ownerDocument.defaultView;
3275
+ this.observer = new win.MutationObserver((records) => {
3276
+ this.onDomMutation(records);
3277
+ });
3278
+ this.observer.observe(root, {
3279
+ subtree: true,
3280
+ childList: true,
3281
+ characterData: true,
3282
+ attributes: true,
3283
+ attributeFilter: EXCLUDED_ATTRIBUTES,
3284
+ attributeOldValue: true
3285
+ });
3286
+ }
3287
+ static create(root) {
3288
+ const scheduler = schedulerFor(root);
3289
+ return scheduler === null ? null : new LogarithmicFadeController(root, scheduler);
3290
+ }
3291
+ update(enabled, active, speedCps = 100, paused = false) {
3292
+ const now = this.scheduler.window.performance.now();
3293
+ if (paused && this.pausedAt === null) this.pausedAt = now;
3294
+ if (!paused && this.pausedAt !== null) {
3295
+ const pauseDuration = now - this.pausedAt;
3296
+ for (const character of this.characters) character.born += pauseDuration;
3297
+ this.pausedAt = null;
3298
+ }
3299
+ this.enabled = enabled;
3300
+ this.active = active;
3301
+ this.speedCps = speedCps;
3302
+ this.reconcile();
3303
+ }
3304
+ /** Invalidate and reconcile from one observer batch (one batch per commit). */
3305
+ onDomMutation(records) {
3306
+ let changed = false;
3307
+ let exclusionAttributesMoved = false;
3308
+ for (const record of records) {
3309
+ if (record.type === "childList") {
3310
+ this.structureRevision += 1;
3311
+ changed = true;
3312
+ continue;
3313
+ }
3314
+ if (record.type === "characterData") {
3315
+ changed = true;
3316
+ continue;
3317
+ }
3318
+ if (!attributeChanged(record)) continue;
3319
+ exclusionAttributesMoved = true;
3320
+ changed = true;
3321
+ }
3322
+ if (!changed) return;
3323
+ if (exclusionAttributesMoved) {
3324
+ this.excludedElements = /* @__PURE__ */ new WeakMap();
3325
+ this.ownExcluded = /* @__PURE__ */ new WeakMap();
3326
+ this.structureRevision += 1;
3327
+ }
3328
+ this.domRevision += 1;
3329
+ this.reconcile();
3330
+ }
3331
+ /**
3332
+ * Whether this element matches a SKIP rule itself or inherits one from an
3333
+ * ancestor — the memoised form of `element.closest(EXCLUDED) !== null`.
3334
+ * Memoised per element and revalidated through the parent chain, so a node
3335
+ * that moves into `code` — or whose ancestor does — is re-decided without
3336
+ * paying the selector walk once per Text node per pass.
3337
+ * @param element - element owning the Text node being classified.
3338
+ * @returns true when text under this element must stay out of the fade.
3339
+ */
3340
+ elementExcluded(element) {
3341
+ const cached = this.excludedElements.get(element);
3342
+ if (cached !== void 0 && cached.parent === element.parentElement) return cached.excluded;
3343
+ const parent = element.parentElement;
3344
+ const inherited = parent === null ? false : this.elementExcluded(parent);
3345
+ let own = this.ownExcluded.get(element);
3346
+ if (own === void 0) {
3347
+ own = element.matches(EXCLUDED);
3348
+ this.ownExcluded.set(element, own);
3349
+ }
3350
+ const excluded = inherited || own;
3351
+ this.excludedElements.set(element, {
3352
+ parent,
3353
+ excluded
3354
+ });
3355
+ return excluded;
3356
+ }
3357
+ /** Rebuild the text-node table after a structural change. */
3358
+ scanNodes() {
3359
+ const nodes = [];
3360
+ const walker = this.root.ownerDocument.createTreeWalker(this.root, NodeFilter.SHOW_TEXT);
3361
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
3362
+ const text = node;
3363
+ const parent = text.parentElement;
3364
+ const eligible = parent !== null && !this.elementExcluded(parent);
3365
+ nodes.push({
3366
+ node: text,
3367
+ start: 0,
3368
+ end: 0,
3369
+ eligible
3370
+ });
3371
+ }
3372
+ this.nodes = nodes;
3373
+ this.scannedStructureRevision = this.structureRevision;
3374
+ fadeHotPathStats.nodeScans += 1;
3375
+ }
3376
+ /**
3377
+ * Re-derive the source text, the per-node spans and the eligibility prefix
3378
+ * sums from the cached table. No DOM read, no selector match, no allocation
3379
+ * beyond the string itself.
3380
+ */
3381
+ readText() {
3382
+ if (this.scannedStructureRevision !== this.structureRevision) this.scanNodes();
3383
+ for (const entry of this.nodes) {
3384
+ if (entry.node.parentNode !== null) continue;
3385
+ this.scanNodes();
3386
+ break;
3387
+ }
3388
+ const nodes = this.nodes;
3389
+ const prefix = this.ineligiblePrefix;
3390
+ if (prefix.length < nodes.length + 1) prefix.length = nodes.length + 1;
3391
+ prefix[0] = 0;
3392
+ let text = "";
3393
+ let offset = 0;
3394
+ for (let index = 0; index < nodes.length; index += 1) {
3395
+ const entry = nodes[index];
3396
+ const data = entry.node.data;
3397
+ entry.start = offset;
3398
+ offset += data.length;
3399
+ entry.end = offset;
3400
+ text += data;
3401
+ prefix[index + 1] = prefix[index] + (entry.eligible ? 0 : 1);
3402
+ }
3403
+ prefix.length = nodes.length + 1;
3404
+ this.ineligiblePrefix = prefix;
3405
+ return text;
3406
+ }
3407
+ /** First cached node whose span ends after `offset` (binary search). */
3408
+ firstNodeEndingAfter(offset) {
3409
+ const nodes = this.nodes;
3410
+ let low = 0;
3411
+ let high = nodes.length;
3412
+ while (low < high) {
3413
+ const mid = low + high >> 1;
3414
+ if (nodes[mid].end > offset) high = mid;
3415
+ else low = mid + 1;
3416
+ }
3417
+ return low;
3418
+ }
3419
+ /** Last cached node whose span starts before `offset` (binary search). */
3420
+ lastNodeStartingBefore(offset) {
3421
+ const nodes = this.nodes;
3422
+ let low = -1;
3423
+ let high = nodes.length - 1;
3424
+ while (low < high) {
3425
+ const mid = low + high + 1 >> 1;
3426
+ if (nodes[mid].start < offset) low = mid;
3427
+ else high = mid - 1;
3428
+ }
3429
+ return low;
3430
+ }
3431
+ clearRanges() {
3432
+ for (const character of this.characters) this.scheduler.highlights[character.bucket]?.delete(character.range);
3433
+ this.characters = [];
3434
+ }
3435
+ restoreColor(element, preserved) {
3436
+ if (preserved.value === "") element.style.removeProperty(COLOR_PROPERTY);
3437
+ else element.style.setProperty(COLOR_PROPERTY, preserved.value, preserved.priority);
3438
+ }
3439
+ rootColorSet = false;
3440
+ ensureRootColor() {
3441
+ if (this.rootColorSet) return;
3442
+ const color = this.scheduler.window.getComputedStyle(this.root).color || "currentColor";
3443
+ this.root.style.setProperty(COLOR_PROPERTY, color);
3444
+ this.rootColorSet = true;
3445
+ }
3446
+ restoreColors() {
3447
+ if (this.rootColorSet) {
3448
+ this.root.style.removeProperty(COLOR_PROPERTY);
3449
+ this.rootColorSet = false;
3450
+ }
3451
+ for (const [element, preserved] of this.colors) this.restoreColor(element, preserved);
3452
+ this.colors.clear();
3453
+ }
3454
+ /**
3455
+ * The document's appearance moved (theme switch, restyle): the captured ink
3456
+ * colour is stale, so drop it and let the next pass read the new one. Costs
3457
+ * nothing while no colour is captured, which is the common case — the read
3458
+ * still happens only when text is actually fading.
3459
+ */
3460
+ invalidateColors() {
3461
+ if (this.disposed || !this.rootColorSet && this.colors.size === 0) return;
3462
+ this.restoreColors();
3463
+ this.domRevision += 1;
3464
+ this.reconcile();
3465
+ }
3466
+ /**
3467
+ * Refresh the fade ranges. Two triggers used to run this 2-3x per frame with
3468
+ * no dirty check: the commit-driven `update()` and the observer batch for the
3469
+ * very same DOM write. Both converge here, and the pass is skipped unless the
3470
+ * DOM revision, the gate, the fade window or the pause state actually moved.
3471
+ */
3472
+ reconcile() {
3473
+ if (this.disposed) return;
3474
+ fadeHotPathStats.reconcileCalls += 1;
3475
+ const tailSize = fadeTailSize(this.speedCps);
3476
+ const paused = this.pausedAt !== null;
3477
+ const domChanged = this.domRevision !== this.reconciledRevision;
3478
+ const paramsChanged = this.enabled !== this.reconciledEnabled || this.active !== this.reconciledActive || paused !== this.reconciledPaused || tailSize !== this.reconciledTailSize && this.characters.length > 0;
3479
+ if (!domChanged && !paramsChanged) return;
3480
+ fadeHotPathStats.reconcilePasses += 1;
3481
+ this.reconciledRevision = this.domRevision;
3482
+ this.reconciledEnabled = this.enabled;
3483
+ this.reconciledActive = this.active;
3484
+ this.reconciledPaused = paused;
3485
+ this.reconciledTailSize = tailSize;
3486
+ const text = this.readText();
3487
+ const previous = this.previous;
3488
+ this.previous = text;
3489
+ const old = this.characters;
3490
+ this.clearRanges();
3491
+ if (!this.enabled) {
3492
+ this.root.classList.remove(LogarithmicFade_module_css_default.scope);
3493
+ this.restoreColors();
3494
+ this.scheduler.pending.delete(this);
3495
+ this.stopIfIdle();
3496
+ return;
3497
+ }
3498
+ this.root.classList.add(LogarithmicFade_module_css_default.scope);
3499
+ this.ensureRootColor();
3500
+ const now = this.pausedAt ?? this.scheduler.window.performance.now();
3501
+ const appended = text.startsWith(previous);
3502
+ let prefix = appended ? previous.length : 0;
3503
+ if (!appended) while (prefix < previous.length && prefix < text.length && previous[prefix] === text[prefix]) prefix += 1;
3504
+ const nodes = this.nodes;
3505
+ const ineligible = this.ineligiblePrefix;
3506
+ const oldestLiveStart = old.reduce((start, character) => now - character.born < 240 && character.end <= prefix ? Math.min(start, character.start) : start, Infinity);
3507
+ const retainedBySpan = /* @__PURE__ */ new Map();
3508
+ for (const character of old) retainedBySpan.set(`${String(character.start)}:${String(character.end)}`, character);
3509
+ this.colorGeneration += 1;
3510
+ const windowStart = Math.max(0, Math.min(text.length - 320, oldestLiveStart < Infinity ? oldestLiveStart : text.length));
3511
+ const tailText = windowStart > 0 ? text.slice(windowStart) : text;
3512
+ const segments = segmenter.segment(tailText);
3513
+ let end = text.length;
3514
+ for (let count = 0; count < 160 && end > 0 && (count < tailSize || end > oldestLiveStart); count += 1) {
3515
+ const localEnd = end - windowStart;
3516
+ if (localEnd <= 0) break;
3517
+ const segment = segments.containing(localEnd - 1);
3518
+ if (segment === void 0) break;
3519
+ const start = segment.index + windowStart;
3520
+ const born = (end <= prefix ? retainedBySpan.get(`${String(start)}:${String(end)}`) : void 0)?.born ?? (this.active && appended && start >= previous.length ? now : null);
3521
+ if (born !== null && now - born < 240 && segment.segment.trim() !== "") {
3522
+ const first = this.firstNodeEndingAfter(start);
3523
+ const last = this.lastNodeStartingBefore(end);
3524
+ if (last >= first && first < nodes.length && nodes[last].end > start && ineligible[last + 1] === ineligible[first]) {
3525
+ const firstPart = nodes[first];
3526
+ const lastPart = nodes[last];
3527
+ const range = this.root.ownerDocument.createRange();
3528
+ range.setStart(firstPart.node, start - firstPart.start);
3529
+ range.setEnd(lastPart.node, end - lastPart.start);
3530
+ this.characters.push({
3531
+ start,
3532
+ end,
3533
+ born,
3534
+ range,
3535
+ bucket: -1
3536
+ });
3537
+ }
3538
+ }
3539
+ end = start;
3540
+ }
3541
+ if (this.paint(now)) {
3542
+ if (this.pausedAt === null) {
3543
+ this.scheduler.pending.add(this);
3544
+ schedule(this.scheduler);
3545
+ } else {
3546
+ this.scheduler.pending.delete(this);
3547
+ this.stopIfIdle();
3548
+ }
3549
+ } else {
3550
+ this.scheduler.pending.delete(this);
3551
+ this.stopIfIdle();
3552
+ }
3553
+ }
3554
+ paint(now) {
3555
+ this.characters = this.characters.filter((character) => {
3556
+ const progress = (now - character.born) / 240;
3557
+ if (progress >= 1 || !this.root.isConnected || !this.root.contains(character.range.startContainer)) {
3558
+ this.scheduler.highlights[character.bucket]?.delete(character.range);
3559
+ return false;
3560
+ }
3561
+ const bucket = Math.min(31, Math.round((logarithmicOpacity(progress) - 0) / 1 * 31));
3562
+ if (bucket !== character.bucket) {
3563
+ this.scheduler.highlights[character.bucket]?.delete(character.range);
3564
+ this.scheduler.highlights[bucket].add(character.range);
3565
+ character.bucket = bucket;
3566
+ }
3567
+ return true;
3568
+ });
3569
+ if (this.characters.length === 0) this.restoreColors();
3570
+ return this.characters.length > 0;
3571
+ }
3572
+ stopIfIdle() {
3573
+ if (this.scheduler.pending.size !== 0) return;
3574
+ if (this.scheduler.taskId === null) return;
3575
+ this.scheduler.coordinator.unregisterTask(this.scheduler.taskId);
3576
+ this.scheduler.taskId = null;
3577
+ }
3578
+ dispose() {
3579
+ if (this.disposed) return;
3580
+ this.disposed = true;
3581
+ this.observer.disconnect();
3582
+ this.clearRanges();
3583
+ this.restoreColors();
3584
+ this.nodes = [];
3585
+ this.scannedStructureRevision = -1;
3586
+ this.root.classList.remove(LogarithmicFade_module_css_default.scope);
3587
+ this.scheduler.pending.delete(this);
3588
+ this.scheduler.clients.delete(this);
3589
+ this.stopIfIdle();
3590
+ if (this.scheduler.clients.size === 0) {
3591
+ for (const [index, highlight] of this.scheduler.highlights.entries()) {
3592
+ const name = highlightName(index);
3593
+ if (this.scheduler.registry.get(name) === highlight) this.scheduler.registry.delete(name);
3594
+ }
3595
+ this.scheduler.unwatchAppearance();
3596
+ schedulers.delete(this.root.ownerDocument);
3597
+ }
3598
+ }
3599
+ };
3600
+ /** active admits new characters; enabled=false also cancels completion linger. */
3601
+ function useLogarithmicFade(rootRef, enabled, active, speedCpsRef, paused = false) {
3602
+ const controller = (0, react.useRef)(null);
3603
+ const committed = (0, react.useRef)(false);
3604
+ (0, react.useLayoutEffect)(() => {
3605
+ return () => {
3606
+ controller.current?.dispose();
3607
+ controller.current = null;
3608
+ };
3609
+ }, [rootRef]);
3610
+ (0, react.useLayoutEffect)(() => {
3611
+ const root = rootRef.current;
3612
+ if (!enabled) {
3613
+ if (controller.current !== null) {
3614
+ controller.current.dispose();
3615
+ controller.current = null;
3616
+ }
3617
+ committed.current = true;
3618
+ return;
3619
+ }
3620
+ if (controller.current === null && root !== null && active) {
3621
+ controller.current = LogarithmicFadeController.create(root);
3622
+ if (committed.current) controller.current?.update(false, false);
3623
+ }
3624
+ controller.current?.update(enabled, active, speedCpsRef?.current, paused);
3625
+ committed.current = true;
3626
+ });
3627
+ }
3628
+ //#endregion
3629
+ //#region src/client/useDecoupledMarkdown.ts
3630
+ /**
3631
+ * Detect whether the given markdown text ends inside an unclosed code block (fence).
3632
+ * Supports both backtick (```) and tilde (~~~) fences per CommonMark rules.
3633
+ *
3634
+ * Rules:
3635
+ * 1. An opening fence begins on a line with 0-3 leading spaces, followed by 3+ backticks or tildes.
3636
+ * 2. An opening backtick fence info-string cannot contain backticks.
3637
+ * 3. A closing fence must have at least as many characters as the opening fence, matching fence char,
3638
+ * and cannot have non-whitespace content after the fence characters.
3639
+ * 4. While inside a code block, all lines belong to the block until a matching closing fence is found.
3640
+ */
3641
+ function hasUnclosedCodeFence(text) {
3642
+ if (!text.includes("```") && !text.includes("~~~")) return false;
3643
+ let inFence = false;
3644
+ let fenceChar = "";
3645
+ let fenceLen = 0;
3646
+ let start = 0;
3647
+ const len = text.length;
3648
+ while (start < len) {
3649
+ let end = text.indexOf("\n", start);
3650
+ if (end === -1) end = len;
3651
+ let line = text.slice(start, end);
3652
+ if (line.endsWith("\r")) line = line.slice(0, -1);
3653
+ let indent = 0;
3654
+ while (indent < line.length && line[indent] === " " && indent < 4) indent++;
3655
+ if (indent < 4) {
3656
+ const rest = line.slice(indent);
3657
+ const firstChar = rest[0];
3658
+ if (firstChar === "`" || firstChar === "~") {
3659
+ let count = 0;
3660
+ while (count < rest.length && rest[count] === firstChar) count++;
3661
+ if (count >= 3) {
3662
+ const afterFence = rest.slice(count);
3663
+ if (!inFence) {
3664
+ if (firstChar !== "`" || !afterFence.includes("`")) {
3665
+ inFence = true;
3666
+ fenceChar = firstChar;
3667
+ fenceLen = count;
3668
+ }
3669
+ } else if (firstChar === fenceChar && count >= fenceLen) {
3670
+ if (afterFence.trim() === "") {
3671
+ inFence = false;
3672
+ fenceChar = "";
3673
+ fenceLen = 0;
3674
+ }
3675
+ }
3676
+ }
3677
+ }
3678
+ }
3679
+ start = end + 1;
3680
+ }
3681
+ return inFence;
3682
+ }
3683
+ /**
3684
+ * Decouple the smooth 60 FPS animation clock from the heavyweight Markdown AST compilation clock.
3685
+ *
3686
+ * Outside unclosed code blocks (or when settled), Markdown renders 1:1 with `shown`.
3687
+ * Inside unclosed code blocks (where host incremental parsing degenerates to O(n) full-text compilation),
3688
+ * Markdown re-renders are throttled to ~33ms (30Hz).
3689
+ * Upon code block closure or stream end, updates are synchronized immediately without latency.
3690
+ */
3691
+ function useDecoupledMarkdown(shown, live, options) {
3692
+ const throttleMs = options?.throttleMs ?? 33;
3693
+ const [markdownShown, setMarkdownShown] = (0, react.useState)(shown);
3694
+ const lastCommitTimeRef = (0, react.useRef)(0);
3695
+ const timerRef = (0, react.useRef)(null);
3696
+ const shownRef = (0, react.useRef)(shown);
3697
+ shownRef.current = shown;
3698
+ const wasUnclosedRef = (0, react.useRef)(false);
3699
+ (0, react.useEffect)(() => {
3700
+ if (!live) {
3701
+ if (timerRef.current !== null) {
3702
+ clearTimeout(timerRef.current);
3703
+ timerRef.current = null;
3704
+ }
3705
+ setMarkdownShown(shown);
3706
+ lastCommitTimeRef.current = performance.now();
3707
+ wasUnclosedRef.current = false;
3708
+ return;
3709
+ }
3710
+ const unclosed = hasUnclosedCodeFence(shown);
3711
+ const now = performance.now();
3712
+ if (wasUnclosedRef.current && !unclosed) {
3713
+ if (timerRef.current !== null) {
3714
+ clearTimeout(timerRef.current);
3715
+ timerRef.current = null;
3716
+ }
3717
+ wasUnclosedRef.current = false;
3718
+ setMarkdownShown(shown);
3719
+ lastCommitTimeRef.current = now;
3720
+ return;
3721
+ }
3722
+ wasUnclosedRef.current = unclosed;
3723
+ if (!unclosed) {
3724
+ if (timerRef.current !== null) {
3725
+ clearTimeout(timerRef.current);
3726
+ timerRef.current = null;
3727
+ }
3728
+ setMarkdownShown(shown);
3729
+ lastCommitTimeRef.current = now;
3730
+ return;
3731
+ }
3732
+ const elapsed = now - lastCommitTimeRef.current;
3733
+ if (elapsed >= throttleMs) {
3734
+ if (timerRef.current !== null) {
3735
+ clearTimeout(timerRef.current);
3736
+ timerRef.current = null;
3737
+ }
3738
+ setMarkdownShown(shown);
3739
+ lastCommitTimeRef.current = now;
3740
+ } else if (timerRef.current === null) {
3741
+ const delay = Math.max(16, throttleMs - elapsed);
3742
+ timerRef.current = setTimeout(() => {
3743
+ timerRef.current = null;
3744
+ setMarkdownShown(shownRef.current);
3745
+ lastCommitTimeRef.current = performance.now();
3746
+ }, delay);
3747
+ }
3748
+ }, [
3749
+ shown,
3750
+ live,
3751
+ throttleMs
3752
+ ]);
3753
+ (0, react.useEffect)(() => {
3754
+ return () => {
3755
+ if (timerRef.current !== null) {
3756
+ clearTimeout(timerRef.current);
3757
+ timerRef.current = null;
3758
+ }
3759
+ };
3760
+ }, []);
3761
+ return live ? markdownShown : shown;
3762
+ }
3763
+ //#endregion
2608
3764
  //#region src/client/FollowHost.tsx
2609
3765
  /**
2610
3766
  * Document-flow host that owns conversation-port follow while `active`.
@@ -2658,7 +3814,7 @@ window.__ModuleLoader__.load({
2658
3814
  /** Defaults shared by the Host schema and the client-side fallback. */
2659
3815
  const DEFAULT_STREAM_CONFIG = {
2660
3816
  mode: "typewriter",
2661
- preset: "balanced",
3817
+ preset: "silky",
2662
3818
  revealCharsPerSec: 80,
2663
3819
  scrollSpeedPxPerSec: 48,
2664
3820
  maxScrollSpeedPxPerSec: 1e3
@@ -2707,79 +3863,24 @@ window.__ModuleLoader__.load({
2707
3863
  else width += emPx;
2708
3864
  return width;
2709
3865
  }
2710
- function measurePendingTextGeometry(root, visibleText) {
2711
- if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
2712
- root,
2713
- visibleText,
2714
- fontSize: 14,
2715
- wrapThresholdWidth: null
2716
- };
2717
- const rootRect = root.getBoundingClientRect();
2718
- const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
2719
- if (rootWidth <= 0) return {
2720
- root,
2721
- visibleText,
2722
- fontSize: 14,
2723
- wrapThresholdWidth: null
2724
- };
2725
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2726
- let tail = null;
2727
- for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
2728
- const parent = tail?.parentElement ?? root;
2729
- const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
2730
- if (tail === null || typeof document.createRange !== "function") return {
2731
- root,
2732
- visibleText,
2733
- fontSize,
2734
- wrapThresholdWidth: rootWidth
2735
- };
2736
- try {
2737
- const length = tail.textContent?.length ?? 0;
2738
- if (length <= 0) return {
2739
- root,
2740
- visibleText,
2741
- fontSize,
2742
- wrapThresholdWidth: rootWidth
2743
- };
2744
- const range = document.createRange();
2745
- range.setStart(tail, Math.max(0, length - 1));
2746
- range.setEnd(tail, length);
2747
- const tailRect = range.getBoundingClientRect();
2748
- const contentRight = rootRect.right;
2749
- if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
2750
- root,
2751
- visibleText,
2752
- fontSize,
2753
- wrapThresholdWidth: rootWidth
2754
- };
2755
- return {
2756
- root,
2757
- visibleText,
2758
- fontSize,
2759
- wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
2760
- };
2761
- } catch {
2762
- return {
2763
- root,
2764
- visibleText,
2765
- fontSize,
2766
- wrapThresholdWidth: rootWidth
2767
- };
2768
- }
2769
- }
2770
3866
  /** Whether buffered source can reach a new visual line before it drains. */
2771
3867
  function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
2772
3868
  if (pending === "") return false;
2773
3869
  if (/[\r\n]/u.test(pending)) return true;
2774
- const pendingChars = [...pending];
2775
- if (root === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
3870
+ if ([...pending].length >= PREDICTIVE_WRAP_FALLBACK_CHARS) return true;
3871
+ if (root === null) return false;
2776
3872
  let geometry = geometryRef.current;
2777
- if (geometry?.root !== root || geometry.visibleText !== visibleText) {
2778
- geometry = measurePendingTextGeometry(root, visibleText);
3873
+ if (geometry?.root !== root) {
3874
+ const rootWidth = root.clientWidth || 600;
3875
+ geometry = {
3876
+ root,
3877
+ visibleText,
3878
+ fontSize: 14,
3879
+ wrapThresholdWidth: Math.max(120, rootWidth * .4)
3880
+ };
2779
3881
  geometryRef.current = geometry;
2780
3882
  }
2781
- if (geometry.wrapThresholdWidth === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
2782
- return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
3883
+ return approximateInlineWidth(pending, geometry.fontSize) >= (geometry.wrapThresholdWidth ?? geometry.fontSize * PREDICTIVE_WRAP_FALLBACK_CHARS);
2783
3884
  }
2784
3885
  function announcementChunkEnd(source, start) {
2785
3886
  const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
@@ -2896,12 +3997,12 @@ window.__ModuleLoader__.load({
2896
3997
  * and rendered by the Harness `MarkdownText`
2897
3998
  * streaming arm (incremental parse, frozen non-tail blocks), so there is no
2898
3999
  * raw-text tail and no text-to-markdown swap: the tree stays markdown
2899
- * throughout. The last text block owns conversation-port follow so wraps
2900
- * glide instead of snapping. Once the stream closes and the reveal queue
2901
- * drains, the settled full parse (KaTeX math, fence highlighting, file
4000
+ * throughout. The outer assistant owner follows all text growth, including
4001
+ * the final drain, so wraps glide without a completion handoff. Once the
4002
+ * queue drains, the settled full parse (KaTeX math, fence highlighting, file
2902
4003
  * mentions) swaps in exactly once.
2903
4004
  */
2904
- function AnimatedMarkdownText({ text, labels, fileMentions, streaming, motionReduced, ownFollow, followSpeedCpsRef, followRevealedCharsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack, controlScroll = true }) {
4005
+ function AnimatedMarkdownText({ text, labels, fileMentions, streaming, logarithmicFade, motionReduced, followSpeedCpsRef, followRevealedCharsRef, followRevealScaleRef, onPredictiveChange, onRevealActivityChange, preset, shouldHoldBack }) {
2905
4006
  const reduced = motionReduced;
2906
4007
  const [typing, setTyping] = (0, react.useState)(streaming);
2907
4008
  const localSpeedCpsRef = (0, react.useRef)(35);
@@ -2924,6 +4025,8 @@ window.__ModuleLoader__.load({
2924
4025
  });
2925
4026
  const shown = reduced ? text : displayed;
2926
4027
  const live = typing && !reduced;
4028
+ const markdownShown = useDecoupledMarkdown(shown, live);
4029
+ useLogarithmicFade(followRootRef, logarithmicFade && !reduced, live, speedCpsRef);
2927
4030
  (0, react.useEffect)(() => {
2928
4031
  const root = followRootRef.current;
2929
4032
  if (root === null || typeof ResizeObserver === "undefined") return;
@@ -2950,6 +4053,9 @@ window.__ModuleLoader__.load({
2950
4053
  streaming,
2951
4054
  text
2952
4055
  ]);
4056
+ (0, react.useLayoutEffect)(() => {
4057
+ onRevealActivityChange?.(live);
4058
+ }, [live, onRevealActivityChange]);
2953
4059
  (0, react.useEffect)(() => {
2954
4060
  if (typing && !streaming && shown.length === text.length) setTyping(false);
2955
4061
  }, [
@@ -2961,16 +4067,13 @@ window.__ModuleLoader__.load({
2961
4067
  (0, react.useEffect)(() => {
2962
4068
  if (streaming) setTyping(true);
2963
4069
  }, [streaming]);
2964
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
2965
- active: live && ownFollow,
2966
- speedCpsRef,
2967
- revealedCharsRef: followRevealedCharsRef,
2968
- revealScaleRef: followRevealScaleRef,
2969
- predictive: streaming,
2970
- controlScroll,
2971
- hostRef: followRootRef,
4070
+ if (!streaming && !live && text.trim() === "") return null;
4071
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4072
+ ref: followRootRef,
4073
+ className: TypewriterAssistantNodeView_module_css_default.follow,
4074
+ "data-follow-text": "",
2972
4075
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
2973
- text: live ? shown : text,
4076
+ text: live ? markdownShown : text,
2974
4077
  streaming: live,
2975
4078
  labels,
2976
4079
  fileMentions: live ? void 0 : fileMentions
@@ -2991,7 +4094,7 @@ window.__ModuleLoader__.load({
2991
4094
  };
2992
4095
  }
2993
4096
  /**
2994
- * Apply searchable hidden state without unmounting a stable subtree — the
4097
+ * Apply searchable hidden state without unmounting a stable subtree 鈥?the
2995
4098
  * Host's completion fold hides the answer-inline reasoning this way, so the
2996
4099
  * takeover renderer must reproduce it to stay inside the contract: the row
2997
4100
  * disappears into the Host's process summary and comes back on find-in-page.
@@ -3048,21 +4151,29 @@ window.__ModuleLoader__.load({
3048
4151
  * click stay on the disclosure chrome, which the plugin's AnimatedDisclosure
3049
4152
  * renders with a height-animated body (the harness primitive would mount and
3050
4153
  * unmount it, which cannot glide). The row opens only while this block is
3051
- * the streaming tail and closes as soon as thinking ends — a later block,
3052
- * or the assistant node settling — not when the rest of the reply is
4154
+ * the streaming tail and closes as soon as thinking ends 鈥?a later block,
4155
+ * or the assistant node settling 鈥?not when the rest of the reply is
3053
4156
  * still streaming.
3054
4157
  */
3055
- function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
4158
+ function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, logarithmicFade, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, snapAutoCollapse, t }) {
3056
4159
  const reduced = motionReduced;
3057
4160
  const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
3058
4161
  const [autoClosed, setAutoClosed] = (0, react.useState)(false);
3059
4162
  const summaryRef = (0, react.useRef)(null);
4163
+ const fadeRootRef = (0, react.useRef)(null);
4164
+ const thinkBodyRef = fadeRootRef;
4165
+ const localFadeSpeedRef = (0, react.useRef)(35);
4166
+ const fadeSpeedRef = followSpeedCpsRef ?? localFadeSpeedRef;
4167
+ const userScrolledRef = (0, react.useRef)(false);
4168
+ const rafIdRef = (0, react.useRef)(0);
4169
+ const followActiveRef = (0, react.useRef)(false);
4170
+ followActiveRef.current = running && expanded;
3060
4171
  const commitAnchorRef = (0, react.useRef)(null);
3061
4172
  const displayed = useSmoothStreamContent(text, {
3062
4173
  enabled: running && !reduced,
3063
4174
  preset,
3064
4175
  shouldHoldBack,
3065
- speedCpsRef: followSpeedCpsRef,
4176
+ speedCpsRef: fadeSpeedRef,
3066
4177
  revealScaleRef: followRevealScaleRef,
3067
4178
  onRevealCommit: () => {
3068
4179
  notifyFollowCommit(commitAnchorRef.current);
@@ -3070,13 +4181,70 @@ window.__ModuleLoader__.load({
3070
4181
  });
3071
4182
  const shown = running && !reduced ? displayed : text;
3072
4183
  const summary = running ? latestLine(shown) : firstLine(text);
4184
+ useLogarithmicFade(fadeRootRef, logarithmicFade && !reduced && expanded, running, fadeSpeedRef);
3073
4185
  (0, react.useLayoutEffect)(() => {
3074
4186
  if (thinkAutoExpand) {
3075
4187
  setExpanded(running);
3076
4188
  setAutoClosed(!running);
3077
4189
  }
4190
+ if (running) userScrolledRef.current = false;
3078
4191
  notifyFollowCommit(commitAnchorRef.current);
3079
4192
  }, [running, thinkAutoExpand]);
4193
+ (0, react.useEffect)(() => {
4194
+ if (!running || !expanded || userScrolledRef.current) return;
4195
+ const el = thinkBodyRef.current;
4196
+ if (el === null) return;
4197
+ if (rafIdRef.current === 0) rafIdRef.current = requestAnimationFrame(() => {
4198
+ rafIdRef.current = 0;
4199
+ if (!followActiveRef.current || userScrolledRef.current || el === null) return;
4200
+ if (el.scrollHeight - el.scrollTop - el.clientHeight > 2) el.scrollTop = Number.MAX_SAFE_INTEGER;
4201
+ });
4202
+ }, [
4203
+ running,
4204
+ expanded,
4205
+ shown
4206
+ ]);
4207
+ (0, react.useEffect)(() => {
4208
+ return () => {
4209
+ if (rafIdRef.current !== 0) {
4210
+ cancelAnimationFrame(rafIdRef.current);
4211
+ rafIdRef.current = 0;
4212
+ }
4213
+ };
4214
+ }, []);
4215
+ (0, react.useEffect)(() => {
4216
+ const el = thinkBodyRef.current;
4217
+ if (el === null) return;
4218
+ let isPointerDown = false;
4219
+ const onWheel = (e) => {
4220
+ if (e.deltaY < 0) userScrolledRef.current = true;
4221
+ else if (e.deltaY > 0) {
4222
+ if (el.scrollHeight - el.scrollTop - el.clientHeight <= 30) userScrolledRef.current = false;
4223
+ }
4224
+ };
4225
+ const onPointerDown = () => {
4226
+ isPointerDown = true;
4227
+ };
4228
+ const onPointerUp = () => {
4229
+ isPointerDown = false;
4230
+ };
4231
+ const onScroll = () => {
4232
+ if (el.scrollHeight - el.scrollTop - el.clientHeight <= 30) userScrolledRef.current = false;
4233
+ else if (isPointerDown) userScrolledRef.current = true;
4234
+ };
4235
+ el.addEventListener("wheel", onWheel, { passive: true });
4236
+ el.addEventListener("pointerdown", onPointerDown, { passive: true });
4237
+ window.addEventListener("pointerup", onPointerUp, { passive: true });
4238
+ window.addEventListener("pointercancel", onPointerUp, { passive: true });
4239
+ el.addEventListener("scroll", onScroll, { passive: true });
4240
+ return () => {
4241
+ el.removeEventListener("wheel", onWheel);
4242
+ el.removeEventListener("pointerdown", onPointerDown);
4243
+ window.removeEventListener("pointerup", onPointerUp);
4244
+ window.removeEventListener("pointercancel", onPointerUp);
4245
+ el.removeEventListener("scroll", onScroll);
4246
+ };
4247
+ }, []);
3080
4248
  (0, react.useEffect)(() => {
3081
4249
  const element = summaryRef.current;
3082
4250
  if (element === null) return;
@@ -3097,14 +4265,14 @@ window.__ModuleLoader__.load({
3097
4265
  leadingClassName: TypewriterAssistantNodeView_module_css_default.thinkLeading,
3098
4266
  titleClassName: TypewriterAssistantNodeView_module_css_default.thinkTitle,
3099
4267
  chevronClassName: TypewriterAssistantNodeView_module_css_default.thinkChevron,
3100
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
3101
- title: "Think",
4268
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconThink, { size: 14 }),
4269
+ title: t("message.think"),
3102
4270
  open: expanded,
3103
4271
  onToggle: () => {
3104
4272
  setAutoClosed(false);
3105
4273
  setExpanded((value) => !value);
3106
4274
  },
3107
- bodyTransition: !autoClosed,
4275
+ bodyTransition: !autoClosed || !snapAutoCollapse,
3108
4276
  collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3109
4277
  className: TypewriterAssistantNodeView_module_css_default.thinkSeparator,
3110
4278
  "aria-hidden": true
@@ -3115,6 +4283,7 @@ window.__ModuleLoader__.load({
3115
4283
  children: summary
3116
4284
  })] }),
3117
4285
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4286
+ ref: fadeRootRef,
3118
4287
  className: TypewriterAssistantNodeView_module_css_default.thinkBody,
3119
4288
  children: shown
3120
4289
  })
@@ -3127,14 +4296,17 @@ window.__ModuleLoader__.load({
3127
4296
  * streaming is revealed by the smoother through the Harness Markdown
3128
4297
  * renderer at a rate that tracks arrival. Reasoning blocks keep the
3129
4298
  * built-in Think disclosure and only receive a smoothed text feed; the
3130
- * outer node owns conversation-port follow while streaming; the final text
3131
- * block keeps ownership while its settled reveal queue drains. The FPS guard
3132
- * holds offscreen reveals when the frame rate is degraded. Settled text
3133
- * renders with the full Markdown pipeline.
4299
+ * outer node owns conversation-port follow through both streaming and the
4300
+ * final text drain. Keeping one owner avoids a lifecycle handoff that would
4301
+ * reopen and later retire a second runway. The FPS guard holds offscreen
4302
+ * reveals when the frame rate is degraded. Settled text renders with the full
4303
+ * Markdown pipeline.
3134
4304
  */
3135
- 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, controlScroll = true, motionPreference = DEFAULT_STREAM_SETTINGS.motionPreference, node, useTurnData, openFile, loadImage, fileMentions, turnProcess, t }) {
4305
+ 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, logarithmicFade = DEFAULT_STREAM_SETTINGS.logarithmicFade, controlScroll = true, motionPreference = DEFAULT_STREAM_SETTINGS.motionPreference, node, groupPart, useTurnData, openFile, loadImage, fileMentions, turnProcess, t }) {
3136
4306
  const data = node.data;
3137
4307
  const streaming = data.status === "running";
4308
+ const blocks = groupPart === "reasoning" ? data.blocks.filter((block) => block.kind === "reasoning") : groupPart === "response" ? data.blocks.filter((block) => block.kind !== "reasoning") : data.blocks;
4309
+ const rowOwnsPort = groupPart !== "reasoning";
3138
4310
  const reduced = useMotionReduced(motionPreference);
3139
4311
  const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
3140
4312
  const revealProcess = (0, react.useCallback)(() => {
@@ -3144,7 +4316,16 @@ window.__ModuleLoader__.load({
3144
4316
  const rootSpeedRef = (0, react.useRef)(35);
3145
4317
  const rootRevealedCharsRef = (0, react.useRef)(0);
3146
4318
  const rootRevealScaleRef = (0, react.useRef)(1);
3147
- const reasoningTailIndex = streaming && data.blocks[data.blocks.length - 1]?.kind === "reasoning" ? data.blocks.length - 1 : -1;
4319
+ const previousStreamingRef = (0, react.useRef)(streaming);
4320
+ const [textRevealActive, setTextRevealActive] = (0, react.useState)(false);
4321
+ const completionCandidate = !streaming && previousStreamingRef.current && blocks.some((block) => block.kind === "text" && block.text.trim() !== "");
4322
+ (0, react.useLayoutEffect)(() => {
4323
+ previousStreamingRef.current = streaming;
4324
+ }, [streaming]);
4325
+ const updateTextRevealActivity = (0, react.useCallback)((active) => {
4326
+ setTextRevealActive((previous) => previous === active ? previous : active);
4327
+ }, []);
4328
+ const reasoningTailIndex = streaming && blocks[blocks.length - 1]?.kind === "reasoning" ? blocks.length - 1 : -1;
3148
4329
  const reasoningOwnsSpeed = reasoningTailIndex !== -1;
3149
4330
  const rootPredictiveRef = (0, react.useRef)(false);
3150
4331
  const previousReasoningTailRef = (0, react.useRef)(-1);
@@ -3183,34 +4364,38 @@ window.__ModuleLoader__.load({
3183
4364
  const imageLoader = loadImage ?? (async () => {
3184
4365
  throw new Error(t("image.serviceUnavailable"));
3185
4366
  });
3186
- if (!(streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call"))) return null;
3187
- const announcementText = data.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
4367
+ if (!(streaming || data.status === "interrupted" || blocks.some((block) => block.kind !== "tool-call"))) return null;
4368
+ const announcementText = blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
4369
+ const stepTailIsReasoning = data.blocks[data.blocks.length - 1]?.kind === "reasoning";
3188
4370
  const rendered = [];
3189
- const last = data.blocks.length - 1;
4371
+ const last = blocks.length - 1;
3190
4372
  let lastFollow = -1;
3191
- for (let index = 0; index < data.blocks.length; index += 1) {
3192
- const kind = data.blocks[index]?.kind;
4373
+ let lastText = -1;
4374
+ for (let index = 0; index < blocks.length; index += 1) {
4375
+ const kind = blocks[index]?.kind;
3193
4376
  if (kind === "text" || kind === "reasoning") lastFollow = index;
4377
+ if (kind === "text") lastText = index;
3194
4378
  }
3195
- for (let index = 0; index < data.blocks.length; index += 1) {
3196
- const block = data.blocks[index];
4379
+ for (let index = 0; index < blocks.length; index += 1) {
4380
+ const block = blocks[index];
3197
4381
  if (block === void 0) continue;
3198
4382
  switch (block.kind) {
3199
4383
  case "text":
4384
+ if (!streaming && block.text.trim() === "") break;
3200
4385
  rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
3201
4386
  text: block.text,
3202
4387
  labels: markdownLabels,
3203
4388
  fileMentions: mentions,
3204
4389
  streaming,
4390
+ logarithmicFade: logarithmicFade && data.status !== "interrupted",
3205
4391
  motionReduced: reduced,
3206
- ownFollow: !streaming && index === lastFollow,
3207
4392
  followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
3208
4393
  followRevealedCharsRef: index === lastFollow ? rootRevealedCharsRef : void 0,
3209
4394
  followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
3210
4395
  onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
4396
+ onRevealActivityChange: index === lastText ? updateTextRevealActivity : void 0,
3211
4397
  preset,
3212
- shouldHoldBack,
3213
- controlScroll
4398
+ shouldHoldBack
3214
4399
  }, index));
3215
4400
  break;
3216
4401
  case "reasoning":
@@ -3219,13 +4404,15 @@ window.__ModuleLoader__.load({
3219
4404
  reveal: revealProcess,
3220
4405
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
3221
4406
  text: block.text,
3222
- running: streaming && index === last,
4407
+ running: streaming && index === last && (groupPart !== "reasoning" || stepTailIsReasoning),
3223
4408
  preset,
3224
4409
  thinkAutoExpand,
4410
+ logarithmicFade: logarithmicFade && data.status !== "interrupted",
3225
4411
  motionReduced: reduced,
3226
4412
  shouldHoldBack,
3227
4413
  followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
3228
4414
  followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
4415
+ snapAutoCollapse: groupPart === void 0,
3229
4416
  t
3230
4417
  })
3231
4418
  }, index));
@@ -3233,8 +4420,8 @@ window.__ModuleLoader__.load({
3233
4420
  case "image": {
3234
4421
  const start = index;
3235
4422
  const group = [block];
3236
- while (index + 1 < data.blocks.length) {
3237
- const next = data.blocks[index + 1];
4423
+ while (index + 1 < blocks.length) {
4424
+ const next = blocks[index + 1];
3238
4425
  if (next === void 0 || next.kind !== "image") break;
3239
4426
  group.push(next);
3240
4427
  index += 1;
@@ -3263,12 +4450,12 @@ window.__ModuleLoader__.load({
3263
4450
  text: announcementText,
3264
4451
  active: streaming && !reduced
3265
4452
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
3266
- active: streaming && !reduced,
4453
+ active: !reduced && (streaming || completionCandidate || textRevealActive),
3267
4454
  speedCpsRef: rootSpeedRef,
3268
4455
  revealedCharsRef: rootRevealedCharsRef,
3269
4456
  revealScaleRef: rootRevealScaleRef,
3270
4457
  predictiveRef: rootPredictiveRef,
3271
- controlScroll,
4458
+ controlScroll: controlScroll && rowOwnsPort,
3272
4459
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3273
4460
  className: TypewriterAssistantNodeView_module_css_default.body,
3274
4461
  children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -3371,12 +4558,18 @@ window.__ModuleLoader__.load({
3371
4558
  }
3372
4559
  const pending = /* @__PURE__ */ new Set();
3373
4560
  const internalWrites = /* @__PURE__ */ new WeakMap();
3374
- let rafId = 0;
4561
+ let frameTaskId = null;
3375
4562
  let lastFrame = null;
3376
4563
  let debt = 0;
3377
4564
  let stopped = false;
3378
4565
  let announcedSettled = false;
3379
4566
  const streamId = `dom-${Math.random().toString(36).slice(2)}`;
4567
+ const coordinator = FrameCoordinator.forDocument();
4568
+ const stopFrameTask = () => {
4569
+ if (frameTaskId === null) return;
4570
+ coordinator.unregisterTask(frameTaskId);
4571
+ frameTaskId = null;
4572
+ };
3380
4573
  const announceSettled = () => {
3381
4574
  lastFrame = null;
3382
4575
  debt = 0;
@@ -3425,21 +4618,20 @@ window.__ModuleLoader__.load({
3425
4618
  });
3426
4619
  };
3427
4620
  const scheduleFrame = () => {
3428
- if (stopped || pending.size === 0 || rafId !== 0) return;
3429
- rafId = requestAnimationFrame(frame);
4621
+ if (stopped || pending.size === 0 || frameTaskId !== null) return;
4622
+ frameTaskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
3430
4623
  };
3431
4624
  const frame = (now) => {
3432
- rafId = 0;
3433
- if (stopped) return;
4625
+ if (stopped) return false;
3434
4626
  if (pending.size === 0) {
3435
4627
  announceSettled();
3436
- return;
4628
+ stopFrameTask();
4629
+ return false;
3437
4630
  }
3438
4631
  announcedSettled = false;
3439
4632
  if (lastFrame === null) {
3440
4633
  lastFrame = now;
3441
- scheduleFrame();
3442
- return;
4634
+ return true;
3443
4635
  }
3444
4636
  const elapsed = Math.max(0, now - lastFrame);
3445
4637
  lastFrame = now;
@@ -3489,10 +4681,11 @@ window.__ModuleLoader__.load({
3489
4681
  speedCps: step.speedCps,
3490
4682
  targetChars,
3491
4683
  displayedChars,
3492
- active: pending.size > 0
4684
+ active: pending.size > 0,
4685
+ producerComplete: false
3493
4686
  });
3494
4687
  if (pending.size === 0) announceSettled();
3495
- else scheduleFrame();
4688
+ return pending.size > 0;
3496
4689
  };
3497
4690
  visit(root, revealInitial);
3498
4691
  const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver((mutations) => {
@@ -3521,7 +4714,7 @@ window.__ModuleLoader__.load({
3521
4714
  else scheduleFrame();
3522
4715
  return () => {
3523
4716
  stopped = true;
3524
- cancelAnimationFrame(rafId);
4717
+ stopFrameTask();
3525
4718
  observer?.disconnect();
3526
4719
  for (const [node, record] of records) {
3527
4720
  const controlled = record.chars.slice(0, record.shown).join("");
@@ -3540,8 +4733,8 @@ window.__ModuleLoader__.load({
3540
4733
  ]);
3541
4734
  }
3542
4735
  //#endregion
3543
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/AgentRowEntrance.module.css.mjs
3544
- const css$2 = "._7P9_Ta_surface{min-width:0}._7P9_Ta_surface[data-entrance=active]{will-change:opacity, transform;animation:.22s cubic-bezier(.2,.8,.2,1) both _7P9_Ta_dsh-smooth-stream-agent-row-in}@keyframes _7P9_Ta_dsh-smooth-stream-agent-row-in{0%{opacity:0;clip-path:inset(0 100% 0 0);transform:translateY(4px)}55%{opacity:1;clip-path:inset(0 18% 0 0)}to{opacity:1;clip-path:inset(0);transform:translate(0,0)}}@media (prefers-reduced-motion:reduce){._7P9_Ta_surface[data-entrance=active]{opacity:1;clip-path:none;will-change:auto;animation:none;transform:none}}";
4736
+ //#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/AgentRowEntrance.module.css.mjs
4737
+ const css$2 = ".Mhu9Iq_surface{min-width:0}.Mhu9Iq_surface[data-entrance=active]{will-change:opacity;animation:.22s cubic-bezier(.2,.8,.2,1) both Mhu9Iq_dsh-smooth-stream-agent-row-in}@keyframes Mhu9Iq_dsh-smooth-stream-agent-row-in{0%{opacity:0;clip-path:inset(0 100% 0 0)}55%{opacity:1;clip-path:inset(0 18% 0 0)}to{opacity:1;clip-path:inset(0)}}@media (prefers-reduced-motion:reduce){.Mhu9Iq_surface[data-entrance=active]{opacity:1;clip-path:none;will-change:auto;animation:none}}";
3545
4738
  const tagId$2 = "dsh-smooth-stream/AgentRowEntrance.module.css";
3546
4739
  if (typeof document !== "undefined") {
3547
4740
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
@@ -3554,8 +4747,8 @@ window.__ModuleLoader__.load({
3554
4747
  tag.textContent = css$2;
3555
4748
  }
3556
4749
  var AgentRowEntrance_module_css_default = {
3557
- "dsh-smooth-stream-agent-row-in": "_7P9_Ta_dsh-smooth-stream-agent-row-in",
3558
- "surface": "_7P9_Ta_surface"
4750
+ "surface": "Mhu9Iq_surface",
4751
+ "dsh-smooth-stream-agent-row-in": "Mhu9Iq_dsh-smooth-stream-agent-row-in"
3559
4752
  };
3560
4753
  //#endregion
3561
4754
  //#region src/client/TypewriterToolNodeView.tsx
@@ -3733,8 +4926,8 @@ window.__ModuleLoader__.load({
3733
4926
  };
3734
4927
  }
3735
4928
  //#endregion
3736
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
3737
- const css$1 = ".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_field+.XNGeRG_field{border-top:1px solid var(--dsw-alias-border-l2)}.XNGeRG_fieldDisabled{opacity:.5}.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_choiceRow{flex-wrap:wrap;gap:6px 14px;margin:2px 0 0;padding:0;display:flex}.XNGeRG_choice{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:5px;font-size:12px;line-height:1.5;display:flex}.XNGeRG_choiceInput{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_choice:has(.XNGeRG_choiceInput:disabled){cursor:default;opacity:.55}.XNGeRG_choiceInput:disabled{cursor:default}.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}";
4929
+ //#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
4930
+ const css$1 = ".v5eDwW_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}.v5eDwW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.v5eDwW_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.v5eDwW_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}.v5eDwW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.v5eDwW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.v5eDwW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.v5eDwW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.v5eDwW_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}.v5eDwW_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}.v5eDwW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.v5eDwW_chevronOpen{transform:rotate(180deg)}.v5eDwW_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.v5eDwW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.v5eDwW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.v5eDwW_field+.v5eDwW_field{border-top:1px solid var(--dsw-alias-border-l2)}.v5eDwW_fieldDisabled{opacity:.5}.v5eDwW_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.v5eDwW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.v5eDwW_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.v5eDwW_toggle:disabled{cursor:default}.v5eDwW_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.v5eDwW_choiceRow{flex-wrap:wrap;gap:6px 14px;margin:2px 0 0;padding:0;display:flex}.v5eDwW_choice{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:5px;font-size:12px;line-height:1.5;display:flex}.v5eDwW_choiceInput{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.v5eDwW_choice:has(.v5eDwW_choiceInput:disabled){cursor:default;opacity:.55}.v5eDwW_choiceInput:disabled{cursor:default}.v5eDwW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.v5eDwW_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.v5eDwW_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.v5eDwW_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.v5eDwW_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}.v5eDwW_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.v5eDwW_save,.v5eDwW_discard,.v5eDwW_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.v5eDwW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.v5eDwW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.v5eDwW_discard:hover:not(:disabled),.v5eDwW_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.v5eDwW_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}.v5eDwW_discard:disabled,.v5eDwW_save:disabled,.v5eDwW_update:disabled{opacity:.4;cursor:default}.v5eDwW_discard:focus-visible,.v5eDwW_save:focus-visible,.v5eDwW_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
3738
4931
  const tagId$1 = "dsh-smooth-stream/SmoothStreamCard.module.css";
3739
4932
  if (typeof document !== "undefined") {
3740
4933
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
@@ -3747,35 +4940,35 @@ window.__ModuleLoader__.load({
3747
4940
  tag.textContent = css$1;
3748
4941
  }
3749
4942
  var SmoothStreamCard_module_css_default = {
3750
- "body": "XNGeRG_body",
3751
- "fieldDisabled": "XNGeRG_fieldDisabled",
3752
- "readOnly": "XNGeRG_readOnly",
3753
- "field": "XNGeRG_field",
3754
- "choiceInput": "XNGeRG_choiceInput",
3755
- "headText": "XNGeRG_headText",
3756
- "version": "XNGeRG_version",
3757
- "choice": "XNGeRG_choice",
3758
- "description": "XNGeRG_description",
3759
- "pending": "XNGeRG_pending",
3760
- "fieldHead": "XNGeRG_fieldHead",
3761
- "label": "XNGeRG_label",
3762
- "toggle": "XNGeRG_toggle",
3763
- "hint": "XNGeRG_hint",
3764
- "save": "XNGeRG_save",
3765
- "updateRow": "XNGeRG_updateRow",
3766
- "chevron": "XNGeRG_chevron",
3767
- "cardOpen": "XNGeRG_cardOpen",
3768
- "card": "XNGeRG_card",
3769
- "header": "XNGeRG_header",
3770
- "failure": "XNGeRG_failure",
3771
- "failed": "XNGeRG_failed",
3772
- "chevronOpen": "XNGeRG_chevronOpen",
3773
- "discard": "XNGeRG_discard",
3774
- "updateCopy": "XNGeRG_updateCopy",
3775
- "update": "XNGeRG_update",
3776
- "footer": "XNGeRG_footer",
3777
- "name": "XNGeRG_name",
3778
- "choiceRow": "XNGeRG_choiceRow"
4943
+ "footer": "v5eDwW_footer",
4944
+ "name": "v5eDwW_name",
4945
+ "body": "v5eDwW_body",
4946
+ "fieldHead": "v5eDwW_fieldHead",
4947
+ "fieldDisabled": "v5eDwW_fieldDisabled",
4948
+ "choice": "v5eDwW_choice",
4949
+ "chevronOpen": "v5eDwW_chevronOpen",
4950
+ "update": "v5eDwW_update",
4951
+ "description": "v5eDwW_description",
4952
+ "card": "v5eDwW_card",
4953
+ "pending": "v5eDwW_pending",
4954
+ "updateRow": "v5eDwW_updateRow",
4955
+ "readOnly": "v5eDwW_readOnly",
4956
+ "chevron": "v5eDwW_chevron",
4957
+ "cardOpen": "v5eDwW_cardOpen",
4958
+ "headText": "v5eDwW_headText",
4959
+ "choiceRow": "v5eDwW_choiceRow",
4960
+ "failure": "v5eDwW_failure",
4961
+ "failed": "v5eDwW_failed",
4962
+ "header": "v5eDwW_header",
4963
+ "save": "v5eDwW_save",
4964
+ "toggle": "v5eDwW_toggle",
4965
+ "field": "v5eDwW_field",
4966
+ "choiceInput": "v5eDwW_choiceInput",
4967
+ "version": "v5eDwW_version",
4968
+ "updateCopy": "v5eDwW_updateCopy",
4969
+ "label": "v5eDwW_label",
4970
+ "discard": "v5eDwW_discard",
4971
+ "hint": "v5eDwW_hint"
3779
4972
  };
3780
4973
  //#endregion
3781
4974
  //#region src/client/SmoothStreamCard.tsx
@@ -3820,7 +5013,7 @@ window.__ModuleLoader__.load({
3820
5013
  className: SmoothStreamCard_module_css_default.pending,
3821
5014
  children: t("unsaved")
3822
5015
  }) : null,
3823
- /* @__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 })
5016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: open ? `${SmoothStreamCard_module_css_default.chevron} ${SmoothStreamCard_module_css_default.chevronOpen}` : SmoothStreamCard_module_css_default.chevron })
3824
5017
  ]
3825
5018
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3826
5019
  className: SmoothStreamCard_module_css_default.body,
@@ -3891,6 +5084,27 @@ window.__ModuleLoader__.load({
3891
5084
  children: t("controlScrollHint")
3892
5085
  })]
3893
5086
  }),
5087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5088
+ className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
5089
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5090
+ className: SmoothStreamCard_module_css_default.fieldHead,
5091
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5092
+ className: SmoothStreamCard_module_css_default.label,
5093
+ children: t("logarithmicFade")
5094
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5095
+ type: "checkbox",
5096
+ className: SmoothStreamCard_module_css_default.toggle,
5097
+ checked: state.logarithmicFade,
5098
+ disabled: !state.writable || state.saving || !state.enabled,
5099
+ onChange: (event) => {
5100
+ props.edit({ logarithmicFade: event.target.checked });
5101
+ }
5102
+ })]
5103
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5104
+ className: SmoothStreamCard_module_css_default.hint,
5105
+ children: t("logarithmicFadeHint")
5106
+ })]
5107
+ }),
3894
5108
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3895
5109
  className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
3896
5110
  children: [
@@ -3942,6 +5156,57 @@ window.__ModuleLoader__.load({
3942
5156
  })
3943
5157
  ]
3944
5158
  }),
5159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5160
+ className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
5161
+ children: [
5162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5163
+ className: SmoothStreamCard_module_css_default.fieldHead,
5164
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5165
+ className: SmoothStreamCard_module_css_default.label,
5166
+ children: t("preset")
5167
+ })
5168
+ }),
5169
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5170
+ className: SmoothStreamCard_module_css_default.hint,
5171
+ children: t("presetHint")
5172
+ }),
5173
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5174
+ className: SmoothStreamCard_module_css_default.choiceRow,
5175
+ role: "radiogroup",
5176
+ "aria-label": t("preset"),
5177
+ children: [
5178
+ [
5179
+ "realtime",
5180
+ "presetRealtime",
5181
+ "presetRealtimeHint"
5182
+ ],
5183
+ [
5184
+ "balanced",
5185
+ "presetBalanced",
5186
+ "presetBalancedHint"
5187
+ ],
5188
+ [
5189
+ "silky",
5190
+ "presetSilky",
5191
+ "presetSilkyHint"
5192
+ ]
5193
+ ].map(([value, label, hint]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5194
+ className: SmoothStreamCard_module_css_default.choice,
5195
+ title: t(hint),
5196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5197
+ type: "radio",
5198
+ className: SmoothStreamCard_module_css_default.choiceInput,
5199
+ name: "smooth-stream-preset",
5200
+ checked: state.preset === value,
5201
+ disabled: !state.writable || state.saving || !state.enabled,
5202
+ onChange: () => {
5203
+ props.edit({ preset: value });
5204
+ }
5205
+ }), t(label)]
5206
+ }, value))
5207
+ })
5208
+ ]
5209
+ }),
3945
5210
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3946
5211
  className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
3947
5212
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -4003,7 +5268,7 @@ window.__ModuleLoader__.load({
4003
5268
  onClick: props.upgrade,
4004
5269
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4005
5270
  "aria-hidden": "true",
4006
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
5271
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, {})
4007
5272
  }), t(state.upgrading ? "updating" : "update")]
4008
5273
  })]
4009
5274
  }),
@@ -4084,12 +5349,14 @@ window.__ModuleLoader__.load({
4084
5349
  hooks: { smoothStreamCard: this.store },
4085
5350
  edit: (patch) => {
4086
5351
  if (this.saving) return;
4087
- if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.motionPreference !== void 0 || patch.thinkAutoExpand !== void 0) this.stagedBase = {
5352
+ if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.preset !== void 0 || patch.motionPreference !== void 0 || patch.thinkAutoExpand !== void 0 || patch.logarithmicFade !== void 0) this.stagedBase = {
4088
5353
  ...this.baseValues(),
4089
5354
  ...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
4090
5355
  ...patch.controlScroll === void 0 ? {} : { controlScroll: patch.controlScroll },
5356
+ ...patch.preset === void 0 ? {} : { preset: patch.preset },
4091
5357
  ...patch.motionPreference === void 0 ? {} : { motionPreference: patch.motionPreference },
4092
- ...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand }
5358
+ ...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand },
5359
+ ...patch.logarithmicFade === void 0 ? {} : { logarithmicFade: patch.logarithmicFade }
4093
5360
  };
4094
5361
  if (this.loadedDebug !== void 0 && (patch.debugEnabled !== void 0 || patch.debugTuning !== void 0)) this.stagedDebug = {
4095
5362
  ...this.debugValues(),
@@ -4142,8 +5409,10 @@ window.__ModuleLoader__.load({
4142
5409
  return this.stagedBase ?? {
4143
5410
  enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
4144
5411
  controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
5412
+ preset: this.loaded?.preset ?? DEFAULT_STREAM_SETTINGS.preset,
4145
5413
  motionPreference: this.loaded?.motionPreference ?? DEFAULT_STREAM_SETTINGS.motionPreference,
4146
- thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand
5414
+ thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
5415
+ logarithmicFade: this.loaded?.logarithmicFade ?? DEFAULT_STREAM_SETTINGS.logarithmicFade
4147
5416
  };
4148
5417
  }
4149
5418
  debugValues() {
@@ -4263,12 +5532,20 @@ window.__ModuleLoader__.load({
4263
5532
  "npm",
4264
5533
  "development",
4265
5534
  "unmanaged"
4266
- ].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.controlScroll !== "boolean" || ![
5535
+ ].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.controlScroll !== "boolean" || data.preset !== void 0 && ![
5536
+ "realtime",
5537
+ "balanced",
5538
+ "silky"
5539
+ ].includes(data.preset) || ![
4267
5540
  "auto",
4268
5541
  "force-smooth",
4269
5542
  "force-reduced"
4270
- ].includes(data.motionPreference) || typeof data.thinkAutoExpand !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
4271
- return data;
5543
+ ].includes(data.motionPreference) || typeof data.thinkAutoExpand !== "boolean" || data.logarithmicFade !== void 0 && typeof data.logarithmicFade !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
5544
+ return {
5545
+ ...data,
5546
+ preset: data.preset ?? "balanced",
5547
+ logarithmicFade: data.logarithmicFade ?? true
5548
+ };
4272
5549
  }
4273
5550
  function upgradeView(value) {
4274
5551
  if (record(value)?.restartRequired !== true) throw new Error("dsh-smooth-stream: malformed update response");
@@ -4297,8 +5574,10 @@ window.__ModuleLoader__.load({
4297
5574
  return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, {
4298
5575
  enabled: settings.enabled,
4299
5576
  controlScroll: settings.controlScroll,
5577
+ preset: settings.preset,
4300
5578
  motionPreference: settings.motionPreference,
4301
5579
  thinkAutoExpand: settings.thinkAutoExpand,
5580
+ logarithmicFade: settings.logarithmicFade,
4302
5581
  ...settings.debugEnabled === void 0 || settings.debugTuning === void 0 ? {} : {
4303
5582
  debugEnabled: settings.debugEnabled,
4304
5583
  debugTuning: settings.debugTuning
@@ -4320,8 +5599,8 @@ window.__ModuleLoader__.load({
4320
5599
  };
4321
5600
  }
4322
5601
  //#endregion
4323
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/DebugPanel.module.css.mjs
4324
- const css = ".m4_tdG_trigger,.m4_tdG_iconButton{appearance:none;width:32px;height:32px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}.m4_tdG_trigger:hover,.m4_tdG_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.m4_tdG_iconButton:disabled{opacity:.45;cursor:not-allowed}.m4_tdG_triggerActive{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-business-primary)}.m4_tdG_trigger:focus-visible,.m4_tdG_iconButton:focus-visible,.m4_tdG_secondaryButton:focus-visible,.m4_tdG_primaryButton:focus-visible,.m4_tdG_number:focus-visible,.m4_tdG_range:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.m4_tdG_panel{z-index:30;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);width:min(328px,100vw - 32px);min-height:280px;box-shadow:0 12px 32px color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent);color:var(--dsw-alias-label-primary);border-radius:8px;flex-direction:column;display:flex;position:fixed;top:88px;bottom:16px;right:16px;overflow:hidden}.m4_tdG_panelHeader{border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:8px;min-height:48px;padding:0 8px 0 14px;display:flex}.m4_tdG_statusDot{background:var(--dsw-alias-label-caption);border-radius:50%;flex:none;width:7px;height:7px}.m4_tdG_statusLive{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}.m4_tdG_title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px;overflow:hidden}.m4_tdG_state,.m4_tdG_unsaved{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:10px;line-height:16px}.m4_tdG_unsaved{color:var(--dsw-alias-state-warn-primary)}.m4_tdG_scrollArea{overscroll-behavior:contain;min-height:0;overflow-y:auto}.m4_tdG_section{padding:14px}.m4_tdG_guide{border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin:0;padding:12px 14px;font-size:11px;line-height:1.55}.m4_tdG_section+.m4_tdG_section{border-top:1px solid var(--dsw-alias-border-l2)}.m4_tdG_section h2{color:var(--dsw-alias-label-secondary);margin:0 0 10px;font-size:12px;font-weight:600;line-height:18px}.m4_tdG_metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:0 16px;margin:0;display:grid}.m4_tdG_metric{border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent);justify-content:space-between;align-items:baseline;gap:8px;min-width:0;padding:5px 0;display:flex}.m4_tdG_metric dt{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;overflow:hidden}.m4_tdG_metric dd{min-width:0;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:16px;overflow:hidden}.m4_tdG_metric dd[data-tone=good]{color:var(--dsw-alias-state-success-primary)}.m4_tdG_metric dd[data-tone=warn]{color:var(--dsw-alias-state-warn-primary)}.m4_tdG_control{flex-direction:column;gap:5px;padding:6px 0;display:flex}.m4_tdG_controlHead{color:var(--dsw-alias-label-secondary);justify-content:space-between;align-items:center;gap:10px;font-size:11px;line-height:18px;display:flex}.m4_tdG_controlLabel{align-items:center;gap:4px;min-width:0;display:inline-flex}.m4_tdG_infoButton{appearance:none;width:20px;height:20px;color:var(--dsw-alias-label-tertiary);cursor:help;background:0 0;border:0;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex}.m4_tdG_infoButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.m4_tdG_infoButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.m4_tdG_numberWrap{flex:none;align-items:center;gap:4px;display:inline-flex}.m4_tdG_number{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:68px;height:26px;color:var(--dsw-alias-label-primary);font:inherit;font-variant-numeric:tabular-nums;text-align:right;border-radius:5px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.m4_tdG_unit{width:30px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px}.m4_tdG_range{width:100%;height:18px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer;margin:0}.m4_tdG_range:disabled,.m4_tdG_number:disabled{opacity:.5;cursor:default}.m4_tdG_footer{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);flex:none;align-items:center;gap:6px;min-height:50px;padding:8px 10px;display:flex}.m4_tdG_footerSpacer{flex:1}.m4_tdG_secondaryButton,.m4_tdG_primaryButton{appearance:none;border:1px solid var(--dsw-alias-border-l2);min-height:30px;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:6px;justify-content:center;align-items:center;gap:5px;padding:4px 9px;font-size:11px;line-height:18px;display:inline-flex}.m4_tdG_primaryButton{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:#0000}.m4_tdG_secondaryButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.m4_tdG_secondaryButton:disabled,.m4_tdG_primaryButton:disabled{opacity:.4;cursor:default}.m4_tdG_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}@media (width<=900px){.m4_tdG_trigger,.m4_tdG_iconButton{width:44px;height:44px}.m4_tdG_panel{top:auto;right:12px;bottom:max(12px, env(safe-area-inset-bottom));width:auto;max-height:min(70dvh,640px);left:12px}.m4_tdG_secondaryButton,.m4_tdG_primaryButton{min-height:44px}}@media (prefers-reduced-motion:reduce){.m4_tdG_statusLive{box-shadow:none}}";
5602
+ //#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/DebugPanel.module.css.mjs
5603
+ const css = "._6TTfZq_trigger,._6TTfZq_iconButton{appearance:none;width:32px;height:32px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}._6TTfZq_trigger:hover,._6TTfZq_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._6TTfZq_iconButton:disabled{opacity:.45;cursor:not-allowed}._6TTfZq_triggerActive{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-business-primary)}._6TTfZq_trigger:focus-visible,._6TTfZq_iconButton:focus-visible,._6TTfZq_secondaryButton:focus-visible,._6TTfZq_primaryButton:focus-visible,._6TTfZq_number:focus-visible,._6TTfZq_range:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}._6TTfZq_panel{z-index:30;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);width:min(328px,100vw - 32px);min-height:280px;box-shadow:0 12px 32px color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent);color:var(--dsw-alias-label-primary);border-radius:8px;flex-direction:column;display:flex;position:fixed;top:88px;bottom:16px;right:16px;overflow:hidden}._6TTfZq_panelHeader{border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:8px;min-height:48px;padding:0 8px 0 14px;display:flex}._6TTfZq_statusDot{background:var(--dsw-alias-label-caption);border-radius:50%;flex:none;width:7px;height:7px}._6TTfZq_statusLive{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}._6TTfZq_title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px;overflow:hidden}._6TTfZq_state,._6TTfZq_unsaved{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:10px;line-height:16px}._6TTfZq_unsaved{color:var(--dsw-alias-state-warn-primary)}._6TTfZq_scrollArea{overscroll-behavior:contain;min-height:0;overflow-y:auto}._6TTfZq_section{padding:14px}._6TTfZq_guide{border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin:0;padding:12px 14px;font-size:11px;line-height:1.55}._6TTfZq_section+._6TTfZq_section{border-top:1px solid var(--dsw-alias-border-l2)}._6TTfZq_section h2{color:var(--dsw-alias-label-secondary);margin:0 0 10px;font-size:12px;font-weight:600;line-height:18px}._6TTfZq_metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:0 16px;margin:0;display:grid}._6TTfZq_metric{border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent);justify-content:space-between;align-items:baseline;gap:8px;min-width:0;padding:5px 0;display:flex}._6TTfZq_metric dt{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;overflow:hidden}._6TTfZq_metric dd{min-width:0;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:16px;overflow:hidden}._6TTfZq_metric dd[data-tone=good]{color:var(--dsw-alias-state-success-primary)}._6TTfZq_metric dd[data-tone=warn]{color:var(--dsw-alias-state-warn-primary)}._6TTfZq_control{flex-direction:column;gap:5px;padding:6px 0;display:flex}._6TTfZq_controlHead{color:var(--dsw-alias-label-secondary);justify-content:space-between;align-items:center;gap:10px;font-size:11px;line-height:18px;display:flex}._6TTfZq_controlLabel{align-items:center;gap:4px;min-width:0;display:inline-flex}._6TTfZq_infoButton{appearance:none;width:20px;height:20px;color:var(--dsw-alias-label-tertiary);cursor:help;background:0 0;border:0;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex}._6TTfZq_infoButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._6TTfZq_infoButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}._6TTfZq_numberWrap{flex:none;align-items:center;gap:4px;display:inline-flex}._6TTfZq_number{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:68px;height:26px;color:var(--dsw-alias-label-primary);font:inherit;font-variant-numeric:tabular-nums;text-align:right;border-radius:5px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}._6TTfZq_unit{width:30px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px}._6TTfZq_range{width:100%;height:18px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer;margin:0}._6TTfZq_range:disabled,._6TTfZq_number:disabled{opacity:.5;cursor:default}._6TTfZq_footer{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);flex:none;align-items:center;gap:6px;min-height:50px;padding:8px 10px;display:flex}._6TTfZq_footerSpacer{flex:1}._6TTfZq_secondaryButton,._6TTfZq_primaryButton{appearance:none;border:1px solid var(--dsw-alias-border-l2);min-height:30px;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:6px;justify-content:center;align-items:center;gap:5px;padding:4px 9px;font-size:11px;line-height:18px;display:inline-flex}._6TTfZq_primaryButton{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:#0000}._6TTfZq_secondaryButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}._6TTfZq_secondaryButton:disabled,._6TTfZq_primaryButton:disabled{opacity:.4;cursor:default}._6TTfZq_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}@media (width<=900px){._6TTfZq_trigger,._6TTfZq_iconButton{width:44px;height:44px}._6TTfZq_panel{top:auto;right:12px;bottom:max(12px, env(safe-area-inset-bottom));width:auto;max-height:min(70dvh,640px);left:12px}._6TTfZq_secondaryButton,._6TTfZq_primaryButton{min-height:44px}}@media (prefers-reduced-motion:reduce){._6TTfZq_statusLive{box-shadow:none}}";
4325
5604
  const tagId = "dsh-smooth-stream/DebugPanel.module.css";
4326
5605
  if (typeof document !== "undefined") {
4327
5606
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
@@ -4334,37 +5613,49 @@ window.__ModuleLoader__.load({
4334
5613
  tag.textContent = css;
4335
5614
  }
4336
5615
  var DebugPanel_module_css_default = {
4337
- "iconButton": "m4_tdG_iconButton",
4338
- "triggerActive": "m4_tdG_triggerActive",
4339
- "controlHead": "m4_tdG_controlHead",
4340
- "number": "m4_tdG_number",
4341
- "range": "m4_tdG_range",
4342
- "statusLive": "m4_tdG_statusLive",
4343
- "title": "m4_tdG_title",
4344
- "controlLabel": "m4_tdG_controlLabel",
4345
- "guide": "m4_tdG_guide",
4346
- "infoButton": "m4_tdG_infoButton",
4347
- "numberWrap": "m4_tdG_numberWrap",
4348
- "footer": "m4_tdG_footer",
4349
- "control": "m4_tdG_control",
4350
- "secondaryButton": "m4_tdG_secondaryButton",
4351
- "unsaved": "m4_tdG_unsaved",
4352
- "metrics": "m4_tdG_metrics",
4353
- "scrollArea": "m4_tdG_scrollArea",
4354
- "state": "m4_tdG_state",
4355
- "metric": "m4_tdG_metric",
4356
- "panel": "m4_tdG_panel",
4357
- "trigger": "m4_tdG_trigger",
4358
- "statusDot": "m4_tdG_statusDot",
4359
- "section": "m4_tdG_section",
4360
- "unit": "m4_tdG_unit",
4361
- "footerSpacer": "m4_tdG_footerSpacer",
4362
- "visuallyHidden": "m4_tdG_visuallyHidden",
4363
- "primaryButton": "m4_tdG_primaryButton",
4364
- "panelHeader": "m4_tdG_panelHeader"
5616
+ "footerSpacer": "_6TTfZq_footerSpacer",
5617
+ "primaryButton": "_6TTfZq_primaryButton",
5618
+ "statusDot": "_6TTfZq_statusDot",
5619
+ "section": "_6TTfZq_section",
5620
+ "numberWrap": "_6TTfZq_numberWrap",
5621
+ "unsaved": "_6TTfZq_unsaved",
5622
+ "trigger": "_6TTfZq_trigger",
5623
+ "metric": "_6TTfZq_metric",
5624
+ "panel": "_6TTfZq_panel",
5625
+ "range": "_6TTfZq_range",
5626
+ "iconButton": "_6TTfZq_iconButton",
5627
+ "panelHeader": "_6TTfZq_panelHeader",
5628
+ "footer": "_6TTfZq_footer",
5629
+ "controlLabel": "_6TTfZq_controlLabel",
5630
+ "triggerActive": "_6TTfZq_triggerActive",
5631
+ "number": "_6TTfZq_number",
5632
+ "statusLive": "_6TTfZq_statusLive",
5633
+ "controlHead": "_6TTfZq_controlHead",
5634
+ "guide": "_6TTfZq_guide",
5635
+ "unit": "_6TTfZq_unit",
5636
+ "control": "_6TTfZq_control",
5637
+ "state": "_6TTfZq_state",
5638
+ "scrollArea": "_6TTfZq_scrollArea",
5639
+ "visuallyHidden": "_6TTfZq_visuallyHidden",
5640
+ "title": "_6TTfZq_title",
5641
+ "metrics": "_6TTfZq_metrics",
5642
+ "secondaryButton": "_6TTfZq_secondaryButton",
5643
+ "infoButton": "_6TTfZq_infoButton"
4365
5644
  };
4366
5645
  //#endregion
4367
5646
  //#region src/client/DebugPanel.tsx
5647
+ /**
5648
+ * Locale key per terminal-follow phase. `terminal-drain` and `host-cascade`
5649
+ * are the two that must never be confused: the first is the engine finishing
5650
+ * ordinary text, the second is a hostile host commit that needs temporary
5651
+ * credit.
5652
+ */
5653
+ const PHASE_LABEL = {
5654
+ "live": "debugPhaseLive",
5655
+ "terminal-drain": "debugPhaseDrain",
5656
+ "host-cascade": "debugPhaseCascade",
5657
+ "natural": "debugPhaseNatural"
5658
+ };
4368
5659
  const REVEAL_CONTROLS = [
4369
5660
  {
4370
5661
  key: "revealScale",
@@ -4491,7 +5782,7 @@ window.__ModuleLoader__.load({
4491
5782
  type: "button",
4492
5783
  "aria-label": label,
4493
5784
  title: t(control.tip),
4494
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, {})
5785
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconQuestion, {})
4495
5786
  })
4496
5787
  })]
4497
5788
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -4587,7 +5878,7 @@ window.__ModuleLoader__.load({
4587
5878
  onClick: () => {
4588
5879
  copyDiagnostics();
4589
5880
  },
4590
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {})
5881
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCopy, {})
4591
5882
  }),
4592
5883
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4593
5884
  className: DebugPanel_module_css_default.iconButton,
@@ -4600,7 +5891,7 @@ window.__ModuleLoader__.load({
4600
5891
  props.edit({ debugEnabled: false });
4601
5892
  props.save();
4602
5893
  },
4603
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
5894
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconClose, {})
4604
5895
  }),
4605
5896
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4606
5897
  className: DebugPanel_module_css_default.visuallyHidden,
@@ -4668,6 +5959,28 @@ window.__ModuleLoader__.load({
4668
5959
  label: t("debugCapacity"),
4669
5960
  value: `${fixed(metrics.followCapacityPx)} px`
4670
5961
  }),
5962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
5963
+ label: t("debugTerminalPhase"),
5964
+ value: t(PHASE_LABEL[metrics.followTerminalPhase]),
5965
+ tone: metrics.followTerminalPhase === "host-cascade" ? "warn" : void 0
5966
+ }),
5967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
5968
+ label: t("debugOwnedRunway"),
5969
+ value: `${fixed(metrics.followRunwayPx)} px`
5970
+ }),
5971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
5972
+ label: t("debugTerminalBudget"),
5973
+ value: `${fixed(metrics.followTerminalBudgetPx)} px`
5974
+ }),
5975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
5976
+ label: t("debugBaselineShift"),
5977
+ value: `${fixed(metrics.followBaselineShiftPx)} px`
5978
+ }),
5979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
5980
+ label: t("debugAnchorDelta"),
5981
+ value: metrics.followAnchorDeltaPx === null ? "—" : `${fixed(metrics.followAnchorDeltaPx)} px`,
5982
+ tone: (metrics.followAnchorDeltaPx ?? 0) > .35 ? "warn" : void 0
5983
+ }),
4671
5984
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4672
5985
  label: t("debugAppliedScale"),
4673
5986
  value: `${fixed(metrics.followRevealScale, 2)}x`
@@ -4713,7 +6026,7 @@ window.__ModuleLoader__.load({
4713
6026
  type: "button",
4714
6027
  disabled: !state.writable,
4715
6028
  onClick: props.reset,
4716
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {}), t("debugReset")]
6029
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefreshSmall, {}), t("debugReset")]
4717
6030
  }),
4718
6031
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: DebugPanel_module_css_default.footerSpacer }),
4719
6032
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -4743,7 +6056,7 @@ window.__ModuleLoader__.load({
4743
6056
  onClick: () => {
4744
6057
  setOpen((current) => !current);
4745
6058
  },
4746
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCodeOutline16, {})
6059
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCode, {})
4747
6060
  }), typeof document === "undefined" || panel === null ? null : (0, react_dom.createPortal)(panel, document.body)] });
4748
6061
  }
4749
6062
  //#endregion
@@ -4751,6 +6064,37 @@ window.__ModuleLoader__.load({
4751
6064
  /** Locale bundles for the smooth-stream plugin configuration card. */
4752
6065
  /** Dictionary namespace owned by this plugin's settings card. */
4753
6066
  const NS = "settings.smoothStream";
6067
+ /**
6068
+ * Namespace owning the conversation keys THIS PLUGIN must be able to render
6069
+ * but the Harness does not always provide.
6070
+ *
6071
+ * The plugin consumes keys from two Harness namespaces, and which one owns a
6072
+ * given key is not stable across Harness versions:
6073
+ *
6074
+ * - `conversation` (`dsh-client-ui-conversation`) owns the `image.*` family in
6075
+ * every version, and also owned `message.stopped`, `message.unknownBlock`
6076
+ * and `json.truncated` before those moved to `chat`.
6077
+ * - `chat` (`dsh-client-ui-chat`, present from 0.1.5 on) owns `message.think`
6078
+ * and now owns the three keys above. It does not exist at all in rc.6, the
6079
+ * version this package still pins in devDependencies.
6080
+ *
6081
+ * Neither namespace alone can serve every key this plugin renders, so
6082
+ * `index.ts` layers them. This plugin-owned namespace is the final layer; it
6083
+ * exists because a plugin may NOT register into `chat` or `conversation` —
6084
+ * `LocaleRuntime.register` throws when the (namespace, locale) pair already
6085
+ * has an owner, and the Harness owns both.
6086
+ */
6087
+ const CHAT_NS = "smoothStream.chat";
6088
+ /** English fallback for {@link SmoothStreamChatKey}. */
6089
+ const chatEn = {
6090
+ "message.think": "Think",
6091
+ "image.serviceUnavailable": "Image service unavailable"
6092
+ };
6093
+ /** Simplified Chinese fallback for {@link SmoothStreamChatKey}. */
6094
+ const chatZh = {
6095
+ "message.think": "思考",
6096
+ "image.serviceUnavailable": "图片服务不可用"
6097
+ };
4754
6098
  /** English copy. */
4755
6099
  const en = {
4756
6100
  title: "Smooth stream",
@@ -4759,6 +6103,14 @@ window.__ModuleLoader__.load({
4759
6103
  enabledHint: "Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.",
4760
6104
  controlScroll: "Take over scrolling",
4761
6105
  controlScrollHint: "On by default; smooth-stream writes the conversation scroll position. Turn off to leave bottom-follow to Harness.",
6106
+ preset: "Pacing preset",
6107
+ presetHint: "Smoothing cadence profile for text reveal and glide.",
6108
+ presetRealtime: "Realtime",
6109
+ presetRealtimeHint: "Aggressive cadence, low latency, tracks output closely.",
6110
+ presetBalanced: "Balanced",
6111
+ presetBalancedHint: "Default balance between pace and visual rhythm.",
6112
+ presetSilky: "Silky",
6113
+ presetSilkyHint: "Higher backlog buffer, gentle EMA damping, maximum fluidity.",
4762
6114
  motionPreference: "Motion",
4763
6115
  motionPreferenceHint: "How streaming responds to the system reduce-motion setting.",
4764
6116
  motionAuto: "Follow system",
@@ -4768,6 +6120,8 @@ window.__ModuleLoader__.load({
4768
6120
  motionForceReduced: "Always raw",
4769
6121
  motionForceReducedHint: "Render raw text even when the system allows motion.",
4770
6122
  thinkAutoExpand: "Auto-expand thinking",
6123
+ logarithmicFade: "Logarithmic fade",
6124
+ logarithmicFadeHint: "Fade new answer and expanded thinking text into view. Follows the motion preference.",
4771
6125
  thinkAutoExpandHint: "Open the thinking block while it streams. Turn off to keep it collapsed.",
4772
6126
  debugEnabled: "Show render diagnostics",
4773
6127
  debugEnabledHint: "Show live streaming and scroll metrics on the right side of the chat. Tune values there, then save them here.",
@@ -4800,6 +6154,15 @@ window.__ModuleLoader__.load({
4800
6154
  debugReserve: "Reserve",
4801
6155
  debugCapacity: "Capacity",
4802
6156
  debugAppliedScale: "Applied scale",
6157
+ debugTerminalPhase: "Terminal phase",
6158
+ debugOwnedRunway: "Owned runway",
6159
+ debugTerminalBudget: "Terminal budget",
6160
+ debugBaselineShift: "Baseline shift",
6161
+ debugAnchorDelta: "Anchor Δ",
6162
+ debugPhaseLive: "live",
6163
+ debugPhaseDrain: "terminal-drain",
6164
+ debugPhaseCascade: "host-cascade",
6165
+ debugPhaseNatural: "natural",
4803
6166
  debugRevealMultiplier: "Reveal multiplier",
4804
6167
  debugQueuePressure: "Queue pressure",
4805
6168
  debugMaxReveal: "Maximum reveal",
@@ -4846,6 +6209,14 @@ window.__ModuleLoader__.load({
4846
6209
  enabledHint: "由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。",
4847
6210
  controlScroll: "接管滚动",
4848
6211
  controlScrollHint: "默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。",
6212
+ preset: "手感预设",
6213
+ presetHint: "文字吐字节奏与视口跟随阻尼风格。",
6214
+ presetRealtime: "激进快速 (realtime)",
6215
+ presetRealtimeHint: "缓冲低、流速快,紧跟模型生成。",
6216
+ presetBalanced: "均衡自然 (balanced)",
6217
+ presetBalancedHint: "官方默认,平衡流速与节奏感。",
6218
+ presetSilky: "极致丝滑 (silky)",
6219
+ presetSilkyHint: "字符积压缓冲加大、阻尼更高、EMA 平滑度更好,视觉呼吸感最强。",
4849
6220
  motionPreference: "动效偏好",
4850
6221
  motionPreferenceHint: "流式渲染如何响应系统的「减少动态效果」设置。",
4851
6222
  motionAuto: "跟随系统",
@@ -4855,6 +6226,8 @@ window.__ModuleLoader__.load({
4855
6226
  motionForceReduced: "始终原始",
4856
6227
  motionForceReducedHint: "即使系统允许动效,也直接呈现原始文本。",
4857
6228
  thinkAutoExpand: "自动展开思考",
6229
+ logarithmicFade: "对数淡入",
6230
+ logarithmicFadeHint: "让回答正文和展开的思考文字由淡变实,遵循动效偏好。",
4858
6231
  thinkAutoExpandHint: "思考块在流式时自动展开;关闭后保持折叠,可手动展开。",
4859
6232
  debugEnabled: "显示渲染调试面板",
4860
6233
  debugEnabledHint: "在聊天右侧显示流式渲染和滚动的实时参数,可在面板中调节并在这里保存。",
@@ -4887,6 +6260,15 @@ window.__ModuleLoader__.load({
4887
6260
  debugReserve: "预留空间",
4888
6261
  debugCapacity: "安全容量",
4889
6262
  debugAppliedScale: "实际倍率",
6263
+ debugTerminalPhase: "收尾阶段",
6264
+ debugOwnedRunway: "实体留白",
6265
+ debugTerminalBudget: "收尾预算",
6266
+ debugBaselineShift: "基线位移",
6267
+ debugAnchorDelta: "锚点位移",
6268
+ debugPhaseLive: "流式中",
6269
+ debugPhaseDrain: "收尾排空",
6270
+ debugPhaseCascade: "宿主交接",
6271
+ debugPhaseNatural: "自然态",
4890
6272
  debugRevealMultiplier: "揭示倍率",
4891
6273
  debugQueuePressure: "队列压力",
4892
6274
  debugMaxReveal: "最大揭示速度",
@@ -4947,12 +6329,33 @@ window.__ModuleLoader__.load({
4947
6329
  * generic follow boundary. This is deliberately keyed by the owner that
4948
6330
  * provides the renderer, not by individual tool names, so new Context,
4949
6331
  * Command, and Tool rows are covered automatically.
6332
+ *
6333
+ * Host chrome and controllers are excluded on a different rule: `turn-process`
6334
+ * (the completed-turn process summary control) and `turn-tail` (the turn usage
6335
+ * footer) are not Agent output. The Host renders them as bare controls and
6336
+ * flips their visibility itself, so wrapping them has three measured costs and
6337
+ * no benefit:
6338
+ *
6339
+ * 1. The wrapper element defeats ChatView's `.flowItem:empty { display: none }`
6340
+ * rule. A `turn-process` renderer returns null whenever the Host's fold is
6341
+ * unavailable (e.g. `processWindowReady === false`), and that null render is
6342
+ * exactly what the `:empty` rule exists to erase; with a wrapper the row
6343
+ * stays in flow and still consumes the 16px column gap, painting an empty
6344
+ * band where the fold control should be.
6345
+ * 2. The summary label (`N tool calls · M messages`) is not streamed model
6346
+ * text, so pushing it through the character-reveal engine is wrong by
6347
+ * construction.
6348
+ * 3. The row would inherit the generic entrance animation (opacity + clip-path)
6349
+ * on a row the Host may mount and hide inside a single commit.
4950
6350
  */
4951
6351
  const SKIP_WRAP = /* @__PURE__ */ new Set([
4952
6352
  "assistant-step",
4953
6353
  "user",
4954
6354
  "steering",
4955
- "command-input"
6355
+ "command-input",
6356
+ "turn-process",
6357
+ "turn-tail",
6358
+ "tool-call"
4956
6359
  ]);
4957
6360
  /** React function/class or an exotic component such as memo/forwardRef/lazy. */
4958
6361
  function isWrappableComponent(value) {
@@ -5048,7 +6451,7 @@ window.__ModuleLoader__.load({
5048
6451
  refresh() {
5049
6452
  const next = this.read();
5050
6453
  const pending = this.card?.getSnapshot().status === "loading";
5051
- if (pending === this.pending && next.enabled === this.value.enabled && next.controlScroll === this.value.controlScroll && next.motionPreference === this.value.motionPreference && next.thinkAutoExpand === this.value.thinkAutoExpand && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
6454
+ if (pending === this.pending && next.enabled === this.value.enabled && next.controlScroll === this.value.controlScroll && next.preset === this.value.preset && next.motionPreference === this.value.motionPreference && next.thinkAutoExpand === this.value.thinkAutoExpand && next.logarithmicFade === this.value.logarithmicFade && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
5052
6455
  this.pending = pending;
5053
6456
  this.value = next;
5054
6457
  for (const listener of this.listeners) listener();
@@ -5057,6 +6460,15 @@ window.__ModuleLoader__.load({
5057
6460
  takeoverEnabled() {
5058
6461
  return !this.pending && this.value.enabled;
5059
6462
  }
6463
+ /**
6464
+ * True only once an attached settings service has produced its loaded values.
6465
+ * Until then — and when no settings service is composed at all — `value` is
6466
+ * still the shared client default, so callers that must not let that default
6467
+ * shadow the install-time config have to ask before trusting it.
6468
+ */
6469
+ ready() {
6470
+ return this.card?.getSnapshot().status === "ready";
6471
+ }
5060
6472
  getSnapshot = () => this.value;
5061
6473
  subscribe = (listener) => {
5062
6474
  this.listeners.add(listener);
@@ -5080,6 +6492,49 @@ window.__ModuleLoader__.load({
5080
6492
  const config = readBootConfig();
5081
6493
  const settings = new SettingsCell();
5082
6494
  const useControlScroll = () => (0, react.useSyncExternalStore)(settings.subscribe, () => settings.getSnapshot().controlScroll, () => settings.getSnapshot().controlScroll);
6495
+ /**
6496
+ * Layered `t` for the assistant renderer.
6497
+ *
6498
+ * The renderer's keys have no single owner: `conversation` owns the
6499
+ * `image.*` family in every Harness version, `chat` (0.1.5+) owns
6500
+ * `message.think`, and three `message.*` keys MOVED from `conversation` to
6501
+ * `chat` between the version this package pins and the current one. Binding
6502
+ * the slot to either namespace alone degrades a real slice of the UI to raw
6503
+ * keys, which is the defect this replaces.
6504
+ *
6505
+ * The order is deliberate: `conversation` first, so the pinned Harness keeps
6506
+ * resolving the keys it still owns there; then `chat` for what moved or is
6507
+ * new; then this plugin's own namespace for keys no Harness version
6508
+ * provides. A namespace that is not registered returns its key unchanged
6509
+ * (`LocaleRuntime.bind` does not throw), so an older Harness without `chat`
6510
+ * falls straight through.
6511
+ *
6512
+ * The reference is built once and held stable: the seat feeds a memoized
6513
+ * renderer, and a fresh identity per render would defeat that memoization.
6514
+ * Until the locale service arrives the seat's own binding stays in use.
6515
+ */
6516
+ let assistantT;
6517
+ ctx.inject(["locale"], (localeCtx) => {
6518
+ localeCtx.effect(() => localeCtx.locale.register(CHAT_NS, {
6519
+ zh: chatZh,
6520
+ en: chatEn
6521
+ }), "dsh-smooth-stream: conversation fallback dictionary");
6522
+ const conversationT = localeCtx.locale.bind("conversation");
6523
+ const chatT = localeCtx.locale.bind("chat");
6524
+ const fallbackT = localeCtx.locale.bind(CHAT_NS);
6525
+ const merged = (key, params) => {
6526
+ const primary = conversationT(key, params);
6527
+ if (primary !== key) return primary;
6528
+ const secondary = chatT(key, params);
6529
+ if (secondary !== key) return secondary;
6530
+ return fallbackT(key, params);
6531
+ };
6532
+ const bound = merged;
6533
+ assistantT = bound;
6534
+ return () => {
6535
+ if (assistantT === bound) assistantT = void 0;
6536
+ };
6537
+ });
5083
6538
  ctx.inject([
5084
6539
  "slots",
5085
6540
  "locale",
@@ -5142,18 +6597,21 @@ window.__ModuleLoader__.load({
5142
6597
  const preferences = (0, react.useSyncExternalStore)(settings.subscribe, settings.getSnapshot, settings.getSnapshot);
5143
6598
  return (0, react.createElement)(TypewriterAssistantNodeView, {
5144
6599
  ...props,
6600
+ ...assistantT === void 0 ? {} : { t: assistantT },
5145
6601
  mode: config.mode,
5146
- preset: config.preset,
6602
+ preset: settings.ready() ? preferences.preset ?? config.preset : config.preset,
5147
6603
  revealCharsPerSec: config.revealCharsPerSec,
5148
6604
  scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
5149
6605
  maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
5150
6606
  thinkAutoExpand: preferences.thinkAutoExpand,
6607
+ logarithmicFade: preferences.logarithmicFade,
5151
6608
  controlScroll: preferences.controlScroll,
5152
6609
  motionPreference: preferences.motionPreference
5153
6610
  });
5154
6611
  };
5155
6612
  ctx.slots.inject("conversation.chat.node", () => {
5156
6613
  let releaseTakeover;
6614
+ let usedSwap = false;
5157
6615
  const syncTakeover = () => {
5158
6616
  if (!settings.takeoverEnabled()) {
5159
6617
  releaseTakeover?.();
@@ -5162,7 +6620,16 @@ window.__ModuleLoader__.load({
5162
6620
  }
5163
6621
  if (releaseTakeover !== void 0) return;
5164
6622
  const unwrap = wrapAgentChatRows(ctx, useControlScroll);
5165
- const unshadow = ctx.slots.register({
6623
+ const builtin = ctx.slots.entries("conversation.chat.node").find((entry) => entry.options.key === "assistant-step" && entry.options.registrant !== "dsh-smooth-stream" && entry.component !== configured);
6624
+ let unshadow;
6625
+ if (builtin !== void 0 && isWrappableComponent(builtin.component)) {
6626
+ usedSwap = true;
6627
+ const inner = builtin.component;
6628
+ builtin.component = configured;
6629
+ unshadow = () => {
6630
+ if (builtin.component === configured) builtin.component = inner;
6631
+ };
6632
+ } else unshadow = ctx.slots.register({
5166
6633
  name: "conversation.chat.node",
5167
6634
  key: "assistant-step",
5168
6635
  priority: -100,
@@ -5174,10 +6641,18 @@ window.__ModuleLoader__.load({
5174
6641
  unshadow();
5175
6642
  };
5176
6643
  };
6644
+ const offSlotChange = ctx.on("slots/changed", (key) => {
6645
+ if (key !== "conversation.chat.node" || usedSwap || releaseTakeover === void 0) return;
6646
+ if (!settings.takeoverEnabled()) return;
6647
+ releaseTakeover();
6648
+ releaseTakeover = void 0;
6649
+ syncTakeover();
6650
+ });
5177
6651
  const unsubscribe = settings.subscribe(syncTakeover);
5178
6652
  syncTakeover();
5179
6653
  return () => {
5180
6654
  unsubscribe();
6655
+ offSlotChange();
5181
6656
  releaseTakeover?.();
5182
6657
  };
5183
6658
  });