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 +2084 -2067
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/auto-collapse-controller.ts +3 -3
- package/src/client/teleprompterGlide.ts +26 -7
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
|
-
"
|
|
28
|
-
"
|
|
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
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"disclosureRow": "I17U7q_disclosureRow",
|
|
30
|
+
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
31
|
+
"think": "I17U7q_think",
|
|
32
|
+
"disclosureContent": "I17U7q_disclosureContent",
|
|
37
33
|
"thinkLeading": "I17U7q_thinkLeading",
|
|
38
|
-
"
|
|
34
|
+
"disclosureRow": "I17U7q_disclosureRow",
|
|
35
|
+
"stopped": "I17U7q_stopped",
|
|
39
36
|
"thinkChevron": "I17U7q_thinkChevron",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
37
|
+
"thinkSummary": "I17U7q_thinkSummary",
|
|
38
|
+
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
42
39
|
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
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/
|
|
819
|
+
//#region src/client/teleprompterGlide.ts
|
|
820
820
|
/**
|
|
821
|
-
*
|
|
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
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
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
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
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
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
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
|
-
*
|
|
860
|
-
*
|
|
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
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
return
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
|
1316
|
-
|
|
1317
|
-
return el.style.display !== "none";
|
|
1067
|
+
function runwayOffsetOf(port) {
|
|
1068
|
+
return followRunways.get(port)?.offset ?? 0;
|
|
1318
1069
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*/
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
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
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
-
/**
|
|
1468
|
-
function
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
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
|
-
*
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
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
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
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
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
reportFollow(port, activeRef.current);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return {
|
|
1707
|
+
root,
|
|
1708
|
+
visibleText,
|
|
1709
|
+
fontSize,
|
|
1710
|
+
wrapThresholdWidth: rootWidth
|
|
2142
1711
|
};
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
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
|
|
2160
|
-
|
|
2161
|
-
const
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
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
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
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
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
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
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
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
|
-
*
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
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
|
|
2371
|
-
const
|
|
2372
|
-
const
|
|
2373
|
-
|
|
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
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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/
|
|
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
|
-
*
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
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
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
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
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
if (
|
|
2573
|
-
|
|
2574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2599
|
-
|
|
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
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
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
|
-
*
|
|
2621
|
-
*
|
|
2622
|
-
*
|
|
2623
|
-
*
|
|
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
|
|
2631
|
-
const
|
|
2632
|
-
const
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
const
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
const
|
|
2653
|
-
|
|
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
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2700
|
-
|
|
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
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
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
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
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
|
-
*
|
|
2788
|
-
*
|
|
2789
|
-
*
|
|
2790
|
-
*
|
|
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
|
-
|
|
2797
|
-
const
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
const
|
|
2801
|
-
const
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
const
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
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
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
-
"
|
|
3372
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
3904
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
3924
|
-
"metrics": "m4_tdG_metrics"
|
|
3941
|
+
"guide": "m4_tdG_guide"
|
|
3925
3942
|
};
|
|
3926
3943
|
//#endregion
|
|
3927
3944
|
//#region src/client/DebugPanel.tsx
|