dsh-smooth-stream 0.4.0 → 0.4.1

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
@@ -24,27 +24,27 @@ window.__ModuleLoader__.load({
24
24
  tag.textContent = css$2;
25
25
  }
26
26
  var TypewriterAssistantNodeView_module_css_default = {
27
- "think": "I17U7q_think",
28
- "thinkTitle": "I17U7q_thinkTitle",
29
- "thinkSummary": "I17U7q_thinkSummary",
30
- "thinkBody": "I17U7q_thinkBody",
31
- "disclosureLeading": "I17U7q_disclosureLeading",
27
+ "thinkSeparator": "I17U7q_thinkSeparator",
28
+ "body": "I17U7q_body",
32
29
  "disclosureIconIdle": "I17U7q_disclosureIconIdle",
33
- "disclosureChevronHover": "I17U7q_disclosureChevronHover",
34
- "follow": "I17U7q_follow",
35
- "disclosureRoot": "I17U7q_disclosureRoot",
36
- "disclosureRow": "I17U7q_disclosureRow",
30
+ "disclosureTitle": "I17U7q_disclosureTitle",
31
+ "think": "I17U7q_think",
32
+ "disclosureContent": "I17U7q_disclosureContent",
37
33
  "thinkLeading": "I17U7q_thinkLeading",
38
- "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
34
+ "disclosureRow": "I17U7q_disclosureRow",
35
+ "stopped": "I17U7q_stopped",
39
36
  "thinkChevron": "I17U7q_thinkChevron",
40
- "root": "I17U7q_root",
41
- "body": "I17U7q_body",
37
+ "thinkSummary": "I17U7q_thinkSummary",
38
+ "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
42
39
  "visuallyHidden": "I17U7q_visuallyHidden",
43
- "stopped": "I17U7q_stopped",
44
- "thinkSeparator": "I17U7q_thinkSeparator",
45
- "disclosureTitle": "I17U7q_disclosureTitle",
46
- "disclosureContent": "I17U7q_disclosureContent",
47
- "thinkRow": "I17U7q_thinkRow"
40
+ "root": "I17U7q_root",
41
+ "disclosureRoot": "I17U7q_disclosureRoot",
42
+ "disclosureLeading": "I17U7q_disclosureLeading",
43
+ "thinkRow": "I17U7q_thinkRow",
44
+ "follow": "I17U7q_follow",
45
+ "disclosureChevronHover": "I17U7q_disclosureChevronHover",
46
+ "thinkBody": "I17U7q_thinkBody",
47
+ "thinkTitle": "I17U7q_thinkTitle"
48
48
  };
49
49
  //#endregion
50
50
  //#region src/client/AnimatedDisclosure.tsx
@@ -816,2112 +816,2129 @@ window.__ModuleLoader__.load({
816
816
  };
817
817
  }
818
818
  //#endregion
819
- //#region src/client/auto-collapse-controller.ts
819
+ //#region src/client/teleprompterGlide.ts
820
820
  /**
821
- * Auto-collapse coordinator: folds the work process of finished turns behind
822
- * one summary row so only the model's final answer stays visible.
821
+ * Conversation-port follow while an assistant reply streams.
823
822
  *
824
- * The Harness ChatView lays a conversation out as a flat list of flow items
825
- * (`[data-chat-flow] > [data-chat-flow-kind=…]`). This controller watches that
826
- * list, splits it into turn segments at `user`/`steering` boundaries closed by
827
- * `turn-tail`, and — once a segment has settled — hides its thinking rows,
828
- * tool/command/context seats and intermediate replies, leaving one clickable
829
- * `已处理 {时长}` row plus the final answer. Clicking the row expands the full
830
- * process again; the choice survives later renders because it lives in the
831
- * controller, not the DOM.
823
+ * A sub-stepped spring physics engine drives a float `animatedH`, rather
824
+ * than restarting native smooth-scroll animations as every glyph lands.
825
+ * Remaining lag rides a small compositor transform while the real scrollport
826
+ * stays at its floor. The transform is bounded by the measured paint gap
827
+ * before conversation chrome. This follower:
832
828
  *
833
- * Adapted from the standalone `dsh-auto-collapse` plugin (level-1 folding
834
- * only): no second-level chips, no merged thinking rows, no status-text
835
- * rewrite, no transition animation — the collapse decision itself stays
836
- * instantaneous and reversible.
829
+ * - marks programmatic writes via `data-follow-owned` for compatible hosts;
830
+ * - sets `overflow-anchor: none` so CSS scroll-anchoring does not snap;
831
+ * - restores `animatedH` in a ResizeObserver (before paint) so a layout
832
+ * pass cannot flash a snapped frame;
833
+ * - expresses safe lag as a compositor transform on message rows;
834
+ * - opens a speed-adaptive layout runway before fast output wraps, preserving
835
+ * the reference spring constants at every reveal speed;
836
+ * - catches up any lag that cannot fit before turn status / composer chrome,
837
+ * so fixed chrome never has to counter-shift and the host stays at-bottom;
838
+ * - never clips or overlays streamed text.
837
839
  *
838
- * React coexistence follows the upstream design: injected rows are plain
839
- * siblings/prepend children the vdom diff never claims, native rows are only
840
- * ever touched through an inline-`display` ledger that records the precise
841
- * original value and restores it on stop, session switch, or foreign takeover
842
- * (a written-value comparison plus an ownership sentinel catches another
843
- * script rewriting styles behind our back). A MutationObserver batches real
844
- * structure changes into rAF passes; a low-frequency self-rearming audit pass
845
- * reconciles unobservable style writes; a setTimeout fallback keeps passes
846
- * flowing when background tabs suspend rAF.
840
+ * A real reader gesture receives the effective visual position before the
841
+ * transform clears. Lifecycle completion instead settles at the floor.
842
+ *
843
+ * Directional wheel/touch intent unpins immediately; pointer/key input falls
844
+ * back to an upward scroll delta from the engine's own written position. A
845
+ * reader release re-acquires only after returning to the real floor.
847
846
  */
848
- const STYLE_ID = "dshss-auto-collapse-style";
849
- /** Class of the injected per-turn summary row; also consumed by the follow
850
- * engine so the row rides the same lag transform as native message rows. */
851
- const PROCESSED_CLASS = "dshss-processed";
852
- /** Inline custom property marking elements whose display this plugin owns. */
853
- const DISPLAY_OWNED_PROP = "--dshss-display-owned";
854
- /** Unobservable external style writes are reconciled by this audit cadence. */
855
- const AUDIT_TICK_MS = 1e3;
856
- /** Background-tab fallback that flushes a scheduled pass when rAF is frozen. */
857
- const PASS_FALLBACK_MS = 60;
858
847
  /**
859
- * Fold motion, deliberately minimal: layout changes land synchronously (the
860
- * display toggle happens in the same tick as the decision) and nothing
861
- * animates geometry at all — the earlier container-height squeeze was removed
862
- * because real sessions showed residual drift after the pin released (other
863
- * layout participants keep settling around the fold). What remains is one
864
- * paint-only flourish: the summary row fades its opacity in. A fold is
865
- * therefore a single reflow followed by a fade — the most stable shape
866
- * possible.
848
+ * Programmatic follow marker retained for hosts that recognize external
849
+ * scroll ownership. Current Harness also sees the write land at the floor.
867
850
  */
868
- const FOLD_ROW_FADE_MS = 160;
869
- const FOLD_EASE_OUT = "cubic-bezier(0.22, 0.61, 0.36, 1)";
870
- const ZH_COPY = {
871
- processedLabel: "已处理",
872
- expandTitle: "展开工作过程",
873
- collapseTitle: "收起工作过程",
874
- formatDuration: formatZhDuration
875
- };
876
- const EN_COPY = {
877
- processedLabel: "Processed",
878
- expandTitle: "Show the work process",
879
- collapseTitle: "Hide the work process",
880
- formatDuration: formatEnDuration
881
- };
882
- function resolveFoldCopy() {
883
- let lang = "";
884
- try {
885
- lang = document.documentElement.lang || navigator.language || "";
886
- } catch {
887
- lang = "";
888
- }
889
- return lang.toLowerCase().startsWith("zh") ? ZH_COPY : EN_COPY;
851
+ const FOLLOW_OWNED_ATTR = "data-follow-owned";
852
+ /** Sub-pixel settle threshold; clearing below this cannot produce a visible rebound. */
853
+ const FOLLOW_SETTLE_EPSILON_PX = .25;
854
+ /** Safe-lag occupancy band over which reveal pressure is progressively reduced. */
855
+ const FOLLOW_BACKPRESSURE_START_RATIO = .25;
856
+ const GESTURE_EVENTS = [
857
+ "wheel",
858
+ "touchstart",
859
+ "touchmove",
860
+ "touchend",
861
+ "touchcancel",
862
+ "pointerdown",
863
+ "keydown"
864
+ ];
865
+ /** Visible runway needed for the current reveal pressure. */
866
+ function computeFollowReserve(speedCps, runwayPx = 48) {
867
+ const available = Math.max(0, runwayPx);
868
+ if (available <= 0) return 0;
869
+ if (speedCps <= 90) return 0;
870
+ const normalized = Math.min(1, Math.max(0, (speedCps - 90) / 510));
871
+ const minimum = Math.min(available, 16);
872
+ return minimum + normalized * (available - minimum);
890
873
  }
891
- const FOLD_CSS = `
892
- .${PROCESSED_CLASS} {
893
- box-sizing: border-box;
894
- display: inline-flex;
895
- align-self: flex-start;
896
- width: fit-content;
897
- max-width: 100%;
898
- align-items: center;
899
- gap: 6px;
900
- padding: 2px 0;
901
- border: none;
902
- background: none;
903
- font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
904
- color: var(--dsw-alias-label-tertiary, rgba(127, 127, 127, 0.9));
905
- cursor: pointer;
906
- user-select: none;
907
- border-radius: 4px;
908
- transition: color 0.15s ease;
909
- }
910
- .${PROCESSED_CLASS}:hover {
911
- color: var(--dsw-alias-label-primary, inherit);
912
- background: transparent;
913
- }
914
- .${PROCESSED_CLASS}:focus-visible {
915
- outline: 2px solid var(--dsw-alias-state-focus-ring, rgba(77, 107, 254, 0.8));
916
- outline-offset: 2px;
917
- }
918
- .${PROCESSED_CLASS}-chevron {
919
- display: inline-flex;
920
- flex: none;
921
- width: 14px;
922
- height: 14px;
923
- opacity: 0.55;
924
- transform: rotate(-90deg);
925
- transition: transform 0.12s ease, opacity 0.1s ease;
926
- }
927
- .${PROCESSED_CLASS}:hover .${PROCESSED_CLASS}-chevron {
928
- opacity: 0.9;
929
- }
930
- .${PROCESSED_CLASS}[aria-expanded="true"] .${PROCESSED_CLASS}-chevron {
931
- transform: rotate(0deg);
932
- }
933
- @media (prefers-reduced-motion: reduce) {
934
- .${PROCESSED_CLASS},
935
- .${PROCESSED_CLASS}-chevron {
936
- transition: none;
937
- }
938
- }
939
- `;
940
- var AutoCollapseController = class {
941
- observer = null;
942
- raf = 0;
943
- timer = 0;
944
- auditTimer = 0;
945
- /** True between start() and stop(); the preference toggle re-enters both. */
946
- running = false;
947
- lastPassError = "";
948
- flow = null;
949
- /** Stable segment key → summary row + gesture state. */
950
- segmentStates = /* @__PURE__ */ new Map();
951
- /** First sighting of a running segment, for turns without a host duration. */
952
- runningSince = /* @__PURE__ */ new Map();
953
- /** Segments that settled once: a resumed run restarts local timing. */
954
- completedOnce = /* @__PURE__ */ new Set();
955
- /** Precise pre-plugin display values, restored on stop/switch/takeover. */
956
- originalDisplay = /* @__PURE__ */ new WeakMap();
957
- writtenDisplay = /* @__PURE__ */ new WeakMap();
958
- controlledDisplay = /* @__PURE__ */ new Set();
959
- /** Reply-presence verdicts cached per message until a mutation hits it. */
960
- bodyTextCache = /* @__PURE__ */ new WeakMap();
961
- dirtyMessages = /* @__PURE__ */ new Set();
962
- onVisibilityChange = () => {
963
- if (typeof document === "undefined" || document.hidden !== true) this.schedule();
874
+ /**
875
+ * Reveal-rate multiplier needed to retain one-wrap headroom for the spring.
876
+ * Throttling starts only after a quarter of the safe transform is occupied;
877
+ * a constrained paint lands at the minimum immediately so the next reveal
878
+ * commit cannot keep feeding an already-full visual buffer.
879
+ */
880
+ function computeFollowRevealScale(lagPx, capacityPx, constrained = false, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
881
+ if (constrained) return tuning.backpressureMinScale;
882
+ if (!Number.isFinite(capacityPx)) return 1;
883
+ if (capacityPx <= 0) return lagPx > 0 ? tuning.backpressureMinScale : 1;
884
+ const ratio = Math.min(1, Math.max(0, lagPx / capacityPx));
885
+ if (ratio >= .75) return tuning.backpressureMinScale;
886
+ const progress = Math.min(1, Math.max(0, (ratio - FOLLOW_BACKPRESSURE_START_RATIO) / .5));
887
+ const eased = progress * progress * (3 - 2 * progress);
888
+ return 1 - (1 - tuning.backpressureMinScale) * eased;
889
+ }
890
+ /**
891
+ * Semi-implicit spring integration with four substeps per <=32ms slice.
892
+ * @param dtMs - Frame delta in ms.
893
+ * @param input - Current visible lag and carried physics velocity.
894
+ * @returns The position advance, its fraction, and next velocity.
895
+ */
896
+ function computeFollowStep(dtMs, input, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
897
+ if (input.lag <= .1 || dtMs <= 0) return {
898
+ advancePx: 0,
899
+ lerpStep: 0,
900
+ velocityPxPerSec: 0
964
901
  };
965
- /** Re-run a pass immediately (settings changes, diagnostics, tests).
966
- * Reply-presence verdicts are dropped wholesale: manual refreshes are rare,
967
- * and a conservative rebuild beats a stale classification. */
968
- refresh() {
969
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
970
- this.dirtyMessages.clear();
971
- this.schedule();
972
- }
973
- start() {
974
- if (this.running) return;
975
- this.running = true;
976
- injectStyle();
977
- try {
978
- this.observer = new MutationObserver((records) => {
979
- if (this.shouldSchedule(records)) {
980
- this.markDirty(records);
981
- this.schedule();
982
- }
983
- });
984
- this.observer.observe(document.body, {
985
- childList: true,
986
- subtree: true,
987
- attributes: true,
988
- attributeFilter: ["data-state"],
989
- characterData: true
990
- });
991
- this.armAuditLoop();
992
- this.schedule();
993
- } catch (error) {
994
- this.reportError(error);
995
- throw error;
996
- }
997
- }
998
- stop() {
999
- this.running = false;
1000
- if (this.raf !== 0) cancelAnimationFrame(this.raf);
1001
- this.raf = 0;
1002
- if (this.timer !== 0) clearTimeout(this.timer);
1003
- this.timer = 0;
1004
- if (this.auditTimer !== 0) clearTimeout(this.auditTimer);
1005
- this.auditTimer = 0;
1006
- if (typeof document !== "undefined" && typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", this.onVisibilityChange);
1007
- this.observer?.disconnect();
1008
- this.observer = null;
1009
- this.switchFlow(null);
1010
- removeStyle();
1011
- }
1012
- /** Low-frequency reconciliation for style writes no observer can see. */
1013
- armAuditLoop() {
1014
- if (typeof document !== "undefined" && typeof document.addEventListener === "function") document.addEventListener("visibilitychange", this.onVisibilityChange);
1015
- this.rearmAudit();
902
+ let lag = input.lag;
903
+ let velocity = Math.max(0, input.velocityPxPerSec ?? 0);
904
+ const elapsedMs = Math.min(32, dtMs);
905
+ const slices = Math.max(1, Math.ceil(elapsedMs / 32));
906
+ const subDt = elapsedMs / 1e3 / slices / 4;
907
+ for (let slice = 0; slice < slices; slice += 1) for (let substep = 0; substep < 4; substep += 1) {
908
+ const acceleration = (tuning.springStiffness * lag - tuning.springDamping * velocity) / tuning.springMass;
909
+ velocity = Math.max(0, velocity + acceleration * subDt);
910
+ const advance = velocity * subDt;
911
+ if (advance >= lag) return {
912
+ advancePx: input.lag,
913
+ lerpStep: 1,
914
+ velocityPxPerSec: 0
915
+ };
916
+ lag -= advance;
1016
917
  }
1017
- rearmAudit() {
1018
- if (!this.running || this.auditTimer !== 0) return;
1019
- this.auditTimer = setTimeout(() => {
1020
- this.auditTimer = 0;
1021
- if (!this.running) return;
1022
- if (typeof document !== "undefined" && document.hidden === true) {
1023
- this.rearmAudit();
1024
- return;
1025
- }
1026
- this.schedule();
1027
- this.rearmAudit();
1028
- }, AUDIT_TICK_MS);
918
+ const advancePx = input.lag - lag;
919
+ return {
920
+ advancePx,
921
+ lerpStep: advancePx / input.lag,
922
+ velocityPxPerSec: velocity
923
+ };
924
+ }
925
+ /** Element whose resize signals flow growth for the before-paint restore. */
926
+ function resizeProxyOf(port) {
927
+ return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
928
+ }
929
+ /**
930
+ * Outermost message surfaces; nested tool rows ride their parent.
931
+ *
932
+ * Another plugin may insert its own element as a flow sibling of the Chat rows
933
+ * (meow-memory's fold bar is one; so is this plugin's own fold summary row).
934
+ * Such a row carries no `data-chat-anchor-key`, so selecting only anchored
935
+ * rows would shift the conversation while leaving the foreign row at its
936
+ * natural offset, letting the shifted rows paint over it. Every direct flow
937
+ * child therefore rides the same transform, keeping the visual order of the
938
+ * column intact.
939
+ */
940
+ function shiftSurfacesOf(port) {
941
+ const transcript = port.querySelector("[data-chat-transcript]");
942
+ if (transcript !== null) return [transcript];
943
+ const anchored = [...port.querySelectorAll("[data-chat-anchor-key]")].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
944
+ const flow = port.querySelector("[data-chat-flow]");
945
+ if (flow === null) return anchored;
946
+ const status = turnStatusOf(port);
947
+ const anchoredSet = new Set(anchored);
948
+ const __dbg = [...flow.children].filter((child) => child instanceof HTMLElement && child !== status && (anchoredSet.has(child) || child.querySelector("[data-chat-anchor-key]") === null));
949
+ console.log("[engine] reached end of shiftSurfacesOf, n=", __dbg.length);
950
+ globalThis.__dshssProbe = __dbg.map((e) => `${e.tagName}:${e.getAttribute("data-chat-anchor-key") ?? e.className}`);
951
+ return __dbg;
952
+ }
953
+ function currentShiftOf(element) {
954
+ return Number(/translate3d\(0(?:px)?,\s*(-?[\d.]+)px,\s*0(?:px)?\)/.exec(element.style.transform)?.[1] ?? 0);
955
+ }
956
+ function setShift(element, px) {
957
+ if (Math.abs(px) > .01) {
958
+ if (Math.abs(currentShiftOf(element) - px) <= .01 && element.style.willChange === "transform" && element.style.clipPath === "") return;
959
+ element.style.transform = `translate3d(0, ${px}px, 0)`;
960
+ element.style.willChange = "transform";
961
+ } else {
962
+ if (element.style.transform === "" && element.style.willChange === "" && element.style.clipPath === "") return;
963
+ element.style.transform = "";
964
+ element.style.willChange = "";
1029
965
  }
1030
- /** Body-level observations only need to wake us up near the active flow. */
1031
- shouldSchedule(records) {
1032
- if (records.length === 0 || this.flow === null || !this.flow.isConnected) return true;
1033
- return records.some((record) => nodeWithin(record.target, this.flow) || nodeWithin(this.flow, record.target));
966
+ element.style.clipPath = "";
967
+ }
968
+ function turnStatusOf(port) {
969
+ return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
970
+ }
971
+ /** Height committed by one newly mounted Chat row, including its flex gap. */
972
+ function entranceExtentOf(root) {
973
+ const row = root.closest("[data-chat-flow-key]") ?? root;
974
+ const rect = row.getBoundingClientRect();
975
+ const height = Math.max(0, rect.height, rect.bottom - rect.top, row.offsetHeight);
976
+ let previous = row.previousElementSibling;
977
+ while (previous instanceof HTMLElement) {
978
+ const previousRect = previous.getBoundingClientRect();
979
+ if (previousRect.height > 0 || previousRect.bottom > previousRect.top) return Math.max(height, rect.bottom - previousRect.bottom);
980
+ previous = previous.previousElementSibling;
1034
981
  }
1035
- /** Walk each record to its flow child so reply-cache invalidation stays
1036
- * targeted; anything unattributable invalidates conservatively. */
1037
- markDirty(records) {
1038
- const flow = this.flow;
1039
- if (flow === null || !flow.isConnected) return;
1040
- if (records.length === 0) {
1041
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
1042
- this.dirtyMessages.clear();
1043
- return;
1044
- }
1045
- for (const record of records) {
1046
- let current = record.target;
1047
- while (current !== null && current.parentNode !== flow) current = current.parentNode;
1048
- if (!(current instanceof HTMLElement)) {
1049
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
1050
- this.dirtyMessages.clear();
1051
- return;
1052
- }
1053
- this.dirtyMessages.add(current);
1054
- }
982
+ return height;
983
+ }
984
+ /**
985
+ * The plugin can be reinjected without replacing the conversation DOM. Keep
986
+ * runway ownership in the page realm so a fresh bundle adopts the existing
987
+ * margin instead of treating it as host layout and adding another 48px.
988
+ */
989
+ const FOLLOW_RUNWAYS_SYMBOL = Symbol.for("dsh-smooth-stream.follow-runways");
990
+ const followRunwayRegistry = globalThis;
991
+ const followRunways = followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] ?? /* @__PURE__ */ new WeakMap();
992
+ followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
993
+ const followPaintLimits = /* @__PURE__ */ new WeakMap();
994
+ function invalidatePaintLimit(port) {
995
+ followPaintLimits.delete(port);
996
+ }
997
+ /** Logical position and velocity survive a React owner handoff and finish. */
998
+ const followMotionStates = /* @__PURE__ */ new WeakMap();
999
+ function restoreRunway(port) {
1000
+ const runway = followRunways.get(port);
1001
+ if (runway === void 0) return;
1002
+ runway.element.style[runway.property] = runway.original;
1003
+ followRunways.delete(port);
1004
+ invalidatePaintLimit(port);
1005
+ }
1006
+ function isLegacyRunway(value) {
1007
+ if (value === "") return false;
1008
+ const terms = [...value.matchAll(/([\d.]+)px/g)];
1009
+ if (terms.length === 0 || value.replaceAll(/calc|px|[\d.+()\s]/g, "") !== "") return false;
1010
+ return terms.every(([, raw]) => {
1011
+ const px = Number(raw);
1012
+ return Number.isFinite(px) && px >= 48 && Math.abs(px % 48) <= Number.EPSILON;
1013
+ });
1014
+ }
1015
+ /** Remove unowned runway residue written by v0.3.3 and earlier bundles. */
1016
+ function migrateLegacyRunway(port, surfaces, status, composer) {
1017
+ if (followRunways.has(port)) return;
1018
+ let migrated = false;
1019
+ if (status !== null && isLegacyRunway(status.style.marginTop)) {
1020
+ status.style.marginTop = "";
1021
+ migrated = true;
1055
1022
  }
1056
- schedule() {
1057
- if (!this.running || this.raf !== 0) return;
1058
- this.raf = requestAnimationFrame(() => {
1059
- this.raf = 0;
1060
- if (this.timer !== 0) {
1061
- clearTimeout(this.timer);
1062
- this.timer = 0;
1063
- }
1064
- this.runPass();
1065
- });
1066
- if (this.timer !== 0) clearTimeout(this.timer);
1067
- this.timer = setTimeout(() => {
1068
- this.timer = 0;
1069
- if (this.raf !== 0) {
1070
- cancelAnimationFrame(this.raf);
1071
- this.raf = 0;
1072
- this.runPass();
1073
- }
1074
- }, PASS_FALLBACK_MS);
1023
+ const last = surfaces.at(-1);
1024
+ if (status === null && composer !== null && last !== void 0 && isLegacyRunway(last.style.marginBottom)) {
1025
+ last.style.marginBottom = "";
1026
+ migrated = true;
1075
1027
  }
1076
- runPass() {
1077
- try {
1078
- this.pass();
1079
- this.lastPassError = "";
1080
- } catch (error) {
1081
- this.reportError(error);
1082
- }
1028
+ if (migrated) invalidatePaintLimit(port);
1029
+ }
1030
+ function ensureRunway(port, surfaces, runwayPx = 48) {
1031
+ const status = turnStatusOf(port);
1032
+ const composer = port.querySelector("[data-composer-seat]");
1033
+ migrateLegacyRunway(port, surfaces, status, composer);
1034
+ const naturalHeight = Math.max(0, port.scrollHeight - runwayOffsetOf(port));
1035
+ if (runwayPx <= 0 || port.clientHeight <= 0 || naturalHeight <= port.clientHeight) {
1036
+ restoreRunway(port);
1037
+ return;
1083
1038
  }
1084
- reportError(error) {
1085
- const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1086
- if (message === this.lastPassError) return;
1087
- this.lastPassError = message;
1088
- console.error("[dsh-smooth-stream] auto-collapse pass failed", error);
1039
+ const target = status === null ? {
1040
+ element: composer === null ? void 0 : surfaces.at(-1),
1041
+ property: "marginBottom"
1042
+ } : {
1043
+ element: status,
1044
+ property: "marginTop"
1045
+ };
1046
+ if (target.element === void 0) {
1047
+ restoreRunway(port);
1048
+ return;
1089
1049
  }
1090
- pass() {
1091
- if (!this.running) return;
1092
- const nextFlow = findFlow();
1093
- if (nextFlow !== this.flow) this.switchFlow(nextFlow);
1094
- const flow = this.flow;
1095
- if (flow === null) return;
1096
- for (const el of this.dirtyMessages) this.bodyTextCache.delete(el);
1097
- this.dirtyMessages.clear();
1098
- const segments = buildSegments(flow, (el) => this.hasBodyCached(el));
1099
- const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
1100
- for (const segment of segments) {
1101
- if (!segment.running) continue;
1102
- if (this.completedOnce.has(segment.key)) {
1103
- this.completedOnce.delete(segment.key);
1104
- this.runningSince.delete(segment.key);
1105
- }
1106
- if (!this.runningSince.has(segment.key)) this.runningSince.set(segment.key, Date.now());
1107
- }
1108
- const completedKeys = /* @__PURE__ */ new Set();
1109
- for (const segment of segments) {
1110
- if (!segment.closed || segment.running || !segment.hasWork) continue;
1111
- completedKeys.add(segment.key);
1112
- this.completedOnce.add(segment.key);
1113
- let state = this.segmentStates.get(segment.key);
1114
- if (state === void 0) {
1115
- state = {
1116
- key: segment.key,
1117
- row: null,
1118
- expanded: false,
1119
- snapshot: segment
1120
- };
1121
- this.segmentStates.set(segment.key, state);
1122
- } else state.snapshot = segment;
1123
- const started = this.runningSince.get(segment.key);
1124
- const parsed = segment.boundary === null ? void 0 : parseTurnDuration(segment.boundary);
1125
- if (parsed !== void 0) state.duration = parsed;
1126
- else if (state.duration === void 0 && started !== void 0) state.duration = Date.now() - started;
1127
- if (state.row === null || !state.row.isConnected) state.row = this.createProcessedRow(state);
1128
- this.syncProcessedRow(state);
1129
- }
1130
- for (const [key, state] of [...this.segmentStates]) {
1131
- if (completedKeys.has(key)) continue;
1132
- state.row?.remove();
1133
- this.segmentStates.delete(key);
1134
- }
1135
- const desiredHidden = /* @__PURE__ */ new Set();
1136
- for (const segment of segments) {
1137
- const state = this.segmentStates.get(segment.key);
1138
- if (state !== void 0 && !state.expanded) {
1139
- for (const seat of segment.hideSeats) this.hideElement(seat, desiredHidden);
1140
- for (const row of segment.finalThinkRows) this.hideElement(row, desiredHidden);
1141
- if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
1142
- } else {
1143
- for (const seat of segment.hideSeats) this.restoreElement(seat);
1144
- for (const row of segment.finalThinkRows) this.restoreElement(row);
1145
- if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
1146
- }
1147
- }
1148
- for (const segment of segments) {
1149
- if (!hasVisibleSegmentWork(segment)) {
1150
- const state = this.segmentStates.get(segment.key);
1151
- if (state !== void 0 && state.row !== null) {
1152
- state.row.remove();
1153
- state.row = null;
1154
- }
1155
- }
1156
- for (const seat of segment.hideSeats) this.retainDisplayControl(seat, desiredHidden);
1157
- for (const row of segment.finalThinkRows) this.retainDisplayControl(row, desiredHidden);
1158
- if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
1159
- }
1160
- this.restoreUnusedDisplays(desiredHidden);
1161
- for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
1162
- for (const key of [...this.runningSince.keys()]) if (!liveSegmentKeys.has(key)) this.runningSince.delete(key);
1163
- for (const key of [...this.completedOnce]) if (!liveSegmentKeys.has(key)) this.completedOnce.delete(key);
1164
- }
1165
- /** Flow swap means a session switch: fully restore the old tree, rebuild. */
1166
- switchFlow(next) {
1167
- if (next === this.flow) return;
1168
- for (const state of this.segmentStates.values()) state.row?.remove();
1169
- this.segmentStates.clear();
1170
- this.runningSince.clear();
1171
- this.completedOnce.clear();
1172
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
1173
- this.dirtyMessages.clear();
1174
- this.restoreAllDisplays();
1175
- this.flow = next;
1176
- }
1177
- /** Keep already-controlled elements in the desired-hidden set so the sweep
1178
- * does not flip them back mid-flight of an external change. */
1179
- retainDisplayControl(el, desiredHidden) {
1180
- if (this.controlledDisplay.has(el)) desiredHidden.add(el);
1181
- }
1182
- /** Restore every element the ledger controls that this pass no longer wants
1183
- * hidden; elements absent from `desired` come back to their true display. */
1184
- restoreUnusedDisplays(desired) {
1185
- for (const el of [...this.controlledDisplay]) if (!desired.has(el)) this.restoreElement(el);
1186
- }
1187
- /** Restore every element still under the display ledger, then retire the
1188
- * ledger itself (stop / session switch). */
1189
- restoreAllDisplays() {
1190
- for (const el of [...this.controlledDisplay]) this.restoreElement(el);
1191
- this.controlledDisplay.clear();
1192
- this.originalDisplay = /* @__PURE__ */ new WeakMap();
1193
- this.writtenDisplay = /* @__PURE__ */ new WeakMap();
1194
- }
1195
- createProcessedRow(state) {
1196
- const row = createProcessedRowElement(state.duration);
1197
- row.addEventListener("click", () => {
1198
- state.expanded = !state.expanded;
1199
- this.syncProcessedRow(state);
1200
- this.schedule();
1201
- });
1202
- return row;
1203
- }
1204
- syncProcessedRow(state) {
1205
- const row = state.row;
1206
- if (row === null) return;
1207
- const copy = resolveFoldCopy();
1208
- const text = row.firstElementChild;
1209
- const label = state.duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(state.duration)}`;
1210
- if (text !== null && text.textContent !== label) text.textContent = label;
1211
- const expanded = String(state.expanded);
1212
- if (row.getAttribute("aria-expanded") !== expanded) row.setAttribute("aria-expanded", expanded);
1213
- const title = state.expanded ? copy.collapseTitle : copy.expandTitle;
1214
- if (row.title !== title) row.title = title;
1215
- }
1216
- placeProcessedRow(flow, state) {
1217
- const row = state.row;
1218
- if (row === null) return;
1219
- if (!state.snapshot.hasWork || !hasVisibleSegmentWork(state.snapshot)) {
1220
- row.remove();
1221
- state.row = null;
1222
- return;
1223
- }
1224
- let target = state.snapshot.firstWork ?? state.snapshot.finalStep ?? state.snapshot.boundary;
1225
- if (target === null || target.parentElement !== flow) {
1226
- row.remove();
1227
- state.row = null;
1228
- return;
1229
- }
1230
- if (row.parentElement !== flow || row.nextElementSibling !== target) {
1231
- target.before(row);
1232
- this.revealVisualOnce(row);
1233
- }
1234
- }
1235
- hasBodyCached(el) {
1236
- const cached = this.bodyTextCache.get(el);
1237
- if (cached !== void 0) return cached;
1238
- const value = hasBodyContent(el);
1239
- this.bodyTextCache.set(el, value);
1240
- return value;
1241
- }
1242
- /** True when the recorded display was changed (or the ownership sentinel
1243
- * wiped) behind the ledger's back; callers hand control back to reality. */
1244
- displayForeign(el) {
1245
- const written = this.writtenDisplay.get(el);
1246
- if (written === void 0) return false;
1247
- return el.style.getPropertyValue(DISPLAY_OWNED_PROP) === "" || el.style.display !== written;
1248
- }
1249
- releaseDisplayLedger(el) {
1250
- this.originalDisplay.delete(el);
1251
- this.writtenDisplay.delete(el);
1252
- this.controlledDisplay.delete(el);
1253
- el.style.removeProperty(DISPLAY_OWNED_PROP);
1254
- }
1255
- hideElement(el, desired) {
1256
- desired.add(el);
1257
- if (!this.originalDisplay.has(el) && !isDisplayed(el)) return;
1258
- if (!this.originalDisplay.has(el) || this.displayForeign(el)) {
1259
- this.originalDisplay.set(el, el.style.display);
1260
- this.writtenDisplay.set(el, el.style.display);
1261
- el.style.setProperty(DISPLAY_OWNED_PROP, "1");
1262
- }
1263
- this.controlledDisplay.add(el);
1264
- if (el.style.display === "none") return;
1265
- el.style.display = "none";
1266
- this.writtenDisplay.set(el, "none");
1267
- }
1268
- restoreElement(el) {
1269
- if (!this.originalDisplay.has(el)) return;
1270
- if (this.displayForeign(el)) {
1271
- this.releaseDisplayLedger(el);
1272
- return;
1273
- }
1274
- const original = this.originalDisplay.get(el);
1275
- if (el.style.display !== original) el.style.display = original;
1276
- this.releaseDisplayLedger(el);
1277
- }
1278
- /** Whether WAAPI exists and the user has not asked for reduced motion. */
1279
- canAnimate(el) {
1280
- if (typeof el.animate !== "function") return false;
1281
- try {
1282
- if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
1283
- } catch {
1284
- return false;
1285
- }
1286
- return true;
1287
- }
1288
- /** First-appearance flourish for the plugin-owned summary row: a pure
1289
- * opacity fade. Paint-only, so it cannot shift anyone's layout. */
1290
- revealVisualOnce(row) {
1291
- if (row.dataset.dshssShown === "1") return;
1292
- row.dataset.dshssShown = "1";
1293
- if (!this.canAnimate(row)) return;
1294
- row.animate([{ opacity: "0" }, { opacity: "1" }], {
1295
- duration: FOLD_ROW_FADE_MS,
1296
- easing: FOLD_EASE_OUT
1297
- });
1298
- }
1299
- };
1300
- /** Visible chat flow container, falling back to the first one rendered. */
1301
- function findFlow() {
1302
- const flows = document.querySelectorAll("[data-chat-flow]");
1303
- for (const flow of flows) if (flow.offsetParent !== null || flow.getBoundingClientRect().width > 0) return flow;
1304
- return flows[0] ?? null;
1305
- }
1306
- /** parentNode walk that also accepts Text mutation targets. */
1307
- function nodeWithin(node, ancestor) {
1308
- for (let current = node; current !== null; current = current.parentNode) if (current === ancestor) return true;
1309
- return false;
1310
- }
1311
- /** Top-level message order, excluding this plugin's own summary rows. */
1312
- function flowItems(flow) {
1313
- return [...flow.children].filter((el) => el instanceof HTMLElement && !el.classList.contains("dshss-processed"));
1050
+ const element = target.element;
1051
+ const current = followRunways.get(port);
1052
+ if (current?.element === element && current.property === target.property && current.requestedPx === runwayPx) return;
1053
+ restoreRunway(port);
1054
+ const beforeHeight = port.scrollHeight;
1055
+ const original = element.style[target.property];
1056
+ element.style[target.property] = original === "" ? `${runwayPx}px` : `calc(${original} + ${runwayPx}px)`;
1057
+ const offset = Math.max(0, port.scrollHeight - beforeHeight);
1058
+ followRunways.set(port, {
1059
+ element,
1060
+ offset,
1061
+ property: target.property,
1062
+ original,
1063
+ requestedPx: runwayPx
1064
+ });
1065
+ invalidatePaintLimit(port);
1314
1066
  }
1315
- function isDisplayed(el) {
1316
- if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
1317
- return el.style.display !== "none";
1067
+ function runwayOffsetOf(port) {
1068
+ return followRunways.get(port)?.offset ?? 0;
1318
1069
  }
1319
- function stableElementKey(el, fallbackIndex) {
1320
- const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
1321
- return `${kind}:${el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`}`;
1070
+ /** Available paint room below the last message before fixed conversation chrome. */
1071
+ function safeShiftLimit(port, surfaces) {
1072
+ const last = surfaces.at(-1);
1073
+ if (last === void 0) return 0;
1074
+ const status = turnStatusOf(port);
1075
+ const composer = port.querySelector("[data-composer-seat]");
1076
+ const cached = followPaintLimits.get(port);
1077
+ 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;
1078
+ const ceiling = [status, composer].filter((element) => element !== null).map((element) => ({
1079
+ element,
1080
+ rect: element.getBoundingClientRect()
1081
+ })).filter(({ rect }) => Number.isFinite(rect.top) && Number.isFinite(rect.bottom) && rect.bottom > rect.top).sort((first, second) => first.rect.top - second.rect.top)[0];
1082
+ if (ceiling === void 0) return status === null && composer === null ? Number.POSITIVE_INFINITY : runwayOffsetOf(port);
1083
+ const ceilingTop = ceiling.rect.top - currentShiftOf(ceiling.element);
1084
+ const naturalBottom = last.getBoundingClientRect().bottom - currentShiftOf(last);
1085
+ const limit = Math.max(0, ceilingTop - naturalBottom - 1);
1086
+ followPaintLimits.set(port, {
1087
+ clientHeight: port.clientHeight,
1088
+ limit,
1089
+ measuredAtMs: performance.now(),
1090
+ composer,
1091
+ status,
1092
+ surface: last
1093
+ });
1094
+ return limit;
1322
1095
  }
1323
- /** Kinds that can open a history stretch lacking an explicit user marker. */
1324
- function opensTurnWork(el) {
1325
- const kind = el.getAttribute("data-chat-flow-kind");
1326
- return kind === "assistant-step" || kind === "assistant" || kind === "tool-call" || kind === "command" || kind === "manual-compaction";
1096
+ function setFollowScrollTop(port, nextTop) {
1097
+ if (Math.abs(port.scrollTop - nextTop) > .01) port.scrollTop = nextTop;
1098
+ followScrollLedgers.set(port, port.scrollTop);
1099
+ const ownedTop = String(port.scrollTop);
1100
+ if (port.getAttribute(FOLLOW_OWNED_ATTR) !== ownedTop) port.setAttribute(FOLLOW_OWNED_ATTR, ownedTop);
1327
1101
  }
1328
1102
  /**
1329
- * Split the flow into turn segments. `user`/`steering` rows close the previous
1330
- * segment and open the next; `turn-tail` closes and resets the opener. Leading
1331
- * context before the first user row joins that user's segment, matching how
1332
- * the host presents context injection.
1333
- */
1334
- function buildSegments(flow, hasBody) {
1335
- const items = flowItems(flow);
1336
- const itemIndex = new Map(items.map((el, index) => [el, index]));
1337
- const snapshots = [];
1338
- let contentStart = 0;
1339
- let startMarker = null;
1340
- const append = (end, boundary, closed) => {
1341
- if (end < contentStart) return;
1342
- const range = items.slice(contentStart, end);
1343
- const hideSeats = /* @__PURE__ */ new Set();
1344
- const bodySteps = [];
1345
- let running = false;
1346
- for (const el of range) {
1347
- const classified = classifyItem(el, hasBody);
1348
- if (classified.fold === "body") bodySteps.push(el);
1349
- else if (classified.fold === "seat") hideSeats.add(el);
1350
- if (classified.running) running = true;
1351
- }
1352
- const finalStep = bodySteps.at(-1) ?? null;
1353
- const middleSteps = new Set(bodySteps.slice(0, -1));
1354
- for (const step of middleSteps) hideSeats.add(step);
1355
- const finalThinkRows = finalStep === null ? [] : thinkRowsIn(finalStep);
1356
- const firstWork = range.find((el) => hideSeats.has(el)) ?? finalStep;
1357
- const identity = startMarker ?? range.find((el) => opensTurnWork(el)) ?? boundary;
1358
- const identityIndex = identity === null ? contentStart : itemIndex.get(identity) ?? contentStart;
1359
- const key = `${startMarker === null ? "leading" : "segment"}:${identity === null ? `open:${contentStart}` : stableElementKey(identity, identityIndex)}`;
1360
- snapshots.push({
1361
- key,
1362
- boundary,
1363
- startMarker,
1364
- hideSeats,
1365
- middleSteps,
1366
- finalStep,
1367
- finalThinkRows,
1368
- firstWork,
1369
- closed,
1370
- running,
1371
- hasWork: hideSeats.size > 0 || finalThinkRows.length > 0
1372
- });
1373
- };
1374
- items.forEach((el, index) => {
1375
- const kind = el.getAttribute("data-chat-flow-kind");
1376
- if (kind === "user" || kind === "steering") {
1377
- if (startMarker !== null) {
1378
- append(index, el, true);
1379
- contentStart = index + 1;
1380
- } else if (items.slice(contentStart, index).some((el) => opensTurnWork(el))) {
1381
- append(index, el, true);
1382
- contentStart = index + 1;
1383
- }
1384
- startMarker = el;
1385
- return;
1386
- }
1387
- if (kind === "turn-tail") {
1388
- append(index, el, true);
1389
- contentStart = index + 1;
1390
- startMarker = null;
1391
- }
1392
- });
1393
- if (contentStart < items.length) append(items.length, null, false);
1394
- return snapshots;
1103
+ * Last scrollTop this engine wrote or accepted, per port. Reader intent is a
1104
+ * real upward delta from this ledger; a key press or touch while pinned
1105
+ * (typing in the composer) must not release the pin, because a released pin
1106
+ * can never re-acquire while content streams away from the reader position.
1107
+ */
1108
+ const followScrollLedgers = /* @__PURE__ */ new WeakMap();
1109
+ const followActivityAt = /* @__PURE__ */ new WeakMap();
1110
+ /** Whether this port was owned recently enough to identify a closing tail row. */
1111
+ function hasRecentConversationFollow(port, windowMs = 250) {
1112
+ const last = followActivityAt.get(port);
1113
+ return last !== void 0 && performance.now() - last <= windowMs;
1114
+ }
1115
+ function readerScrolledUp(port) {
1116
+ return port.scrollTop < (followScrollLedgers.get(port) ?? 0) - 8;
1395
1117
  }
1396
1118
  /**
1397
- * Classify one top-level flow item against the Harness's own node vocabulary.
1398
- * Reply-bearing assistant messages become `body` candidates (last one per
1399
- * segment wins as the final answer); core work process — tool-call seats,
1400
- * command/compaction cards, context injection, thinking-only messages —
1401
- * becomes a hideable `seat`. Everything else is skipped untouched: boundaries,
1402
- * empty decorations, and crucially any kind this module does not own, so rows
1403
- * contributed by other plugins' slots are never folded or hidden.
1119
+ * Paint a bounded visual lag and return the effective logical extent.
1120
+ *
1121
+ * This is the final geometry invariant, not merely an animation preference:
1122
+ * any lag beyond the real gap to status/composer chrome is caught up in the
1123
+ * same frame. Carrying that excess in `scrollTop` would move the transcript
1124
+ * toward fixed chrome and also make the host expose jump-to-bottom.
1404
1125
  */
1405
- function classifyItem(el, hasBody) {
1406
- const kind = el.getAttribute("data-chat-flow-kind");
1407
- if (kind === "user" || kind === "steering" || kind === "turn-tail") return {
1408
- fold: "skip",
1409
- running: false
1410
- };
1411
- if (kind === "assistant-step" || kind === "assistant") {
1412
- if (hasBody(el)) return {
1413
- fold: "body",
1414
- running: subtreeRunning(el)
1415
- };
1416
- if (thinkRowsIn(el).length > 0) return {
1417
- fold: "seat",
1418
- running: subtreeRunning(el)
1419
- };
1420
- return {
1421
- fold: "skip",
1422
- running: false
1423
- };
1424
- }
1425
- if (kind === "tool-call") {
1426
- const rows = topCallRowsIn(el);
1427
- if (rows.length === 0 || !rows.every((row) => hasNativeCardChrome(row))) return {
1428
- fold: "skip",
1429
- running: subtreeRunning(el)
1430
- };
1431
- return {
1432
- fold: "seat",
1433
- running: subtreeRunning(el)
1434
- };
1126
+ function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 48) {
1127
+ const surfaces = shiftSurfacesOf(port);
1128
+ ensureRunway(port, surfaces, runwayPx);
1129
+ const contentHeight = Math.max(0, port.scrollHeight);
1130
+ const runwayOffset = runwayOffsetOf(port);
1131
+ const targetHeight = Math.max(0, contentHeight - runwayOffset);
1132
+ const floor = Math.max(0, contentHeight - port.clientHeight);
1133
+ const extent = Math.min(targetHeight, Math.max(0, animatedH));
1134
+ if (port.style.overflowAnchor !== "none") port.style.overflowAnchor = "none";
1135
+ if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
1136
+ if (floor <= 0) {
1137
+ setFollowScrollTop(port, 0);
1138
+ followMotionStates.set(port, {
1139
+ capacityPx: Number.POSITIVE_INFINITY,
1140
+ constrained: false,
1141
+ extent: targetHeight,
1142
+ lagPx: 0,
1143
+ reservePx: 0,
1144
+ velocityPxPerSec: 0
1145
+ });
1146
+ for (const surface of surfaces) setShift(surface, 0);
1147
+ const status = turnStatusOf(port);
1148
+ if (status !== null) setShift(status, 0);
1149
+ return targetHeight;
1435
1150
  }
1436
- if (kind === "context" || kind === "command" || kind === "manual-compaction") return {
1437
- fold: "seat",
1438
- running: subtreeRunning(el)
1439
- };
1440
- return {
1441
- fold: "skip",
1442
- running: false
1443
- };
1151
+ setFollowScrollTop(port, floor);
1152
+ const limit = floor > 0 ? safeShiftLimit(port, surfaces) : 0;
1153
+ const visibleReserve = Math.min(runwayOffset, Math.max(0, reservePx));
1154
+ const baselineShift = runwayOffset - visibleReserve;
1155
+ const requestedLag = Math.max(0, targetHeight - extent);
1156
+ const shift = Math.min(baselineShift + requestedLag, Math.max(0, limit));
1157
+ const effectiveLag = Math.max(0, shift - baselineShift);
1158
+ const capacityPx = Math.max(0, limit - baselineShift);
1159
+ const effectiveExtent = targetHeight - effectiveLag;
1160
+ followMotionStates.set(port, {
1161
+ capacityPx,
1162
+ constrained: requestedLag > effectiveLag + FOLLOW_SETTLE_EPSILON_PX,
1163
+ extent: effectiveExtent,
1164
+ lagPx: effectiveLag,
1165
+ reservePx: visibleReserve,
1166
+ velocityPxPerSec
1167
+ });
1168
+ for (const surface of surfaces) setShift(surface, shift);
1169
+ const status = turnStatusOf(port);
1170
+ if (status !== null) setShift(status, 0);
1171
+ return effectiveExtent;
1444
1172
  }
1445
- /** Native work-card shape: every core card family stamps `data-variant`
1446
- * together with `data-state`, whether composed through ToolRow or hand-rolled
1447
- * (Bash). Custom toolviews use their own namespaced attributes instead. */
1448
- function hasNativeCardChrome(row) {
1449
- return row.querySelector("[data-variant][data-state]") !== null;
1173
+ function clearMotion(port) {
1174
+ port.removeAttribute(FOLLOW_OWNED_ATTR);
1175
+ port.style.overflowAnchor = "";
1176
+ port.style.scrollBehavior = "";
1177
+ for (const surface of shiftSurfacesOf(port)) setShift(surface, 0);
1178
+ const status = turnStatusOf(port);
1179
+ if (status !== null) setShift(status, 0);
1450
1180
  }
1451
- /** Whether anything inside the item reports a running lifecycle. Deliberately
1452
- * broad: staying "running" too long only delays a collapse, collapsing early
1453
- * would cut off live output. Native cards stamp `data-state` next to
1454
- * `data-tool`/`data-variant`; custom views use their own vocabulary and are
1455
- * intentionally invisible here. */
1456
- function subtreeRunning(el) {
1457
- if (el.getAttribute("data-state") === "running") return true;
1458
- for (const root of el.querySelectorAll("[data-tool][data-state], [data-variant][data-state]")) if (root.getAttribute("data-state") === "running") return true;
1459
- return false;
1181
+ function clearVisual(port) {
1182
+ clearMotion(port);
1183
+ restoreRunway(port);
1184
+ followMotionStates.delete(port);
1185
+ invalidatePaintLimit(port);
1460
1186
  }
1461
- function hasVisibleSegmentWork(segment) {
1462
- const candidates = [...segment.hideSeats, ...segment.middleSteps];
1463
- if (segment.startMarker !== null) candidates.push(segment.startMarker);
1464
- if (segment.finalStep !== null) candidates.push(segment.finalStep);
1465
- return candidates.some(isDisplayed);
1187
+ /** Keep an already-promoted surface at zero until one stable final paint lands. */
1188
+ function holdCompositorAtRest(element) {
1189
+ element.style.transform = "translate3d(0, 0px, 0)";
1190
+ element.style.willChange = "transform";
1191
+ element.style.clipPath = "";
1466
1192
  }
1467
- /** Top-level tool card rows of a seat (excluding sub-dispatch nests). */
1468
- function topCallRowsIn(el) {
1469
- const rows = [];
1470
- for (const row of el.querySelectorAll("[data-chat-call-id]")) {
1471
- if (row.closest("[data-subcalls]") !== null) continue;
1472
- if (row.closest("[data-chat-call-id]") !== row) continue;
1473
- rows.push(row);
1193
+ /** Remove equal offsets, land on the floor, then retire the compositor quietly. */
1194
+ function finishAtNaturalFloor(port) {
1195
+ const surfaces = shiftSurfacesOf(port);
1196
+ const status = turnStatusOf(port);
1197
+ const promoted = [...surfaces, ...status === null ? [] : [status]].filter((element) => element.style.transform !== "" || element.style.willChange === "transform");
1198
+ const promotedSet = new Set(promoted);
1199
+ restoreRunway(port);
1200
+ settleAtFloor(port);
1201
+ port.removeAttribute(FOLLOW_OWNED_ATTR);
1202
+ port.style.overflowAnchor = "";
1203
+ port.style.scrollBehavior = "";
1204
+ for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
1205
+ else setShift(surface, 0);
1206
+ if (status !== null) {
1207
+ if (promotedSet.has(status)) holdCompositorAtRest(status);
1208
+ else setShift(status, 0);
1474
1209
  }
1475
- return rows;
1210
+ followMotionStates.delete(port);
1211
+ if (promoted.length === 0) return;
1212
+ requestAnimationFrame(() => {
1213
+ requestAnimationFrame(() => {
1214
+ if (port.hasAttribute(FOLLOW_OWNED_ATTR)) return;
1215
+ for (const element of promoted) if (Math.abs(currentShiftOf(element)) <= .01) setShift(element, 0);
1216
+ });
1217
+ });
1476
1218
  }
1477
- /** Reasoning rows of a message: native think variants outside tool cards. */
1478
- function thinkRowsIn(el) {
1479
- const rows = [];
1480
- for (const row of el.querySelectorAll("[data-variant=\"think\"]:not([data-tool])")) {
1481
- if (row.closest("[data-chat-call-id]") !== null) continue;
1482
- if (row.closest("[data-subcalls]") !== null) continue;
1483
- rows.push(row);
1484
- }
1485
- return rows;
1219
+ function settleAtFloor(port) {
1220
+ setFollowScrollTop(port, Math.max(0, port.scrollHeight - port.clientHeight));
1486
1221
  }
1222
+ /** Only the newest active follower may write one port's shared visual state. */
1223
+ const followLeaders = /* @__PURE__ */ new WeakMap();
1224
+ let followGeneration = 0;
1487
1225
  /**
1488
- * Reply detection beyond reasoning rows and tool cards: any non-empty text or
1489
- * media counts, because Markdown paragraphs necessarily carry text nodes.
1490
- * Rendered markdown class names are build-time hashes, so structure-based
1491
- * detection is the only stable contract.
1492
- */
1493
- function hasBodyContent(el) {
1494
- const kind = el.getAttribute("data-chat-flow-kind");
1495
- if (kind === "command" || kind === "manual-compaction") return false;
1496
- if (hasBodyText(el)) return true;
1497
- const excluded = "[data-variant=\"think\"], [data-chat-call-id]";
1498
- for (const media of el.querySelectorAll("img, video, audio, canvas")) if (media.closest(excluded) === null) return true;
1499
- return false;
1500
- }
1501
- function hasBodyText(el) {
1502
- const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
1503
- let node;
1504
- while ((node = walker.nextNode()) !== null) {
1505
- if (node.data.trim() === "") continue;
1506
- const parent = node.parentElement;
1507
- if (parent !== null && parent.closest("[data-variant=\"think\"], [data-chat-call-id], [data-variant=\"others\"][data-state]") !== null) continue;
1508
- return true;
1509
- }
1510
- return false;
1511
- }
1512
- /**
1513
- * Official turn duration from the closing element: localized tails read
1514
- * `用时 33秒` / `用时 2分05秒` or `Ran for 33s` / `Ran for 2m 05s`; newer
1515
- * tails only carry an end timestamp, which is diffed against the opening user
1516
- * row's timestamp.
1517
- */
1518
- function parseTurnDuration(boundary) {
1519
- const text = boundary.textContent ?? "";
1520
- const zh = text.match(/用时\s*(\d+)分(\d+)秒|用时\s*(\d+)秒/);
1521
- if (zh !== null) {
1522
- if (zh[1] !== void 0 && zh[2] !== void 0) return Number(zh[1]) * 6e4 + Number(zh[2]) * 1e3;
1523
- if (zh[3] !== void 0) return Number(zh[3]) * 1e3;
1524
- return;
1525
- }
1526
- const en = text.match(/Ran for\s*(?:(\d+)m\s*)?(\d+)s/);
1527
- if (en !== null) return ((en[1] === void 0 ? 0 : Number(en[1])) * 60 + Number(en[2])) * 1e3;
1528
- const end = parseTimeText(text);
1529
- const start = findTurnStart(boundary);
1530
- if (end !== void 0 && start !== void 0 && end > start) return end - start;
1531
- }
1532
- /** DSH timestamp text (`8月14日 21:56` / `2026年8月14日 22:11`) → epoch ms. */
1533
- function parseTimeText(text) {
1534
- const m = text.match(/(?:(\d{4})年)?(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})/);
1535
- if (m === null) return void 0;
1536
- const year = m[1] !== void 0 ? Number(m[1]) : (/* @__PURE__ */ new Date()).getFullYear();
1537
- const t = new Date(year, Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5])).getTime();
1538
- return Number.isNaN(t) ? void 0 : t;
1539
- }
1540
- /** Latest turn-opening timestamp at or before the boundary element. */
1541
- function findTurnStart(boundary) {
1542
- const flow = boundary.parentElement;
1543
- if (flow === null) return void 0;
1544
- let best = null;
1545
- for (const s of flow.querySelectorAll("[class*=\"timeStart\"]")) {
1546
- const pos = s.compareDocumentPosition(boundary);
1547
- if ((pos & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 || (pos & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0 || s === boundary) best = s;
1548
- else break;
1549
- }
1550
- if (best === null) return void 0;
1551
- return parseTimeText(best.textContent ?? "");
1552
- }
1553
- /** Compact duration pieces shared by both locales: seconds under a minute,
1554
- * whole minutes drop the seconds, hours drop to minute granularity. */
1555
- function durationParts(ms) {
1556
- const s = Math.round(ms / 1e3);
1557
- return {
1558
- h: Math.floor(s / 3600),
1559
- m: Math.floor(s % 3600 / 60),
1560
- s: s % 60
1561
- };
1562
- }
1563
- /** 毫秒 → 中文紧凑时长(14秒 / 2分05秒 / 15分 / 3小时2分)。 */
1564
- function formatZhDuration(ms) {
1565
- const { h, m, s } = durationParts(ms);
1566
- if (h > 0) return m > 0 ? `${h}小时${m}分` : `${h}小时`;
1567
- if (s < 60 && m === 0 && h === 0) return `${s}秒`;
1568
- if (s === 0) return `${m}分`;
1569
- return `${m}分${String(s).padStart(2, "0")}秒`;
1570
- }
1571
- /** Milliseconds → compact English duration (14s / 2m05s / 15m / 3h2m). */
1572
- function formatEnDuration(ms) {
1573
- const { h, m, s } = durationParts(ms);
1574
- if (h > 0) return m > 0 ? `${h}h${m}m` : `${h}h`;
1575
- if (m === 0 && h === 0) return `${s}s`;
1576
- if (s === 0) return `${m}m`;
1577
- return `${m}m${String(s).padStart(2, "0")}s`;
1578
- }
1579
- /** Native disclosure chevron (IconChevronDownOutline14, 14x14). */
1580
- const CHEVRON_PATH = "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z";
1581
- function createChevronIcon(className) {
1582
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1583
- svg.setAttribute("width", "14");
1584
- svg.setAttribute("height", "14");
1585
- svg.setAttribute("class", className);
1586
- svg.setAttribute("viewBox", "0 0 14 14");
1587
- svg.setAttribute("fill", "none");
1588
- svg.setAttribute("aria-hidden", "true");
1589
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1590
- path.setAttribute("d", CHEVRON_PATH);
1591
- path.setAttribute("fill", "currentColor");
1592
- svg.appendChild(path);
1593
- return svg;
1594
- }
1595
- function createProcessedRowElement(duration) {
1596
- const copy = resolveFoldCopy();
1597
- const btn = document.createElement("button");
1598
- btn.type = "button";
1599
- btn.className = PROCESSED_CLASS;
1600
- btn.setAttribute("aria-expanded", "false");
1601
- const text = document.createElement("span");
1602
- text.textContent = duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(duration)}`;
1603
- btn.append(text, createChevronIcon(`${PROCESSED_CLASS}-chevron`));
1604
- btn.title = copy.expandTitle;
1605
- return btn;
1606
- }
1607
- function injectStyle() {
1608
- if (document.getElementById(STYLE_ID) !== null) return;
1609
- const style = document.createElement("style");
1610
- style.id = STYLE_ID;
1611
- style.textContent = FOLD_CSS;
1612
- document.head.appendChild(style);
1613
- }
1614
- function removeStyle() {
1615
- document.getElementById(STYLE_ID)?.remove();
1616
- }
1617
- //#endregion
1618
- //#region src/client/teleprompterGlide.ts
1619
- /**
1620
- * Conversation-port follow while an assistant reply streams.
1621
- *
1622
- * A sub-stepped spring physics engine drives a float `animatedH`, rather
1623
- * than restarting native smooth-scroll animations as every glyph lands.
1624
- * Remaining lag rides a small compositor transform while the real scrollport
1625
- * stays at its floor. The transform is bounded by the measured paint gap
1626
- * before conversation chrome. This follower:
1627
- *
1628
- * - marks programmatic writes via `data-follow-owned` for compatible hosts;
1629
- * - sets `overflow-anchor: none` so CSS scroll-anchoring does not snap;
1630
- * - restores `animatedH` in a ResizeObserver (before paint) so a layout
1631
- * pass cannot flash a snapped frame;
1632
- * - expresses safe lag as a compositor transform on message rows;
1633
- * - opens a speed-adaptive layout runway before fast output wraps, preserving
1634
- * the reference spring constants at every reveal speed;
1635
- * - catches up any lag that cannot fit before turn status / composer chrome,
1636
- * so fixed chrome never has to counter-shift and the host stays at-bottom;
1637
- * - never clips or overlays streamed text.
1638
- *
1639
- * A real reader gesture receives the effective visual position before the
1640
- * transform clears. Lifecycle completion instead settles at the floor.
1226
+ * Own the conversation scrollport's bottom-follow while `active` is true.
1641
1227
  *
1642
- * Directional wheel/touch intent unpins immediately; pointer/key input falls
1643
- * back to an upward scroll delta from the engine's own written position. A
1644
- * reader release re-acquires only after returning to the real floor.
1645
- */
1646
- /**
1647
- * Programmatic follow marker retained for hosts that recognize external
1648
- * scroll ownership. Current Harness also sees the write land at the floor.
1649
- */
1650
- const FOLLOW_OWNED_ATTR = "data-follow-owned";
1651
- /** Sub-pixel settle threshold; clearing below this cannot produce a visible rebound. */
1652
- const FOLLOW_SETTLE_EPSILON_PX = .25;
1653
- /** Safe-lag occupancy band over which reveal pressure is progressively reduced. */
1654
- const FOLLOW_BACKPRESSURE_START_RATIO = .25;
1655
- const GESTURE_EVENTS = [
1656
- "wheel",
1657
- "touchstart",
1658
- "touchmove",
1659
- "touchend",
1660
- "touchcancel",
1661
- "pointerdown",
1662
- "keydown"
1663
- ];
1664
- /** Visible runway needed for the current reveal pressure. */
1665
- function computeFollowReserve(speedCps, runwayPx = 48) {
1666
- const available = Math.max(0, runwayPx);
1667
- if (available <= 0) return 0;
1668
- if (speedCps <= 90) return 0;
1669
- const normalized = Math.min(1, Math.max(0, (speedCps - 90) / 510));
1670
- const minimum = Math.min(available, 16);
1671
- return minimum + normalized * (available - minimum);
1672
- }
1673
- /**
1674
- * Reveal-rate multiplier needed to retain one-wrap headroom for the spring.
1675
- * Throttling starts only after a quarter of the safe transform is occupied;
1676
- * a constrained paint lands at the minimum immediately so the next reveal
1677
- * commit cannot keep feeding an already-full visual buffer.
1678
- */
1679
- function computeFollowRevealScale(lagPx, capacityPx, constrained = false, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
1680
- if (constrained) return tuning.backpressureMinScale;
1681
- if (!Number.isFinite(capacityPx)) return 1;
1682
- if (capacityPx <= 0) return lagPx > 0 ? tuning.backpressureMinScale : 1;
1683
- const ratio = Math.min(1, Math.max(0, lagPx / capacityPx));
1684
- if (ratio >= .75) return tuning.backpressureMinScale;
1685
- const progress = Math.min(1, Math.max(0, (ratio - FOLLOW_BACKPRESSURE_START_RATIO) / .5));
1686
- const eased = progress * progress * (3 - 2 * progress);
1687
- return 1 - (1 - tuning.backpressureMinScale) * eased;
1688
- }
1689
- /**
1690
- * Semi-implicit spring integration with four substeps per <=32ms slice.
1691
- * @param dtMs - Frame delta in ms.
1692
- * @param input - Current visible lag and carried physics velocity.
1693
- * @returns The position advance, its fraction, and next velocity.
1228
+ * @param rootRef - An element inside the conversation scrollport.
1229
+ * @param active - True while the reply is still revealing.
1230
+ * @param speedCpsRef - Live reveal-rate EMA from the smoother.
1231
+ * @param revealScaleRef - Optional backpressure control for text reveal.
1232
+ * @param predictive - Whether to reserve paint room ahead of growth.
1233
+ * @param entrance - Whether the first committed row height should glide in.
1234
+ * @param onEntranceSettled - Releases a one-shot entrance owner after catch-up.
1235
+ * @param predictiveRef - Optional live visibility gate for predictive runway.
1236
+ * @param entranceExtentRef - Optional measured growth delta for a generic row.
1694
1237
  */
1695
- function computeFollowStep(dtMs, input, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
1696
- if (input.lag <= .1 || dtMs <= 0) return {
1697
- advancePx: 0,
1698
- lerpStep: 0,
1699
- velocityPxPerSec: 0
1700
- };
1701
- let lag = input.lag;
1702
- let velocity = Math.max(0, input.velocityPxPerSec ?? 0);
1703
- const elapsedMs = Math.min(32, dtMs);
1704
- const slices = Math.max(1, Math.ceil(elapsedMs / 32));
1705
- const subDt = elapsedMs / 1e3 / slices / 4;
1706
- for (let slice = 0; slice < slices; slice += 1) for (let substep = 0; substep < 4; substep += 1) {
1707
- const acceleration = (tuning.springStiffness * lag - tuning.springDamping * velocity) / tuning.springMass;
1708
- velocity = Math.max(0, velocity + acceleration * subDt);
1709
- const advance = velocity * subDt;
1710
- if (advance >= lag) return {
1711
- advancePx: input.lag,
1712
- lerpStep: 1,
1713
- velocityPxPerSec: 0
1714
- };
1715
- lag -= advance;
1716
- }
1717
- const advancePx = input.lag - lag;
1718
- return {
1719
- advancePx,
1720
- lerpStep: advancePx / input.lag,
1721
- velocityPxPerSec: velocity
1722
- };
1723
- }
1724
- /** Element whose resize signals flow growth for the before-paint restore. */
1725
- function resizeProxyOf(port) {
1726
- return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
1727
- }
1728
- /** Outermost message surfaces; nested tool rows ride their parent. */
1729
- /** @internal Test seam: the equal-lag transform set for one scrollport. */
1730
- function shiftSurfacesOf(port) {
1731
- const transcript = port.querySelector("[data-chat-transcript]");
1732
- if (transcript !== null) return [transcript];
1733
- return [...port.querySelectorAll(`[data-chat-anchor-key], .${PROCESSED_CLASS}`)].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
1734
- }
1735
- function currentShiftOf(element) {
1736
- return Number(/translate3d\(0(?:px)?,\s*(-?[\d.]+)px,\s*0(?:px)?\)/.exec(element.style.transform)?.[1] ?? 0);
1737
- }
1738
- function setShift(element, px) {
1739
- if (Math.abs(px) > .01) {
1740
- if (Math.abs(currentShiftOf(element) - px) <= .01 && element.style.willChange === "transform" && element.style.clipPath === "") return;
1741
- element.style.transform = `translate3d(0, ${px}px, 0)`;
1742
- element.style.willChange = "transform";
1743
- } else {
1744
- if (element.style.transform === "" && element.style.willChange === "" && element.style.clipPath === "") return;
1745
- element.style.transform = "";
1746
- element.style.willChange = "";
1747
- }
1748
- element.style.clipPath = "";
1749
- }
1750
- function turnStatusOf(port) {
1751
- return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
1752
- }
1753
- /** Height committed by one newly mounted Chat row, including its flex gap. */
1754
- function entranceExtentOf(root) {
1755
- const row = root.closest("[data-chat-flow-key]") ?? root;
1756
- const rect = row.getBoundingClientRect();
1757
- const height = Math.max(0, rect.height, rect.bottom - rect.top, row.offsetHeight);
1758
- let previous = row.previousElementSibling;
1759
- while (previous instanceof HTMLElement) {
1760
- const previousRect = previous.getBoundingClientRect();
1761
- if (previousRect.height > 0 || previousRect.bottom > previousRect.top) return Math.max(height, rect.bottom - previousRect.bottom);
1762
- previous = previous.previousElementSibling;
1763
- }
1764
- return height;
1765
- }
1766
- /**
1767
- * The plugin can be reinjected without replacing the conversation DOM. Keep
1768
- * runway ownership in the page realm so a fresh bundle adopts the existing
1769
- * margin instead of treating it as host layout and adding another 48px.
1770
- */
1771
- const FOLLOW_RUNWAYS_SYMBOL = Symbol.for("dsh-smooth-stream.follow-runways");
1772
- const followRunwayRegistry = globalThis;
1773
- const followRunways = followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] ?? /* @__PURE__ */ new WeakMap();
1774
- followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
1775
- const followPaintLimits = /* @__PURE__ */ new WeakMap();
1776
- function invalidatePaintLimit(port) {
1777
- followPaintLimits.delete(port);
1778
- }
1779
- /** Logical position and velocity survive a React owner handoff and finish. */
1780
- const followMotionStates = /* @__PURE__ */ new WeakMap();
1781
- function restoreRunway(port) {
1782
- const runway = followRunways.get(port);
1783
- if (runway === void 0) return;
1784
- runway.element.style[runway.property] = runway.original;
1785
- followRunways.delete(port);
1786
- invalidatePaintLimit(port);
1787
- }
1788
- function isLegacyRunway(value) {
1789
- if (value === "") return false;
1790
- const terms = [...value.matchAll(/([\d.]+)px/g)];
1791
- if (terms.length === 0 || value.replaceAll(/calc|px|[\d.+()\s]/g, "") !== "") return false;
1792
- return terms.every(([, raw]) => {
1793
- const px = Number(raw);
1794
- return Number.isFinite(px) && px >= 48 && Math.abs(px % 48) <= Number.EPSILON;
1795
- });
1796
- }
1797
- /** Remove unowned runway residue written by v0.3.3 and earlier bundles. */
1798
- function migrateLegacyRunway(port, surfaces, status, composer) {
1799
- if (followRunways.has(port)) return;
1800
- let migrated = false;
1801
- if (status !== null && isLegacyRunway(status.style.marginTop)) {
1802
- status.style.marginTop = "";
1803
- migrated = true;
1804
- }
1805
- const last = surfaces.at(-1);
1806
- if (status === null && composer !== null && last !== void 0 && isLegacyRunway(last.style.marginBottom)) {
1807
- last.style.marginBottom = "";
1808
- migrated = true;
1809
- }
1810
- if (migrated) invalidatePaintLimit(port);
1811
- }
1812
- function ensureRunway(port, surfaces, runwayPx = 48) {
1813
- const status = turnStatusOf(port);
1814
- const composer = port.querySelector("[data-composer-seat]");
1815
- migrateLegacyRunway(port, surfaces, status, composer);
1816
- const naturalHeight = Math.max(0, port.scrollHeight - runwayOffsetOf(port));
1817
- if (runwayPx <= 0 || port.clientHeight <= 0 || naturalHeight <= port.clientHeight) {
1818
- restoreRunway(port);
1819
- return;
1820
- }
1821
- const target = status === null ? {
1822
- element: composer === null ? void 0 : surfaces.at(-1),
1823
- property: "marginBottom"
1824
- } : {
1825
- element: status,
1826
- property: "marginTop"
1827
- };
1828
- if (target.element === void 0) {
1829
- restoreRunway(port);
1830
- return;
1831
- }
1832
- const element = target.element;
1833
- const current = followRunways.get(port);
1834
- if (current?.element === element && current.property === target.property && current.requestedPx === runwayPx) return;
1835
- restoreRunway(port);
1836
- const beforeHeight = port.scrollHeight;
1837
- const original = element.style[target.property];
1838
- element.style[target.property] = original === "" ? `${runwayPx}px` : `calc(${original} + ${runwayPx}px)`;
1839
- const offset = Math.max(0, port.scrollHeight - beforeHeight);
1840
- followRunways.set(port, {
1841
- element,
1842
- offset,
1843
- property: target.property,
1844
- original,
1845
- requestedPx: runwayPx
1846
- });
1847
- invalidatePaintLimit(port);
1848
- }
1849
- function runwayOffsetOf(port) {
1850
- return followRunways.get(port)?.offset ?? 0;
1851
- }
1852
- /** Available paint room below the last message before fixed conversation chrome. */
1853
- function safeShiftLimit(port, surfaces) {
1854
- const last = surfaces.at(-1);
1855
- if (last === void 0) return 0;
1856
- const status = turnStatusOf(port);
1857
- const composer = port.querySelector("[data-composer-seat]");
1858
- const cached = followPaintLimits.get(port);
1859
- 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;
1860
- const ceiling = [status, composer].filter((element) => element !== null).map((element) => ({
1861
- element,
1862
- rect: element.getBoundingClientRect()
1863
- })).filter(({ rect }) => Number.isFinite(rect.top) && Number.isFinite(rect.bottom) && rect.bottom > rect.top).sort((first, second) => first.rect.top - second.rect.top)[0];
1864
- if (ceiling === void 0) return status === null && composer === null ? Number.POSITIVE_INFINITY : runwayOffsetOf(port);
1865
- const ceilingTop = ceiling.rect.top - currentShiftOf(ceiling.element);
1866
- const naturalBottom = last.getBoundingClientRect().bottom - currentShiftOf(last);
1867
- const limit = Math.max(0, ceilingTop - naturalBottom - 1);
1868
- followPaintLimits.set(port, {
1869
- clientHeight: port.clientHeight,
1870
- limit,
1871
- measuredAtMs: performance.now(),
1872
- composer,
1873
- status,
1874
- surface: last
1875
- });
1876
- return limit;
1877
- }
1878
- function setFollowScrollTop(port, nextTop) {
1879
- if (Math.abs(port.scrollTop - nextTop) > .01) port.scrollTop = nextTop;
1880
- followScrollLedgers.set(port, port.scrollTop);
1881
- const ownedTop = String(port.scrollTop);
1882
- if (port.getAttribute(FOLLOW_OWNED_ATTR) !== ownedTop) port.setAttribute(FOLLOW_OWNED_ATTR, ownedTop);
1883
- }
1884
- /**
1885
- * Last scrollTop this engine wrote or accepted, per port. Reader intent is a
1886
- * real upward delta from this ledger; a key press or touch while pinned
1887
- * (typing in the composer) must not release the pin, because a released pin
1888
- * can never re-acquire while content streams away from the reader position.
1889
- */
1890
- const followScrollLedgers = /* @__PURE__ */ new WeakMap();
1891
- const followActivityAt = /* @__PURE__ */ new WeakMap();
1892
- /** Whether this port was owned recently enough to identify a closing tail row. */
1893
- function hasRecentConversationFollow(port, windowMs = 250) {
1894
- const last = followActivityAt.get(port);
1895
- return last !== void 0 && performance.now() - last <= windowMs;
1896
- }
1897
- function readerScrolledUp(port) {
1898
- return port.scrollTop < (followScrollLedgers.get(port) ?? 0) - 8;
1899
- }
1900
- /**
1901
- * Paint a bounded visual lag and return the effective logical extent.
1902
- *
1903
- * This is the final geometry invariant, not merely an animation preference:
1904
- * any lag beyond the real gap to status/composer chrome is caught up in the
1905
- * same frame. Carrying that excess in `scrollTop` would move the transcript
1906
- * toward fixed chrome and also make the host expose jump-to-bottom.
1907
- */
1908
- function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 48) {
1909
- const surfaces = shiftSurfacesOf(port);
1910
- ensureRunway(port, surfaces, runwayPx);
1911
- const contentHeight = Math.max(0, port.scrollHeight);
1912
- const runwayOffset = runwayOffsetOf(port);
1913
- const targetHeight = Math.max(0, contentHeight - runwayOffset);
1914
- const floor = Math.max(0, contentHeight - port.clientHeight);
1915
- const extent = Math.min(targetHeight, Math.max(0, animatedH));
1916
- if (port.style.overflowAnchor !== "none") port.style.overflowAnchor = "none";
1917
- if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
1918
- if (floor <= 0) {
1919
- setFollowScrollTop(port, 0);
1920
- followMotionStates.set(port, {
1921
- capacityPx: Number.POSITIVE_INFINITY,
1922
- constrained: false,
1923
- extent: targetHeight,
1924
- lagPx: 0,
1925
- reservePx: 0,
1926
- velocityPxPerSec: 0
1927
- });
1928
- for (const surface of surfaces) setShift(surface, 0);
1929
- const status = turnStatusOf(port);
1930
- if (status !== null) setShift(status, 0);
1931
- return targetHeight;
1932
- }
1933
- setFollowScrollTop(port, floor);
1934
- const limit = floor > 0 ? safeShiftLimit(port, surfaces) : 0;
1935
- const visibleReserve = Math.min(runwayOffset, Math.max(0, reservePx));
1936
- const baselineShift = runwayOffset - visibleReserve;
1937
- const requestedLag = Math.max(0, targetHeight - extent);
1938
- const shift = Math.min(baselineShift + requestedLag, Math.max(0, limit));
1939
- const effectiveLag = Math.max(0, shift - baselineShift);
1940
- const capacityPx = Math.max(0, limit - baselineShift);
1941
- const effectiveExtent = targetHeight - effectiveLag;
1942
- followMotionStates.set(port, {
1943
- capacityPx,
1944
- constrained: requestedLag > effectiveLag + FOLLOW_SETTLE_EPSILON_PX,
1945
- extent: effectiveExtent,
1946
- lagPx: effectiveLag,
1947
- reservePx: visibleReserve,
1948
- velocityPxPerSec
1949
- });
1950
- for (const surface of surfaces) setShift(surface, shift);
1951
- const status = turnStatusOf(port);
1952
- if (status !== null) setShift(status, 0);
1953
- return effectiveExtent;
1954
- }
1955
- function clearMotion(port) {
1956
- port.removeAttribute(FOLLOW_OWNED_ATTR);
1957
- port.style.overflowAnchor = "";
1958
- port.style.scrollBehavior = "";
1959
- for (const surface of shiftSurfacesOf(port)) setShift(surface, 0);
1960
- const status = turnStatusOf(port);
1961
- if (status !== null) setShift(status, 0);
1962
- }
1963
- function clearVisual(port) {
1964
- clearMotion(port);
1965
- restoreRunway(port);
1966
- followMotionStates.delete(port);
1967
- invalidatePaintLimit(port);
1968
- }
1969
- /** Keep an already-promoted surface at zero until one stable final paint lands. */
1970
- function holdCompositorAtRest(element) {
1971
- element.style.transform = "translate3d(0, 0px, 0)";
1972
- element.style.willChange = "transform";
1973
- element.style.clipPath = "";
1238
+ function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef) {
1239
+ const activeRef = (0, react.useRef)(active);
1240
+ const entranceRef = (0, react.useRef)(entrance);
1241
+ const onEntranceSettledRef = (0, react.useRef)(onEntranceSettled);
1242
+ entranceRef.current = entrance;
1243
+ onEntranceSettledRef.current = onEntranceSettled;
1244
+ (0, react.useEffect)(() => {
1245
+ activeRef.current = active;
1246
+ }, [active]);
1247
+ (0, react.useLayoutEffect)(() => {
1248
+ if (!active) return;
1249
+ const owner = {};
1250
+ const generation = ++followGeneration;
1251
+ let rafId = 0;
1252
+ let last = performance.now();
1253
+ let following = true;
1254
+ let primed = false;
1255
+ let animatedH = 0;
1256
+ let reservePx = 0;
1257
+ let velocityPxPerSec = 0;
1258
+ let interacting = false;
1259
+ let readerGestureIntent = false;
1260
+ let readerReleased = false;
1261
+ let touchStartY = null;
1262
+ let interactTimer = null;
1263
+ let port = null;
1264
+ let resize = null;
1265
+ let holding = null;
1266
+ let entrancePending = entranceRef.current;
1267
+ const finishEntrance = () => {
1268
+ if (!entrancePending) return;
1269
+ entrancePending = false;
1270
+ onEntranceSettledRef.current?.();
1271
+ };
1272
+ const updateRevealScale = (next, elapsedMs, urgent = false) => {
1273
+ if (revealScaleRef === void 0) return;
1274
+ const tuning = debugRuntime.activeTuning();
1275
+ const state = followMotionStates.get(next);
1276
+ const target = state === void 0 ? 1 : computeFollowRevealScale(state.lagPx, state.capacityPx, state.constrained, tuning);
1277
+ const current = Math.min(1, Math.max(tuning.backpressureMinScale, revealScaleRef.current));
1278
+ if (target < current || urgent) {
1279
+ revealScaleRef.current = Math.min(current, target);
1280
+ return;
1281
+ }
1282
+ const releaseStep = 1 - Math.exp(-Math.max(0, elapsedMs) / 240);
1283
+ revealScaleRef.current = current + (target - current) * releaseStep;
1284
+ };
1285
+ const releaseRevealScale = () => {
1286
+ if (revealScaleRef !== void 0) revealScaleRef.current = 1;
1287
+ };
1288
+ const reportFollow = (next, isActive) => {
1289
+ const state = followMotionStates.get(next);
1290
+ debugRuntime.reportFollow(next, {
1291
+ lagPx: state?.lagPx ?? Math.max(0, next.scrollHeight - next.clientHeight - next.scrollTop),
1292
+ velocityPxPerSec: state?.velocityPxPerSec ?? 0,
1293
+ reservePx: state?.reservePx ?? 0,
1294
+ capacityPx: state?.capacityPx ?? 0,
1295
+ revealScale: revealScaleRef?.current ?? 1,
1296
+ following,
1297
+ constrained: state?.constrained ?? false,
1298
+ scrollTop: next.scrollTop,
1299
+ scrollHeight: next.scrollHeight,
1300
+ clientHeight: next.clientHeight,
1301
+ active: isActive
1302
+ });
1303
+ };
1304
+ const isLeader = (next) => followLeaders.get(next)?.owner === owner;
1305
+ const hold = (next) => {
1306
+ followActivityAt.set(next, performance.now());
1307
+ if (holding === next && isLeader(next)) return;
1308
+ holding = next;
1309
+ const leader = followLeaders.get(next);
1310
+ if (leader === void 0 || generation > leader.generation) followLeaders.set(next, {
1311
+ generation,
1312
+ owner
1313
+ });
1314
+ };
1315
+ const drop = (next) => {
1316
+ if (holding === next) holding = null;
1317
+ if (isLeader(next)) {
1318
+ clearMotion(next);
1319
+ followMotionStates.delete(next);
1320
+ releaseRevealScale();
1321
+ debugRuntime.reportFollow(next, null);
1322
+ }
1323
+ };
1324
+ const handBackVisual = (next) => {
1325
+ const shift = currentShiftOf(shiftSurfacesOf(next).at(-1) ?? next);
1326
+ const visualTop = Math.max(0, next.scrollTop - Math.max(0, shift));
1327
+ clearMotion(next);
1328
+ restoreRunway(next);
1329
+ const floor = Math.max(0, next.scrollHeight - next.clientHeight);
1330
+ next.scrollTop = Math.min(visualTop, Math.max(0, floor - 26));
1331
+ followScrollLedgers.set(next, next.scrollTop);
1332
+ };
1333
+ const markGesture = (event) => {
1334
+ interacting = true;
1335
+ if (event.type === "wheel") {
1336
+ const deltaY = event.deltaY;
1337
+ if (Number.isFinite(deltaY) && deltaY < 0) readerGestureIntent = true;
1338
+ } else if (event.type === "touchstart") touchStartY = event.touches[0]?.clientY ?? null;
1339
+ else if (event.type === "touchmove") {
1340
+ const touch = event.touches[0];
1341
+ if (touch !== void 0) {
1342
+ if (touchStartY === null) touchStartY = touch.clientY;
1343
+ if (touch.clientY - touchStartY > 1) readerGestureIntent = true;
1344
+ }
1345
+ } else if (event.type === "touchend" || event.type === "touchcancel") touchStartY = null;
1346
+ if (interactTimer !== null) clearTimeout(interactTimer);
1347
+ interactTimer = setTimeout(() => {
1348
+ interacting = false;
1349
+ readerGestureIntent = false;
1350
+ interactTimer = null;
1351
+ }, 800);
1352
+ };
1353
+ const restoreBeforePaint = () => {
1354
+ if (!following || port === null || !isLeader(port)) return;
1355
+ invalidatePaintLimit(port);
1356
+ const tuning = debugRuntime.activeTuning();
1357
+ animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1358
+ updateRevealScale(port, 0, true);
1359
+ reportFollow(port, activeRef.current);
1360
+ };
1361
+ const bindPort = (next) => {
1362
+ if (port === next) return;
1363
+ if (port !== null) {
1364
+ for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
1365
+ resize?.disconnect();
1366
+ }
1367
+ port = next;
1368
+ invalidatePaintLimit(port);
1369
+ for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
1370
+ if (typeof ResizeObserver !== "undefined") {
1371
+ resize = new ResizeObserver(restoreBeforePaint);
1372
+ resize.observe(port);
1373
+ const proxy = resizeProxyOf(port);
1374
+ if (proxy !== null) resize.observe(proxy);
1375
+ }
1376
+ };
1377
+ const frame = (now) => {
1378
+ rafId = requestAnimationFrame(frame);
1379
+ const elapsedMs = Math.max(.001, now - last);
1380
+ const dt = Math.min(32, elapsedMs);
1381
+ const tuning = debugRuntime.activeTuning();
1382
+ last = now;
1383
+ const root = rootRef.current;
1384
+ if (root === null) return;
1385
+ const nextPort = root.closest("[data-conversation-scroll]");
1386
+ if (nextPort === null) return;
1387
+ bindPort(nextPort);
1388
+ if (nextPort.clientHeight <= 0) return;
1389
+ const reportedLag = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight) - nextPort.scrollTop;
1390
+ const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
1391
+ if (!primed) {
1392
+ const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
1393
+ if (inherited === void 0) {
1394
+ const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
1395
+ animatedH = Math.max(0, nextPort.scrollHeight - entranceExtent);
1396
+ reservePx = 0;
1397
+ velocityPxPerSec = 0;
1398
+ following = Math.max(0, reportedLag - entranceExtent) <= 25;
1399
+ } else {
1400
+ animatedH = Math.min(nextPort.scrollHeight, inherited.extent);
1401
+ reservePx = inherited.reservePx;
1402
+ velocityPxPerSec = inherited.velocityPxPerSec;
1403
+ following = true;
1404
+ }
1405
+ if (following) {
1406
+ hold(nextPort);
1407
+ if (isLeader(nextPort)) {
1408
+ animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1409
+ updateRevealScale(nextPort, elapsedMs);
1410
+ reportFollow(nextPort, activeRef.current);
1411
+ const runwayOffset = runwayOffsetOf(nextPort);
1412
+ if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
1413
+ } else finishEntrance();
1414
+ } else finishEntrance();
1415
+ primed = true;
1416
+ return;
1417
+ }
1418
+ if (!following && (!interacting || readerReleased && !readerGestureIntent && reportedLag <= 1) && reportedLag <= (readerReleased ? 1 : 25)) {
1419
+ following = true;
1420
+ readerReleased = false;
1421
+ animatedH = extent;
1422
+ reservePx = 0;
1423
+ velocityPxPerSec = 0;
1424
+ followScrollLedgers.set(nextPort, nextPort.scrollTop);
1425
+ hold(nextPort);
1426
+ } else if (following && interacting && (readerGestureIntent || readerScrolledUp(nextPort))) {
1427
+ following = false;
1428
+ readerGestureIntent = false;
1429
+ readerReleased = true;
1430
+ handBackVisual(nextPort);
1431
+ animatedH = nextPort.scrollHeight;
1432
+ reservePx = 0;
1433
+ velocityPxPerSec = 0;
1434
+ drop(nextPort);
1435
+ finishEntrance();
1436
+ }
1437
+ if (!activeRef.current || !following) {
1438
+ followScrollLedgers.set(nextPort, nextPort.scrollTop);
1439
+ reportFollow(nextPort, activeRef.current);
1440
+ return;
1441
+ }
1442
+ hold(nextPort);
1443
+ if (!isLeader(nextPort)) {
1444
+ finishEntrance();
1445
+ return;
1446
+ }
1447
+ const runwayOffset = runwayOffsetOf(nextPort);
1448
+ const contentHeight = nextPort.scrollHeight;
1449
+ const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
1450
+ const predictGrowth = predictiveRef?.current ?? predictive;
1451
+ const reserveTarget = !predictGrowth ? 0 : computeFollowReserve(speedCpsRef.current, tuning.runwayPx);
1452
+ const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
1453
+ reservePx += (reserveTarget - reservePx) * reserveStep;
1454
+ const step = computeFollowStep(dt, {
1455
+ lag,
1456
+ speedEma: speedCpsRef.current,
1457
+ velocityPxPerSec
1458
+ }, tuning);
1459
+ if (lag <= .1) {
1460
+ animatedH = contentHeight - runwayOffset;
1461
+ velocityPxPerSec = 0;
1462
+ } else {
1463
+ const minimumLag = predictGrowth ? 0 : Math.max(0, reservePx);
1464
+ animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
1465
+ velocityPxPerSec = step.velocityPxPerSec;
1466
+ }
1467
+ animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1468
+ updateRevealScale(nextPort, elapsedMs);
1469
+ reportFollow(nextPort, true);
1470
+ if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
1471
+ };
1472
+ frame(performance.now());
1473
+ return () => {
1474
+ cancelAnimationFrame(rafId);
1475
+ if (interactTimer !== null) clearTimeout(interactTimer);
1476
+ resize?.disconnect();
1477
+ if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
1478
+ const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
1479
+ if (host === null) return;
1480
+ holding = null;
1481
+ if (!isLeader(host)) return;
1482
+ const preserveReader = interacting && (readerGestureIntent || readerScrolledUp(host));
1483
+ if (!following || !primed) {
1484
+ clearVisual(host);
1485
+ followLeaders.delete(host);
1486
+ releaseRevealScale();
1487
+ debugRuntime.reportFollow(host, null);
1488
+ return;
1489
+ }
1490
+ if (preserveReader) {
1491
+ handBackVisual(host);
1492
+ clearVisual(host);
1493
+ followLeaders.delete(host);
1494
+ releaseRevealScale();
1495
+ debugRuntime.reportFollow(host, null);
1496
+ return;
1497
+ }
1498
+ const completionTuning = debugRuntime.activeTuning();
1499
+ ensureRunway(host, shiftSurfacesOf(host), completionTuning.runwayPx);
1500
+ const completionRunway = runwayOffsetOf(host);
1501
+ const completionMinimumLag = Math.max(0, reservePx);
1502
+ animatedH = Math.min(animatedH, host.scrollHeight - completionRunway - completionMinimumLag);
1503
+ settleAtFloor(host);
1504
+ animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionTuning.runwayPx);
1505
+ reportFollow(host, false);
1506
+ const runwayOffset = runwayOffsetOf(host);
1507
+ if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && reservePx <= .25) {
1508
+ finishAtNaturalFloor(host);
1509
+ followLeaders.delete(host);
1510
+ releaseRevealScale();
1511
+ debugRuntime.reportFollow(host, null);
1512
+ return;
1513
+ }
1514
+ for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
1515
+ const stopSettleListeners = () => {
1516
+ for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
1517
+ if (interactTimer !== null) {
1518
+ clearTimeout(interactTimer);
1519
+ interactTimer = null;
1520
+ }
1521
+ };
1522
+ let settleLast = performance.now();
1523
+ const settleFrame = (now) => {
1524
+ if (!isLeader(host)) {
1525
+ stopSettleListeners();
1526
+ return;
1527
+ }
1528
+ if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
1529
+ readerGestureIntent = false;
1530
+ handBackVisual(host);
1531
+ clearVisual(host);
1532
+ followLeaders.delete(host);
1533
+ releaseRevealScale();
1534
+ debugRuntime.reportFollow(host, null);
1535
+ stopSettleListeners();
1536
+ return;
1537
+ }
1538
+ const dt = Math.min(32, Math.max(0, now - settleLast));
1539
+ const tuning = debugRuntime.activeTuning();
1540
+ settleLast = now;
1541
+ const runwayOffset = runwayOffsetOf(host);
1542
+ const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
1543
+ const reserveStep = 1 - Math.exp(-dt / tuning.reserveResponseMs);
1544
+ reservePx += (0 - reservePx) * reserveStep;
1545
+ if (lag <= .25 && reservePx <= .25) {
1546
+ animatedH = host.scrollHeight - runwayOffset;
1547
+ reservePx = 0;
1548
+ velocityPxPerSec = 0;
1549
+ finishAtNaturalFloor(host);
1550
+ followLeaders.delete(host);
1551
+ releaseRevealScale();
1552
+ debugRuntime.reportFollow(host, null);
1553
+ stopSettleListeners();
1554
+ return;
1555
+ }
1556
+ const step = computeFollowStep(dt, {
1557
+ lag,
1558
+ speedEma: speedCpsRef.current,
1559
+ velocityPxPerSec
1560
+ }, tuning);
1561
+ const minimumLag = Math.max(0, reservePx);
1562
+ animatedH = Math.min(host.scrollHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
1563
+ velocityPxPerSec = step.velocityPxPerSec;
1564
+ settleAtFloor(host);
1565
+ animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1566
+ reportFollow(host, false);
1567
+ requestAnimationFrame(settleFrame);
1568
+ };
1569
+ requestAnimationFrame(settleFrame);
1570
+ };
1571
+ }, [
1572
+ active,
1573
+ rootRef,
1574
+ speedCpsRef,
1575
+ revealScaleRef,
1576
+ predictive,
1577
+ predictiveRef
1578
+ ]);
1974
1579
  }
1975
- /** Remove equal offsets, land on the floor, then retire the compositor quietly. */
1976
- function finishAtNaturalFloor(port) {
1977
- const surfaces = shiftSurfacesOf(port);
1978
- const status = turnStatusOf(port);
1979
- const promoted = [...surfaces, ...status === null ? [] : [status]].filter((element) => element.style.transform !== "" || element.style.willChange === "transform");
1980
- const promotedSet = new Set(promoted);
1981
- restoreRunway(port);
1982
- settleAtFloor(port);
1983
- port.removeAttribute(FOLLOW_OWNED_ATTR);
1984
- port.style.overflowAnchor = "";
1985
- port.style.scrollBehavior = "";
1986
- for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
1987
- else setShift(surface, 0);
1988
- if (status !== null) {
1989
- if (promotedSet.has(status)) holdCompositorAtRest(status);
1990
- else setShift(status, 0);
1991
- }
1992
- followMotionStates.delete(port);
1993
- if (promoted.length === 0) return;
1994
- requestAnimationFrame(() => {
1995
- requestAnimationFrame(() => {
1996
- if (port.hasAttribute(FOLLOW_OWNED_ATTR)) return;
1997
- for (const element of promoted) if (Math.abs(currentShiftOf(element)) <= .01) setShift(element, 0);
1580
+ //#endregion
1581
+ //#region src/client/FollowHost.tsx
1582
+ /**
1583
+ * Document-flow host that owns conversation-port follow while `active`.
1584
+ * Shared by assistant blocks and every other Agent Chat row. `onGrowth` lets
1585
+ * generic wrapped renderers re-arm one glide when their DOM grows without
1586
+ * requiring a business-kind-specific lifecycle predicate.
1587
+ */
1588
+ function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealScaleRef, predictive = true, predictiveRef, hostRef, children }) {
1589
+ const localRootRef = (0, react.useRef)(null);
1590
+ const rootRef = hostRef ?? localRootRef;
1591
+ useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef);
1592
+ (0, react.useEffect)(() => {
1593
+ if (onGrowth === void 0 || typeof ResizeObserver === "undefined") return;
1594
+ const root = rootRef.current;
1595
+ if (root === null) return;
1596
+ let previousHeight = null;
1597
+ const observer = new ResizeObserver((entries) => {
1598
+ const nextHeight = entries[0]?.contentRect.height ?? root.getBoundingClientRect().height;
1599
+ if (!Number.isFinite(nextHeight)) return;
1600
+ if (previousHeight !== null && nextHeight > previousHeight + .5) onGrowth(nextHeight - previousHeight);
1601
+ previousHeight = nextHeight;
1998
1602
  });
1603
+ observer.observe(root);
1604
+ return () => {
1605
+ observer.disconnect();
1606
+ };
1607
+ }, [onGrowth]);
1608
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1609
+ ref: rootRef,
1610
+ className: TypewriterAssistantNodeView_module_css_default.follow,
1611
+ children
1999
1612
  });
2000
1613
  }
2001
- function settleAtFloor(port) {
2002
- setFollowScrollTop(port, Math.max(0, port.scrollHeight - port.clientHeight));
2003
- }
2004
- /** Only the newest active follower may write one port's shared visual state. */
2005
- const followLeaders = /* @__PURE__ */ new WeakMap();
2006
- let followGeneration = 0;
1614
+ //#endregion
1615
+ //#region src/config.ts
1616
+ /** Defaults shared by the Host schema and the client-side fallback. */
1617
+ const DEFAULT_STREAM_CONFIG = {
1618
+ mode: "typewriter",
1619
+ preset: "balanced",
1620
+ revealCharsPerSec: 80,
1621
+ scrollSpeedPxPerSec: 48,
1622
+ maxScrollSpeedPxPerSec: 1e3
1623
+ };
2007
1624
  /**
2008
- * Own the conversation scrollport's bottom-follow while `active` is true.
2009
- *
2010
- * @param rootRef - An element inside the conversation scrollport.
2011
- * @param active - True while the reply is still revealing.
2012
- * @param speedCpsRef - Live reveal-rate EMA from the smoother.
2013
- * @param revealScaleRef - Optional backpressure control for text reveal.
2014
- * @param predictive - Whether to reserve paint room ahead of growth.
2015
- * @param entrance - Whether the first committed row height should glide in.
2016
- * @param onEntranceSettled - Releases a one-shot entrance owner after catch-up.
2017
- * @param predictiveRef - Optional live visibility gate for predictive runway.
2018
- * @param entranceExtentRef - Optional measured growth delta for a generic row.
1625
+ * Window global the Host writes into the served index HTML. The browser boot
1626
+ * graph carries no per-entry config, so this inline script is the only
1627
+ * Host-to-client configuration channel for a composed web plugin.
2019
1628
  */
2020
- function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef) {
2021
- const activeRef = (0, react.useRef)(active);
2022
- const entranceRef = (0, react.useRef)(entrance);
2023
- const onEntranceSettledRef = (0, react.useRef)(onEntranceSettled);
2024
- entranceRef.current = entrance;
2025
- onEntranceSettledRef.current = onEntranceSettled;
1629
+ const STREAM_BOOT_GLOBAL = "__DSH_SMOOTH_STREAM_CONFIG__";
1630
+ //#endregion
1631
+ //#region src/client/TypewriterAssistantNodeView.tsx
1632
+ function usePrefersReducedMotion() {
1633
+ const [reduced, setReduced] = (0, react.useState)(() => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true);
2026
1634
  (0, react.useEffect)(() => {
2027
- activeRef.current = active;
2028
- }, [active]);
2029
- (0, react.useLayoutEffect)(() => {
2030
- if (!active) return;
2031
- const owner = {};
2032
- const generation = ++followGeneration;
2033
- let rafId = 0;
2034
- let last = performance.now();
2035
- let following = true;
2036
- let primed = false;
2037
- let animatedH = 0;
2038
- let reservePx = 0;
2039
- let velocityPxPerSec = 0;
2040
- let interacting = false;
2041
- let readerGestureIntent = false;
2042
- let readerReleased = false;
2043
- let touchStartY = null;
2044
- let interactTimer = null;
2045
- let port = null;
2046
- let resize = null;
2047
- let holding = null;
2048
- let entrancePending = entranceRef.current;
2049
- const finishEntrance = () => {
2050
- if (!entrancePending) return;
2051
- entrancePending = false;
2052
- onEntranceSettledRef.current?.();
2053
- };
2054
- const updateRevealScale = (next, elapsedMs, urgent = false) => {
2055
- if (revealScaleRef === void 0) return;
2056
- const tuning = debugRuntime.activeTuning();
2057
- const state = followMotionStates.get(next);
2058
- const target = state === void 0 ? 1 : computeFollowRevealScale(state.lagPx, state.capacityPx, state.constrained, tuning);
2059
- const current = Math.min(1, Math.max(tuning.backpressureMinScale, revealScaleRef.current));
2060
- if (target < current || urgent) {
2061
- revealScaleRef.current = Math.min(current, target);
2062
- return;
2063
- }
2064
- const releaseStep = 1 - Math.exp(-Math.max(0, elapsedMs) / 240);
2065
- revealScaleRef.current = current + (target - current) * releaseStep;
2066
- };
2067
- const releaseRevealScale = () => {
2068
- if (revealScaleRef !== void 0) revealScaleRef.current = 1;
2069
- };
2070
- const reportFollow = (next, isActive) => {
2071
- const state = followMotionStates.get(next);
2072
- debugRuntime.reportFollow(next, {
2073
- lagPx: state?.lagPx ?? Math.max(0, next.scrollHeight - next.clientHeight - next.scrollTop),
2074
- velocityPxPerSec: state?.velocityPxPerSec ?? 0,
2075
- reservePx: state?.reservePx ?? 0,
2076
- capacityPx: state?.capacityPx ?? 0,
2077
- revealScale: revealScaleRef?.current ?? 1,
2078
- following,
2079
- constrained: state?.constrained ?? false,
2080
- scrollTop: next.scrollTop,
2081
- scrollHeight: next.scrollHeight,
2082
- clientHeight: next.clientHeight,
2083
- active: isActive
2084
- });
2085
- };
2086
- const isLeader = (next) => followLeaders.get(next)?.owner === owner;
2087
- const hold = (next) => {
2088
- followActivityAt.set(next, performance.now());
2089
- if (holding === next && isLeader(next)) return;
2090
- holding = next;
2091
- const leader = followLeaders.get(next);
2092
- if (leader === void 0 || generation > leader.generation) followLeaders.set(next, {
2093
- generation,
2094
- owner
2095
- });
2096
- };
2097
- const drop = (next) => {
2098
- if (holding === next) holding = null;
2099
- if (isLeader(next)) {
2100
- clearMotion(next);
2101
- followMotionStates.delete(next);
2102
- releaseRevealScale();
2103
- debugRuntime.reportFollow(next, null);
2104
- }
1635
+ if (typeof window === "undefined" || window.matchMedia === void 0) return;
1636
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
1637
+ const onChange = () => setReduced(query.matches);
1638
+ query.addEventListener("change", onChange);
1639
+ return () => query.removeEventListener("change", onChange);
1640
+ }, []);
1641
+ return reduced;
1642
+ }
1643
+ /** Conservative fallback before the streaming Markdown tail has geometry. */
1644
+ const PREDICTIVE_WRAP_FALLBACK_CHARS = 32;
1645
+ const STREAM_ANNOUNCEMENT_INTERVAL_MS = 800;
1646
+ const STREAM_ANNOUNCEMENT_MAX_CHARS = 320;
1647
+ function approximateInlineWidth(text, emPx) {
1648
+ let width = 0;
1649
+ for (const char of text) if (/\s/u.test(char)) width += emPx * .33;
1650
+ else if (/^[\x00-\x7f]$/u.test(char)) width += emPx * .56;
1651
+ else width += emPx;
1652
+ return width;
1653
+ }
1654
+ function measurePendingTextGeometry(root, visibleText) {
1655
+ if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
1656
+ root,
1657
+ visibleText,
1658
+ fontSize: 14,
1659
+ wrapThresholdWidth: null
1660
+ };
1661
+ const rootRect = root.getBoundingClientRect();
1662
+ const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
1663
+ if (rootWidth <= 0) return {
1664
+ root,
1665
+ visibleText,
1666
+ fontSize: 14,
1667
+ wrapThresholdWidth: null
1668
+ };
1669
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1670
+ let tail = null;
1671
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
1672
+ const parent = tail?.parentElement ?? root;
1673
+ const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
1674
+ if (tail === null || typeof document.createRange !== "function") return {
1675
+ root,
1676
+ visibleText,
1677
+ fontSize,
1678
+ wrapThresholdWidth: rootWidth
1679
+ };
1680
+ try {
1681
+ const length = tail.textContent?.length ?? 0;
1682
+ if (length <= 0) return {
1683
+ root,
1684
+ visibleText,
1685
+ fontSize,
1686
+ wrapThresholdWidth: rootWidth
2105
1687
  };
2106
- const handBackVisual = (next) => {
2107
- const shift = currentShiftOf(shiftSurfacesOf(next).at(-1) ?? next);
2108
- const visualTop = Math.max(0, next.scrollTop - Math.max(0, shift));
2109
- clearMotion(next);
2110
- restoreRunway(next);
2111
- const floor = Math.max(0, next.scrollHeight - next.clientHeight);
2112
- next.scrollTop = Math.min(visualTop, Math.max(0, floor - 26));
2113
- followScrollLedgers.set(next, next.scrollTop);
1688
+ const range = document.createRange();
1689
+ range.setStart(tail, Math.max(0, length - 1));
1690
+ range.setEnd(tail, length);
1691
+ const tailRect = range.getBoundingClientRect();
1692
+ const contentRight = rootRect.right;
1693
+ if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
1694
+ root,
1695
+ visibleText,
1696
+ fontSize,
1697
+ wrapThresholdWidth: rootWidth
2114
1698
  };
2115
- const markGesture = (event) => {
2116
- interacting = true;
2117
- if (event.type === "wheel") {
2118
- const deltaY = event.deltaY;
2119
- if (Number.isFinite(deltaY) && deltaY < 0) readerGestureIntent = true;
2120
- } else if (event.type === "touchstart") touchStartY = event.touches[0]?.clientY ?? null;
2121
- else if (event.type === "touchmove") {
2122
- const touch = event.touches[0];
2123
- if (touch !== void 0) {
2124
- if (touchStartY === null) touchStartY = touch.clientY;
2125
- if (touch.clientY - touchStartY > 1) readerGestureIntent = true;
2126
- }
2127
- } else if (event.type === "touchend" || event.type === "touchcancel") touchStartY = null;
2128
- if (interactTimer !== null) clearTimeout(interactTimer);
2129
- interactTimer = setTimeout(() => {
2130
- interacting = false;
2131
- readerGestureIntent = false;
2132
- interactTimer = null;
2133
- }, 800);
1699
+ return {
1700
+ root,
1701
+ visibleText,
1702
+ fontSize,
1703
+ wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
2134
1704
  };
2135
- const restoreBeforePaint = () => {
2136
- if (!following || port === null || !isLeader(port)) return;
2137
- invalidatePaintLimit(port);
2138
- const tuning = debugRuntime.activeTuning();
2139
- animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
2140
- updateRevealScale(port, 0, true);
2141
- reportFollow(port, activeRef.current);
1705
+ } catch {
1706
+ return {
1707
+ root,
1708
+ visibleText,
1709
+ fontSize,
1710
+ wrapThresholdWidth: rootWidth
2142
1711
  };
2143
- const bindPort = (next) => {
2144
- if (port === next) return;
2145
- if (port !== null) {
2146
- for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
2147
- resize?.disconnect();
2148
- }
2149
- port = next;
2150
- invalidatePaintLimit(port);
2151
- for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
2152
- if (typeof ResizeObserver !== "undefined") {
2153
- resize = new ResizeObserver(restoreBeforePaint);
2154
- resize.observe(port);
2155
- const proxy = resizeProxyOf(port);
2156
- if (proxy !== null) resize.observe(proxy);
2157
- }
1712
+ }
1713
+ }
1714
+ /** Whether buffered source can reach a new visual line before it drains. */
1715
+ function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
1716
+ if (pending === "") return false;
1717
+ if (/[\r\n]/u.test(pending)) return true;
1718
+ const pendingChars = [...pending];
1719
+ if (root === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
1720
+ let geometry = geometryRef.current;
1721
+ if (geometry?.root !== root || geometry.visibleText !== visibleText) {
1722
+ geometry = measurePendingTextGeometry(root, visibleText);
1723
+ geometryRef.current = geometry;
1724
+ }
1725
+ if (geometry.wrapThresholdWidth === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
1726
+ return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
1727
+ }
1728
+ function announcementChunkEnd(source, start) {
1729
+ const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
1730
+ if (hardEnd === source.length) return hardEnd;
1731
+ const softStart = start + Math.floor(STREAM_ANNOUNCEMENT_MAX_CHARS * .6);
1732
+ for (let index = hardEnd - 1; index >= softStart; index -= 1) if (/[\s.,;:!?]/u.test(source[index] ?? "")) return index + 1;
1733
+ return hardEnd;
1734
+ }
1735
+ /** A commit-driven live region isolated from the visible Markdown subtree. */
1736
+ const StreamAnnouncement = (0, react.memo)(function StreamAnnouncement({ text, active }) {
1737
+ const [announcement, setAnnouncement] = (0, react.useState)({
1738
+ text: "",
1739
+ revision: 0,
1740
+ present: active
1741
+ });
1742
+ const sourceRef = (0, react.useRef)(text);
1743
+ const activeRef = (0, react.useRef)(active);
1744
+ const announcedOffsetRef = (0, react.useRef)(active ? 0 : text.length);
1745
+ const drainSourceRef = (0, react.useRef)(null);
1746
+ const timerRef = (0, react.useRef)(null);
1747
+ (0, react.useEffect)(() => {
1748
+ const clearTimer = () => {
1749
+ if (timerRef.current === null) return;
1750
+ clearTimeout(timerRef.current);
1751
+ timerRef.current = null;
2158
1752
  };
2159
- const frame = (now) => {
2160
- rafId = requestAnimationFrame(frame);
2161
- const elapsedMs = Math.max(.001, now - last);
2162
- const dt = Math.min(32, elapsedMs);
2163
- const tuning = debugRuntime.activeTuning();
2164
- last = now;
2165
- const root = rootRef.current;
2166
- if (root === null) return;
2167
- const nextPort = root.closest("[data-conversation-scroll]");
2168
- if (nextPort === null) return;
2169
- bindPort(nextPort);
2170
- if (nextPort.clientHeight <= 0) return;
2171
- const reportedLag = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight) - nextPort.scrollTop;
2172
- const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
2173
- if (!primed) {
2174
- const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
2175
- if (inherited === void 0) {
2176
- const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
2177
- animatedH = Math.max(0, nextPort.scrollHeight - entranceExtent);
2178
- reservePx = 0;
2179
- velocityPxPerSec = 0;
2180
- following = Math.max(0, reportedLag - entranceExtent) <= 25;
2181
- } else {
2182
- animatedH = Math.min(nextPort.scrollHeight, inherited.extent);
2183
- reservePx = inherited.reservePx;
2184
- velocityPxPerSec = inherited.velocityPxPerSec;
2185
- following = true;
2186
- }
2187
- if (following) {
2188
- hold(nextPort);
2189
- if (isLeader(nextPort)) {
2190
- animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
2191
- updateRevealScale(nextPort, elapsedMs);
2192
- reportFollow(nextPort, activeRef.current);
2193
- const runwayOffset = runwayOffsetOf(nextPort);
2194
- if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
2195
- } else finishEntrance();
2196
- } else finishEntrance();
2197
- primed = true;
2198
- return;
2199
- }
2200
- if (!following && (!interacting || readerReleased && !readerGestureIntent && reportedLag <= 1) && reportedLag <= (readerReleased ? 1 : 25)) {
2201
- following = true;
2202
- readerReleased = false;
2203
- animatedH = extent;
2204
- reservePx = 0;
2205
- velocityPxPerSec = 0;
2206
- followScrollLedgers.set(nextPort, nextPort.scrollTop);
2207
- hold(nextPort);
2208
- } else if (following && interacting && (readerGestureIntent || readerScrolledUp(nextPort))) {
2209
- following = false;
2210
- readerGestureIntent = false;
2211
- readerReleased = true;
2212
- handBackVisual(nextPort);
2213
- animatedH = nextPort.scrollHeight;
2214
- reservePx = 0;
2215
- velocityPxPerSec = 0;
2216
- drop(nextPort);
2217
- finishEntrance();
2218
- }
2219
- if (!activeRef.current || !following) {
2220
- followScrollLedgers.set(nextPort, nextPort.scrollTop);
2221
- reportFollow(nextPort, activeRef.current);
2222
- return;
2223
- }
2224
- hold(nextPort);
2225
- if (!isLeader(nextPort)) {
2226
- finishEntrance();
2227
- return;
2228
- }
2229
- const runwayOffset = runwayOffsetOf(nextPort);
2230
- const contentHeight = nextPort.scrollHeight;
2231
- const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
2232
- const predictGrowth = predictiveRef?.current ?? predictive;
2233
- const reserveTarget = !predictGrowth ? 0 : computeFollowReserve(speedCpsRef.current, tuning.runwayPx);
2234
- const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
2235
- reservePx += (reserveTarget - reservePx) * reserveStep;
2236
- const step = computeFollowStep(dt, {
2237
- lag,
2238
- speedEma: speedCpsRef.current,
2239
- velocityPxPerSec
2240
- }, tuning);
2241
- if (lag <= .1) {
2242
- animatedH = contentHeight - runwayOffset;
2243
- velocityPxPerSec = 0;
2244
- } else {
2245
- const minimumLag = predictGrowth ? 0 : Math.max(0, reservePx);
2246
- animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
2247
- velocityPxPerSec = step.velocityPxPerSec;
2248
- }
2249
- animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
2250
- updateRevealScale(nextPort, elapsedMs);
2251
- reportFollow(nextPort, true);
2252
- if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
1753
+ const publishNext = (source) => {
1754
+ const start = Math.min(announcedOffsetRef.current, source.length);
1755
+ const end = announcementChunkEnd(source, start);
1756
+ if (end <= start) return false;
1757
+ announcedOffsetRef.current = end;
1758
+ setAnnouncement((previous) => ({
1759
+ text: source.slice(start, end),
1760
+ revision: previous.revision + 1,
1761
+ present: true
1762
+ }));
1763
+ return end < source.length;
2253
1764
  };
2254
- frame(performance.now());
2255
- return () => {
2256
- cancelAnimationFrame(rafId);
2257
- if (interactTimer !== null) clearTimeout(interactTimer);
2258
- resize?.disconnect();
2259
- if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
2260
- const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
2261
- if (host === null) return;
2262
- holding = null;
2263
- if (!isLeader(host)) return;
2264
- const preserveReader = interacting && (readerGestureIntent || readerScrolledUp(host));
2265
- if (!following || !primed) {
2266
- clearVisual(host);
2267
- followLeaders.delete(host);
2268
- releaseRevealScale();
2269
- debugRuntime.reportFollow(host, null);
2270
- return;
2271
- }
2272
- if (preserveReader) {
2273
- handBackVisual(host);
2274
- clearVisual(host);
2275
- followLeaders.delete(host);
2276
- releaseRevealScale();
2277
- debugRuntime.reportFollow(host, null);
1765
+ const hideAfterLinger = () => {
1766
+ timerRef.current = setTimeout(() => {
1767
+ timerRef.current = null;
1768
+ if (activeRef.current) return;
1769
+ drainSourceRef.current = null;
1770
+ setAnnouncement((previous) => ({
1771
+ ...previous,
1772
+ present: false
1773
+ }));
1774
+ }, STREAM_ANNOUNCEMENT_INTERVAL_MS);
1775
+ };
1776
+ const drainNext = () => {
1777
+ const source = drainSourceRef.current;
1778
+ if (source === null) return;
1779
+ if (!publishNext(source)) {
1780
+ hideAfterLinger();
2278
1781
  return;
2279
1782
  }
2280
- const completionTuning = debugRuntime.activeTuning();
2281
- ensureRunway(host, shiftSurfacesOf(host), completionTuning.runwayPx);
2282
- const completionRunway = runwayOffsetOf(host);
2283
- const completionMinimumLag = Math.max(0, reservePx);
2284
- animatedH = Math.min(animatedH, host.scrollHeight - completionRunway - completionMinimumLag);
2285
- settleAtFloor(host);
2286
- animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionTuning.runwayPx);
2287
- reportFollow(host, false);
2288
- const runwayOffset = runwayOffsetOf(host);
2289
- if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && reservePx <= .25) {
2290
- finishAtNaturalFloor(host);
2291
- followLeaders.delete(host);
2292
- releaseRevealScale();
2293
- debugRuntime.reportFollow(host, null);
2294
- return;
1783
+ timerRef.current = setTimeout(() => {
1784
+ timerRef.current = null;
1785
+ if (activeRef.current) return;
1786
+ drainNext();
1787
+ }, STREAM_ANNOUNCEMENT_INTERVAL_MS);
1788
+ };
1789
+ const scheduleLive = () => {
1790
+ if (timerRef.current !== null || announcedOffsetRef.current >= sourceRef.current.length) return;
1791
+ timerRef.current = setTimeout(() => {
1792
+ timerRef.current = null;
1793
+ if (!activeRef.current) return;
1794
+ const source = sourceRef.current;
1795
+ if (publishNext(source)) scheduleLive();
1796
+ }, STREAM_ANNOUNCEMENT_INTERVAL_MS);
1797
+ };
1798
+ const wasActive = activeRef.current;
1799
+ const previousSource = sourceRef.current;
1800
+ if (!(!active && !wasActive && drainSourceRef.current !== null) && (!text.startsWith(previousSource) || announcedOffsetRef.current > text.length)) announcedOffsetRef.current = 0;
1801
+ sourceRef.current = text;
1802
+ activeRef.current = active;
1803
+ if (active) {
1804
+ if (!wasActive) {
1805
+ clearTimer();
1806
+ drainSourceRef.current = null;
1807
+ setAnnouncement((previous) => ({
1808
+ text: "",
1809
+ revision: previous.revision + 1,
1810
+ present: true
1811
+ }));
2295
1812
  }
2296
- for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
2297
- const stopSettleListeners = () => {
2298
- for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
2299
- if (interactTimer !== null) {
2300
- clearTimeout(interactTimer);
2301
- interactTimer = null;
2302
- }
2303
- };
2304
- let settleLast = performance.now();
2305
- const settleFrame = (now) => {
2306
- if (!isLeader(host)) {
2307
- stopSettleListeners();
2308
- return;
2309
- }
2310
- if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
2311
- readerGestureIntent = false;
2312
- handBackVisual(host);
2313
- clearVisual(host);
2314
- followLeaders.delete(host);
2315
- releaseRevealScale();
2316
- debugRuntime.reportFollow(host, null);
2317
- stopSettleListeners();
2318
- return;
2319
- }
2320
- const dt = Math.min(32, Math.max(0, now - settleLast));
2321
- const tuning = debugRuntime.activeTuning();
2322
- settleLast = now;
2323
- const runwayOffset = runwayOffsetOf(host);
2324
- const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
2325
- const reserveStep = 1 - Math.exp(-dt / tuning.reserveResponseMs);
2326
- reservePx += (0 - reservePx) * reserveStep;
2327
- if (lag <= .25 && reservePx <= .25) {
2328
- animatedH = host.scrollHeight - runwayOffset;
2329
- reservePx = 0;
2330
- velocityPxPerSec = 0;
2331
- finishAtNaturalFloor(host);
2332
- followLeaders.delete(host);
2333
- releaseRevealScale();
2334
- debugRuntime.reportFollow(host, null);
2335
- stopSettleListeners();
2336
- return;
2337
- }
2338
- const step = computeFollowStep(dt, {
2339
- lag,
2340
- speedEma: speedCpsRef.current,
2341
- velocityPxPerSec
2342
- }, tuning);
2343
- const minimumLag = Math.max(0, reservePx);
2344
- animatedH = Math.min(host.scrollHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
2345
- velocityPxPerSec = step.velocityPxPerSec;
2346
- settleAtFloor(host);
2347
- animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
2348
- reportFollow(host, false);
2349
- requestAnimationFrame(settleFrame);
2350
- };
2351
- requestAnimationFrame(settleFrame);
1813
+ scheduleLive();
1814
+ return;
1815
+ }
1816
+ if (!wasActive) {
1817
+ if (drainSourceRef.current === null) announcedOffsetRef.current = text.length;
1818
+ return;
1819
+ }
1820
+ clearTimer();
1821
+ drainSourceRef.current = text;
1822
+ drainNext();
1823
+ }, [active, text]);
1824
+ (0, react.useEffect)(() => () => {
1825
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
1826
+ timerRef.current = null;
1827
+ }, []);
1828
+ if (!active && !announcement.present) return null;
1829
+ const activating = active && !activeRef.current;
1830
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1831
+ className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
1832
+ "aria-live": "polite",
1833
+ "aria-atomic": "true",
1834
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activating ? "" : announcement.text }, announcement.revision)
1835
+ });
1836
+ });
1837
+ /**
1838
+ * Smooth streaming text arm. While the reply runs, the accumulated source is
1839
+ * revealed through the smoother at a rate that tracks the model's arrival
1840
+ * and rendered by the Harness `MarkdownText`
1841
+ * streaming arm (incremental parse, frozen non-tail blocks), so there is no
1842
+ * raw-text tail and no text-to-markdown swap: the tree stays markdown
1843
+ * throughout. The last text block owns conversation-port follow so wraps
1844
+ * glide instead of snapping. Once the stream closes and the reveal queue
1845
+ * drains, the settled full parse (KaTeX math, fence highlighting, file
1846
+ * mentions) swaps in exactly once.
1847
+ */
1848
+ function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, ownFollow, followSpeedCpsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack }) {
1849
+ const reduced = usePrefersReducedMotion();
1850
+ const [typing, setTyping] = (0, react.useState)(streaming);
1851
+ const localSpeedCpsRef = (0, react.useRef)(35);
1852
+ const followRootRef = (0, react.useRef)(null);
1853
+ const predictionSourceRef = (0, react.useRef)(null);
1854
+ const predictionStateRef = (0, react.useRef)(false);
1855
+ const predictionGeometryRef = (0, react.useRef)(null);
1856
+ const speedCpsRef = followSpeedCpsRef ?? localSpeedCpsRef;
1857
+ const displayed = useSmoothStreamContent(text, {
1858
+ enabled: typing && !reduced,
1859
+ inputComplete: !streaming,
1860
+ preset,
1861
+ shouldHoldBack,
1862
+ speedCpsRef,
1863
+ revealScaleRef: followRevealScaleRef
1864
+ });
1865
+ const shown = reduced ? text : displayed;
1866
+ const live = typing && !reduced;
1867
+ (0, react.useEffect)(() => {
1868
+ const root = followRootRef.current;
1869
+ if (root === null || typeof ResizeObserver === "undefined") return;
1870
+ const observer = new ResizeObserver(() => {
1871
+ if (predictionGeometryRef.current?.root === root) predictionGeometryRef.current = null;
1872
+ });
1873
+ observer.observe(root);
1874
+ return () => {
1875
+ observer.disconnect();
2352
1876
  };
1877
+ }, []);
1878
+ (0, react.useLayoutEffect)(() => {
1879
+ if (onPredictiveChange === void 0) return;
1880
+ const pending = text.slice(shown.length);
1881
+ const sourceChanged = predictionSourceRef.current !== text;
1882
+ const next = !live || !streaming || pending === "" ? false : sourceChanged ? pendingTextCanGrow(followRootRef.current, shown, pending, predictionGeometryRef) : predictionStateRef.current;
1883
+ predictionSourceRef.current = text;
1884
+ predictionStateRef.current = next;
1885
+ onPredictiveChange(next);
2353
1886
  }, [
2354
- active,
2355
- rootRef,
2356
- speedCpsRef,
2357
- revealScaleRef,
2358
- predictive,
2359
- predictiveRef
1887
+ live,
1888
+ onPredictiveChange,
1889
+ shown,
1890
+ streaming,
1891
+ text
1892
+ ]);
1893
+ (0, react.useEffect)(() => {
1894
+ if (typing && !streaming && shown.length === text.length) setTyping(false);
1895
+ }, [
1896
+ shown,
1897
+ streaming,
1898
+ text,
1899
+ typing
2360
1900
  ]);
1901
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
1902
+ active: live && ownFollow,
1903
+ speedCpsRef,
1904
+ revealScaleRef: followRevealScaleRef,
1905
+ predictive: streaming,
1906
+ hostRef: followRootRef,
1907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1908
+ text: live ? shown : text,
1909
+ streaming: live,
1910
+ codeLabels,
1911
+ fileMentions: live ? void 0 : fileMentions
1912
+ })
1913
+ });
1914
+ }
1915
+ function imageLabels(t) {
1916
+ return {
1917
+ image: t("image.label"),
1918
+ open: t("image.openOriginal"),
1919
+ openNamed: (label) => t("image.openOriginalLabel", { label }),
1920
+ loading: t("image.loading"),
1921
+ loadFailed: t("image.loadFailed"),
1922
+ lightbox: {
1923
+ dialog: t("image.preview"),
1924
+ close: t("image.closePreview")
1925
+ }
1926
+ };
1927
+ }
1928
+ function firstLine(text) {
1929
+ const newline = text.indexOf("\n");
1930
+ return newline === -1 ? text : text.slice(0, newline);
1931
+ }
1932
+ function latestLine(text) {
1933
+ const visible = text.trimEnd();
1934
+ const newline = visible.lastIndexOf("\n");
1935
+ return newline === -1 ? visible : visible.slice(newline + 1);
2361
1936
  }
2362
- //#endregion
2363
- //#region src/client/FollowHost.tsx
2364
1937
  /**
2365
- * Document-flow host that owns conversation-port follow while `active`.
2366
- * Shared by assistant blocks and every other Agent Chat row. `onGrowth` lets
2367
- * generic wrapped renderers re-arm one glide when their DOM grows without
2368
- * requiring a business-kind-specific lifecycle predicate.
1938
+ * Built-in Think disclosure with a smoothed `text` feed. Chevron and row
1939
+ * click stay on the disclosure chrome, which the plugin's AnimatedDisclosure
1940
+ * renders with a height-animated body (the harness primitive would mount and
1941
+ * unmount it, which cannot glide). The row opens only while this block is
1942
+ * the streaming tail and closes as soon as thinking ends — a later block,
1943
+ * or the assistant node settling — not when the rest of the reply is
1944
+ * still streaming.
2369
1945
  */
2370
- function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealScaleRef, predictive = true, predictiveRef, hostRef, children }) {
2371
- const localRootRef = (0, react.useRef)(null);
2372
- const rootRef = hostRef ?? localRootRef;
2373
- useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef);
1946
+ function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
1947
+ const reduced = usePrefersReducedMotion();
1948
+ const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
1949
+ const summaryRef = (0, react.useRef)(null);
1950
+ const displayed = useSmoothStreamContent(text, {
1951
+ enabled: running && !reduced,
1952
+ preset,
1953
+ shouldHoldBack,
1954
+ speedCpsRef: followSpeedCpsRef,
1955
+ revealScaleRef: followRevealScaleRef
1956
+ });
1957
+ const shown = running && !reduced ? displayed : text;
1958
+ const summary = running ? latestLine(shown) : firstLine(text);
1959
+ (0, react.useLayoutEffect)(() => {
1960
+ if (thinkAutoExpand) setExpanded(running);
1961
+ }, [running, thinkAutoExpand]);
2374
1962
  (0, react.useEffect)(() => {
2375
- if (onGrowth === void 0 || typeof ResizeObserver === "undefined") return;
2376
- const root = rootRef.current;
2377
- if (root === null) return;
2378
- let previousHeight = null;
2379
- const observer = new ResizeObserver((entries) => {
2380
- const nextHeight = entries[0]?.contentRect.height ?? root.getBoundingClientRect().height;
2381
- if (!Number.isFinite(nextHeight)) return;
2382
- if (previousHeight !== null && nextHeight > previousHeight + .5) onGrowth(nextHeight - previousHeight);
2383
- previousHeight = nextHeight;
2384
- });
2385
- observer.observe(root);
2386
- return () => {
2387
- observer.disconnect();
2388
- };
2389
- }, [onGrowth]);
1963
+ const element = summaryRef.current;
1964
+ if (element === null) return;
1965
+ element.scrollLeft = running ? element.scrollWidth - element.clientWidth : 0;
1966
+ }, [running, summary]);
2390
1967
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2391
- ref: rootRef,
2392
1968
  className: TypewriterAssistantNodeView_module_css_default.follow,
2393
- children
1969
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1970
+ className: TypewriterAssistantNodeView_module_css_default.think,
1971
+ "data-variant": "think",
1972
+ "data-state": running ? "running" : "ok",
1973
+ children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1974
+ className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
1975
+ children: t("row.running")
1976
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedDisclosure, {
1977
+ rowClassName: TypewriterAssistantNodeView_module_css_default.thinkRow,
1978
+ leadingClassName: TypewriterAssistantNodeView_module_css_default.thinkLeading,
1979
+ titleClassName: TypewriterAssistantNodeView_module_css_default.thinkTitle,
1980
+ chevronClassName: TypewriterAssistantNodeView_module_css_default.thinkChevron,
1981
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
1982
+ title: "Think",
1983
+ open: expanded,
1984
+ onToggle: () => {
1985
+ setExpanded((value) => !value);
1986
+ },
1987
+ collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1988
+ className: TypewriterAssistantNodeView_module_css_default.thinkSeparator,
1989
+ "aria-hidden": true
1990
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1991
+ ref: summaryRef,
1992
+ className: TypewriterAssistantNodeView_module_css_default.thinkSummary,
1993
+ "data-follow-end": running || void 0,
1994
+ children: summary
1995
+ })] }),
1996
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1997
+ className: TypewriterAssistantNodeView_module_css_default.thinkBody,
1998
+ children: shown
1999
+ })
2000
+ })]
2001
+ })
2394
2002
  });
2395
2003
  }
2004
+ /**
2005
+ * Assistant node renderer for the typewriter overlay. Text observed while
2006
+ * streaming is revealed by the smoother through the Harness Markdown
2007
+ * renderer at a rate that tracks arrival. Reasoning blocks keep the
2008
+ * built-in Think disclosure and only receive a smoothed text feed; the
2009
+ * outer node owns conversation-port follow while streaming; the final text
2010
+ * block keeps ownership while its settled reveal queue drains. The FPS guard
2011
+ * holds offscreen reveals when the frame rate is degraded. Settled text
2012
+ * renders with the full Markdown pipeline.
2013
+ */
2014
+ const TypewriterAssistantNodeView = (0, react.memo)(function TypewriterAssistantNodeView({ mode: _mode = DEFAULT_STREAM_CONFIG.mode, preset = DEFAULT_STREAM_CONFIG.preset, revealCharsPerSec: _revealCharsPerSec = DEFAULT_STREAM_CONFIG.revealCharsPerSec, scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec, maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec, thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand, node, useTurnData, openFile, loadImage, fileMentions, t }) {
2015
+ const data = node.data;
2016
+ const streaming = data.status === "running";
2017
+ const reduced = usePrefersReducedMotion();
2018
+ const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
2019
+ const rootSpeedRef = (0, react.useRef)(35);
2020
+ const rootRevealScaleRef = (0, react.useRef)(1);
2021
+ const reasoningTailIndex = streaming && data.blocks[data.blocks.length - 1]?.kind === "reasoning" ? data.blocks.length - 1 : -1;
2022
+ const reasoningOwnsSpeed = reasoningTailIndex !== -1;
2023
+ const rootPredictiveRef = (0, react.useRef)(false);
2024
+ const previousReasoningTailRef = (0, react.useRef)(-1);
2025
+ if (reasoningTailIndex !== previousReasoningTailRef.current) {
2026
+ rootPredictiveRef.current = false;
2027
+ if (!reasoningOwnsSpeed) rootSpeedRef.current = 35;
2028
+ previousReasoningTailRef.current = reasoningTailIndex;
2029
+ }
2030
+ const updateTextPrediction = (0, react.useMemo)(() => (predictive) => {
2031
+ rootPredictiveRef.current = predictive;
2032
+ }, []);
2033
+ const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
2034
+ const tail = useTurnData("turn-tail");
2035
+ const owner = (0, react.useMemo)(() => {
2036
+ if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
2037
+ if (tail?.closing?.finalNode.seq !== data.finalNode.seq) return void 0;
2038
+ return {
2039
+ turn,
2040
+ seq: data.finalNode.seq,
2041
+ openFile
2042
+ };
2043
+ }, [
2044
+ data.finalNode,
2045
+ openFile,
2046
+ tail,
2047
+ turn
2048
+ ]);
2049
+ const mentions = (0, react.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
2050
+ const codeLabels = (0, react.useMemo)(() => ({
2051
+ copyLabel: t("copy"),
2052
+ copiedLabel: t("copied")
2053
+ }), [t]);
2054
+ const imageLoader = loadImage ?? (async () => {
2055
+ throw new Error(t("image.serviceUnavailable"));
2056
+ });
2057
+ if (!(streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call"))) return null;
2058
+ const announcementText = data.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
2059
+ const rendered = [];
2060
+ const last = data.blocks.length - 1;
2061
+ let lastFollow = -1;
2062
+ for (let index = 0; index < data.blocks.length; index += 1) {
2063
+ const kind = data.blocks[index]?.kind;
2064
+ if (kind === "text" || kind === "reasoning") lastFollow = index;
2065
+ }
2066
+ for (let index = 0; index < data.blocks.length; index += 1) {
2067
+ const block = data.blocks[index];
2068
+ if (block === void 0) continue;
2069
+ switch (block.kind) {
2070
+ case "text":
2071
+ rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
2072
+ text: block.text,
2073
+ codeLabels,
2074
+ fileMentions: mentions,
2075
+ streaming,
2076
+ ownFollow: !streaming && index === lastFollow,
2077
+ followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
2078
+ followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
2079
+ onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
2080
+ preset,
2081
+ shouldHoldBack
2082
+ }, index));
2083
+ break;
2084
+ case "reasoning":
2085
+ rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
2086
+ text: block.text,
2087
+ running: streaming && index === last,
2088
+ preset,
2089
+ thinkAutoExpand,
2090
+ shouldHoldBack,
2091
+ followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
2092
+ followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
2093
+ t
2094
+ }, index));
2095
+ break;
2096
+ case "image": {
2097
+ const start = index;
2098
+ const group = [block];
2099
+ while (index + 1 < data.blocks.length) {
2100
+ const next = data.blocks[index + 1];
2101
+ if (next === void 0 || next.kind !== "image") break;
2102
+ group.push(next);
2103
+ index += 1;
2104
+ }
2105
+ rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_attachment.ImageGallery, {
2106
+ images: group,
2107
+ load: imageLoader,
2108
+ align: "start",
2109
+ labels: imageLabels(t)
2110
+ }, start));
2111
+ break;
2112
+ }
2113
+ case "tool-call": break;
2114
+ case "other": rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
2115
+ label: t("message.unknownBlock"),
2116
+ payload: block.block,
2117
+ truncatedLabel: (total) => t("json.truncated", { total })
2118
+ }, index));
2119
+ }
2120
+ }
2121
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2122
+ ref: guardRef,
2123
+ className: TypewriterAssistantNodeView_module_css_default.root,
2124
+ "data-streaming": streaming || void 0,
2125
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StreamAnnouncement, {
2126
+ text: announcementText,
2127
+ active: streaming && !reduced
2128
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
2129
+ active: streaming && !reduced,
2130
+ speedCpsRef: rootSpeedRef,
2131
+ revealScaleRef: rootRevealScaleRef,
2132
+ predictiveRef: rootPredictiveRef,
2133
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2134
+ className: TypewriterAssistantNodeView_module_css_default.body,
2135
+ children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2136
+ className: TypewriterAssistantNodeView_module_css_default.stopped,
2137
+ children: t("message.stopped")
2138
+ })]
2139
+ })
2140
+ })]
2141
+ });
2142
+ });
2396
2143
  //#endregion
2397
- //#region src/config.ts
2398
- /** Defaults shared by the Host schema and the client-side fallback. */
2399
- const DEFAULT_STREAM_CONFIG = {
2400
- mode: "typewriter",
2401
- preset: "balanced",
2402
- revealCharsPerSec: 80,
2403
- scrollSpeedPxPerSec: 48,
2404
- maxScrollSpeedPxPerSec: 1e3
2405
- };
2144
+ //#region src/client/auto-collapse-controller.ts
2406
2145
  /**
2407
- * Window global the Host writes into the served index HTML. The browser boot
2408
- * graph carries no per-entry config, so this inline script is the only
2409
- * Host-to-client configuration channel for a composed web plugin.
2146
+ * Auto-collapse coordinator: folds the work process of finished turns behind
2147
+ * one summary row so only the model's final answer stays visible.
2148
+ *
2149
+ * The Harness ChatView lays a conversation out as a flat list of flow items
2150
+ * (`[data-chat-flow] > [data-chat-flow-kind=…]`). This controller watches that
2151
+ * list, splits it into turn segments at `user`/`steering` boundaries closed by
2152
+ * `turn-tail`, and — once a segment has settled — hides its thinking rows,
2153
+ * tool/command/context seats and intermediate replies, leaving one clickable
2154
+ * `已处理 {时长}` row plus the final answer. Clicking the row expands the full
2155
+ * process again; the choice survives later renders because it lives in the
2156
+ * controller, not the DOM.
2157
+ *
2158
+ * Adapted from the standalone `dsh-auto-collapse` plugin (level-1 folding
2159
+ * only): no second-level chips, no merged thinking rows, no status-text
2160
+ * rewrite, no transition animation — the collapse decision itself stays
2161
+ * instantaneous and reversible.
2162
+ *
2163
+ * React coexistence follows the upstream design: injected rows are plain
2164
+ * siblings/prepend children the vdom diff never claims, native rows are only
2165
+ * ever touched through an inline-`display` ledger that records the precise
2166
+ * original value and restores it on stop, session switch, or foreign takeover
2167
+ * (a written-value comparison plus an ownership sentinel catches another
2168
+ * script rewriting styles behind our back). A MutationObserver batches real
2169
+ * structure changes into rAF passes; a low-frequency self-rearming audit pass
2170
+ * reconciles unobservable style writes; a setTimeout fallback keeps passes
2171
+ * flowing when background tabs suspend rAF.
2172
+ */
2173
+ const STYLE_ID = "dshss-auto-collapse-style";
2174
+ /** Class of the injected per-turn summary row; the follow engine's generalized
2175
+ * surface walk picks it up as an ordinary foreign flow sibling. */
2176
+ const PROCESSED_CLASS = "dshss-processed";
2177
+ /** Inline custom property marking elements whose display this plugin owns. */
2178
+ const DISPLAY_OWNED_PROP = "--dshss-display-owned";
2179
+ /** Unobservable external style writes are reconciled by this audit cadence. */
2180
+ const AUDIT_TICK_MS = 1e3;
2181
+ /** Background-tab fallback that flushes a scheduled pass when rAF is frozen. */
2182
+ const PASS_FALLBACK_MS = 60;
2183
+ /**
2184
+ * Fold motion, deliberately minimal: layout changes land synchronously (the
2185
+ * display toggle happens in the same tick as the decision) and nothing
2186
+ * animates geometry at all — the earlier container-height squeeze was removed
2187
+ * because real sessions showed residual drift after the pin released (other
2188
+ * layout participants keep settling around the fold). What remains is one
2189
+ * paint-only flourish: the summary row fades its opacity in. A fold is
2190
+ * therefore a single reflow followed by a fade — the most stable shape
2191
+ * possible.
2410
2192
  */
2411
- const STREAM_BOOT_GLOBAL = "__DSH_SMOOTH_STREAM_CONFIG__";
2412
- //#endregion
2413
- //#region src/client/TypewriterAssistantNodeView.tsx
2414
- function usePrefersReducedMotion() {
2415
- const [reduced, setReduced] = (0, react.useState)(() => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true);
2416
- (0, react.useEffect)(() => {
2417
- if (typeof window === "undefined" || window.matchMedia === void 0) return;
2418
- const query = window.matchMedia("(prefers-reduced-motion: reduce)");
2419
- const onChange = () => setReduced(query.matches);
2420
- query.addEventListener("change", onChange);
2421
- return () => query.removeEventListener("change", onChange);
2422
- }, []);
2423
- return reduced;
2424
- }
2425
- /** Conservative fallback before the streaming Markdown tail has geometry. */
2426
- const PREDICTIVE_WRAP_FALLBACK_CHARS = 32;
2427
- const STREAM_ANNOUNCEMENT_INTERVAL_MS = 800;
2428
- const STREAM_ANNOUNCEMENT_MAX_CHARS = 320;
2429
- function approximateInlineWidth(text, emPx) {
2430
- let width = 0;
2431
- for (const char of text) if (/\s/u.test(char)) width += emPx * .33;
2432
- else if (/^[\x00-\x7f]$/u.test(char)) width += emPx * .56;
2433
- else width += emPx;
2434
- return width;
2435
- }
2436
- function measurePendingTextGeometry(root, visibleText) {
2437
- if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
2438
- root,
2439
- visibleText,
2440
- fontSize: 14,
2441
- wrapThresholdWidth: null
2442
- };
2443
- const rootRect = root.getBoundingClientRect();
2444
- const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
2445
- if (rootWidth <= 0) return {
2446
- root,
2447
- visibleText,
2448
- fontSize: 14,
2449
- wrapThresholdWidth: null
2450
- };
2451
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2452
- let tail = null;
2453
- for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
2454
- const parent = tail?.parentElement ?? root;
2455
- const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
2456
- if (tail === null || typeof document.createRange !== "function") return {
2457
- root,
2458
- visibleText,
2459
- fontSize,
2460
- wrapThresholdWidth: rootWidth
2461
- };
2193
+ const FOLD_ROW_FADE_MS = 160;
2194
+ const FOLD_EASE_OUT = "cubic-bezier(0.22, 0.61, 0.36, 1)";
2195
+ const ZH_COPY = {
2196
+ processedLabel: "已处理",
2197
+ expandTitle: "展开工作过程",
2198
+ collapseTitle: "收起工作过程",
2199
+ formatDuration: formatZhDuration
2200
+ };
2201
+ const EN_COPY = {
2202
+ processedLabel: "Processed",
2203
+ expandTitle: "Show the work process",
2204
+ collapseTitle: "Hide the work process",
2205
+ formatDuration: formatEnDuration
2206
+ };
2207
+ function resolveFoldCopy() {
2208
+ let lang = "";
2462
2209
  try {
2463
- const length = tail.textContent?.length ?? 0;
2464
- if (length <= 0) return {
2465
- root,
2466
- visibleText,
2467
- fontSize,
2468
- wrapThresholdWidth: rootWidth
2469
- };
2470
- const range = document.createRange();
2471
- range.setStart(tail, Math.max(0, length - 1));
2472
- range.setEnd(tail, length);
2473
- const tailRect = range.getBoundingClientRect();
2474
- const contentRight = rootRect.right;
2475
- if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
2476
- root,
2477
- visibleText,
2478
- fontSize,
2479
- wrapThresholdWidth: rootWidth
2480
- };
2481
- return {
2482
- root,
2483
- visibleText,
2484
- fontSize,
2485
- wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
2486
- };
2210
+ lang = document.documentElement.lang || navigator.language || "";
2487
2211
  } catch {
2488
- return {
2489
- root,
2490
- visibleText,
2491
- fontSize,
2492
- wrapThresholdWidth: rootWidth
2493
- };
2212
+ lang = "";
2494
2213
  }
2214
+ return lang.toLowerCase().startsWith("zh") ? ZH_COPY : EN_COPY;
2495
2215
  }
2496
- /** Whether buffered source can reach a new visual line before it drains. */
2497
- function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
2498
- if (pending === "") return false;
2499
- if (/[\r\n]/u.test(pending)) return true;
2500
- const pendingChars = [...pending];
2501
- if (root === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
2502
- let geometry = geometryRef.current;
2503
- if (geometry?.root !== root || geometry.visibleText !== visibleText) {
2504
- geometry = measurePendingTextGeometry(root, visibleText);
2505
- geometryRef.current = geometry;
2216
+ const FOLD_CSS = `
2217
+ .${PROCESSED_CLASS} {
2218
+ box-sizing: border-box;
2219
+ display: inline-flex;
2220
+ align-self: flex-start;
2221
+ width: fit-content;
2222
+ max-width: 100%;
2223
+ align-items: center;
2224
+ gap: 6px;
2225
+ padding: 2px 0;
2226
+ border: none;
2227
+ background: none;
2228
+ font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
2229
+ color: var(--dsw-alias-label-tertiary, rgba(127, 127, 127, 0.9));
2230
+ cursor: pointer;
2231
+ user-select: none;
2232
+ border-radius: 4px;
2233
+ transition: color 0.15s ease;
2234
+ }
2235
+ .${PROCESSED_CLASS}:hover {
2236
+ color: var(--dsw-alias-label-primary, inherit);
2237
+ background: transparent;
2238
+ }
2239
+ .${PROCESSED_CLASS}:focus-visible {
2240
+ outline: 2px solid var(--dsw-alias-state-focus-ring, rgba(77, 107, 254, 0.8));
2241
+ outline-offset: 2px;
2242
+ }
2243
+ .${PROCESSED_CLASS}-chevron {
2244
+ display: inline-flex;
2245
+ flex: none;
2246
+ width: 14px;
2247
+ height: 14px;
2248
+ opacity: 0.55;
2249
+ transform: rotate(-90deg);
2250
+ transition: transform 0.12s ease, opacity 0.1s ease;
2251
+ }
2252
+ .${PROCESSED_CLASS}:hover .${PROCESSED_CLASS}-chevron {
2253
+ opacity: 0.9;
2254
+ }
2255
+ .${PROCESSED_CLASS}[aria-expanded="true"] .${PROCESSED_CLASS}-chevron {
2256
+ transform: rotate(0deg);
2257
+ }
2258
+ @media (prefers-reduced-motion: reduce) {
2259
+ .${PROCESSED_CLASS},
2260
+ .${PROCESSED_CLASS}-chevron {
2261
+ transition: none;
2262
+ }
2263
+ }
2264
+ `;
2265
+ var AutoCollapseController = class {
2266
+ observer = null;
2267
+ raf = 0;
2268
+ timer = 0;
2269
+ auditTimer = 0;
2270
+ /** True between start() and stop(); the preference toggle re-enters both. */
2271
+ running = false;
2272
+ lastPassError = "";
2273
+ flow = null;
2274
+ /** Stable segment key → summary row + gesture state. */
2275
+ segmentStates = /* @__PURE__ */ new Map();
2276
+ /** First sighting of a running segment, for turns without a host duration. */
2277
+ runningSince = /* @__PURE__ */ new Map();
2278
+ /** Segments that settled once: a resumed run restarts local timing. */
2279
+ completedOnce = /* @__PURE__ */ new Set();
2280
+ /** Precise pre-plugin display values, restored on stop/switch/takeover. */
2281
+ originalDisplay = /* @__PURE__ */ new WeakMap();
2282
+ writtenDisplay = /* @__PURE__ */ new WeakMap();
2283
+ controlledDisplay = /* @__PURE__ */ new Set();
2284
+ /** Reply-presence verdicts cached per message until a mutation hits it. */
2285
+ bodyTextCache = /* @__PURE__ */ new WeakMap();
2286
+ dirtyMessages = /* @__PURE__ */ new Set();
2287
+ onVisibilityChange = () => {
2288
+ if (typeof document === "undefined" || document.hidden !== true) this.schedule();
2289
+ };
2290
+ /** Re-run a pass immediately (settings changes, diagnostics, tests).
2291
+ * Reply-presence verdicts are dropped wholesale: manual refreshes are rare,
2292
+ * and a conservative rebuild beats a stale classification. */
2293
+ refresh() {
2294
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
2295
+ this.dirtyMessages.clear();
2296
+ this.schedule();
2297
+ }
2298
+ start() {
2299
+ if (this.running) return;
2300
+ this.running = true;
2301
+ injectStyle();
2302
+ try {
2303
+ this.observer = new MutationObserver((records) => {
2304
+ if (this.shouldSchedule(records)) {
2305
+ this.markDirty(records);
2306
+ this.schedule();
2307
+ }
2308
+ });
2309
+ this.observer.observe(document.body, {
2310
+ childList: true,
2311
+ subtree: true,
2312
+ attributes: true,
2313
+ attributeFilter: ["data-state"],
2314
+ characterData: true
2315
+ });
2316
+ this.armAuditLoop();
2317
+ this.schedule();
2318
+ } catch (error) {
2319
+ this.reportError(error);
2320
+ throw error;
2321
+ }
2322
+ }
2323
+ stop() {
2324
+ this.running = false;
2325
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
2326
+ this.raf = 0;
2327
+ if (this.timer !== 0) clearTimeout(this.timer);
2328
+ this.timer = 0;
2329
+ if (this.auditTimer !== 0) clearTimeout(this.auditTimer);
2330
+ this.auditTimer = 0;
2331
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", this.onVisibilityChange);
2332
+ this.observer?.disconnect();
2333
+ this.observer = null;
2334
+ this.switchFlow(null);
2335
+ removeStyle();
2336
+ }
2337
+ /** Low-frequency reconciliation for style writes no observer can see. */
2338
+ armAuditLoop() {
2339
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") document.addEventListener("visibilitychange", this.onVisibilityChange);
2340
+ this.rearmAudit();
2341
+ }
2342
+ rearmAudit() {
2343
+ if (!this.running || this.auditTimer !== 0) return;
2344
+ this.auditTimer = setTimeout(() => {
2345
+ this.auditTimer = 0;
2346
+ if (!this.running) return;
2347
+ if (typeof document !== "undefined" && document.hidden === true) {
2348
+ this.rearmAudit();
2349
+ return;
2350
+ }
2351
+ this.schedule();
2352
+ this.rearmAudit();
2353
+ }, AUDIT_TICK_MS);
2506
2354
  }
2507
- if (geometry.wrapThresholdWidth === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
2508
- return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
2509
- }
2510
- function announcementChunkEnd(source, start) {
2511
- const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
2512
- if (hardEnd === source.length) return hardEnd;
2513
- const softStart = start + Math.floor(STREAM_ANNOUNCEMENT_MAX_CHARS * .6);
2514
- for (let index = hardEnd - 1; index >= softStart; index -= 1) if (/[\s.,;:!?]/u.test(source[index] ?? "")) return index + 1;
2515
- return hardEnd;
2516
- }
2517
- /** A commit-driven live region isolated from the visible Markdown subtree. */
2518
- const StreamAnnouncement = (0, react.memo)(function StreamAnnouncement({ text, active }) {
2519
- const [announcement, setAnnouncement] = (0, react.useState)({
2520
- text: "",
2521
- revision: 0,
2522
- present: active
2523
- });
2524
- const sourceRef = (0, react.useRef)(text);
2525
- const activeRef = (0, react.useRef)(active);
2526
- const announcedOffsetRef = (0, react.useRef)(active ? 0 : text.length);
2527
- const drainSourceRef = (0, react.useRef)(null);
2528
- const timerRef = (0, react.useRef)(null);
2529
- (0, react.useEffect)(() => {
2530
- const clearTimer = () => {
2531
- if (timerRef.current === null) return;
2532
- clearTimeout(timerRef.current);
2533
- timerRef.current = null;
2534
- };
2535
- const publishNext = (source) => {
2536
- const start = Math.min(announcedOffsetRef.current, source.length);
2537
- const end = announcementChunkEnd(source, start);
2538
- if (end <= start) return false;
2539
- announcedOffsetRef.current = end;
2540
- setAnnouncement((previous) => ({
2541
- text: source.slice(start, end),
2542
- revision: previous.revision + 1,
2543
- present: true
2544
- }));
2545
- return end < source.length;
2546
- };
2547
- const hideAfterLinger = () => {
2548
- timerRef.current = setTimeout(() => {
2549
- timerRef.current = null;
2550
- if (activeRef.current) return;
2551
- drainSourceRef.current = null;
2552
- setAnnouncement((previous) => ({
2553
- ...previous,
2554
- present: false
2555
- }));
2556
- }, STREAM_ANNOUNCEMENT_INTERVAL_MS);
2557
- };
2558
- const drainNext = () => {
2559
- const source = drainSourceRef.current;
2560
- if (source === null) return;
2561
- if (!publishNext(source)) {
2562
- hideAfterLinger();
2355
+ /** Body-level observations only need to wake us up near the active flow. */
2356
+ shouldSchedule(records) {
2357
+ if (records.length === 0 || this.flow === null || !this.flow.isConnected) return true;
2358
+ return records.some((record) => nodeWithin(record.target, this.flow) || nodeWithin(this.flow, record.target));
2359
+ }
2360
+ /** Walk each record to its flow child so reply-cache invalidation stays
2361
+ * targeted; anything unattributable invalidates conservatively. */
2362
+ markDirty(records) {
2363
+ const flow = this.flow;
2364
+ if (flow === null || !flow.isConnected) return;
2365
+ if (records.length === 0) {
2366
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
2367
+ this.dirtyMessages.clear();
2368
+ return;
2369
+ }
2370
+ for (const record of records) {
2371
+ let current = record.target;
2372
+ while (current !== null && current.parentNode !== flow) current = current.parentNode;
2373
+ if (!(current instanceof HTMLElement)) {
2374
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
2375
+ this.dirtyMessages.clear();
2563
2376
  return;
2564
2377
  }
2565
- timerRef.current = setTimeout(() => {
2566
- timerRef.current = null;
2567
- if (activeRef.current) return;
2568
- drainNext();
2569
- }, STREAM_ANNOUNCEMENT_INTERVAL_MS);
2570
- };
2571
- const scheduleLive = () => {
2572
- if (timerRef.current !== null || announcedOffsetRef.current >= sourceRef.current.length) return;
2573
- timerRef.current = setTimeout(() => {
2574
- timerRef.current = null;
2575
- if (!activeRef.current) return;
2576
- const source = sourceRef.current;
2577
- if (publishNext(source)) scheduleLive();
2578
- }, STREAM_ANNOUNCEMENT_INTERVAL_MS);
2579
- };
2580
- const wasActive = activeRef.current;
2581
- const previousSource = sourceRef.current;
2582
- if (!(!active && !wasActive && drainSourceRef.current !== null) && (!text.startsWith(previousSource) || announcedOffsetRef.current > text.length)) announcedOffsetRef.current = 0;
2583
- sourceRef.current = text;
2584
- activeRef.current = active;
2585
- if (active) {
2586
- if (!wasActive) {
2587
- clearTimer();
2588
- drainSourceRef.current = null;
2589
- setAnnouncement((previous) => ({
2590
- text: "",
2591
- revision: previous.revision + 1,
2592
- present: true
2593
- }));
2378
+ this.dirtyMessages.add(current);
2379
+ }
2380
+ }
2381
+ schedule() {
2382
+ if (!this.running || this.raf !== 0) return;
2383
+ this.raf = requestAnimationFrame(() => {
2384
+ this.raf = 0;
2385
+ if (this.timer !== 0) {
2386
+ clearTimeout(this.timer);
2387
+ this.timer = 0;
2594
2388
  }
2595
- scheduleLive();
2389
+ this.runPass();
2390
+ });
2391
+ if (this.timer !== 0) clearTimeout(this.timer);
2392
+ this.timer = setTimeout(() => {
2393
+ this.timer = 0;
2394
+ if (this.raf !== 0) {
2395
+ cancelAnimationFrame(this.raf);
2396
+ this.raf = 0;
2397
+ this.runPass();
2398
+ }
2399
+ }, PASS_FALLBACK_MS);
2400
+ }
2401
+ runPass() {
2402
+ try {
2403
+ this.pass();
2404
+ this.lastPassError = "";
2405
+ } catch (error) {
2406
+ this.reportError(error);
2407
+ }
2408
+ }
2409
+ reportError(error) {
2410
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2411
+ if (message === this.lastPassError) return;
2412
+ this.lastPassError = message;
2413
+ console.error("[dsh-smooth-stream] auto-collapse pass failed", error);
2414
+ }
2415
+ pass() {
2416
+ if (!this.running) return;
2417
+ const nextFlow = findFlow();
2418
+ if (nextFlow !== this.flow) this.switchFlow(nextFlow);
2419
+ const flow = this.flow;
2420
+ if (flow === null) return;
2421
+ for (const el of this.dirtyMessages) this.bodyTextCache.delete(el);
2422
+ this.dirtyMessages.clear();
2423
+ const segments = buildSegments(flow, (el) => this.hasBodyCached(el));
2424
+ const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
2425
+ for (const segment of segments) {
2426
+ if (!segment.running) continue;
2427
+ if (this.completedOnce.has(segment.key)) {
2428
+ this.completedOnce.delete(segment.key);
2429
+ this.runningSince.delete(segment.key);
2430
+ }
2431
+ if (!this.runningSince.has(segment.key)) this.runningSince.set(segment.key, Date.now());
2432
+ }
2433
+ const completedKeys = /* @__PURE__ */ new Set();
2434
+ for (const segment of segments) {
2435
+ if (!segment.closed || segment.running || !segment.hasWork) continue;
2436
+ completedKeys.add(segment.key);
2437
+ this.completedOnce.add(segment.key);
2438
+ let state = this.segmentStates.get(segment.key);
2439
+ if (state === void 0) {
2440
+ state = {
2441
+ key: segment.key,
2442
+ row: null,
2443
+ expanded: false,
2444
+ snapshot: segment
2445
+ };
2446
+ this.segmentStates.set(segment.key, state);
2447
+ } else state.snapshot = segment;
2448
+ const started = this.runningSince.get(segment.key);
2449
+ const parsed = segment.boundary === null ? void 0 : parseTurnDuration(segment.boundary);
2450
+ if (parsed !== void 0) state.duration = parsed;
2451
+ else if (state.duration === void 0 && started !== void 0) state.duration = Date.now() - started;
2452
+ if (state.row === null || !state.row.isConnected) state.row = this.createProcessedRow(state);
2453
+ this.syncProcessedRow(state);
2454
+ }
2455
+ for (const [key, state] of [...this.segmentStates]) {
2456
+ if (completedKeys.has(key)) continue;
2457
+ state.row?.remove();
2458
+ this.segmentStates.delete(key);
2459
+ }
2460
+ const desiredHidden = /* @__PURE__ */ new Set();
2461
+ for (const segment of segments) {
2462
+ const state = this.segmentStates.get(segment.key);
2463
+ if (state !== void 0 && !state.expanded) {
2464
+ for (const seat of segment.hideSeats) this.hideElement(seat, desiredHidden);
2465
+ for (const row of segment.finalThinkRows) this.hideElement(row, desiredHidden);
2466
+ if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
2467
+ } else {
2468
+ for (const seat of segment.hideSeats) this.restoreElement(seat);
2469
+ for (const row of segment.finalThinkRows) this.restoreElement(row);
2470
+ if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
2471
+ }
2472
+ }
2473
+ for (const segment of segments) {
2474
+ if (!hasVisibleSegmentWork(segment)) {
2475
+ const state = this.segmentStates.get(segment.key);
2476
+ if (state !== void 0 && state.row !== null) {
2477
+ state.row.remove();
2478
+ state.row = null;
2479
+ }
2480
+ }
2481
+ for (const seat of segment.hideSeats) this.retainDisplayControl(seat, desiredHidden);
2482
+ for (const row of segment.finalThinkRows) this.retainDisplayControl(row, desiredHidden);
2483
+ if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
2484
+ }
2485
+ this.restoreUnusedDisplays(desiredHidden);
2486
+ for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
2487
+ for (const key of [...this.runningSince.keys()]) if (!liveSegmentKeys.has(key)) this.runningSince.delete(key);
2488
+ for (const key of [...this.completedOnce]) if (!liveSegmentKeys.has(key)) this.completedOnce.delete(key);
2489
+ }
2490
+ /** Flow swap means a session switch: fully restore the old tree, rebuild. */
2491
+ switchFlow(next) {
2492
+ if (next === this.flow) return;
2493
+ for (const state of this.segmentStates.values()) state.row?.remove();
2494
+ this.segmentStates.clear();
2495
+ this.runningSince.clear();
2496
+ this.completedOnce.clear();
2497
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
2498
+ this.dirtyMessages.clear();
2499
+ this.restoreAllDisplays();
2500
+ this.flow = next;
2501
+ }
2502
+ /** Keep already-controlled elements in the desired-hidden set so the sweep
2503
+ * does not flip them back mid-flight of an external change. */
2504
+ retainDisplayControl(el, desiredHidden) {
2505
+ if (this.controlledDisplay.has(el)) desiredHidden.add(el);
2506
+ }
2507
+ /** Restore every element the ledger controls that this pass no longer wants
2508
+ * hidden; elements absent from `desired` come back to their true display. */
2509
+ restoreUnusedDisplays(desired) {
2510
+ for (const el of [...this.controlledDisplay]) if (!desired.has(el)) this.restoreElement(el);
2511
+ }
2512
+ /** Restore every element still under the display ledger, then retire the
2513
+ * ledger itself (stop / session switch). */
2514
+ restoreAllDisplays() {
2515
+ for (const el of [...this.controlledDisplay]) this.restoreElement(el);
2516
+ this.controlledDisplay.clear();
2517
+ this.originalDisplay = /* @__PURE__ */ new WeakMap();
2518
+ this.writtenDisplay = /* @__PURE__ */ new WeakMap();
2519
+ }
2520
+ createProcessedRow(state) {
2521
+ const row = createProcessedRowElement(state.duration);
2522
+ row.addEventListener("click", () => {
2523
+ state.expanded = !state.expanded;
2524
+ this.syncProcessedRow(state);
2525
+ this.schedule();
2526
+ });
2527
+ return row;
2528
+ }
2529
+ syncProcessedRow(state) {
2530
+ const row = state.row;
2531
+ if (row === null) return;
2532
+ const copy = resolveFoldCopy();
2533
+ const text = row.firstElementChild;
2534
+ const label = state.duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(state.duration)}`;
2535
+ if (text !== null && text.textContent !== label) text.textContent = label;
2536
+ const expanded = String(state.expanded);
2537
+ if (row.getAttribute("aria-expanded") !== expanded) row.setAttribute("aria-expanded", expanded);
2538
+ const title = state.expanded ? copy.collapseTitle : copy.expandTitle;
2539
+ if (row.title !== title) row.title = title;
2540
+ }
2541
+ placeProcessedRow(flow, state) {
2542
+ const row = state.row;
2543
+ if (row === null) return;
2544
+ if (!state.snapshot.hasWork || !hasVisibleSegmentWork(state.snapshot)) {
2545
+ row.remove();
2546
+ state.row = null;
2596
2547
  return;
2597
2548
  }
2598
- if (!wasActive) {
2599
- if (drainSourceRef.current === null) announcedOffsetRef.current = text.length;
2549
+ let target = state.snapshot.firstWork ?? state.snapshot.finalStep ?? state.snapshot.boundary;
2550
+ if (target === null || target.parentElement !== flow) {
2551
+ row.remove();
2552
+ state.row = null;
2600
2553
  return;
2601
2554
  }
2602
- clearTimer();
2603
- drainSourceRef.current = text;
2604
- drainNext();
2605
- }, [active, text]);
2606
- (0, react.useEffect)(() => () => {
2607
- if (timerRef.current !== null) clearTimeout(timerRef.current);
2608
- timerRef.current = null;
2609
- }, []);
2610
- if (!active && !announcement.present) return null;
2611
- const activating = active && !activeRef.current;
2612
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2613
- className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
2614
- "aria-live": "polite",
2615
- "aria-atomic": "true",
2616
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activating ? "" : announcement.text }, announcement.revision)
2617
- });
2618
- });
2555
+ if (row.parentElement !== flow || row.nextElementSibling !== target) {
2556
+ target.before(row);
2557
+ this.revealVisualOnce(row);
2558
+ }
2559
+ }
2560
+ hasBodyCached(el) {
2561
+ const cached = this.bodyTextCache.get(el);
2562
+ if (cached !== void 0) return cached;
2563
+ const value = hasBodyContent(el);
2564
+ this.bodyTextCache.set(el, value);
2565
+ return value;
2566
+ }
2567
+ /** True when the recorded display was changed (or the ownership sentinel
2568
+ * wiped) behind the ledger's back; callers hand control back to reality. */
2569
+ displayForeign(el) {
2570
+ const written = this.writtenDisplay.get(el);
2571
+ if (written === void 0) return false;
2572
+ return el.style.getPropertyValue(DISPLAY_OWNED_PROP) === "" || el.style.display !== written;
2573
+ }
2574
+ releaseDisplayLedger(el) {
2575
+ this.originalDisplay.delete(el);
2576
+ this.writtenDisplay.delete(el);
2577
+ this.controlledDisplay.delete(el);
2578
+ el.style.removeProperty(DISPLAY_OWNED_PROP);
2579
+ }
2580
+ hideElement(el, desired) {
2581
+ desired.add(el);
2582
+ if (!this.originalDisplay.has(el) && !isDisplayed(el)) return;
2583
+ if (!this.originalDisplay.has(el) || this.displayForeign(el)) {
2584
+ this.originalDisplay.set(el, el.style.display);
2585
+ this.writtenDisplay.set(el, el.style.display);
2586
+ el.style.setProperty(DISPLAY_OWNED_PROP, "1");
2587
+ }
2588
+ this.controlledDisplay.add(el);
2589
+ if (el.style.display === "none") return;
2590
+ el.style.display = "none";
2591
+ this.writtenDisplay.set(el, "none");
2592
+ }
2593
+ restoreElement(el) {
2594
+ if (!this.originalDisplay.has(el)) return;
2595
+ if (this.displayForeign(el)) {
2596
+ this.releaseDisplayLedger(el);
2597
+ return;
2598
+ }
2599
+ const original = this.originalDisplay.get(el);
2600
+ if (el.style.display !== original) el.style.display = original;
2601
+ this.releaseDisplayLedger(el);
2602
+ }
2603
+ /** Whether WAAPI exists and the user has not asked for reduced motion. */
2604
+ canAnimate(el) {
2605
+ if (typeof el.animate !== "function") return false;
2606
+ try {
2607
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
2608
+ } catch {
2609
+ return false;
2610
+ }
2611
+ return true;
2612
+ }
2613
+ /** First-appearance flourish for the plugin-owned summary row: a pure
2614
+ * opacity fade. Paint-only, so it cannot shift anyone's layout. */
2615
+ revealVisualOnce(row) {
2616
+ if (row.dataset.dshssShown === "1") return;
2617
+ row.dataset.dshssShown = "1";
2618
+ if (!this.canAnimate(row)) return;
2619
+ row.animate([{ opacity: "0" }, { opacity: "1" }], {
2620
+ duration: FOLD_ROW_FADE_MS,
2621
+ easing: FOLD_EASE_OUT
2622
+ });
2623
+ }
2624
+ };
2625
+ /** Visible chat flow container, falling back to the first one rendered. */
2626
+ function findFlow() {
2627
+ const flows = document.querySelectorAll("[data-chat-flow]");
2628
+ for (const flow of flows) if (flow.offsetParent !== null || flow.getBoundingClientRect().width > 0) return flow;
2629
+ return flows[0] ?? null;
2630
+ }
2631
+ /** parentNode walk that also accepts Text mutation targets. */
2632
+ function nodeWithin(node, ancestor) {
2633
+ for (let current = node; current !== null; current = current.parentNode) if (current === ancestor) return true;
2634
+ return false;
2635
+ }
2636
+ /** Top-level message order, excluding this plugin's own summary rows. */
2637
+ function flowItems(flow) {
2638
+ return [...flow.children].filter((el) => el instanceof HTMLElement && !el.classList.contains(PROCESSED_CLASS));
2639
+ }
2640
+ function isDisplayed(el) {
2641
+ if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
2642
+ return el.style.display !== "none";
2643
+ }
2644
+ function stableElementKey(el, fallbackIndex) {
2645
+ const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
2646
+ return `${kind}:${el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`}`;
2647
+ }
2648
+ /** Kinds that can open a history stretch lacking an explicit user marker. */
2649
+ function opensTurnWork(el) {
2650
+ const kind = el.getAttribute("data-chat-flow-kind");
2651
+ return kind === "assistant-step" || kind === "assistant" || kind === "tool-call" || kind === "command" || kind === "manual-compaction";
2652
+ }
2619
2653
  /**
2620
- * Smooth streaming text arm. While the reply runs, the accumulated source is
2621
- * revealed through the smoother at a rate that tracks the model's arrival
2622
- * and rendered by the Harness `MarkdownText`
2623
- * streaming arm (incremental parse, frozen non-tail blocks), so there is no
2624
- * raw-text tail and no text-to-markdown swap: the tree stays markdown
2625
- * throughout. The last text block owns conversation-port follow so wraps
2626
- * glide instead of snapping. Once the stream closes and the reveal queue
2627
- * drains, the settled full parse (KaTeX math, fence highlighting, file
2628
- * mentions) swaps in exactly once.
2654
+ * Split the flow into turn segments. `user`/`steering` rows close the previous
2655
+ * segment and open the next; `turn-tail` closes and resets the opener. Leading
2656
+ * context before the first user row joins that user's segment, matching how
2657
+ * the host presents context injection.
2629
2658
  */
2630
- function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, ownFollow, followSpeedCpsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack }) {
2631
- const reduced = usePrefersReducedMotion();
2632
- const [typing, setTyping] = (0, react.useState)(streaming);
2633
- const localSpeedCpsRef = (0, react.useRef)(35);
2634
- const followRootRef = (0, react.useRef)(null);
2635
- const predictionSourceRef = (0, react.useRef)(null);
2636
- const predictionStateRef = (0, react.useRef)(false);
2637
- const predictionGeometryRef = (0, react.useRef)(null);
2638
- const speedCpsRef = followSpeedCpsRef ?? localSpeedCpsRef;
2639
- const displayed = useSmoothStreamContent(text, {
2640
- enabled: typing && !reduced,
2641
- inputComplete: !streaming,
2642
- preset,
2643
- shouldHoldBack,
2644
- speedCpsRef,
2645
- revealScaleRef: followRevealScaleRef
2646
- });
2647
- const shown = reduced ? text : displayed;
2648
- const live = typing && !reduced;
2649
- (0, react.useEffect)(() => {
2650
- const root = followRootRef.current;
2651
- if (root === null || typeof ResizeObserver === "undefined") return;
2652
- const observer = new ResizeObserver(() => {
2653
- if (predictionGeometryRef.current?.root === root) predictionGeometryRef.current = null;
2659
+ function buildSegments(flow, hasBody) {
2660
+ const items = flowItems(flow);
2661
+ const itemIndex = new Map(items.map((el, index) => [el, index]));
2662
+ const snapshots = [];
2663
+ let contentStart = 0;
2664
+ let startMarker = null;
2665
+ const append = (end, boundary, closed) => {
2666
+ if (end < contentStart) return;
2667
+ const range = items.slice(contentStart, end);
2668
+ const hideSeats = /* @__PURE__ */ new Set();
2669
+ const bodySteps = [];
2670
+ let running = false;
2671
+ for (const el of range) {
2672
+ const classified = classifyItem(el, hasBody);
2673
+ if (classified.fold === "body") bodySteps.push(el);
2674
+ else if (classified.fold === "seat") hideSeats.add(el);
2675
+ if (classified.running) running = true;
2676
+ }
2677
+ const finalStep = bodySteps.at(-1) ?? null;
2678
+ const middleSteps = new Set(bodySteps.slice(0, -1));
2679
+ for (const step of middleSteps) hideSeats.add(step);
2680
+ const finalThinkRows = finalStep === null ? [] : thinkRowsIn(finalStep);
2681
+ const firstWork = range.find((el) => hideSeats.has(el)) ?? finalStep;
2682
+ const identity = startMarker ?? range.find((el) => opensTurnWork(el)) ?? boundary;
2683
+ const identityIndex = identity === null ? contentStart : itemIndex.get(identity) ?? contentStart;
2684
+ const key = `${startMarker === null ? "leading" : "segment"}:${identity === null ? `open:${contentStart}` : stableElementKey(identity, identityIndex)}`;
2685
+ snapshots.push({
2686
+ key,
2687
+ boundary,
2688
+ startMarker,
2689
+ hideSeats,
2690
+ middleSteps,
2691
+ finalStep,
2692
+ finalThinkRows,
2693
+ firstWork,
2694
+ closed,
2695
+ running,
2696
+ hasWork: hideSeats.size > 0 || finalThinkRows.length > 0
2654
2697
  });
2655
- observer.observe(root);
2656
- return () => {
2657
- observer.disconnect();
2658
- };
2659
- }, []);
2660
- (0, react.useLayoutEffect)(() => {
2661
- if (onPredictiveChange === void 0) return;
2662
- const pending = text.slice(shown.length);
2663
- const sourceChanged = predictionSourceRef.current !== text;
2664
- const next = !live || !streaming || pending === "" ? false : sourceChanged ? pendingTextCanGrow(followRootRef.current, shown, pending, predictionGeometryRef) : predictionStateRef.current;
2665
- predictionSourceRef.current = text;
2666
- predictionStateRef.current = next;
2667
- onPredictiveChange(next);
2668
- }, [
2669
- live,
2670
- onPredictiveChange,
2671
- shown,
2672
- streaming,
2673
- text
2674
- ]);
2675
- (0, react.useEffect)(() => {
2676
- if (typing && !streaming && shown.length === text.length) setTyping(false);
2677
- }, [
2678
- shown,
2679
- streaming,
2680
- text,
2681
- typing
2682
- ]);
2683
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
2684
- active: live && ownFollow,
2685
- speedCpsRef,
2686
- revealScaleRef: followRevealScaleRef,
2687
- predictive: streaming,
2688
- hostRef: followRootRef,
2689
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
2690
- text: live ? shown : text,
2691
- streaming: live,
2692
- codeLabels,
2693
- fileMentions: live ? void 0 : fileMentions
2694
- })
2698
+ };
2699
+ items.forEach((el, index) => {
2700
+ const kind = el.getAttribute("data-chat-flow-kind");
2701
+ if (kind === "user" || kind === "steering") {
2702
+ if (startMarker !== null) {
2703
+ append(index, el, true);
2704
+ contentStart = index + 1;
2705
+ } else if (items.slice(contentStart, index).some((el) => opensTurnWork(el))) {
2706
+ append(index, el, true);
2707
+ contentStart = index + 1;
2708
+ }
2709
+ startMarker = el;
2710
+ return;
2711
+ }
2712
+ if (kind === "turn-tail") {
2713
+ append(index, el, true);
2714
+ contentStart = index + 1;
2715
+ startMarker = null;
2716
+ }
2695
2717
  });
2718
+ if (contentStart < items.length) append(items.length, null, false);
2719
+ return snapshots;
2696
2720
  }
2697
- function imageLabels(t) {
2721
+ /**
2722
+ * Classify one top-level flow item against the Harness's own node vocabulary.
2723
+ * Reply-bearing assistant messages become `body` candidates (last one per
2724
+ * segment wins as the final answer); core work process — tool-call seats,
2725
+ * command/compaction cards, context injection, thinking-only messages —
2726
+ * becomes a hideable `seat`. Everything else is skipped untouched: boundaries,
2727
+ * empty decorations, and crucially any kind this module does not own, so rows
2728
+ * contributed by other plugins' slots are never folded or hidden.
2729
+ */
2730
+ function classifyItem(el, hasBody) {
2731
+ const kind = el.getAttribute("data-chat-flow-kind");
2732
+ if (kind === "user" || kind === "steering" || kind === "turn-tail") return {
2733
+ fold: "skip",
2734
+ running: false
2735
+ };
2736
+ if (kind === "assistant-step" || kind === "assistant") {
2737
+ if (hasBody(el)) return {
2738
+ fold: "body",
2739
+ running: subtreeRunning(el)
2740
+ };
2741
+ if (thinkRowsIn(el).length > 0) return {
2742
+ fold: "seat",
2743
+ running: subtreeRunning(el)
2744
+ };
2745
+ return {
2746
+ fold: "skip",
2747
+ running: false
2748
+ };
2749
+ }
2750
+ if (kind === "tool-call") {
2751
+ const rows = topCallRowsIn(el);
2752
+ if (rows.length === 0 || !rows.every((row) => hasNativeCardChrome(row))) return {
2753
+ fold: "skip",
2754
+ running: subtreeRunning(el)
2755
+ };
2756
+ return {
2757
+ fold: "seat",
2758
+ running: subtreeRunning(el)
2759
+ };
2760
+ }
2761
+ if (kind === "context" || kind === "command" || kind === "manual-compaction") return {
2762
+ fold: "seat",
2763
+ running: subtreeRunning(el)
2764
+ };
2698
2765
  return {
2699
- image: t("image.label"),
2700
- open: t("image.openOriginal"),
2701
- openNamed: (label) => t("image.openOriginalLabel", { label }),
2702
- loading: t("image.loading"),
2703
- loadFailed: t("image.loadFailed"),
2704
- lightbox: {
2705
- dialog: t("image.preview"),
2706
- close: t("image.closePreview")
2707
- }
2766
+ fold: "skip",
2767
+ running: false
2708
2768
  };
2709
2769
  }
2710
- function firstLine(text) {
2711
- const newline = text.indexOf("\n");
2712
- return newline === -1 ? text : text.slice(0, newline);
2770
+ /** Native work-card shape: every core card family stamps `data-variant`
2771
+ * together with `data-state`, whether composed through ToolRow or hand-rolled
2772
+ * (Bash). Custom toolviews use their own namespaced attributes instead. */
2773
+ function hasNativeCardChrome(row) {
2774
+ return row.querySelector("[data-variant][data-state]") !== null;
2713
2775
  }
2714
- function latestLine(text) {
2715
- const visible = text.trimEnd();
2716
- const newline = visible.lastIndexOf("\n");
2717
- return newline === -1 ? visible : visible.slice(newline + 1);
2776
+ /** Whether anything inside the item reports a running lifecycle. Deliberately
2777
+ * broad: staying "running" too long only delays a collapse, collapsing early
2778
+ * would cut off live output. Native cards stamp `data-state` next to
2779
+ * `data-tool`/`data-variant`; custom views use their own vocabulary and are
2780
+ * intentionally invisible here. */
2781
+ function subtreeRunning(el) {
2782
+ if (el.getAttribute("data-state") === "running") return true;
2783
+ for (const root of el.querySelectorAll("[data-tool][data-state], [data-variant][data-state]")) if (root.getAttribute("data-state") === "running") return true;
2784
+ return false;
2718
2785
  }
2719
- /**
2720
- * Built-in Think disclosure with a smoothed `text` feed. Chevron and row
2721
- * click stay on the disclosure chrome, which the plugin's AnimatedDisclosure
2722
- * renders with a height-animated body (the harness primitive would mount and
2723
- * unmount it, which cannot glide). The row opens only while this block is
2724
- * the streaming tail and closes as soon as thinking ends — a later block,
2725
- * or the assistant node settling — not when the rest of the reply is
2726
- * still streaming.
2727
- */
2728
- function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
2729
- const reduced = usePrefersReducedMotion();
2730
- const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
2731
- const summaryRef = (0, react.useRef)(null);
2732
- const displayed = useSmoothStreamContent(text, {
2733
- enabled: running && !reduced,
2734
- preset,
2735
- shouldHoldBack,
2736
- speedCpsRef: followSpeedCpsRef,
2737
- revealScaleRef: followRevealScaleRef
2738
- });
2739
- const shown = running && !reduced ? displayed : text;
2740
- const summary = running ? latestLine(shown) : firstLine(text);
2741
- (0, react.useLayoutEffect)(() => {
2742
- if (thinkAutoExpand) setExpanded(running);
2743
- }, [running, thinkAutoExpand]);
2744
- (0, react.useEffect)(() => {
2745
- const element = summaryRef.current;
2746
- if (element === null) return;
2747
- element.scrollLeft = running ? element.scrollWidth - element.clientWidth : 0;
2748
- }, [running, summary]);
2749
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2750
- className: TypewriterAssistantNodeView_module_css_default.follow,
2751
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2752
- className: TypewriterAssistantNodeView_module_css_default.think,
2753
- "data-variant": "think",
2754
- "data-state": running ? "running" : "ok",
2755
- children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2756
- className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
2757
- children: t("row.running")
2758
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedDisclosure, {
2759
- rowClassName: TypewriterAssistantNodeView_module_css_default.thinkRow,
2760
- leadingClassName: TypewriterAssistantNodeView_module_css_default.thinkLeading,
2761
- titleClassName: TypewriterAssistantNodeView_module_css_default.thinkTitle,
2762
- chevronClassName: TypewriterAssistantNodeView_module_css_default.thinkChevron,
2763
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
2764
- title: "Think",
2765
- open: expanded,
2766
- onToggle: () => {
2767
- setExpanded((value) => !value);
2768
- },
2769
- collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2770
- className: TypewriterAssistantNodeView_module_css_default.thinkSeparator,
2771
- "aria-hidden": true
2772
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2773
- ref: summaryRef,
2774
- className: TypewriterAssistantNodeView_module_css_default.thinkSummary,
2775
- "data-follow-end": running || void 0,
2776
- children: summary
2777
- })] }),
2778
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2779
- className: TypewriterAssistantNodeView_module_css_default.thinkBody,
2780
- children: shown
2781
- })
2782
- })]
2783
- })
2784
- });
2786
+ function hasVisibleSegmentWork(segment) {
2787
+ const candidates = [...segment.hideSeats, ...segment.middleSteps];
2788
+ if (segment.startMarker !== null) candidates.push(segment.startMarker);
2789
+ if (segment.finalStep !== null) candidates.push(segment.finalStep);
2790
+ return candidates.some(isDisplayed);
2791
+ }
2792
+ /** Top-level tool card rows of a seat (excluding sub-dispatch nests). */
2793
+ function topCallRowsIn(el) {
2794
+ const rows = [];
2795
+ for (const row of el.querySelectorAll("[data-chat-call-id]")) {
2796
+ if (row.closest("[data-subcalls]") !== null) continue;
2797
+ if (row.closest("[data-chat-call-id]") !== row) continue;
2798
+ rows.push(row);
2799
+ }
2800
+ return rows;
2801
+ }
2802
+ /** Reasoning rows of a message: native think variants outside tool cards. */
2803
+ function thinkRowsIn(el) {
2804
+ const rows = [];
2805
+ for (const row of el.querySelectorAll("[data-variant=\"think\"]:not([data-tool])")) {
2806
+ if (row.closest("[data-chat-call-id]") !== null) continue;
2807
+ if (row.closest("[data-subcalls]") !== null) continue;
2808
+ rows.push(row);
2809
+ }
2810
+ return rows;
2785
2811
  }
2786
2812
  /**
2787
- * Assistant node renderer for the typewriter overlay. Text observed while
2788
- * streaming is revealed by the smoother through the Harness Markdown
2789
- * renderer at a rate that tracks arrival. Reasoning blocks keep the
2790
- * built-in Think disclosure and only receive a smoothed text feed; the
2791
- * outer node owns conversation-port follow while streaming; the final text
2792
- * block keeps ownership while its settled reveal queue drains. The FPS guard
2793
- * holds offscreen reveals when the frame rate is degraded. Settled text
2794
- * renders with the full Markdown pipeline.
2813
+ * Reply detection beyond reasoning rows and tool cards: any non-empty text or
2814
+ * media counts, because Markdown paragraphs necessarily carry text nodes.
2815
+ * Rendered markdown class names are build-time hashes, so structure-based
2816
+ * detection is the only stable contract.
2795
2817
  */
2796
- const TypewriterAssistantNodeView = (0, react.memo)(function TypewriterAssistantNodeView({ mode: _mode = DEFAULT_STREAM_CONFIG.mode, preset = DEFAULT_STREAM_CONFIG.preset, revealCharsPerSec: _revealCharsPerSec = DEFAULT_STREAM_CONFIG.revealCharsPerSec, scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec, maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec, thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand, node, useTurnData, openFile, loadImage, fileMentions, t }) {
2797
- const data = node.data;
2798
- const streaming = data.status === "running";
2799
- const reduced = usePrefersReducedMotion();
2800
- const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
2801
- const rootSpeedRef = (0, react.useRef)(35);
2802
- const rootRevealScaleRef = (0, react.useRef)(1);
2803
- const reasoningTailIndex = streaming && data.blocks[data.blocks.length - 1]?.kind === "reasoning" ? data.blocks.length - 1 : -1;
2804
- const reasoningOwnsSpeed = reasoningTailIndex !== -1;
2805
- const rootPredictiveRef = (0, react.useRef)(false);
2806
- const previousReasoningTailRef = (0, react.useRef)(-1);
2807
- if (reasoningTailIndex !== previousReasoningTailRef.current) {
2808
- rootPredictiveRef.current = false;
2809
- if (!reasoningOwnsSpeed) rootSpeedRef.current = 35;
2810
- previousReasoningTailRef.current = reasoningTailIndex;
2811
- }
2812
- const updateTextPrediction = (0, react.useMemo)(() => (predictive) => {
2813
- rootPredictiveRef.current = predictive;
2814
- }, []);
2815
- const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
2816
- const tail = useTurnData("turn-tail");
2817
- const owner = (0, react.useMemo)(() => {
2818
- if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
2819
- if (tail?.closing?.finalNode.seq !== data.finalNode.seq) return void 0;
2820
- return {
2821
- turn,
2822
- seq: data.finalNode.seq,
2823
- openFile
2824
- };
2825
- }, [
2826
- data.finalNode,
2827
- openFile,
2828
- tail,
2829
- turn
2830
- ]);
2831
- const mentions = (0, react.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
2832
- const codeLabels = (0, react.useMemo)(() => ({
2833
- copyLabel: t("copy"),
2834
- copiedLabel: t("copied")
2835
- }), [t]);
2836
- const imageLoader = loadImage ?? (async () => {
2837
- throw new Error(t("image.serviceUnavailable"));
2838
- });
2839
- if (!(streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call"))) return null;
2840
- const announcementText = data.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
2841
- const rendered = [];
2842
- const last = data.blocks.length - 1;
2843
- let lastFollow = -1;
2844
- for (let index = 0; index < data.blocks.length; index += 1) {
2845
- const kind = data.blocks[index]?.kind;
2846
- if (kind === "text" || kind === "reasoning") lastFollow = index;
2818
+ function hasBodyContent(el) {
2819
+ const kind = el.getAttribute("data-chat-flow-kind");
2820
+ if (kind === "command" || kind === "manual-compaction") return false;
2821
+ if (hasBodyText(el)) return true;
2822
+ const excluded = "[data-variant=\"think\"], [data-chat-call-id]";
2823
+ for (const media of el.querySelectorAll("img, video, audio, canvas")) if (media.closest(excluded) === null) return true;
2824
+ return false;
2825
+ }
2826
+ function hasBodyText(el) {
2827
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
2828
+ let node;
2829
+ while ((node = walker.nextNode()) !== null) {
2830
+ if (node.data.trim() === "") continue;
2831
+ const parent = node.parentElement;
2832
+ if (parent !== null && parent.closest("[data-variant=\"think\"], [data-chat-call-id], [data-variant=\"others\"][data-state]") !== null) continue;
2833
+ return true;
2847
2834
  }
2848
- for (let index = 0; index < data.blocks.length; index += 1) {
2849
- const block = data.blocks[index];
2850
- if (block === void 0) continue;
2851
- switch (block.kind) {
2852
- case "text":
2853
- rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
2854
- text: block.text,
2855
- codeLabels,
2856
- fileMentions: mentions,
2857
- streaming,
2858
- ownFollow: !streaming && index === lastFollow,
2859
- followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
2860
- followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
2861
- onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
2862
- preset,
2863
- shouldHoldBack
2864
- }, index));
2865
- break;
2866
- case "reasoning":
2867
- rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
2868
- text: block.text,
2869
- running: streaming && index === last,
2870
- preset,
2871
- thinkAutoExpand,
2872
- shouldHoldBack,
2873
- followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
2874
- followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
2875
- t
2876
- }, index));
2877
- break;
2878
- case "image": {
2879
- const start = index;
2880
- const group = [block];
2881
- while (index + 1 < data.blocks.length) {
2882
- const next = data.blocks[index + 1];
2883
- if (next === void 0 || next.kind !== "image") break;
2884
- group.push(next);
2885
- index += 1;
2886
- }
2887
- rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_attachment.ImageGallery, {
2888
- images: group,
2889
- load: imageLoader,
2890
- align: "start",
2891
- labels: imageLabels(t)
2892
- }, start));
2893
- break;
2894
- }
2895
- case "tool-call": break;
2896
- case "other": rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
2897
- label: t("message.unknownBlock"),
2898
- payload: block.block,
2899
- truncatedLabel: (total) => t("json.truncated", { total })
2900
- }, index));
2901
- }
2835
+ return false;
2836
+ }
2837
+ /**
2838
+ * Official turn duration from the closing element: localized tails read
2839
+ * `用时 33秒` / `用时 2分05秒` or `Ran for 33s` / `Ran for 2m 05s`; newer
2840
+ * tails only carry an end timestamp, which is diffed against the opening user
2841
+ * row's timestamp.
2842
+ */
2843
+ function parseTurnDuration(boundary) {
2844
+ const text = boundary.textContent ?? "";
2845
+ const zh = text.match(/用时\s*(\d+)分(\d+)秒|用时\s*(\d+)秒/);
2846
+ if (zh !== null) {
2847
+ if (zh[1] !== void 0 && zh[2] !== void 0) return Number(zh[1]) * 6e4 + Number(zh[2]) * 1e3;
2848
+ if (zh[3] !== void 0) return Number(zh[3]) * 1e3;
2849
+ return;
2902
2850
  }
2903
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2904
- ref: guardRef,
2905
- className: TypewriterAssistantNodeView_module_css_default.root,
2906
- "data-streaming": streaming || void 0,
2907
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StreamAnnouncement, {
2908
- text: announcementText,
2909
- active: streaming && !reduced
2910
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
2911
- active: streaming && !reduced,
2912
- speedCpsRef: rootSpeedRef,
2913
- revealScaleRef: rootRevealScaleRef,
2914
- predictiveRef: rootPredictiveRef,
2915
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2916
- className: TypewriterAssistantNodeView_module_css_default.body,
2917
- children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2918
- className: TypewriterAssistantNodeView_module_css_default.stopped,
2919
- children: t("message.stopped")
2920
- })]
2921
- })
2922
- })]
2923
- });
2924
- });
2851
+ const en = text.match(/Ran for\s*(?:(\d+)m\s*)?(\d+)s/);
2852
+ if (en !== null) return ((en[1] === void 0 ? 0 : Number(en[1])) * 60 + Number(en[2])) * 1e3;
2853
+ const end = parseTimeText(text);
2854
+ const start = findTurnStart(boundary);
2855
+ if (end !== void 0 && start !== void 0 && end > start) return end - start;
2856
+ }
2857
+ /** DSH timestamp text (`8月14日 21:56` / `2026年8月14日 22:11`) → epoch ms. */
2858
+ function parseTimeText(text) {
2859
+ const m = text.match(/(?:(\d{4})年)?(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})/);
2860
+ if (m === null) return void 0;
2861
+ const year = m[1] !== void 0 ? Number(m[1]) : (/* @__PURE__ */ new Date()).getFullYear();
2862
+ const t = new Date(year, Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5])).getTime();
2863
+ return Number.isNaN(t) ? void 0 : t;
2864
+ }
2865
+ /** Latest turn-opening timestamp at or before the boundary element. */
2866
+ function findTurnStart(boundary) {
2867
+ const flow = boundary.parentElement;
2868
+ if (flow === null) return void 0;
2869
+ let best = null;
2870
+ for (const s of flow.querySelectorAll("[class*=\"timeStart\"]")) {
2871
+ const pos = s.compareDocumentPosition(boundary);
2872
+ if ((pos & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 || (pos & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0 || s === boundary) best = s;
2873
+ else break;
2874
+ }
2875
+ if (best === null) return void 0;
2876
+ return parseTimeText(best.textContent ?? "");
2877
+ }
2878
+ /** Compact duration pieces shared by both locales: seconds under a minute,
2879
+ * whole minutes drop the seconds, hours drop to minute granularity. */
2880
+ function durationParts(ms) {
2881
+ const s = Math.round(ms / 1e3);
2882
+ return {
2883
+ h: Math.floor(s / 3600),
2884
+ m: Math.floor(s % 3600 / 60),
2885
+ s: s % 60
2886
+ };
2887
+ }
2888
+ /** 毫秒 → 中文紧凑时长(14秒 / 2分05秒 / 15分 / 3小时2分)。 */
2889
+ function formatZhDuration(ms) {
2890
+ const { h, m, s } = durationParts(ms);
2891
+ if (h > 0) return m > 0 ? `${h}小时${m}分` : `${h}小时`;
2892
+ if (s < 60 && m === 0 && h === 0) return `${s}秒`;
2893
+ if (s === 0) return `${m}分`;
2894
+ return `${m}分${String(s).padStart(2, "0")}秒`;
2895
+ }
2896
+ /** Milliseconds → compact English duration (14s / 2m05s / 15m / 3h2m). */
2897
+ function formatEnDuration(ms) {
2898
+ const { h, m, s } = durationParts(ms);
2899
+ if (h > 0) return m > 0 ? `${h}h${m}m` : `${h}h`;
2900
+ if (m === 0 && h === 0) return `${s}s`;
2901
+ if (s === 0) return `${m}m`;
2902
+ return `${m}m${String(s).padStart(2, "0")}s`;
2903
+ }
2904
+ /** Native disclosure chevron (IconChevronDownOutline14, 14x14). */
2905
+ const CHEVRON_PATH = "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z";
2906
+ function createChevronIcon(className) {
2907
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2908
+ svg.setAttribute("width", "14");
2909
+ svg.setAttribute("height", "14");
2910
+ svg.setAttribute("class", className);
2911
+ svg.setAttribute("viewBox", "0 0 14 14");
2912
+ svg.setAttribute("fill", "none");
2913
+ svg.setAttribute("aria-hidden", "true");
2914
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2915
+ path.setAttribute("d", CHEVRON_PATH);
2916
+ path.setAttribute("fill", "currentColor");
2917
+ svg.appendChild(path);
2918
+ return svg;
2919
+ }
2920
+ function createProcessedRowElement(duration) {
2921
+ const copy = resolveFoldCopy();
2922
+ const btn = document.createElement("button");
2923
+ btn.type = "button";
2924
+ btn.className = PROCESSED_CLASS;
2925
+ btn.setAttribute("aria-expanded", "false");
2926
+ const text = document.createElement("span");
2927
+ text.textContent = duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(duration)}`;
2928
+ btn.append(text, createChevronIcon(`${PROCESSED_CLASS}-chevron`));
2929
+ btn.title = copy.expandTitle;
2930
+ return btn;
2931
+ }
2932
+ function injectStyle() {
2933
+ if (document.getElementById(STYLE_ID) !== null) return;
2934
+ const style = document.createElement("style");
2935
+ style.id = STYLE_ID;
2936
+ style.textContent = FOLD_CSS;
2937
+ document.head.appendChild(style);
2938
+ }
2939
+ function removeStyle() {
2940
+ document.getElementById(STYLE_ID)?.remove();
2941
+ }
2925
2942
  //#endregion
2926
2943
  //#region src/client/useProgressiveDomText.ts
2927
2944
  /**
@@ -3368,32 +3385,32 @@ window.__ModuleLoader__.load({
3368
3385
  tag.textContent = css$1;
3369
3386
  }
3370
3387
  var SmoothStreamCard_module_css_default = {
3371
- "updateRow": "XNGeRG_updateRow",
3372
- "fieldDisabled": "XNGeRG_fieldDisabled",
3373
- "hint": "XNGeRG_hint",
3374
- "chevron": "XNGeRG_chevron",
3375
- "cardOpen": "XNGeRG_cardOpen",
3388
+ "fieldHead": "XNGeRG_fieldHead",
3389
+ "label": "XNGeRG_label",
3376
3390
  "version": "XNGeRG_version",
3377
- "pending": "XNGeRG_pending",
3378
- "failure": "XNGeRG_failure",
3379
3391
  "header": "XNGeRG_header",
3392
+ "body": "XNGeRG_body",
3393
+ "failure": "XNGeRG_failure",
3394
+ "updateRow": "XNGeRG_updateRow",
3380
3395
  "updateCopy": "XNGeRG_updateCopy",
3396
+ "field": "XNGeRG_field",
3397
+ "footer": "XNGeRG_footer",
3398
+ "name": "XNGeRG_name",
3381
3399
  "failed": "XNGeRG_failed",
3382
3400
  "discard": "XNGeRG_discard",
3383
- "name": "XNGeRG_name",
3384
- "save": "XNGeRG_save",
3385
- "fieldHead": "XNGeRG_fieldHead",
3386
- "update": "XNGeRG_update",
3387
- "headText": "XNGeRG_headText",
3388
3401
  "toggle": "XNGeRG_toggle",
3389
- "body": "XNGeRG_body",
3402
+ "fieldDisabled": "XNGeRG_fieldDisabled",
3390
3403
  "readOnly": "XNGeRG_readOnly",
3404
+ "headText": "XNGeRG_headText",
3405
+ "cardOpen": "XNGeRG_cardOpen",
3406
+ "save": "XNGeRG_save",
3407
+ "hint": "XNGeRG_hint",
3408
+ "chevron": "XNGeRG_chevron",
3409
+ "update": "XNGeRG_update",
3410
+ "pending": "XNGeRG_pending",
3391
3411
  "description": "XNGeRG_description",
3392
- "chevronOpen": "XNGeRG_chevronOpen",
3393
3412
  "card": "XNGeRG_card",
3394
- "field": "XNGeRG_field",
3395
- "label": "XNGeRG_label",
3396
- "footer": "XNGeRG_footer"
3413
+ "chevronOpen": "XNGeRG_chevronOpen"
3397
3414
  };
3398
3415
  //#endregion
3399
3416
  //#region src/client/SmoothStreamCard.tsx
@@ -3894,34 +3911,34 @@ window.__ModuleLoader__.load({
3894
3911
  tag.textContent = css;
3895
3912
  }
3896
3913
  var DebugPanel_module_css_default = {
3897
- "infoButton": "m4_tdG_infoButton",
3898
- "unit": "m4_tdG_unit",
3899
- "range": "m4_tdG_range",
3914
+ "control": "m4_tdG_control",
3900
3915
  "secondaryButton": "m4_tdG_secondaryButton",
3916
+ "infoButton": "m4_tdG_infoButton",
3901
3917
  "footerSpacer": "m4_tdG_footerSpacer",
3902
3918
  "visuallyHidden": "m4_tdG_visuallyHidden",
3903
- "title": "m4_tdG_title",
3904
- "number": "m4_tdG_number",
3919
+ "panelHeader": "m4_tdG_panelHeader",
3920
+ "metric": "m4_tdG_metric",
3921
+ "section": "m4_tdG_section",
3922
+ "unsaved": "m4_tdG_unsaved",
3923
+ "state": "m4_tdG_state",
3924
+ "statusDot": "m4_tdG_statusDot",
3925
+ "controlLabel": "m4_tdG_controlLabel",
3926
+ "scrollArea": "m4_tdG_scrollArea",
3905
3927
  "numberWrap": "m4_tdG_numberWrap",
3928
+ "title": "m4_tdG_title",
3929
+ "trigger": "m4_tdG_trigger",
3906
3930
  "footer": "m4_tdG_footer",
3907
- "triggerActive": "m4_tdG_triggerActive",
3908
3931
  "iconButton": "m4_tdG_iconButton",
3909
- "scrollArea": "m4_tdG_scrollArea",
3910
- "section": "m4_tdG_section",
3911
- "trigger": "m4_tdG_trigger",
3912
- "statusDot": "m4_tdG_statusDot",
3913
- "state": "m4_tdG_state",
3914
- "panelHeader": "m4_tdG_panelHeader",
3915
3932
  "panel": "m4_tdG_panel",
3916
- "primaryButton": "m4_tdG_primaryButton",
3917
- "unsaved": "m4_tdG_unsaved",
3918
- "guide": "m4_tdG_guide",
3919
- "metric": "m4_tdG_metric",
3920
3933
  "controlHead": "m4_tdG_controlHead",
3921
- "controlLabel": "m4_tdG_controlLabel",
3934
+ "triggerActive": "m4_tdG_triggerActive",
3935
+ "range": "m4_tdG_range",
3936
+ "unit": "m4_tdG_unit",
3937
+ "primaryButton": "m4_tdG_primaryButton",
3938
+ "number": "m4_tdG_number",
3939
+ "metrics": "m4_tdG_metrics",
3922
3940
  "statusLive": "m4_tdG_statusLive",
3923
- "control": "m4_tdG_control",
3924
- "metrics": "m4_tdG_metrics"
3941
+ "guide": "m4_tdG_guide"
3925
3942
  };
3926
3943
  //#endregion
3927
3944
  //#region src/client/DebugPanel.tsx