dsh-smooth-stream 0.6.1 → 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,13 +4,52 @@ 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$4 = ".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}}";
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}}";
14
53
  const tagId$4 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
15
54
  if (typeof document !== "undefined") {
16
55
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]");
@@ -23,27 +62,27 @@ window.__ModuleLoader__.load({
23
62
  tag.textContent = css$4;
24
63
  }
25
64
  var TypewriterAssistantNodeView_module_css_default = {
26
- "thinkTitle": "I17U7q_thinkTitle",
27
- "disclosureContent": "I17U7q_disclosureContent",
28
- "disclosureLeading": "I17U7q_disclosureLeading",
29
- "thinkSummary": "I17U7q_thinkSummary",
30
- "thinkRow": "I17U7q_thinkRow",
31
- "visuallyHidden": "I17U7q_visuallyHidden",
32
- "disclosureRoot": "I17U7q_disclosureRoot",
33
- "body": "I17U7q_body",
34
- "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
35
- "disclosureIconIdle": "I17U7q_disclosureIconIdle",
36
- "root": "I17U7q_root",
37
- "disclosureChevronHover": "I17U7q_disclosureChevronHover",
38
- "stopped": "I17U7q_stopped",
39
- "think": "I17U7q_think",
40
- "thinkChevron": "I17U7q_thinkChevron",
41
- "thinkBody": "I17U7q_thinkBody",
42
- "follow": "I17U7q_follow",
43
- "thinkSeparator": "I17U7q_thinkSeparator",
44
- "thinkLeading": "I17U7q_thinkLeading",
45
- "disclosureRow": "I17U7q_disclosureRow",
46
- "disclosureTitle": "I17U7q_disclosureTitle"
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,6 +165,7 @@ 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,
131
171
  logarithmicFade: true,
@@ -197,6 +237,21 @@ window.__ModuleLoader__.load({
197
237
  * the renderer and the chat-side panel: renderer loops publish measurements,
198
238
  * while panel edits are staged through the settings-card controller.
199
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
+ }
200
255
  const EMPTY_METRICS = {
201
256
  fps: null,
202
257
  frameMs: null,
@@ -214,6 +269,12 @@ window.__ModuleLoader__.load({
214
269
  followRevealScale: 1,
215
270
  followFollowing: false,
216
271
  followConstrained: false,
272
+ followTerminalPhase: "live",
273
+ followRunwayPx: 0,
274
+ followTerminalBudgetPx: 0,
275
+ followBaselineShiftPx: 0,
276
+ followAnchorDeltaPx: null,
277
+ followRemainingRevealChars: 0,
217
278
  scrollTop: null,
218
279
  scrollHeight: null,
219
280
  clientHeight: null,
@@ -274,6 +335,12 @@ window.__ModuleLoader__.load({
274
335
  followRevealScale: follow?.revealScale ?? 1,
275
336
  followFollowing: follow?.following ?? false,
276
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,
277
344
  scrollTop: follow?.scrollTop ?? null,
278
345
  scrollHeight: follow?.scrollHeight ?? null,
279
346
  clientHeight: follow?.clientHeight ?? null,
@@ -436,6 +503,123 @@ window.__ModuleLoader__.load({
436
503
  }
437
504
  };
438
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
439
623
  //#region src/client/teleprompterGlide.ts
440
624
  /**
441
625
  * Conversation-port follow while an assistant reply streams.
@@ -674,7 +858,7 @@ window.__ModuleLoader__.load({
674
858
  setDirectShift(element, px);
675
859
  }
676
860
  function turnStatusOf(port) {
677
- 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]");
678
862
  }
679
863
  const FOLLOW_FLOW_FILL_SYMBOL = Symbol.for("dsh-smooth-stream.follow-flow-fill");
680
864
  const followFlowFillHost = globalThis;
@@ -687,6 +871,10 @@ window.__ModuleLoader__.load({
687
871
  function flowElementOf(port) {
688
872
  return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
689
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
+ }
690
878
  function ensureFlowFillsPort(port) {
691
879
  const element = flowElementOf(port);
692
880
  const owned = followFlowFills.get(port);
@@ -746,6 +934,8 @@ window.__ModuleLoader__.load({
746
934
  followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
747
935
  const followPaintLimits = /* @__PURE__ */ new WeakMap();
748
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();
749
939
  /** Last painted shift per port, to spread a wrap's one-line step over frames. */
750
940
  const followLastShiftPx = /* @__PURE__ */ new WeakMap();
751
941
  /** Last settled floor per port, to size the shift decay bound against extent collapse. */
@@ -939,7 +1129,7 @@ window.__ModuleLoader__.load({
939
1129
  return false;
940
1130
  }
941
1131
  function setFlowPad(port, px) {
942
- const flow = flowElementOf(port);
1132
+ const flow = flowPadElementOf(port);
943
1133
  if (flow === null) return;
944
1134
  const existing = followSettlePads.get(port);
945
1135
  const original = existing?.original ?? flow.style.paddingBottom;
@@ -977,12 +1167,83 @@ window.__ModuleLoader__.load({
977
1167
  if (!traceActive()) return;
978
1168
  console.log(`[dsh-follow] ${event}`, JSON.stringify(detail));
979
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
+ }
980
1182
  function hostShOf(port) {
981
1183
  return port.scrollHeight;
982
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
+ }
983
1210
  function invalidatePaintLimit(port) {
984
1211
  followPaintLimits.delete(port);
985
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();
986
1247
  /** Logical position and velocity survive a React owner handoff and finish. */
987
1248
  const followMotionStates = /* @__PURE__ */ new WeakMap();
988
1249
  const followReaderHolds = /* @__PURE__ */ new WeakMap();
@@ -1131,6 +1392,7 @@ window.__ModuleLoader__.load({
1131
1392
  if (last === void 0) return 0;
1132
1393
  const status = turnStatusOf(port);
1133
1394
  const composer = port.querySelector("[data-composer-seat]");
1395
+ if (status !== null) followHadStatus.add(port);
1134
1396
  if (status !== null || composer !== null) followHadChrome.add(port);
1135
1397
  const cached = followPaintLimits.get(port);
1136
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;
@@ -1205,6 +1467,35 @@ window.__ModuleLoader__.load({
1205
1467
  function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx, dtMs = 16.7, writeScrollTop = true) {
1206
1468
  const surfaces = shiftSurfacesOf(port);
1207
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
+ }
1208
1499
  {
1209
1500
  const preFloor = Math.max(0, port.scrollHeight - port.clientHeight);
1210
1501
  const lastFloorSeen = followFloorHistory.get(port);
@@ -1216,7 +1507,7 @@ window.__ModuleLoader__.load({
1216
1507
  }
1217
1508
  if (preFloor !== lastFloorSeen) followFloorHistory.set(port, preFloor);
1218
1509
  }
1219
- ensureRunway(port, surfaces, reservePx);
1510
+ ensureRunway(port, surfaces, followTerminalPhases.has(port) ? Math.max(Math.min(72, followTerminalBudgets.get(port) ?? 72), reservePx) : 72);
1220
1511
  const contentHeight2 = Math.max(0, port.scrollHeight);
1221
1512
  const runwayOffset2 = runwayOffsetOf(port);
1222
1513
  const prevOffset2 = followRunwayOffsetHistory.get(port);
@@ -1224,6 +1515,7 @@ window.__ModuleLoader__.load({
1224
1515
  followRunwayOffsetHistory.set(port, runwayOffset2);
1225
1516
  const contentHeight = contentHeight2;
1226
1517
  const runwayOffset = runwayOffset2;
1518
+ followObservedContentHeight.set(port, contentHeight2);
1227
1519
  const targetHeight = Math.max(0, contentHeight - runwayOffset);
1228
1520
  const floor = Math.max(0, contentHeight - port.clientHeight);
1229
1521
  const extent = Math.min(targetHeight, Math.max(0, animatedH));
@@ -1262,7 +1554,15 @@ window.__ModuleLoader__.load({
1262
1554
  if (previousShift !== void 0 && shift < previousShift - maxDecayPx) shift = previousShift - maxDecayPx;
1263
1555
  if (followCompletionSettle.has(port) && previousShift !== void 0 && previousFloor !== void 0) {
1264
1556
  const confirmedGrowthPx = Math.max(0, floor - previousFloor);
1265
- 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);
1266
1566
  }
1267
1567
  followLastShiftPx.set(port, shift);
1268
1568
  const requestedShift = trajectoryShiftPx ?? baselineShift + requestedLag;
@@ -1277,11 +1577,17 @@ window.__ModuleLoader__.load({
1277
1577
  extent: effectiveExtent,
1278
1578
  lagPx: effectiveLag,
1279
1579
  reservePx: visibleReserve,
1280
- 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
1281
1586
  });
1282
1587
  for (const surface of surfaces) setShift(surface, shift);
1283
1588
  const status = turnStatusOf(port);
1284
1589
  if (status !== null) setShift(status, 0);
1590
+ FrameCoordinator.forDocument().markLayoutDirty();
1285
1591
  return effectiveExtent;
1286
1592
  }
1287
1593
  function clearMotion(port) {
@@ -1307,8 +1613,10 @@ window.__ModuleLoader__.load({
1307
1613
  element.style.clipPath = "";
1308
1614
  }
1309
1615
  /** Remove equal offsets, land on the floor, then retire the compositor quietly. */
1310
- function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true) {
1616
+ function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true, deferCompositor = false) {
1311
1617
  followCompletionSettle.delete(port);
1618
+ followTerminalPhases.set(port, "natural");
1619
+ followCompletionGrowthCredit.delete(port);
1312
1620
  followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
1313
1621
  followTrace("finish-enter", {
1314
1622
  sh: hostShOf(port),
@@ -1331,6 +1639,10 @@ window.__ModuleLoader__.load({
1331
1639
  port.removeAttribute(FOLLOW_OWNED_ATTR);
1332
1640
  port.style.overflowAnchor = "";
1333
1641
  port.style.scrollBehavior = "";
1642
+ if (deferCompositor) {
1643
+ followMotionStates.delete(port);
1644
+ return;
1645
+ }
1334
1646
  for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
1335
1647
  else setShift(surface, 0);
1336
1648
  if (status !== null) {
@@ -1384,13 +1696,20 @@ window.__ModuleLoader__.load({
1384
1696
  const startedAsEntrance = entrance;
1385
1697
  const owner = {};
1386
1698
  const generation = ++followGeneration;
1387
- let rafId = 0;
1388
1699
  let last = performance.now();
1389
1700
  let following = true;
1390
1701
  let primed = false;
1391
1702
  let animatedH = 0;
1392
1703
  let reservePx = 0;
1393
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;
1394
1713
  let interacting = false;
1395
1714
  let readerGestureIntent = false;
1396
1715
  let readerReleased = false;
@@ -1414,7 +1733,28 @@ window.__ModuleLoader__.load({
1414
1733
  const revealPhase = new FollowRevealPhaseTracker();
1415
1734
  let holding = null;
1416
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);
1417
1756
  const finishEntrance = () => {
1757
+ releaseEntranceClamp();
1418
1758
  if (!entrancePending) return;
1419
1759
  entrancePending = false;
1420
1760
  onEntranceSettledRef.current?.();
@@ -1437,6 +1777,8 @@ window.__ModuleLoader__.load({
1437
1777
  };
1438
1778
  const reportFollow = (next, isActive) => {
1439
1779
  const state = followMotionStates.get(next);
1780
+ const phase = followTerminalPhases.get(next) ?? (isActive ? "live" : "natural");
1781
+ const runwayPx = state?.runwayPx ?? runwayOffsetOf(next);
1440
1782
  debugRuntime.reportFollow(next, {
1441
1783
  lagPx: state ? state.lagPx : -1,
1442
1784
  velocityPxPerSec: state?.velocityPxPerSec ?? 0,
@@ -1448,7 +1790,13 @@ window.__ModuleLoader__.load({
1448
1790
  scrollTop: next.scrollTop,
1449
1791
  scrollHeight: next.scrollHeight,
1450
1792
  clientHeight: next.clientHeight,
1451
- 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
1452
1800
  });
1453
1801
  };
1454
1802
  const isLeader = (next) => followLeaders.get(next)?.owner === owner;
@@ -1681,23 +2029,29 @@ window.__ModuleLoader__.load({
1681
2029
  observedTail = tail;
1682
2030
  if (tail !== null) resize.observe(tail);
1683
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
+ };
1684
2039
  const frame = (now) => {
1685
- rafId = requestAnimationFrame(frame);
1686
2040
  const elapsedMs = Math.max(.001, now - last);
1687
2041
  const dt = Math.min(32, elapsedMs);
1688
2042
  const tuning = debugRuntime.activeTuning();
1689
2043
  last = now;
1690
2044
  const root = rootRef.current;
1691
- if (root === null) return;
2045
+ if (root === null) return activeRef.current;
1692
2046
  const nextPort = root.closest("[data-conversation-scroll]");
1693
- if (nextPort === null) return;
2047
+ if (nextPort === null) return activeRef.current;
1694
2048
  bindPort(nextPort);
1695
2049
  observeTailSurface();
1696
2050
  resetHostScrollOwnershipForNewTurn(nextPort);
1697
2051
  hostOwnsScroll = followHostScrollPorts.has(nextPort);
1698
2052
  if (activeRef.current) followActivePorts.add(nextPort);
1699
2053
  else followActivePorts.delete(nextPort);
1700
- if (nextPort.clientHeight <= 0) return;
2054
+ if (nextPort.clientHeight <= 0) return true;
1701
2055
  const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
1702
2056
  const reportedLag = floor - nextPort.scrollTop;
1703
2057
  const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
@@ -1705,14 +2059,37 @@ window.__ModuleLoader__.load({
1705
2059
  if (completionSettleGuardsPort(nextPort)) {
1706
2060
  primed = true;
1707
2061
  following = false;
1708
- 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);
1709
2069
  }
1710
2070
  const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
1711
2071
  if (inherited === void 0) {
1712
2072
  const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
1713
2073
  const predictGrowth = predictiveRef?.current ?? predictive;
1714
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
+ }
1715
2091
  const hasStatus = turnStatusOf(nextPort) !== null;
2092
+ if (hasStatus) followHadStatus.add(nextPort);
1716
2093
  reservePx = Math.max(ownedBottomSpaceOf(nextPort), predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0);
1717
2094
  if (!completionSettleGuardsPort(nextPort)) setFlowPad(nextPort, 0);
1718
2095
  statusWasPresent = hasStatus;
@@ -1747,11 +2124,11 @@ window.__ModuleLoader__.load({
1747
2124
  updateRevealScale(nextPort, elapsedMs);
1748
2125
  reportFollow(nextPort, activeRef.current);
1749
2126
  const runwayOffset = runwayOffsetOf(nextPort);
1750
- if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
2127
+ if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) + entranceClampLagPx() <= .25) finishEntrance();
1751
2128
  } else finishEntrance();
1752
2129
  } else finishEntrance();
1753
2130
  primed = true;
1754
- return;
2131
+ return activeRef.current;
1755
2132
  }
1756
2133
  if (!following && (!interacting || readerReleased && !readerGestureIntent && reportedLag <= 1) && reportedLag <= (readerReleased ? 1 : 25)) {
1757
2134
  following = true;
@@ -1777,22 +2154,23 @@ window.__ModuleLoader__.load({
1777
2154
  if (!activeRef.current || !following) {
1778
2155
  followScrollLedgers.set(nextPort, nextPort.scrollTop);
1779
2156
  reportFollow(nextPort, activeRef.current);
1780
- return;
2157
+ return activeRef.current;
1781
2158
  }
1782
2159
  detectHostScroll(nextPort, floor);
1783
2160
  if (hostOwnsScroll) {
1784
2161
  followScrollLedgers.set(nextPort, nextPort.scrollTop);
1785
2162
  reportFollow(nextPort, false);
1786
- return;
2163
+ return false;
1787
2164
  }
1788
2165
  hold(nextPort);
1789
2166
  if (!isLeader(nextPort)) {
1790
2167
  finishEntrance();
1791
- return;
2168
+ return true;
1792
2169
  }
1793
2170
  const predictGrowth = predictiveRef?.current ?? predictive;
1794
2171
  const statusElement = turnStatusOf(nextPort);
1795
2172
  const hasStatus = statusElement !== null;
2173
+ if (hasStatus) followHadStatus.add(nextPort);
1796
2174
  if (statusElement !== null) lastStatusHeightPx = statusElement.offsetHeight;
1797
2175
  const statusJustRemoved = predictGrowth && statusWasPresent === true && !hasStatus;
1798
2176
  if (statusJustRemoved) reservePx = Math.max(reservePx, tuning.runwayPx) + lastStatusHeightPx;
@@ -1802,10 +2180,25 @@ window.__ModuleLoader__.load({
1802
2180
  const effectiveReserveTarget = Math.max(reservePx, pressureReserveTarget);
1803
2181
  const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
1804
2182
  reservePx += (effectiveReserveTarget - reservePx) * reserveStep;
1805
- 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);
1806
2197
  const runwayOffset = runwayOffsetOf(nextPort);
1807
2198
  const contentHeight = nextPort.scrollHeight;
1808
2199
  const floorNow = Math.max(0, contentHeight - nextPort.clientHeight);
2200
+ lastCommitGrowthPx = lastObservedContentHeight > 0 ? Math.max(0, contentHeight - lastObservedContentHeight) : 0;
2201
+ lastObservedContentHeight = contentHeight;
1809
2202
  const trajectoryActive = predictive && root.querySelector("[data-variant=\"think\"]") === null && runwayOffset > 0;
1810
2203
  let trajectoryShift;
1811
2204
  if (trajectoryActive) {
@@ -1863,18 +2256,38 @@ window.__ModuleLoader__.load({
1863
2256
  trajectoryPositionPx = null;
1864
2257
  trajectoryWasActive = false;
1865
2258
  }
1866
- const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
1867
- const step = computeFollowStep(dt, {
1868
- lag,
1869
- speedEma: speedCpsRef.current,
1870
- velocityPxPerSec
1871
- }, tuning);
1872
- 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
+ }
1873
2276
  animatedH = contentHeight - runwayOffset;
1874
- velocityPxPerSec = 0;
1875
2277
  } else {
1876
- animatedH = Math.min(contentHeight - runwayOffset, animatedH + step.advancePx);
1877
- 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
+ }
1878
2291
  }
1879
2292
  }
1880
2293
  animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift, elapsedMs, !hostOwnsScroll);
@@ -1885,12 +2298,15 @@ window.__ModuleLoader__.load({
1885
2298
  if (!activeRef.current) enforceReadingAnchor(nextPort);
1886
2299
  else measureReadingAnchor(nextPort);
1887
2300
  }
1888
- 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;
1889
2303
  };
2304
+ frameTaskRef.id = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
1890
2305
  frame(performance.now());
1891
2306
  return () => {
2307
+ releaseEntranceClamp();
1892
2308
  if (!controlScrollRef.current) {
1893
- cancelAnimationFrame(rafId);
2309
+ stopFollowTask();
1894
2310
  if (port !== null) followActivePorts.delete(port);
1895
2311
  unsubscribeCommit?.();
1896
2312
  resize?.disconnect();
@@ -1906,7 +2322,7 @@ window.__ModuleLoader__.load({
1906
2322
  releaseRevealScale();
1907
2323
  return;
1908
2324
  }
1909
- cancelAnimationFrame(rafId);
2325
+ stopFollowTask();
1910
2326
  if (port !== null) followActivePorts.delete(port);
1911
2327
  unsubscribeCommit?.();
1912
2328
  if (interactTimer !== null) clearTimeout(interactTimer);
@@ -1944,6 +2360,18 @@ window.__ModuleLoader__.load({
1944
2360
  st: Math.round(host.scrollTop),
1945
2361
  pad: Math.round(flowPadOf(host))
1946
2362
  });
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
+ }
1947
2375
  restoreRunway(host);
1948
2376
  setFlowPad(host, 0);
1949
2377
  finishAtNaturalFloor(host, !startedAsEntrance, true);
@@ -1965,6 +2393,9 @@ window.__ModuleLoader__.load({
1965
2393
  return;
1966
2394
  }
1967
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)));
1968
2399
  if (pruneDeadRunway(host)) {
1969
2400
  followTrace("cleanup-dead-margin", {
1970
2401
  sh: host.scrollHeight,
@@ -1991,7 +2422,14 @@ window.__ModuleLoader__.load({
1991
2422
  return;
1992
2423
  }
1993
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
+ };
1994
2431
  const stopSettleListeners = () => {
2432
+ stopSettleTask();
1995
2433
  for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
1996
2434
  resize?.disconnect();
1997
2435
  mutations?.disconnect();
@@ -2025,7 +2463,7 @@ window.__ModuleLoader__.load({
2025
2463
  const settleFrame = (now) => {
2026
2464
  if (!isLeader(host)) {
2027
2465
  stopSettleListeners();
2028
- return;
2466
+ return false;
2029
2467
  }
2030
2468
  if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
2031
2469
  readerGestureIntent = false;
@@ -2036,7 +2474,7 @@ window.__ModuleLoader__.load({
2036
2474
  releaseRevealScale();
2037
2475
  debugRuntime.reportFollow(host, null);
2038
2476
  stopSettleListeners();
2039
- return;
2477
+ return false;
2040
2478
  }
2041
2479
  const dt = Math.min(32, Math.max(0, now - settleLast));
2042
2480
  const tuning = debugRuntime.activeTuning();
@@ -2051,17 +2489,22 @@ window.__ModuleLoader__.load({
2051
2489
  releaseRevealScale();
2052
2490
  debugRuntime.reportFollow(host, null);
2053
2491
  stopSettleListeners();
2054
- return;
2492
+ return false;
2055
2493
  }
2056
2494
  const guardDelta = !settleRetiring && !followActivePorts.has(host) ? enforceReadingAnchor(host, true) : null;
2057
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");
2058
2497
  const settleStatus = turnStatusOf(host);
2059
2498
  const ownedRunwayPx = runwayOffsetOf(host);
2060
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));
2061
2502
  const requestedTransferPx = settleStatus === null ? ownedRunwayPx : Math.min(reservePx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
2062
- const transferredPx = transferRunwayToFlowPad(host, requestedTransferPx);
2503
+ const cappedTransferPx = terminalDrain ? Math.min(requestedTransferPx, Math.max(0, ownedRunwayPx - earnedBudgetPx)) : requestedTransferPx;
2504
+ const transferredPx = transferRunwayToFlowPad(host, cappedTransferPx);
2063
2505
  if (transferredPx > 0) setFlowPad(host, Math.max(0, flowPadOf(host) - transferredPx));
2064
2506
  reservePx = Math.max(0, reservePx - transferredPx);
2507
+ followTerminalBudgets.set(host, Math.max(reservePx, Math.min(earnedBudgetPx, runwayOffsetOf(host))));
2065
2508
  animatedH += transferredPx;
2066
2509
  }
2067
2510
  const runwayOffset = runwayOffsetOf(host);
@@ -2095,7 +2538,7 @@ window.__ModuleLoader__.load({
2095
2538
  releaseRevealScale();
2096
2539
  debugRuntime.reportFollow(host, null);
2097
2540
  stopSettleListeners();
2098
- return;
2541
+ return false;
2099
2542
  }
2100
2543
  const step = computeFollowStep(dt, {
2101
2544
  lag,
@@ -2121,9 +2564,9 @@ window.__ModuleLoader__.load({
2121
2564
  }
2122
2565
  }
2123
2566
  reportFollow(host, false);
2124
- requestAnimationFrame(settleFrame);
2567
+ return true;
2125
2568
  };
2126
- requestAnimationFrame(settleFrame);
2569
+ settleTaskRef.id = coordinator.registerTask({ onSimulate: (_dtMs, now) => settleFrame(now) });
2127
2570
  };
2128
2571
  }, [
2129
2572
  active,
@@ -2434,7 +2877,8 @@ window.__ModuleLoader__.load({
2434
2877
  speedCps: revealSpeedCps,
2435
2878
  targetChars: targetCount,
2436
2879
  displayedChars: displayedCount,
2437
- active: !producerComplete
2880
+ active: !producerComplete || backlog > 0,
2881
+ producerComplete
2438
2882
  });
2439
2883
  if (holdBackRef.current?.() === true) {
2440
2884
  rafRef.current = requestAnimationFrame(tick);
@@ -2550,14 +2994,12 @@ window.__ModuleLoader__.load({
2550
2994
  const elementRef = (0, react.useRef)(null);
2551
2995
  (0, react.useEffect)(() => {
2552
2996
  if (!active) return;
2553
- let rafId = 0;
2554
2997
  let lastDebugReport = 0;
2555
2998
  const frame = (now) => {
2556
- rafId = requestAnimationFrame(frame);
2557
2999
  const fps = fpsRef.current;
2558
3000
  if (fps.lastMs === 0) {
2559
3001
  fps.lastMs = now;
2560
- return;
3002
+ return true;
2561
3003
  }
2562
3004
  const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs));
2563
3005
  fps.lastMs = now;
@@ -2574,10 +3016,12 @@ window.__ModuleLoader__.load({
2574
3016
  debugRuntime.reportFps(currentFps, fps.emaMs, fps.degraded);
2575
3017
  lastDebugReport = now;
2576
3018
  }
3019
+ return true;
2577
3020
  };
2578
- rafId = requestAnimationFrame(frame);
3021
+ const coordinator = FrameCoordinator.forDocument();
3022
+ const taskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
2579
3023
  return () => {
2580
- cancelAnimationFrame(rafId);
3024
+ coordinator.unregisterTask(taskId);
2581
3025
  fpsRef.current = {
2582
3026
  emaMs: 0,
2583
3027
  lastMs: 0,
@@ -2607,8 +3051,8 @@ window.__ModuleLoader__.load({
2607
3051
  };
2608
3052
  }
2609
3053
  //#endregion
2610
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/LogarithmicFade.module.css.mjs
2611
- const css$3 = ".g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-0),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-0){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 0.0%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-1),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-1){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 3.22581%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-2),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-2){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 6.45161%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-3),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-3){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 9.67742%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-4),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-4){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 12.9032%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-5),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-5){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 16.129%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-6),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-6){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 19.3548%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-7),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-7){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 22.5807%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-8),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-8){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 25.8065%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-9),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-9){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 29.0323%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-10),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-10){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 32.2581%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-11),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-11){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 35.4839%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-12),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-12){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 38.7097%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-13),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-13){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 41.9355%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-14),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-14){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 45.1613%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-15),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-15){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 48.3871%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-16),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-16){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 51.6129%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-17),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-17){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 54.8387%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-18),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-18){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 58.0645%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-19),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-19){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 61.2903%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-20),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-20){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 64.5161%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-21),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-21){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 67.7419%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-22),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-22){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 70.9677%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-23),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-23){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 74.1936%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-24),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-24){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 77.4194%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-25),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-25){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 80.6452%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-26),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-26){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 83.871%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-27),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-27){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 87.0968%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-28),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-28){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 90.3226%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-29),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-29){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 93.5484%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-30),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-30){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 96.7742%, transparent)}.g268Pa_scope::highlight(g268Pa_dsh-smooth-stream-log-fade-31),.g268Pa_scope ::highlight(g268Pa_dsh-smooth-stream-log-fade-31){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 100.0%, transparent)}";
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)}";
2612
3056
  const tagId$3 = "dsh-smooth-stream/LogarithmicFade.module.css";
2613
3057
  if (typeof document !== "undefined") {
2614
3058
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]");
@@ -2621,45 +3065,59 @@ window.__ModuleLoader__.load({
2621
3065
  tag.textContent = css$3;
2622
3066
  }
2623
3067
  var LogarithmicFade_module_css_default = {
2624
- "dsh-smooth-stream-log-fade-0": "g268Pa_dsh-smooth-stream-log-fade-0",
2625
- "dsh-smooth-stream-log-fade-30": "g268Pa_dsh-smooth-stream-log-fade-30",
2626
- "dsh-smooth-stream-log-fade-4": "g268Pa_dsh-smooth-stream-log-fade-4",
2627
- "dsh-smooth-stream-log-fade-31": "g268Pa_dsh-smooth-stream-log-fade-31",
2628
- "dsh-smooth-stream-log-fade-9": "g268Pa_dsh-smooth-stream-log-fade-9",
2629
- "dsh-smooth-stream-log-fade-17": "g268Pa_dsh-smooth-stream-log-fade-17",
2630
- "dsh-smooth-stream-log-fade-18": "g268Pa_dsh-smooth-stream-log-fade-18",
2631
- "dsh-smooth-stream-log-fade-19": "g268Pa_dsh-smooth-stream-log-fade-19",
2632
- "dsh-smooth-stream-log-fade-21": "g268Pa_dsh-smooth-stream-log-fade-21",
2633
- "dsh-smooth-stream-log-fade-12": "g268Pa_dsh-smooth-stream-log-fade-12",
2634
- "dsh-smooth-stream-log-fade-8": "g268Pa_dsh-smooth-stream-log-fade-8",
2635
- "dsh-smooth-stream-log-fade-22": "g268Pa_dsh-smooth-stream-log-fade-22",
2636
- "dsh-smooth-stream-log-fade-29": "g268Pa_dsh-smooth-stream-log-fade-29",
2637
- "dsh-smooth-stream-log-fade-1": "g268Pa_dsh-smooth-stream-log-fade-1",
2638
- "dsh-smooth-stream-log-fade-25": "g268Pa_dsh-smooth-stream-log-fade-25",
2639
- "dsh-smooth-stream-log-fade-23": "g268Pa_dsh-smooth-stream-log-fade-23",
2640
- "dsh-smooth-stream-log-fade-26": "g268Pa_dsh-smooth-stream-log-fade-26",
2641
- "dsh-smooth-stream-log-fade-28": "g268Pa_dsh-smooth-stream-log-fade-28",
2642
- "dsh-smooth-stream-log-fade-6": "g268Pa_dsh-smooth-stream-log-fade-6",
2643
- "dsh-smooth-stream-log-fade-20": "g268Pa_dsh-smooth-stream-log-fade-20",
2644
- "dsh-smooth-stream-log-fade-10": "g268Pa_dsh-smooth-stream-log-fade-10",
2645
- "dsh-smooth-stream-log-fade-14": "g268Pa_dsh-smooth-stream-log-fade-14",
2646
- "dsh-smooth-stream-log-fade-24": "g268Pa_dsh-smooth-stream-log-fade-24",
2647
- "dsh-smooth-stream-log-fade-2": "g268Pa_dsh-smooth-stream-log-fade-2",
2648
- "dsh-smooth-stream-log-fade-13": "g268Pa_dsh-smooth-stream-log-fade-13",
2649
- "dsh-smooth-stream-log-fade-16": "g268Pa_dsh-smooth-stream-log-fade-16",
2650
- "dsh-smooth-stream-log-fade-5": "g268Pa_dsh-smooth-stream-log-fade-5",
2651
- "dsh-smooth-stream-log-fade-27": "g268Pa_dsh-smooth-stream-log-fade-27",
2652
- "scope": "g268Pa_scope",
2653
- "dsh-smooth-stream-log-fade-7": "g268Pa_dsh-smooth-stream-log-fade-7",
2654
- "dsh-smooth-stream-log-fade-15": "g268Pa_dsh-smooth-stream-log-fade-15",
2655
- "dsh-smooth-stream-log-fade-3": "g268Pa_dsh-smooth-stream-log-fade-3",
2656
- "dsh-smooth-stream-log-fade-11": "g268Pa_dsh-smooth-stream-log-fade-11"
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"
2657
3101
  };
2658
3102
  const FADE_STEPS = 32;
2659
3103
  const PREFIX = "dsh-smooth-stream-log-fade-";
2660
3104
  const COLOR_PROPERTY = "--dsh-smooth-stream-fade-color";
2661
3105
  const highlightName = (index) => LogarithmicFade_module_css_default[`${PREFIX}${index}`] ?? `${PREFIX}${index}`;
2662
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
+ ];
2663
3121
  function logarithmicOpacity(progress) {
2664
3122
  return 0 + 1 * (1 - Math.log1p(5 * (1 - (Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : 1))) / Math.log(6));
2665
3123
  }
@@ -2668,13 +3126,75 @@ window.__ModuleLoader__.load({
2668
3126
  }
2669
3127
  const schedulers = /* @__PURE__ */ new WeakMap();
2670
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
+ };
2671
3143
  function schedule(scheduler) {
2672
- if (scheduler.frame !== 0 || scheduler.pending.size === 0) return;
2673
- scheduler.frame = scheduler.window.requestAnimationFrame((now) => {
2674
- scheduler.frame = 0;
3144
+ if (scheduler.taskId !== null || scheduler.pending.size === 0) return;
3145
+ scheduler.taskId = scheduler.coordinator.registerTask({ onSimulate: (_dtMs, now) => {
2675
3146
  for (const client of scheduler.pending) if (!client.paint(now)) scheduler.pending.delete(client);
2676
- schedule(scheduler);
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();
2677
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
+ };
2678
3198
  }
2679
3199
  function schedulerFor(root) {
2680
3200
  const doc = root.ownerDocument;
@@ -2690,15 +3210,26 @@ window.__ModuleLoader__.load({
2690
3210
  highlights,
2691
3211
  clients: /* @__PURE__ */ new Set(),
2692
3212
  pending: /* @__PURE__ */ new Set(),
2693
- frame: 0,
3213
+ taskId: null,
2694
3214
  registry: realm.CSS.highlights,
2695
- window: win
3215
+ window: win,
3216
+ coordinator: FrameCoordinator.forDocument(doc),
3217
+ unwatchAppearance: () => {}
2696
3218
  };
3219
+ scheduler.unwatchAppearance = watchAppearance(scheduler);
2697
3220
  schedulers.set(doc, scheduler);
2698
3221
  }
2699
3222
  return scheduler;
2700
3223
  }
2701
- /** Owns ranges only: React retains ownership of every element and Text node. */
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
+ */
2702
3233
  var LogarithmicFadeController = class LogarithmicFadeController {
2703
3234
  root;
2704
3235
  scheduler;
@@ -2711,18 +3242,46 @@ window.__ModuleLoader__.load({
2711
3242
  pausedAt = null;
2712
3243
  disposed = false;
2713
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;
2714
3270
  constructor(root, scheduler) {
2715
3271
  this.root = root;
2716
3272
  this.scheduler = scheduler;
2717
3273
  scheduler.clients.add(this);
2718
3274
  const win = root.ownerDocument.defaultView;
2719
- this.observer = new win.MutationObserver(() => {
2720
- this.reconcile();
3275
+ this.observer = new win.MutationObserver((records) => {
3276
+ this.onDomMutation(records);
2721
3277
  });
2722
3278
  this.observer.observe(root, {
2723
3279
  subtree: true,
2724
3280
  childList: true,
2725
- characterData: true
3281
+ characterData: true,
3282
+ attributes: true,
3283
+ attributeFilter: EXCLUDED_ATTRIBUTES,
3284
+ attributeOldValue: true
2726
3285
  });
2727
3286
  }
2728
3287
  static create(root) {
@@ -2742,80 +3301,240 @@ window.__ModuleLoader__.load({
2742
3301
  this.speedCps = speedCps;
2743
3302
  this.reconcile();
2744
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
+ }
2745
3431
  clearRanges() {
2746
3432
  for (const character of this.characters) this.scheduler.highlights[character.bucket]?.delete(character.range);
2747
3433
  this.characters = [];
2748
- this.restoreColors();
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;
2749
3445
  }
2750
3446
  restoreColors() {
2751
- for (const [element, original] of this.colors) if (original.value === "") element.style.removeProperty(COLOR_PROPERTY);
2752
- else element.style.setProperty(COLOR_PROPERTY, original.value, original.priority);
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);
2753
3452
  this.colors.clear();
2754
3453
  }
2755
- preserveColor(element) {
2756
- if (this.colors.has(element)) return;
2757
- const color = this.scheduler.window.getComputedStyle(element).color;
2758
- this.colors.set(element, {
2759
- value: element.style.getPropertyValue(COLOR_PROPERTY),
2760
- priority: element.style.getPropertyPriority(COLOR_PROPERTY)
2761
- });
2762
- element.style.setProperty(COLOR_PROPERTY, color);
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();
2763
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
+ */
2764
3472
  reconcile() {
2765
3473
  if (this.disposed) return;
2766
- const text = this.root.textContent ?? "";
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();
2767
3487
  const previous = this.previous;
2768
3488
  this.previous = text;
2769
3489
  const old = this.characters;
2770
3490
  this.clearRanges();
2771
3491
  if (!this.enabled) {
2772
3492
  this.root.classList.remove(LogarithmicFade_module_css_default.scope);
3493
+ this.restoreColors();
2773
3494
  this.scheduler.pending.delete(this);
2774
3495
  this.stopIfIdle();
2775
3496
  return;
2776
3497
  }
2777
3498
  this.root.classList.add(LogarithmicFade_module_css_default.scope);
3499
+ this.ensureRootColor();
2778
3500
  const now = this.pausedAt ?? this.scheduler.window.performance.now();
2779
- let prefix = 0;
2780
- while (prefix < previous.length && prefix < text.length && previous[prefix] === text[prefix]) prefix += 1;
2781
3501
  const appended = text.startsWith(previous);
2782
- const nodes = [];
2783
- const walker = this.root.ownerDocument.createTreeWalker(this.root, NodeFilter.SHOW_TEXT);
2784
- let offset = 0;
2785
- for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
2786
- const end = offset + (node.textContent?.length ?? 0);
2787
- nodes.push({
2788
- node,
2789
- start: offset,
2790
- end,
2791
- eligible: node.parentElement?.closest(EXCLUDED) === null
2792
- });
2793
- offset = end;
2794
- }
2795
- const segments = segmenter.segment(text);
2796
- const tailSize = fadeTailSize(this.speedCps);
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;
2797
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);
2798
3513
  let end = text.length;
2799
3514
  for (let count = 0; count < 160 && end > 0 && (count < tailSize || end > oldestLiveStart); count += 1) {
2800
- const segment = segments.containing(end - 1);
3515
+ const localEnd = end - windowStart;
3516
+ if (localEnd <= 0) break;
3517
+ const segment = segments.containing(localEnd - 1);
2801
3518
  if (segment === void 0) break;
2802
- const start = segment.index;
2803
- const parts = nodes.filter((node) => node.end > start && node.start < end);
2804
- const born = old.find((character) => character.start === start && character.end === end && end <= prefix)?.born ?? (this.active && appended && start >= previous.length ? now : null);
2805
- if (born !== null && now - born < 240 && segment.segment.trim() !== "" && parts.length > 0 && parts.every((part) => part.eligible)) {
2806
- const first = parts[0];
2807
- const last = parts[parts.length - 1];
2808
- const range = this.root.ownerDocument.createRange();
2809
- range.setStart(first.node, start - first.start);
2810
- range.setEnd(last.node, end - last.start);
2811
- for (const part of parts) this.preserveColor(part.node.parentElement);
2812
- this.characters.push({
2813
- start,
2814
- end,
2815
- born,
2816
- range,
2817
- bucket: -1
2818
- });
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
+ }
2819
3538
  }
2820
3539
  end = start;
2821
3540
  }
@@ -2852,14 +3571,18 @@ window.__ModuleLoader__.load({
2852
3571
  }
2853
3572
  stopIfIdle() {
2854
3573
  if (this.scheduler.pending.size !== 0) return;
2855
- this.scheduler.window.cancelAnimationFrame(this.scheduler.frame);
2856
- this.scheduler.frame = 0;
3574
+ if (this.scheduler.taskId === null) return;
3575
+ this.scheduler.coordinator.unregisterTask(this.scheduler.taskId);
3576
+ this.scheduler.taskId = null;
2857
3577
  }
2858
3578
  dispose() {
2859
3579
  if (this.disposed) return;
2860
3580
  this.disposed = true;
2861
3581
  this.observer.disconnect();
2862
3582
  this.clearRanges();
3583
+ this.restoreColors();
3584
+ this.nodes = [];
3585
+ this.scannedStructureRevision = -1;
2863
3586
  this.root.classList.remove(LogarithmicFade_module_css_default.scope);
2864
3587
  this.scheduler.pending.delete(this);
2865
3588
  this.scheduler.clients.delete(this);
@@ -2869,6 +3592,7 @@ window.__ModuleLoader__.load({
2869
3592
  const name = highlightName(index);
2870
3593
  if (this.scheduler.registry.get(name) === highlight) this.scheduler.registry.delete(name);
2871
3594
  }
3595
+ this.scheduler.unwatchAppearance();
2872
3596
  schedulers.delete(this.root.ownerDocument);
2873
3597
  }
2874
3598
  }
@@ -2885,7 +3609,15 @@ window.__ModuleLoader__.load({
2885
3609
  }, [rootRef]);
2886
3610
  (0, react.useLayoutEffect)(() => {
2887
3611
  const root = rootRef.current;
2888
- if (controller.current === null && root !== null && enabled && active) {
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) {
2889
3621
  controller.current = LogarithmicFadeController.create(root);
2890
3622
  if (committed.current) controller.current?.update(false, false);
2891
3623
  }
@@ -2894,6 +3626,141 @@ window.__ModuleLoader__.load({
2894
3626
  });
2895
3627
  }
2896
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
2897
3764
  //#region src/client/FollowHost.tsx
2898
3765
  /**
2899
3766
  * Document-flow host that owns conversation-port follow while `active`.
@@ -2947,7 +3814,7 @@ window.__ModuleLoader__.load({
2947
3814
  /** Defaults shared by the Host schema and the client-side fallback. */
2948
3815
  const DEFAULT_STREAM_CONFIG = {
2949
3816
  mode: "typewriter",
2950
- preset: "balanced",
3817
+ preset: "silky",
2951
3818
  revealCharsPerSec: 80,
2952
3819
  scrollSpeedPxPerSec: 48,
2953
3820
  maxScrollSpeedPxPerSec: 1e3
@@ -2996,79 +3863,24 @@ window.__ModuleLoader__.load({
2996
3863
  else width += emPx;
2997
3864
  return width;
2998
3865
  }
2999
- function measurePendingTextGeometry(root, visibleText) {
3000
- if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
3001
- root,
3002
- visibleText,
3003
- fontSize: 14,
3004
- wrapThresholdWidth: null
3005
- };
3006
- const rootRect = root.getBoundingClientRect();
3007
- const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
3008
- if (rootWidth <= 0) return {
3009
- root,
3010
- visibleText,
3011
- fontSize: 14,
3012
- wrapThresholdWidth: null
3013
- };
3014
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
3015
- let tail = null;
3016
- for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
3017
- const parent = tail?.parentElement ?? root;
3018
- const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
3019
- if (tail === null || typeof document.createRange !== "function") return {
3020
- root,
3021
- visibleText,
3022
- fontSize,
3023
- wrapThresholdWidth: rootWidth
3024
- };
3025
- try {
3026
- const length = tail.textContent?.length ?? 0;
3027
- if (length <= 0) return {
3028
- root,
3029
- visibleText,
3030
- fontSize,
3031
- wrapThresholdWidth: rootWidth
3032
- };
3033
- const range = document.createRange();
3034
- range.setStart(tail, Math.max(0, length - 1));
3035
- range.setEnd(tail, length);
3036
- const tailRect = range.getBoundingClientRect();
3037
- const contentRight = rootRect.right;
3038
- if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
3039
- root,
3040
- visibleText,
3041
- fontSize,
3042
- wrapThresholdWidth: rootWidth
3043
- };
3044
- return {
3045
- root,
3046
- visibleText,
3047
- fontSize,
3048
- wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
3049
- };
3050
- } catch {
3051
- return {
3052
- root,
3053
- visibleText,
3054
- fontSize,
3055
- wrapThresholdWidth: rootWidth
3056
- };
3057
- }
3058
- }
3059
3866
  /** Whether buffered source can reach a new visual line before it drains. */
3060
3867
  function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
3061
3868
  if (pending === "") return false;
3062
3869
  if (/[\r\n]/u.test(pending)) return true;
3063
- const pendingChars = [...pending];
3064
- 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;
3065
3872
  let geometry = geometryRef.current;
3066
- if (geometry?.root !== root || geometry.visibleText !== visibleText) {
3067
- 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
+ };
3068
3881
  geometryRef.current = geometry;
3069
3882
  }
3070
- if (geometry.wrapThresholdWidth === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
3071
- return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
3883
+ return approximateInlineWidth(pending, geometry.fontSize) >= (geometry.wrapThresholdWidth ?? geometry.fontSize * PREDICTIVE_WRAP_FALLBACK_CHARS);
3072
3884
  }
3073
3885
  function announcementChunkEnd(source, start) {
3074
3886
  const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
@@ -3185,12 +3997,12 @@ window.__ModuleLoader__.load({
3185
3997
  * and rendered by the Harness `MarkdownText`
3186
3998
  * streaming arm (incremental parse, frozen non-tail blocks), so there is no
3187
3999
  * raw-text tail and no text-to-markdown swap: the tree stays markdown
3188
- * throughout. The last text block owns conversation-port follow so wraps
3189
- * glide instead of snapping. Once the stream closes and the reveal queue
3190
- * 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
3191
4003
  * mentions) swaps in exactly once.
3192
4004
  */
3193
- function AnimatedMarkdownText({ text, labels, fileMentions, streaming, logarithmicFade, 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 }) {
3194
4006
  const reduced = motionReduced;
3195
4007
  const [typing, setTyping] = (0, react.useState)(streaming);
3196
4008
  const localSpeedCpsRef = (0, react.useRef)(35);
@@ -3213,6 +4025,7 @@ window.__ModuleLoader__.load({
3213
4025
  });
3214
4026
  const shown = reduced ? text : displayed;
3215
4027
  const live = typing && !reduced;
4028
+ const markdownShown = useDecoupledMarkdown(shown, live);
3216
4029
  useLogarithmicFade(followRootRef, logarithmicFade && !reduced, live, speedCpsRef);
3217
4030
  (0, react.useEffect)(() => {
3218
4031
  const root = followRootRef.current;
@@ -3240,6 +4053,9 @@ window.__ModuleLoader__.load({
3240
4053
  streaming,
3241
4054
  text
3242
4055
  ]);
4056
+ (0, react.useLayoutEffect)(() => {
4057
+ onRevealActivityChange?.(live);
4058
+ }, [live, onRevealActivityChange]);
3243
4059
  (0, react.useEffect)(() => {
3244
4060
  if (typing && !streaming && shown.length === text.length) setTyping(false);
3245
4061
  }, [
@@ -3251,16 +4067,13 @@ window.__ModuleLoader__.load({
3251
4067
  (0, react.useEffect)(() => {
3252
4068
  if (streaming) setTyping(true);
3253
4069
  }, [streaming]);
3254
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
3255
- active: live && ownFollow,
3256
- speedCpsRef,
3257
- revealedCharsRef: followRevealedCharsRef,
3258
- revealScaleRef: followRevealScaleRef,
3259
- predictive: streaming,
3260
- controlScroll,
3261
- 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": "",
3262
4075
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
3263
- text: live ? shown : text,
4076
+ text: live ? markdownShown : text,
3264
4077
  streaming: live,
3265
4078
  labels,
3266
4079
  fileMentions: live ? void 0 : fileMentions
@@ -3281,7 +4094,7 @@ window.__ModuleLoader__.load({
3281
4094
  };
3282
4095
  }
3283
4096
  /**
3284
- * Apply searchable hidden state without unmounting a stable subtree — the
4097
+ * Apply searchable hidden state without unmounting a stable subtree 鈥?the
3285
4098
  * Host's completion fold hides the answer-inline reasoning this way, so the
3286
4099
  * takeover renderer must reproduce it to stay inside the contract: the row
3287
4100
  * disappears into the Host's process summary and comes back on find-in-page.
@@ -3338,18 +4151,23 @@ window.__ModuleLoader__.load({
3338
4151
  * click stay on the disclosure chrome, which the plugin's AnimatedDisclosure
3339
4152
  * renders with a height-animated body (the harness primitive would mount and
3340
4153
  * unmount it, which cannot glide). The row opens only while this block is
3341
- * the streaming tail and closes as soon as thinking ends — a later block,
3342
- * 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
3343
4156
  * still streaming.
3344
4157
  */
3345
- function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, logarithmicFade, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
4158
+ function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, logarithmicFade, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, snapAutoCollapse, t }) {
3346
4159
  const reduced = motionReduced;
3347
4160
  const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
3348
4161
  const [autoClosed, setAutoClosed] = (0, react.useState)(false);
3349
4162
  const summaryRef = (0, react.useRef)(null);
3350
4163
  const fadeRootRef = (0, react.useRef)(null);
4164
+ const thinkBodyRef = fadeRootRef;
3351
4165
  const localFadeSpeedRef = (0, react.useRef)(35);
3352
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;
3353
4171
  const commitAnchorRef = (0, react.useRef)(null);
3354
4172
  const displayed = useSmoothStreamContent(text, {
3355
4173
  enabled: running && !reduced,
@@ -3369,8 +4187,64 @@ window.__ModuleLoader__.load({
3369
4187
  setExpanded(running);
3370
4188
  setAutoClosed(!running);
3371
4189
  }
4190
+ if (running) userScrolledRef.current = false;
3372
4191
  notifyFollowCommit(commitAnchorRef.current);
3373
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
+ }, []);
3374
4248
  (0, react.useEffect)(() => {
3375
4249
  const element = summaryRef.current;
3376
4250
  if (element === null) return;
@@ -3391,14 +4265,14 @@ window.__ModuleLoader__.load({
3391
4265
  leadingClassName: TypewriterAssistantNodeView_module_css_default.thinkLeading,
3392
4266
  titleClassName: TypewriterAssistantNodeView_module_css_default.thinkTitle,
3393
4267
  chevronClassName: TypewriterAssistantNodeView_module_css_default.thinkChevron,
3394
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
3395
- title: "Think",
4268
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconThink, { size: 14 }),
4269
+ title: t("message.think"),
3396
4270
  open: expanded,
3397
4271
  onToggle: () => {
3398
4272
  setAutoClosed(false);
3399
4273
  setExpanded((value) => !value);
3400
4274
  },
3401
- bodyTransition: !autoClosed,
4275
+ bodyTransition: !autoClosed || !snapAutoCollapse,
3402
4276
  collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3403
4277
  className: TypewriterAssistantNodeView_module_css_default.thinkSeparator,
3404
4278
  "aria-hidden": true
@@ -3422,14 +4296,17 @@ window.__ModuleLoader__.load({
3422
4296
  * streaming is revealed by the smoother through the Harness Markdown
3423
4297
  * renderer at a rate that tracks arrival. Reasoning blocks keep the
3424
4298
  * built-in Think disclosure and only receive a smoothed text feed; the
3425
- * outer node owns conversation-port follow while streaming; the final text
3426
- * block keeps ownership while its settled reveal queue drains. The FPS guard
3427
- * holds offscreen reveals when the frame rate is degraded. Settled text
3428
- * 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.
3429
4304
  */
3430
- 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, 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 }) {
3431
4306
  const data = node.data;
3432
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";
3433
4310
  const reduced = useMotionReduced(motionPreference);
3434
4311
  const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
3435
4312
  const revealProcess = (0, react.useCallback)(() => {
@@ -3439,7 +4316,16 @@ window.__ModuleLoader__.load({
3439
4316
  const rootSpeedRef = (0, react.useRef)(35);
3440
4317
  const rootRevealedCharsRef = (0, react.useRef)(0);
3441
4318
  const rootRevealScaleRef = (0, react.useRef)(1);
3442
- 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;
3443
4329
  const reasoningOwnsSpeed = reasoningTailIndex !== -1;
3444
4330
  const rootPredictiveRef = (0, react.useRef)(false);
3445
4331
  const previousReasoningTailRef = (0, react.useRef)(-1);
@@ -3478,20 +4364,24 @@ window.__ModuleLoader__.load({
3478
4364
  const imageLoader = loadImage ?? (async () => {
3479
4365
  throw new Error(t("image.serviceUnavailable"));
3480
4366
  });
3481
- if (!(streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call"))) return null;
3482
- 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";
3483
4370
  const rendered = [];
3484
- const last = data.blocks.length - 1;
4371
+ const last = blocks.length - 1;
3485
4372
  let lastFollow = -1;
3486
- for (let index = 0; index < data.blocks.length; index += 1) {
3487
- 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;
3488
4376
  if (kind === "text" || kind === "reasoning") lastFollow = index;
4377
+ if (kind === "text") lastText = index;
3489
4378
  }
3490
- for (let index = 0; index < data.blocks.length; index += 1) {
3491
- const block = data.blocks[index];
4379
+ for (let index = 0; index < blocks.length; index += 1) {
4380
+ const block = blocks[index];
3492
4381
  if (block === void 0) continue;
3493
4382
  switch (block.kind) {
3494
4383
  case "text":
4384
+ if (!streaming && block.text.trim() === "") break;
3495
4385
  rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
3496
4386
  text: block.text,
3497
4387
  labels: markdownLabels,
@@ -3499,14 +4389,13 @@ window.__ModuleLoader__.load({
3499
4389
  streaming,
3500
4390
  logarithmicFade: logarithmicFade && data.status !== "interrupted",
3501
4391
  motionReduced: reduced,
3502
- ownFollow: !streaming && index === lastFollow,
3503
4392
  followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
3504
4393
  followRevealedCharsRef: index === lastFollow ? rootRevealedCharsRef : void 0,
3505
4394
  followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
3506
4395
  onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
4396
+ onRevealActivityChange: index === lastText ? updateTextRevealActivity : void 0,
3507
4397
  preset,
3508
- shouldHoldBack,
3509
- controlScroll
4398
+ shouldHoldBack
3510
4399
  }, index));
3511
4400
  break;
3512
4401
  case "reasoning":
@@ -3515,7 +4404,7 @@ window.__ModuleLoader__.load({
3515
4404
  reveal: revealProcess,
3516
4405
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
3517
4406
  text: block.text,
3518
- running: streaming && index === last,
4407
+ running: streaming && index === last && (groupPart !== "reasoning" || stepTailIsReasoning),
3519
4408
  preset,
3520
4409
  thinkAutoExpand,
3521
4410
  logarithmicFade: logarithmicFade && data.status !== "interrupted",
@@ -3523,6 +4412,7 @@ window.__ModuleLoader__.load({
3523
4412
  shouldHoldBack,
3524
4413
  followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
3525
4414
  followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
4415
+ snapAutoCollapse: groupPart === void 0,
3526
4416
  t
3527
4417
  })
3528
4418
  }, index));
@@ -3530,8 +4420,8 @@ window.__ModuleLoader__.load({
3530
4420
  case "image": {
3531
4421
  const start = index;
3532
4422
  const group = [block];
3533
- while (index + 1 < data.blocks.length) {
3534
- const next = data.blocks[index + 1];
4423
+ while (index + 1 < blocks.length) {
4424
+ const next = blocks[index + 1];
3535
4425
  if (next === void 0 || next.kind !== "image") break;
3536
4426
  group.push(next);
3537
4427
  index += 1;
@@ -3560,12 +4450,12 @@ window.__ModuleLoader__.load({
3560
4450
  text: announcementText,
3561
4451
  active: streaming && !reduced
3562
4452
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
3563
- active: streaming && !reduced,
4453
+ active: !reduced && (streaming || completionCandidate || textRevealActive),
3564
4454
  speedCpsRef: rootSpeedRef,
3565
4455
  revealedCharsRef: rootRevealedCharsRef,
3566
4456
  revealScaleRef: rootRevealScaleRef,
3567
4457
  predictiveRef: rootPredictiveRef,
3568
- controlScroll,
4458
+ controlScroll: controlScroll && rowOwnsPort,
3569
4459
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3570
4460
  className: TypewriterAssistantNodeView_module_css_default.body,
3571
4461
  children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -3668,12 +4558,18 @@ window.__ModuleLoader__.load({
3668
4558
  }
3669
4559
  const pending = /* @__PURE__ */ new Set();
3670
4560
  const internalWrites = /* @__PURE__ */ new WeakMap();
3671
- let rafId = 0;
4561
+ let frameTaskId = null;
3672
4562
  let lastFrame = null;
3673
4563
  let debt = 0;
3674
4564
  let stopped = false;
3675
4565
  let announcedSettled = false;
3676
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
+ };
3677
4573
  const announceSettled = () => {
3678
4574
  lastFrame = null;
3679
4575
  debt = 0;
@@ -3722,21 +4618,20 @@ window.__ModuleLoader__.load({
3722
4618
  });
3723
4619
  };
3724
4620
  const scheduleFrame = () => {
3725
- if (stopped || pending.size === 0 || rafId !== 0) return;
3726
- rafId = requestAnimationFrame(frame);
4621
+ if (stopped || pending.size === 0 || frameTaskId !== null) return;
4622
+ frameTaskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
3727
4623
  };
3728
4624
  const frame = (now) => {
3729
- rafId = 0;
3730
- if (stopped) return;
4625
+ if (stopped) return false;
3731
4626
  if (pending.size === 0) {
3732
4627
  announceSettled();
3733
- return;
4628
+ stopFrameTask();
4629
+ return false;
3734
4630
  }
3735
4631
  announcedSettled = false;
3736
4632
  if (lastFrame === null) {
3737
4633
  lastFrame = now;
3738
- scheduleFrame();
3739
- return;
4634
+ return true;
3740
4635
  }
3741
4636
  const elapsed = Math.max(0, now - lastFrame);
3742
4637
  lastFrame = now;
@@ -3786,10 +4681,11 @@ window.__ModuleLoader__.load({
3786
4681
  speedCps: step.speedCps,
3787
4682
  targetChars,
3788
4683
  displayedChars,
3789
- active: pending.size > 0
4684
+ active: pending.size > 0,
4685
+ producerComplete: false
3790
4686
  });
3791
4687
  if (pending.size === 0) announceSettled();
3792
- else scheduleFrame();
4688
+ return pending.size > 0;
3793
4689
  };
3794
4690
  visit(root, revealInitial);
3795
4691
  const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver((mutations) => {
@@ -3818,7 +4714,7 @@ window.__ModuleLoader__.load({
3818
4714
  else scheduleFrame();
3819
4715
  return () => {
3820
4716
  stopped = true;
3821
- cancelAnimationFrame(rafId);
4717
+ stopFrameTask();
3822
4718
  observer?.disconnect();
3823
4719
  for (const [node, record] of records) {
3824
4720
  const controlled = record.chars.slice(0, record.shown).join("");
@@ -3837,8 +4733,8 @@ window.__ModuleLoader__.load({
3837
4733
  ]);
3838
4734
  }
3839
4735
  //#endregion
3840
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/AgentRowEntrance.module.css.mjs
3841
- 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}}";
3842
4738
  const tagId$2 = "dsh-smooth-stream/AgentRowEntrance.module.css";
3843
4739
  if (typeof document !== "undefined") {
3844
4740
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
@@ -3851,8 +4747,8 @@ window.__ModuleLoader__.load({
3851
4747
  tag.textContent = css$2;
3852
4748
  }
3853
4749
  var AgentRowEntrance_module_css_default = {
3854
- "dsh-smooth-stream-agent-row-in": "_7P9_Ta_dsh-smooth-stream-agent-row-in",
3855
- "surface": "_7P9_Ta_surface"
4750
+ "surface": "Mhu9Iq_surface",
4751
+ "dsh-smooth-stream-agent-row-in": "Mhu9Iq_dsh-smooth-stream-agent-row-in"
3856
4752
  };
3857
4753
  //#endregion
3858
4754
  //#region src/client/TypewriterToolNodeView.tsx
@@ -4030,8 +4926,8 @@ window.__ModuleLoader__.load({
4030
4926
  };
4031
4927
  }
4032
4928
  //#endregion
4033
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
4034
- 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}";
4035
4931
  const tagId$1 = "dsh-smooth-stream/SmoothStreamCard.module.css";
4036
4932
  if (typeof document !== "undefined") {
4037
4933
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
@@ -4044,35 +4940,35 @@ window.__ModuleLoader__.load({
4044
4940
  tag.textContent = css$1;
4045
4941
  }
4046
4942
  var SmoothStreamCard_module_css_default = {
4047
- "save": "XNGeRG_save",
4048
- "updateCopy": "XNGeRG_updateCopy",
4049
- "hint": "XNGeRG_hint",
4050
- "version": "XNGeRG_version",
4051
- "update": "XNGeRG_update",
4052
- "card": "XNGeRG_card",
4053
- "choiceRow": "XNGeRG_choiceRow",
4054
- "chevron": "XNGeRG_chevron",
4055
- "failure": "XNGeRG_failure",
4056
- "discard": "XNGeRG_discard",
4057
- "updateRow": "XNGeRG_updateRow",
4058
- "footer": "XNGeRG_footer",
4059
- "body": "XNGeRG_body",
4060
- "field": "XNGeRG_field",
4061
- "failed": "XNGeRG_failed",
4062
- "name": "XNGeRG_name",
4063
- "readOnly": "XNGeRG_readOnly",
4064
- "chevronOpen": "XNGeRG_chevronOpen",
4065
- "toggle": "XNGeRG_toggle",
4066
- "choice": "XNGeRG_choice",
4067
- "choiceInput": "XNGeRG_choiceInput",
4068
- "description": "XNGeRG_description",
4069
- "pending": "XNGeRG_pending",
4070
- "fieldDisabled": "XNGeRG_fieldDisabled",
4071
- "headText": "XNGeRG_headText",
4072
- "fieldHead": "XNGeRG_fieldHead",
4073
- "label": "XNGeRG_label",
4074
- "cardOpen": "XNGeRG_cardOpen",
4075
- "header": "XNGeRG_header"
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"
4076
4972
  };
4077
4973
  //#endregion
4078
4974
  //#region src/client/SmoothStreamCard.tsx
@@ -4117,7 +5013,7 @@ window.__ModuleLoader__.load({
4117
5013
  className: SmoothStreamCard_module_css_default.pending,
4118
5014
  children: t("unsaved")
4119
5015
  }) : null,
4120
- /* @__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 })
4121
5017
  ]
4122
5018
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4123
5019
  className: SmoothStreamCard_module_css_default.body,
@@ -4260,6 +5156,57 @@ window.__ModuleLoader__.load({
4260
5156
  })
4261
5157
  ]
4262
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
+ }),
4263
5210
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4264
5211
  className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
4265
5212
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -4321,7 +5268,7 @@ window.__ModuleLoader__.load({
4321
5268
  onClick: props.upgrade,
4322
5269
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4323
5270
  "aria-hidden": "true",
4324
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
5271
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, {})
4325
5272
  }), t(state.upgrading ? "updating" : "update")]
4326
5273
  })]
4327
5274
  }),
@@ -4402,10 +5349,11 @@ window.__ModuleLoader__.load({
4402
5349
  hooks: { smoothStreamCard: this.store },
4403
5350
  edit: (patch) => {
4404
5351
  if (this.saving) return;
4405
- if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.motionPreference !== void 0 || patch.thinkAutoExpand !== void 0 || patch.logarithmicFade !== 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 = {
4406
5353
  ...this.baseValues(),
4407
5354
  ...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
4408
5355
  ...patch.controlScroll === void 0 ? {} : { controlScroll: patch.controlScroll },
5356
+ ...patch.preset === void 0 ? {} : { preset: patch.preset },
4409
5357
  ...patch.motionPreference === void 0 ? {} : { motionPreference: patch.motionPreference },
4410
5358
  ...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand },
4411
5359
  ...patch.logarithmicFade === void 0 ? {} : { logarithmicFade: patch.logarithmicFade }
@@ -4461,6 +5409,7 @@ window.__ModuleLoader__.load({
4461
5409
  return this.stagedBase ?? {
4462
5410
  enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
4463
5411
  controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
5412
+ preset: this.loaded?.preset ?? DEFAULT_STREAM_SETTINGS.preset,
4464
5413
  motionPreference: this.loaded?.motionPreference ?? DEFAULT_STREAM_SETTINGS.motionPreference,
4465
5414
  thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
4466
5415
  logarithmicFade: this.loaded?.logarithmicFade ?? DEFAULT_STREAM_SETTINGS.logarithmicFade
@@ -4583,13 +5532,18 @@ window.__ModuleLoader__.load({
4583
5532
  "npm",
4584
5533
  "development",
4585
5534
  "unmanaged"
4586
- ].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) || ![
4587
5540
  "auto",
4588
5541
  "force-smooth",
4589
5542
  "force-reduced"
4590
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");
4591
5544
  return {
4592
5545
  ...data,
5546
+ preset: data.preset ?? "balanced",
4593
5547
  logarithmicFade: data.logarithmicFade ?? true
4594
5548
  };
4595
5549
  }
@@ -4620,6 +5574,7 @@ window.__ModuleLoader__.load({
4620
5574
  return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, {
4621
5575
  enabled: settings.enabled,
4622
5576
  controlScroll: settings.controlScroll,
5577
+ preset: settings.preset,
4623
5578
  motionPreference: settings.motionPreference,
4624
5579
  thinkAutoExpand: settings.thinkAutoExpand,
4625
5580
  logarithmicFade: settings.logarithmicFade,
@@ -4644,8 +5599,8 @@ window.__ModuleLoader__.load({
4644
5599
  };
4645
5600
  }
4646
5601
  //#endregion
4647
- //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/DebugPanel.module.css.mjs
4648
- 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}}";
4649
5604
  const tagId = "dsh-smooth-stream/DebugPanel.module.css";
4650
5605
  if (typeof document !== "undefined") {
4651
5606
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
@@ -4658,37 +5613,49 @@ window.__ModuleLoader__.load({
4658
5613
  tag.textContent = css;
4659
5614
  }
4660
5615
  var DebugPanel_module_css_default = {
4661
- "section": "m4_tdG_section",
4662
- "guide": "m4_tdG_guide",
4663
- "metrics": "m4_tdG_metrics",
4664
- "numberWrap": "m4_tdG_numberWrap",
4665
- "statusLive": "m4_tdG_statusLive",
4666
- "visuallyHidden": "m4_tdG_visuallyHidden",
4667
- "statusDot": "m4_tdG_statusDot",
4668
- "secondaryButton": "m4_tdG_secondaryButton",
4669
- "controlLabel": "m4_tdG_controlLabel",
4670
- "title": "m4_tdG_title",
4671
- "control": "m4_tdG_control",
4672
- "trigger": "m4_tdG_trigger",
4673
- "triggerActive": "m4_tdG_triggerActive",
4674
- "infoButton": "m4_tdG_infoButton",
4675
- "footerSpacer": "m4_tdG_footerSpacer",
4676
- "primaryButton": "m4_tdG_primaryButton",
4677
- "footer": "m4_tdG_footer",
4678
- "state": "m4_tdG_state",
4679
- "range": "m4_tdG_range",
4680
- "number": "m4_tdG_number",
4681
- "panel": "m4_tdG_panel",
4682
- "scrollArea": "m4_tdG_scrollArea",
4683
- "metric": "m4_tdG_metric",
4684
- "panelHeader": "m4_tdG_panelHeader",
4685
- "unit": "m4_tdG_unit",
4686
- "controlHead": "m4_tdG_controlHead",
4687
- "iconButton": "m4_tdG_iconButton",
4688
- "unsaved": "m4_tdG_unsaved"
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"
4689
5644
  };
4690
5645
  //#endregion
4691
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
+ };
4692
5659
  const REVEAL_CONTROLS = [
4693
5660
  {
4694
5661
  key: "revealScale",
@@ -4815,7 +5782,7 @@ window.__ModuleLoader__.load({
4815
5782
  type: "button",
4816
5783
  "aria-label": label,
4817
5784
  title: t(control.tip),
4818
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, {})
5785
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconQuestion, {})
4819
5786
  })
4820
5787
  })]
4821
5788
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -4911,7 +5878,7 @@ window.__ModuleLoader__.load({
4911
5878
  onClick: () => {
4912
5879
  copyDiagnostics();
4913
5880
  },
4914
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {})
5881
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCopy, {})
4915
5882
  }),
4916
5883
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4917
5884
  className: DebugPanel_module_css_default.iconButton,
@@ -4924,7 +5891,7 @@ window.__ModuleLoader__.load({
4924
5891
  props.edit({ debugEnabled: false });
4925
5892
  props.save();
4926
5893
  },
4927
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
5894
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconClose, {})
4928
5895
  }),
4929
5896
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4930
5897
  className: DebugPanel_module_css_default.visuallyHidden,
@@ -4992,6 +5959,28 @@ window.__ModuleLoader__.load({
4992
5959
  label: t("debugCapacity"),
4993
5960
  value: `${fixed(metrics.followCapacityPx)} px`
4994
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
+ }),
4995
5984
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4996
5985
  label: t("debugAppliedScale"),
4997
5986
  value: `${fixed(metrics.followRevealScale, 2)}x`
@@ -5037,7 +6026,7 @@ window.__ModuleLoader__.load({
5037
6026
  type: "button",
5038
6027
  disabled: !state.writable,
5039
6028
  onClick: props.reset,
5040
- 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")]
5041
6030
  }),
5042
6031
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: DebugPanel_module_css_default.footerSpacer }),
5043
6032
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -5067,7 +6056,7 @@ window.__ModuleLoader__.load({
5067
6056
  onClick: () => {
5068
6057
  setOpen((current) => !current);
5069
6058
  },
5070
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCodeOutline16, {})
6059
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCode, {})
5071
6060
  }), typeof document === "undefined" || panel === null ? null : (0, react_dom.createPortal)(panel, document.body)] });
5072
6061
  }
5073
6062
  //#endregion
@@ -5075,6 +6064,37 @@ window.__ModuleLoader__.load({
5075
6064
  /** Locale bundles for the smooth-stream plugin configuration card. */
5076
6065
  /** Dictionary namespace owned by this plugin's settings card. */
5077
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
+ };
5078
6098
  /** English copy. */
5079
6099
  const en = {
5080
6100
  title: "Smooth stream",
@@ -5083,6 +6103,14 @@ window.__ModuleLoader__.load({
5083
6103
  enabledHint: "Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.",
5084
6104
  controlScroll: "Take over scrolling",
5085
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.",
5086
6114
  motionPreference: "Motion",
5087
6115
  motionPreferenceHint: "How streaming responds to the system reduce-motion setting.",
5088
6116
  motionAuto: "Follow system",
@@ -5126,6 +6154,15 @@ window.__ModuleLoader__.load({
5126
6154
  debugReserve: "Reserve",
5127
6155
  debugCapacity: "Capacity",
5128
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",
5129
6166
  debugRevealMultiplier: "Reveal multiplier",
5130
6167
  debugQueuePressure: "Queue pressure",
5131
6168
  debugMaxReveal: "Maximum reveal",
@@ -5172,6 +6209,14 @@ window.__ModuleLoader__.load({
5172
6209
  enabledHint: "由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。",
5173
6210
  controlScroll: "接管滚动",
5174
6211
  controlScrollHint: "默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。",
6212
+ preset: "手感预设",
6213
+ presetHint: "文字吐字节奏与视口跟随阻尼风格。",
6214
+ presetRealtime: "激进快速 (realtime)",
6215
+ presetRealtimeHint: "缓冲低、流速快,紧跟模型生成。",
6216
+ presetBalanced: "均衡自然 (balanced)",
6217
+ presetBalancedHint: "官方默认,平衡流速与节奏感。",
6218
+ presetSilky: "极致丝滑 (silky)",
6219
+ presetSilkyHint: "字符积压缓冲加大、阻尼更高、EMA 平滑度更好,视觉呼吸感最强。",
5175
6220
  motionPreference: "动效偏好",
5176
6221
  motionPreferenceHint: "流式渲染如何响应系统的「减少动态效果」设置。",
5177
6222
  motionAuto: "跟随系统",
@@ -5215,6 +6260,15 @@ window.__ModuleLoader__.load({
5215
6260
  debugReserve: "预留空间",
5216
6261
  debugCapacity: "安全容量",
5217
6262
  debugAppliedScale: "实际倍率",
6263
+ debugTerminalPhase: "收尾阶段",
6264
+ debugOwnedRunway: "实体留白",
6265
+ debugTerminalBudget: "收尾预算",
6266
+ debugBaselineShift: "基线位移",
6267
+ debugAnchorDelta: "锚点位移",
6268
+ debugPhaseLive: "流式中",
6269
+ debugPhaseDrain: "收尾排空",
6270
+ debugPhaseCascade: "宿主交接",
6271
+ debugPhaseNatural: "自然态",
5218
6272
  debugRevealMultiplier: "揭示倍率",
5219
6273
  debugQueuePressure: "队列压力",
5220
6274
  debugMaxReveal: "最大揭示速度",
@@ -5275,12 +6329,33 @@ window.__ModuleLoader__.load({
5275
6329
  * generic follow boundary. This is deliberately keyed by the owner that
5276
6330
  * provides the renderer, not by individual tool names, so new Context,
5277
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.
5278
6350
  */
5279
6351
  const SKIP_WRAP = /* @__PURE__ */ new Set([
5280
6352
  "assistant-step",
5281
6353
  "user",
5282
6354
  "steering",
5283
- "command-input"
6355
+ "command-input",
6356
+ "turn-process",
6357
+ "turn-tail",
6358
+ "tool-call"
5284
6359
  ]);
5285
6360
  /** React function/class or an exotic component such as memo/forwardRef/lazy. */
5286
6361
  function isWrappableComponent(value) {
@@ -5376,7 +6451,7 @@ window.__ModuleLoader__.load({
5376
6451
  refresh() {
5377
6452
  const next = this.read();
5378
6453
  const pending = this.card?.getSnapshot().status === "loading";
5379
- 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.logarithmicFade === this.value.logarithmicFade && 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;
5380
6455
  this.pending = pending;
5381
6456
  this.value = next;
5382
6457
  for (const listener of this.listeners) listener();
@@ -5385,6 +6460,15 @@ window.__ModuleLoader__.load({
5385
6460
  takeoverEnabled() {
5386
6461
  return !this.pending && this.value.enabled;
5387
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
+ }
5388
6472
  getSnapshot = () => this.value;
5389
6473
  subscribe = (listener) => {
5390
6474
  this.listeners.add(listener);
@@ -5408,6 +6492,49 @@ window.__ModuleLoader__.load({
5408
6492
  const config = readBootConfig();
5409
6493
  const settings = new SettingsCell();
5410
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
+ });
5411
6538
  ctx.inject([
5412
6539
  "slots",
5413
6540
  "locale",
@@ -5470,8 +6597,9 @@ window.__ModuleLoader__.load({
5470
6597
  const preferences = (0, react.useSyncExternalStore)(settings.subscribe, settings.getSnapshot, settings.getSnapshot);
5471
6598
  return (0, react.createElement)(TypewriterAssistantNodeView, {
5472
6599
  ...props,
6600
+ ...assistantT === void 0 ? {} : { t: assistantT },
5473
6601
  mode: config.mode,
5474
- preset: config.preset,
6602
+ preset: settings.ready() ? preferences.preset ?? config.preset : config.preset,
5475
6603
  revealCharsPerSec: config.revealCharsPerSec,
5476
6604
  scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
5477
6605
  maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
@@ -5483,6 +6611,7 @@ window.__ModuleLoader__.load({
5483
6611
  };
5484
6612
  ctx.slots.inject("conversation.chat.node", () => {
5485
6613
  let releaseTakeover;
6614
+ let usedSwap = false;
5486
6615
  const syncTakeover = () => {
5487
6616
  if (!settings.takeoverEnabled()) {
5488
6617
  releaseTakeover?.();
@@ -5491,7 +6620,16 @@ window.__ModuleLoader__.load({
5491
6620
  }
5492
6621
  if (releaseTakeover !== void 0) return;
5493
6622
  const unwrap = wrapAgentChatRows(ctx, useControlScroll);
5494
- 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({
5495
6633
  name: "conversation.chat.node",
5496
6634
  key: "assistant-step",
5497
6635
  priority: -100,
@@ -5503,10 +6641,18 @@ window.__ModuleLoader__.load({
5503
6641
  unshadow();
5504
6642
  };
5505
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
+ });
5506
6651
  const unsubscribe = settings.subscribe(syncTakeover);
5507
6652
  syncTakeover();
5508
6653
  return () => {
5509
6654
  unsubscribe();
6655
+ offSlotChange();
5510
6656
  releaseTakeover?.();
5511
6657
  };
5512
6658
  });