dsh-smooth-stream 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +13 -0
- package/README.md +13 -0
- package/lib/client.js +801 -1040
- package/lib/client.js.map +1 -1
- package/lib/index.js +6 -6
- package/package.json +11 -5
- package/src/client/AgentRowEntrance.module.css +43 -0
- package/src/client/FollowHost.tsx +18 -1
- package/src/client/SmoothStreamCard.tsx +10 -10
- package/src/client/TypewriterAssistantNodeView.tsx +15 -5
- package/src/client/TypewriterToolNodeView.tsx +9 -1
- package/src/client/clientStore.ts +71 -12
- package/src/client/index.ts +13 -29
- package/src/client/locales.ts +5 -5
- package/src/client/smooth-stream-card-controller.ts +6 -6
- package/src/client/smooth-stream-settings-api.ts +4 -4
- package/src/client/teleprompterGlide.ts +1021 -75
- package/src/plugin.ts +7 -6
- package/src/settings-api.ts +2 -2
- package/src/settings.ts +6 -8
- package/src/client/auto-collapse-controller.ts +0 -1072
package/lib/client.js
CHANGED
|
@@ -10,39 +10,39 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
11
|
let react_dom = require("react-dom");
|
|
12
12
|
//#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
|
|
13
|
-
const css$
|
|
14
|
-
const tagId$
|
|
13
|
+
const css$3 = ".I17U7q_root{min-width:0;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:16px;line-height:28px;display:flex}.I17U7q_body{flex-direction:column;gap:16px;min-width:0;display:flex}.I17U7q_think{flex-direction:column;display:flex}.I17U7q_thinkRow{position:relative;overflow:hidden}.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{content:\"\";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite I17U7q_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes I17U7q_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.I17U7q_thinkLeading{flex-shrink:0}.I17U7q_thinkChevron{color:var(--dsw-alias-label-secondary)}.I17U7q_thinkTitle{font-weight:400}.I17U7q_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.I17U7q_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.I17U7q_thinkSummary[data-follow-end]{text-overflow:clip}.I17U7q_thinkBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.I17U7q_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.I17U7q_disclosureRow{cursor:pointer;align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.I17U7q_disclosureLeading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.I17U7q_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.I17U7q_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureIconIdle{opacity:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureChevronHover{opacity:1}.I17U7q_disclosureTitle{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.I17U7q_disclosureContent{visibility:visible;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s;grid-template-rows:1fr;display:grid}.I17U7q_disclosureContent[data-collapsed]{visibility:hidden;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s var(--ds-transition-duration,.2s);grid-template-rows:0fr}.I17U7q_disclosureContent[data-no-transition]{transition:none}.I17U7q_disclosureContent>*{min-height:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{animation:none}.I17U7q_disclosureContent,.I17U7q_disclosureContent[data-collapsed]{transition:none}}.I17U7q_stopped{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:6px;align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}.I17U7q_visuallyHidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.I17U7q_follow{contain:layout style;min-width:0}.I17U7q_root :not(pre)>code{line-height:inherit;white-space:normal;overflow-wrap:anywhere;vertical-align:baseline;display:inline}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
|
|
14
|
+
const tagId$3 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
|
|
15
15
|
if (typeof document !== "undefined") {
|
|
16
|
-
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
16
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]");
|
|
17
17
|
if (tag === null) {
|
|
18
18
|
tag = document.createElement("style");
|
|
19
19
|
tag.dataset.plugin = "dsh-smooth-stream";
|
|
20
|
-
tag.dataset.pluginCss = tagId$
|
|
20
|
+
tag.dataset.pluginCss = tagId$3;
|
|
21
21
|
document.head.appendChild(tag);
|
|
22
22
|
}
|
|
23
|
-
tag.textContent = css$
|
|
23
|
+
tag.textContent = css$3;
|
|
24
24
|
}
|
|
25
25
|
var TypewriterAssistantNodeView_module_css_default = {
|
|
26
|
-
"thinkTitle": "I17U7q_thinkTitle",
|
|
27
|
-
"thinkLeading": "I17U7q_thinkLeading",
|
|
28
|
-
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
29
|
-
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
30
|
-
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
31
|
-
"root": "I17U7q_root",
|
|
32
|
-
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
33
|
-
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
34
|
-
"follow": "I17U7q_follow",
|
|
35
|
-
"disclosureContent": "I17U7q_disclosureContent",
|
|
36
|
-
"stopped": "I17U7q_stopped",
|
|
37
|
-
"thinkSummary": "I17U7q_thinkSummary",
|
|
38
|
-
"disclosureRow": "I17U7q_disclosureRow",
|
|
39
26
|
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
27
|
+
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
40
28
|
"thinkRow": "I17U7q_thinkRow",
|
|
41
29
|
"thinkChevron": "I17U7q_thinkChevron",
|
|
42
|
-
"
|
|
30
|
+
"root": "I17U7q_root",
|
|
43
31
|
"body": "I17U7q_body",
|
|
44
|
-
"
|
|
32
|
+
"disclosureRow": "I17U7q_disclosureRow",
|
|
33
|
+
"stopped": "I17U7q_stopped",
|
|
34
|
+
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
35
|
+
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
36
|
+
"thinkSummary": "I17U7q_thinkSummary",
|
|
37
|
+
"think": "I17U7q_think",
|
|
38
|
+
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
45
39
|
"disclosureChevronHover": "I17U7q_disclosureChevronHover",
|
|
40
|
+
"thinkLeading": "I17U7q_thinkLeading",
|
|
41
|
+
"disclosureContent": "I17U7q_disclosureContent",
|
|
42
|
+
"follow": "I17U7q_follow",
|
|
43
|
+
"disclosureLeading": "I17U7q_disclosureLeading",
|
|
44
|
+
"thinkTitle": "I17U7q_thinkTitle",
|
|
45
|
+
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
46
46
|
"thinkBody": "I17U7q_thinkBody"
|
|
47
47
|
};
|
|
48
48
|
//#endregion
|
|
@@ -125,33 +125,67 @@ window.__ModuleLoader__.load({
|
|
|
125
125
|
/** Defaults shared by the Host schema and the client-side fallback. */
|
|
126
126
|
const DEFAULT_STREAM_SETTINGS = {
|
|
127
127
|
enabled: true,
|
|
128
|
+
controlScroll: true,
|
|
128
129
|
thinkAutoExpand: true,
|
|
129
|
-
autoCollapse: true,
|
|
130
130
|
debugEnabled: false,
|
|
131
131
|
debugTuning: DEFAULT_STREAM_DEBUG_TUNING
|
|
132
132
|
};
|
|
133
133
|
//#endregion
|
|
134
134
|
//#region src/client/clientStore.ts
|
|
135
|
+
function fallbackSnapshotStore(init) {
|
|
136
|
+
let state = init;
|
|
137
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
138
|
+
return {
|
|
139
|
+
getSnapshot: () => state,
|
|
140
|
+
subscribe: (listener) => {
|
|
141
|
+
listeners.add(listener);
|
|
142
|
+
return () => {
|
|
143
|
+
listeners.delete(listener);
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
update: (mutator) => {
|
|
147
|
+
const draft = Object.create(Object.getPrototypeOf(state), Object.getOwnPropertyDescriptors(state));
|
|
148
|
+
mutator(draft);
|
|
149
|
+
state = draft;
|
|
150
|
+
for (const listener of [...listeners]) listener(state);
|
|
151
|
+
},
|
|
152
|
+
set: (next) => {
|
|
153
|
+
state = next;
|
|
154
|
+
for (const listener of [...listeners]) listener(state);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
135
158
|
/**
|
|
136
159
|
* Runtime require: the module-loader factory scopes its own resolver over the
|
|
137
160
|
* booting kernel's table; under Node (vitest) there is no global require, so
|
|
138
161
|
* fall back to `createRequire`.
|
|
139
162
|
*/
|
|
140
163
|
function pickRequire() {
|
|
141
|
-
if (typeof require === "function")
|
|
142
|
-
|
|
164
|
+
if (typeof require === "function") try {
|
|
165
|
+
return require;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (typeof process !== "undefined" && typeof process.getBuiltinModule === "function") try {
|
|
143
170
|
const { createRequire } = process.getBuiltinModule("node:module");
|
|
144
171
|
return createRequire(process.cwd() + "/package.json");
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
145
174
|
}
|
|
146
|
-
|
|
175
|
+
return null;
|
|
147
176
|
}
|
|
148
177
|
let clientStore;
|
|
149
178
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
const req = pickRequire();
|
|
180
|
+
if (req !== null) try {
|
|
181
|
+
clientStore = req("@deepseek-ai/dsh-client-store");
|
|
182
|
+
} catch {
|
|
183
|
+
try {
|
|
184
|
+
clientStore = req("@deepseek-ai/dsh-client-runtime/client");
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
const createSnapshotStore = clientStore?.createSnapshotStore ?? fallbackSnapshotStore;
|
|
155
189
|
//#endregion
|
|
156
190
|
//#region src/client/debugRuntime.ts
|
|
157
191
|
/**
|
|
@@ -433,8 +467,15 @@ window.__ModuleLoader__.load({
|
|
|
433
467
|
* scroll ownership. Current Harness also sees the write land at the floor.
|
|
434
468
|
*/
|
|
435
469
|
const FOLLOW_OWNED_ATTR = "data-follow-owned";
|
|
436
|
-
/**
|
|
437
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Duration of completion runway retirement. The final pad is visible motion:
|
|
472
|
+
* its shrinking floor brings the transcript down to its natural resting
|
|
473
|
+
* position. 1.5s keeps the default 72px runway at 48px/s (0.8px per 60Hz
|
|
474
|
+
* frame), matching the configured default reading-follow velocity instead of
|
|
475
|
+
* the old 160ms / 450px/s staircase that looked like repeated completion
|
|
476
|
+
* jumps.
|
|
477
|
+
*/
|
|
478
|
+
const FOLLOW_RUNWAY_RETIRE_MS = 1500;
|
|
438
479
|
/** Runway size emitted by bundles before the 72px predictive runway. */
|
|
439
480
|
const LEGACY_RUNWAY_PX = 48;
|
|
440
481
|
/** Safe-lag occupancy band over which reveal pressure is progressively reduced. */
|
|
@@ -591,7 +632,7 @@ window.__ModuleLoader__.load({
|
|
|
591
632
|
* Outermost message surfaces; nested tool rows ride their parent.
|
|
592
633
|
*
|
|
593
634
|
* Another plugin may insert its own element as a flow sibling of the Chat rows
|
|
594
|
-
* (meow-memory's fold bar is one
|
|
635
|
+
* (meow-memory's fold bar is one).
|
|
595
636
|
* Such a row carries no `data-chat-anchor-key`, so selecting only anchored
|
|
596
637
|
* rows would shift the conversation while leaving the foreign row at its
|
|
597
638
|
* natural offset, letting the shifted rows paint over it. Every direct flow
|
|
@@ -705,12 +746,237 @@ window.__ModuleLoader__.load({
|
|
|
705
746
|
const followHadChrome = /* @__PURE__ */ new WeakSet();
|
|
706
747
|
/** Last painted shift per port, to spread a wrap's one-line step over frames. */
|
|
707
748
|
const followLastShiftPx = /* @__PURE__ */ new WeakMap();
|
|
749
|
+
/** Last settled floor per port, to size the shift decay bound against extent collapse. */
|
|
750
|
+
const followLastFloorPx = /* @__PURE__ */ new WeakMap();
|
|
751
|
+
const followGuardAnchors = /* @__PURE__ */ new WeakMap();
|
|
752
|
+
/**
|
|
753
|
+
* Ports whose completion settle loop owns the follow. The settle drains the
|
|
754
|
+
* reveal, retires the pad and guards the cascade; the swap that ends the
|
|
755
|
+
* turn REMOUNTS the follower arms in the same frame cluster, and a freshly
|
|
756
|
+
* mounted arm primes with a higher generation and would otherwise steal the
|
|
757
|
+
* port mid-drain — its observers miss the cascade mutations (armed after
|
|
758
|
+
* the fact) and its state initialization re-materializes engine space.
|
|
759
|
+
* Ownership is released only when the settle finishes, the reader gestures,
|
|
760
|
+
* or a genuinely NEW turn arrives (a user row joined since the handoff).
|
|
761
|
+
*/
|
|
762
|
+
const followCompletionSettle = /* @__PURE__ */ new WeakSet();
|
|
763
|
+
/** User-row count at handoff, for the new-turn check above. */
|
|
764
|
+
const followCompletionSettleRows = /* @__PURE__ */ new WeakMap();
|
|
765
|
+
/** User rows below the flow, or -1 when the host does not label rows with
|
|
766
|
+
* `data-chat-flow-kind` (the engine's own audit benches): the new-turn
|
|
767
|
+
* check is unsupported there and ownership guarding must stay OFF. */
|
|
768
|
+
function countUserRows(port) {
|
|
769
|
+
const flow = flowElementOf(port);
|
|
770
|
+
if (flow === null) return -1;
|
|
771
|
+
let count = 0;
|
|
772
|
+
let sawKind = false;
|
|
773
|
+
for (const child of flow.children) {
|
|
774
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
775
|
+
const kind = child.getAttribute("data-chat-flow-kind");
|
|
776
|
+
if (kind !== null) sawKind = true;
|
|
777
|
+
if (kind === "user") count++;
|
|
778
|
+
}
|
|
779
|
+
return sawKind ? count : -1;
|
|
780
|
+
}
|
|
781
|
+
/** True while this port's completion settle owns the follow and no new turn
|
|
782
|
+
* has arrived since the handoff. */
|
|
783
|
+
function completionSettleGuardsPort(port) {
|
|
784
|
+
if (!followCompletionSettle.has(port)) return false;
|
|
785
|
+
const baseline = followCompletionSettleRows.get(port) ?? -1;
|
|
786
|
+
const current = countUserRows(port);
|
|
787
|
+
return baseline >= 0 && current >= 0 && current <= baseline;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Ports whose completion settle loop owns the follow. A settle loop drains,
|
|
791
|
+
* retires the pad and guards the cascade; an arm that primes while this is
|
|
792
|
+
* set with `active === false` (the settled-side static arm mounting in the
|
|
793
|
+
* very swap frame) must NOT seize leadership — the swap re-renders the node
|
|
794
|
+
* view, a fresh arm would otherwise steal the port mid-drain with
|
|
795
|
+
* `reservePx = ownedBottomSpace` (the pad!), re-materialize an equal runway
|
|
796
|
+
* through applyVisual, double-count the extent and fight the settle's own
|
|
797
|
+
* guard for the rest of the window. A streaming arm (active === true, the
|
|
798
|
+
* next turn) still takes over normally and the settle yields.
|
|
799
|
+
*/
|
|
800
|
+
function readingAnchorOf(port) {
|
|
801
|
+
const flow = flowElementOf(port);
|
|
802
|
+
if (flow === null) return null;
|
|
803
|
+
let anchor = null;
|
|
804
|
+
for (const child of flow.children) {
|
|
805
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
806
|
+
if (child.getAttribute("data-chat-flow-kind") === "assistant" || child.querySelector("[data-variant=\"think\"]") !== null) anchor = child;
|
|
807
|
+
}
|
|
808
|
+
return anchor ?? shiftSurfacesOf(port).at(-1) ?? null;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Measure the reading surface's screen delta since the last guard pass.
|
|
812
|
+
* Returns null when there is nothing comparable yet (first observation), or
|
|
813
|
+
* when the anchor changed identity in a way that is NOT the in-place
|
|
814
|
+
* live→settled replacement (a new turn's row joined — nothing jumped,
|
|
815
|
+
* re-seed). Compensation sites must store the POST-compensation held
|
|
816
|
+
* position via `holdGuardAnchor`, or the guard would read its own
|
|
817
|
+
* correction as a fresh jump and oscillate.
|
|
818
|
+
*/
|
|
819
|
+
function measureReadingAnchor(port) {
|
|
820
|
+
const anchor = readingAnchorOf(port);
|
|
821
|
+
if (anchor === null) {
|
|
822
|
+
followGuardAnchors.delete(port);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
const rect = anchor.getBoundingClientRect();
|
|
826
|
+
if (!(rect.width > 0 || rect.height > 0)) return null;
|
|
827
|
+
const top = rect.top;
|
|
828
|
+
const shift = currentShiftOf(anchor);
|
|
829
|
+
const pad = flowPadOf(port);
|
|
830
|
+
const scrollTop = port.scrollTop;
|
|
831
|
+
const scrollHeight = port.scrollHeight;
|
|
832
|
+
const flow = flowElementOf(port);
|
|
833
|
+
const index = flow === null ? -1 : [...flow.children].indexOf(anchor);
|
|
834
|
+
const stored = followGuardAnchors.get(port);
|
|
835
|
+
if (stored === void 0) {
|
|
836
|
+
followGuardAnchors.set(port, {
|
|
837
|
+
element: anchor,
|
|
838
|
+
top,
|
|
839
|
+
index,
|
|
840
|
+
shift,
|
|
841
|
+
pad,
|
|
842
|
+
scrollTop,
|
|
843
|
+
scrollHeight
|
|
844
|
+
});
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
if (Math.abs(scrollHeight - stored.scrollHeight) <= .5 && Math.abs(scrollTop - stored.scrollTop) > .5) {
|
|
848
|
+
followScrollLedgers.set(port, scrollTop);
|
|
849
|
+
followGuardAnchors.set(port, {
|
|
850
|
+
element: anchor,
|
|
851
|
+
top,
|
|
852
|
+
index,
|
|
853
|
+
shift,
|
|
854
|
+
pad,
|
|
855
|
+
scrollTop,
|
|
856
|
+
scrollHeight
|
|
857
|
+
});
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
const delta = top - stored.top - (shift - stored.shift) + (pad - stored.pad);
|
|
861
|
+
if (stored.element === anchor) {
|
|
862
|
+
followGuardAnchors.set(port, {
|
|
863
|
+
element: anchor,
|
|
864
|
+
top,
|
|
865
|
+
index,
|
|
866
|
+
shift,
|
|
867
|
+
pad,
|
|
868
|
+
scrollTop,
|
|
869
|
+
scrollHeight
|
|
870
|
+
});
|
|
871
|
+
return {
|
|
872
|
+
anchor,
|
|
873
|
+
index,
|
|
874
|
+
top,
|
|
875
|
+
delta
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
if (!stored.element.isConnected && stored.index === index) return {
|
|
879
|
+
anchor,
|
|
880
|
+
index,
|
|
881
|
+
top,
|
|
882
|
+
delta
|
|
883
|
+
};
|
|
884
|
+
followGuardAnchors.set(port, {
|
|
885
|
+
element: anchor,
|
|
886
|
+
top,
|
|
887
|
+
index,
|
|
888
|
+
shift,
|
|
889
|
+
pad,
|
|
890
|
+
scrollTop,
|
|
891
|
+
scrollHeight
|
|
892
|
+
});
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
/** Record the position the reader should keep seeing after a correction. */
|
|
896
|
+
function holdGuardAnchor(port, measured, heldTop) {
|
|
897
|
+
followGuardAnchors.set(port, {
|
|
898
|
+
element: measured.anchor,
|
|
899
|
+
index: measured.index,
|
|
900
|
+
top: heldTop,
|
|
901
|
+
shift: currentShiftOf(measured.anchor),
|
|
902
|
+
pad: flowPadOf(port),
|
|
903
|
+
scrollTop: port.scrollTop,
|
|
904
|
+
scrollHeight: port.scrollHeight
|
|
905
|
+
});
|
|
906
|
+
}
|
|
708
907
|
/** Last runway offset seen per port, to rebase the extent when the margin size changes. */
|
|
709
908
|
const followRunwayOffsetHistory = /* @__PURE__ */ new WeakMap();
|
|
710
909
|
/** Last observed scroll floor per port, for the slack→overflow runway re-measure. */
|
|
711
910
|
const followFloorHistory = /* @__PURE__ */ new WeakMap();
|
|
712
911
|
/** One-shot flag: the transition frame must paint the full runway as baseline. */
|
|
713
912
|
const followSlackTransition = /* @__PURE__ */ new WeakSet();
|
|
913
|
+
const followSettlePads = /* @__PURE__ */ new WeakMap();
|
|
914
|
+
/**
|
|
915
|
+
* Retired follower space lives as `padding-bottom` on the FLOW element — the
|
|
916
|
+
* one node the engine already owns styles on (flow-fill min-height) and the
|
|
917
|
+
* host never rewrites. Host completion commits routinely REPLACE row elements
|
|
918
|
+
* (live→settled swap re-keys the assistant row, the status row unmounts), and
|
|
919
|
+
* any engine space written on those rows dies with them, sinking the floor
|
|
920
|
+
* and slamming the pinned transcript for a frame. The flow survives.
|
|
921
|
+
*/
|
|
922
|
+
function flowPadOf(port) {
|
|
923
|
+
return followSettlePads.get(port)?.px ?? 0;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* After a loss is re-opened as pad, a registry entry whose element is no
|
|
927
|
+
* longer connected claims extent that no longer exists; drop it so the next
|
|
928
|
+
* `ensureRunway` re-measures fresh instead of double-counting.
|
|
929
|
+
*/
|
|
930
|
+
function pruneDeadRunway(port) {
|
|
931
|
+
const runway = followRunways.get(port);
|
|
932
|
+
if (runway !== void 0 && !runway.element.isConnected) {
|
|
933
|
+
restoreRunway(port);
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
function setFlowPad(port, px) {
|
|
939
|
+
const flow = flowElementOf(port);
|
|
940
|
+
if (flow === null) return;
|
|
941
|
+
const existing = followSettlePads.get(port);
|
|
942
|
+
const original = existing?.original ?? flow.style.paddingBottom;
|
|
943
|
+
if (px <= .25) {
|
|
944
|
+
if (existing !== void 0) {
|
|
945
|
+
flow.style.paddingBottom = existing.original;
|
|
946
|
+
followSettlePads.delete(port);
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
flow.style.paddingBottom = original === "" ? `${px}px` : `calc(${original} + ${px}px)`;
|
|
951
|
+
followSettlePads.set(port, {
|
|
952
|
+
element: flow,
|
|
953
|
+
original,
|
|
954
|
+
px
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
/** Extent the follower owns below the content: the live runway margin plus
|
|
958
|
+
* the retired completion pad. At adopt the pad is reclaimed into the fresh
|
|
959
|
+
* reservation (same frame, pre-paint), so the floor never steps. */
|
|
960
|
+
function ownedBottomSpaceOf(port) {
|
|
961
|
+
return runwayOffsetOf(port) + flowPadOf(port);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Completion-window diagnostics. Armed when a follower hands off to its
|
|
965
|
+
* settle loop; every engine decision and every externally-written scroll
|
|
966
|
+
* position inside the window prints one compact console line, so a live
|
|
967
|
+
* host session can be diffed against the lab without a debugger.
|
|
968
|
+
*/
|
|
969
|
+
let followTraceUntilMs = 0;
|
|
970
|
+
function traceActive() {
|
|
971
|
+
return debugRuntime.isEnabled() && performance.now() < followTraceUntilMs;
|
|
972
|
+
}
|
|
973
|
+
function followTrace(event, detail) {
|
|
974
|
+
if (!traceActive()) return;
|
|
975
|
+
console.log(`[dsh-follow] ${event}`, JSON.stringify(detail));
|
|
976
|
+
}
|
|
977
|
+
function hostShOf(port) {
|
|
978
|
+
return port.scrollHeight;
|
|
979
|
+
}
|
|
714
980
|
function invalidatePaintLimit(port) {
|
|
715
981
|
followPaintLimits.delete(port);
|
|
716
982
|
}
|
|
@@ -831,6 +1097,31 @@ window.__ModuleLoader__.load({
|
|
|
831
1097
|
function runwayOffsetOf(port) {
|
|
832
1098
|
return followRunways.get(port)?.offset ?? 0;
|
|
833
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Move owned runway margin into the persistent flow pad without changing the
|
|
1102
|
+
* scroll extent. Returns the actual layout pixels removed from the margin.
|
|
1103
|
+
*/
|
|
1104
|
+
function transferRunwayToFlowPad(port, requestedPx) {
|
|
1105
|
+
const runway = followRunways.get(port);
|
|
1106
|
+
if (runway === void 0 || requestedPx <= 0 || !runway.element.isConnected) return 0;
|
|
1107
|
+
const nextRequestedPx = Math.max(0, runway.requestedPx - requestedPx);
|
|
1108
|
+
const beforeOffset = runway.offset;
|
|
1109
|
+
const beforeHeight = port.scrollHeight;
|
|
1110
|
+
runway.element.style[runway.property] = nextRequestedPx <= .25 ? runway.original : runway.original === "" ? `${nextRequestedPx}px` : `calc(${runway.original} + ${nextRequestedPx}px)`;
|
|
1111
|
+
const nextOffset = Math.max(0, beforeOffset + port.scrollHeight - beforeHeight);
|
|
1112
|
+
const transferredPx = Math.max(0, beforeOffset - nextOffset);
|
|
1113
|
+
if (nextRequestedPx <= .25 || nextOffset <= .25) followRunways.delete(port);
|
|
1114
|
+
else followRunways.set(port, {
|
|
1115
|
+
...runway,
|
|
1116
|
+
offset: nextOffset,
|
|
1117
|
+
requestedPx: nextRequestedPx
|
|
1118
|
+
});
|
|
1119
|
+
if (transferredPx > 0) {
|
|
1120
|
+
setFlowPad(port, flowPadOf(port) + transferredPx);
|
|
1121
|
+
invalidatePaintLimit(port);
|
|
1122
|
+
}
|
|
1123
|
+
return transferredPx;
|
|
1124
|
+
}
|
|
834
1125
|
/** Available paint room below the last message before fixed conversation chrome. */
|
|
835
1126
|
function safeShiftLimit(port, surfaces) {
|
|
836
1127
|
const last = surfaces.at(-1);
|
|
@@ -859,6 +1150,13 @@ window.__ModuleLoader__.load({
|
|
|
859
1150
|
return limit;
|
|
860
1151
|
}
|
|
861
1152
|
function setFollowScrollTop(port, nextTop) {
|
|
1153
|
+
const ledger = followScrollLedgers.get(port);
|
|
1154
|
+
if (port.getAttribute(FOLLOW_OWNED_ATTR) === null) port.setAttribute(FOLLOW_OWNED_ATTR, "active");
|
|
1155
|
+
if (ledger !== void 0 && traceActive() && Math.abs(port.scrollTop - ledger) > 1) followTrace("external-scroll", {
|
|
1156
|
+
from: Math.round(port.scrollTop),
|
|
1157
|
+
to: Math.round(nextTop),
|
|
1158
|
+
ledger: Math.round(ledger)
|
|
1159
|
+
});
|
|
862
1160
|
if (Math.abs(port.scrollTop - nextTop) > .01) port.scrollTop = nextTop;
|
|
863
1161
|
followScrollLedgers.set(port, port.scrollTop);
|
|
864
1162
|
const ownedTop = String(port.scrollTop);
|
|
@@ -872,6 +1170,19 @@ window.__ModuleLoader__.load({
|
|
|
872
1170
|
*/
|
|
873
1171
|
const followScrollLedgers = /* @__PURE__ */ new WeakMap();
|
|
874
1172
|
const followActivityAt = /* @__PURE__ */ new WeakMap();
|
|
1173
|
+
/**
|
|
1174
|
+
* Scroll ownership must survive follower-arm remounts. Per-closure state let a
|
|
1175
|
+
* new text/tool arm reset the strike counter, so the same host write kept
|
|
1176
|
+
* triggering another engine write and repainting the visible up/down fight.
|
|
1177
|
+
*/
|
|
1178
|
+
const followHostScrollPorts = /* @__PURE__ */ new WeakMap();
|
|
1179
|
+
/** Clear host ownership only when a new user turn actually starts. */
|
|
1180
|
+
function resetHostScrollOwnershipForNewTurn(port) {
|
|
1181
|
+
const ownership = followHostScrollPorts.get(port);
|
|
1182
|
+
if (ownership === void 0) return;
|
|
1183
|
+
const userRows = countUserRows(port);
|
|
1184
|
+
if (userRows >= 0 && userRows > ownership.userRows) followHostScrollPorts.delete(port);
|
|
1185
|
+
}
|
|
875
1186
|
/** Whether this port was owned recently enough to identify a closing tail row. */
|
|
876
1187
|
function hasRecentConversationFollow(port, windowMs = 250) {
|
|
877
1188
|
const last = followActivityAt.get(port);
|
|
@@ -888,7 +1199,7 @@ window.__ModuleLoader__.load({
|
|
|
888
1199
|
* same frame. Carrying that excess in `scrollTop` would move the transcript
|
|
889
1200
|
* toward fixed chrome and also make the host expose jump-to-bottom.
|
|
890
1201
|
*/
|
|
891
|
-
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx) {
|
|
1202
|
+
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx, dtMs = 16.7, writeScrollTop = true) {
|
|
892
1203
|
const surfaces = shiftSurfacesOf(port);
|
|
893
1204
|
ensureFlowFillsPort(port);
|
|
894
1205
|
{
|
|
@@ -900,10 +1211,7 @@ window.__ModuleLoader__.load({
|
|
|
900
1211
|
ensureRunway(port, surfaces, owned?.requestedPx ?? runwayPx);
|
|
901
1212
|
followSlackTransition.add(port);
|
|
902
1213
|
}
|
|
903
|
-
if (preFloor !== lastFloorSeen)
|
|
904
|
-
invalidatePaintLimit(port);
|
|
905
|
-
followFloorHistory.set(port, preFloor);
|
|
906
|
-
}
|
|
1214
|
+
if (preFloor !== lastFloorSeen) followFloorHistory.set(port, preFloor);
|
|
907
1215
|
}
|
|
908
1216
|
ensureRunway(port, surfaces, reservePx);
|
|
909
1217
|
const contentHeight2 = Math.max(0, port.scrollHeight);
|
|
@@ -920,7 +1228,7 @@ window.__ModuleLoader__.load({
|
|
|
920
1228
|
if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
|
|
921
1229
|
if (floor <= 0) {
|
|
922
1230
|
followRunwayOffsetHistory.set(port, 0);
|
|
923
|
-
setFollowScrollTop(port, 0);
|
|
1231
|
+
if (writeScrollTop) setFollowScrollTop(port, 0);
|
|
924
1232
|
followMotionStates.set(port, {
|
|
925
1233
|
capacityPx: Number.POSITIVE_INFINITY,
|
|
926
1234
|
constrained: false,
|
|
@@ -944,15 +1252,25 @@ window.__ModuleLoader__.load({
|
|
|
944
1252
|
const motionShift = Math.min(trajectoryShiftPx ?? baselineShift + requestedLag, availableShift);
|
|
945
1253
|
let shift = Math.max(motionShift, promoteAtRest && motionShift <= .01 && availableShift > 0 ? .1 : 0);
|
|
946
1254
|
const previousShift = followLastShiftPx.get(port);
|
|
947
|
-
|
|
1255
|
+
const previousFloor = followLastFloorPx.get(port);
|
|
1256
|
+
const floorDropPx = Math.max(0, (previousFloor ?? floor) - floor);
|
|
1257
|
+
const maxDecayPx = Math.max(dtMs <= 0 ? 8 : Math.max(1, 8 / 16.67 * dtMs), floorDropPx);
|
|
1258
|
+
followLastFloorPx.set(port, floor);
|
|
1259
|
+
if (previousShift !== void 0 && shift < previousShift - maxDecayPx) shift = previousShift - maxDecayPx;
|
|
1260
|
+
if (followCompletionSettle.has(port) && previousShift !== void 0 && previousFloor !== void 0) {
|
|
1261
|
+
const confirmedGrowthPx = Math.max(0, floor - previousFloor);
|
|
1262
|
+
if (shift > previousShift + confirmedGrowthPx) shift = previousShift + confirmedGrowthPx;
|
|
1263
|
+
}
|
|
948
1264
|
followLastShiftPx.set(port, shift);
|
|
949
|
-
const
|
|
1265
|
+
const requestedShift = trajectoryShiftPx ?? baselineShift + requestedLag;
|
|
1266
|
+
const effectiveLag = Math.max(0, shift - baselineShift);
|
|
950
1267
|
const capacityPx = Math.max(0, limit - baselineShift);
|
|
951
1268
|
const effectiveExtent = targetHeight - effectiveLag;
|
|
952
|
-
|
|
1269
|
+
const isConstrained = requestedShift > availableShift + .25 || limit <= 0 && requestedShift > baselineShift;
|
|
1270
|
+
if (writeScrollTop) setFollowScrollTop(port, floor);
|
|
953
1271
|
followMotionStates.set(port, {
|
|
954
1272
|
capacityPx,
|
|
955
|
-
constrained:
|
|
1273
|
+
constrained: isConstrained,
|
|
956
1274
|
extent: effectiveExtent,
|
|
957
1275
|
lagPx: effectiveLag,
|
|
958
1276
|
reservePx: visibleReserve,
|
|
@@ -976,6 +1294,7 @@ window.__ModuleLoader__.load({
|
|
|
976
1294
|
restoreRunway(port);
|
|
977
1295
|
followMotionStates.delete(port);
|
|
978
1296
|
followLastShiftPx.delete(port);
|
|
1297
|
+
followLastFloorPx.delete(port);
|
|
979
1298
|
invalidatePaintLimit(port);
|
|
980
1299
|
}
|
|
981
1300
|
/** Keep an already-promoted surface at zero until one stable final paint lands. */
|
|
@@ -985,20 +1304,27 @@ window.__ModuleLoader__.load({
|
|
|
985
1304
|
element.style.clipPath = "";
|
|
986
1305
|
}
|
|
987
1306
|
/** Remove equal offsets, land on the floor, then retire the compositor quietly. */
|
|
988
|
-
function finishAtNaturalFloor(port, retainCompositor = true) {
|
|
1307
|
+
function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true) {
|
|
1308
|
+
followCompletionSettle.delete(port);
|
|
1309
|
+
followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
|
|
1310
|
+
followTrace("finish-enter", {
|
|
1311
|
+
sh: hostShOf(port),
|
|
1312
|
+
st: Math.round(port.scrollTop),
|
|
1313
|
+
pad: Math.round(flowPadOf(port)),
|
|
1314
|
+
retain: retainCompositor
|
|
1315
|
+
});
|
|
989
1316
|
const surfaces = shiftSurfacesOf(port);
|
|
990
1317
|
const status = turnStatusOf(port);
|
|
991
1318
|
if (!retainCompositor) {
|
|
992
1319
|
restoreRunway(port);
|
|
993
|
-
settleAtFloor(port);
|
|
1320
|
+
if (writeScrollTop) settleAtFloor(port);
|
|
994
1321
|
clearMotion(port);
|
|
995
1322
|
followMotionStates.delete(port);
|
|
996
1323
|
return;
|
|
997
1324
|
}
|
|
998
1325
|
const promoted = [...surfaces, ...status === null ? [] : [status]].filter((element) => element.style.transform !== "" || element.style.willChange === "transform");
|
|
999
1326
|
const promotedSet = new Set(promoted);
|
|
1000
|
-
|
|
1001
|
-
settleAtFloor(port);
|
|
1327
|
+
if (writeScrollTop) settleAtFloor(port);
|
|
1002
1328
|
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1003
1329
|
port.style.overflowAnchor = "";
|
|
1004
1330
|
port.style.scrollBehavior = "";
|
|
@@ -1023,6 +1349,8 @@ window.__ModuleLoader__.load({
|
|
|
1023
1349
|
/** Only the newest active follower may write one port's shared visual state. */
|
|
1024
1350
|
const followLeaders = /* @__PURE__ */ new WeakMap();
|
|
1025
1351
|
let followGeneration = 0;
|
|
1352
|
+
/** Ports with a live streaming arm; completion guards must not fight them. */
|
|
1353
|
+
const followActivePorts = /* @__PURE__ */ new WeakSet();
|
|
1026
1354
|
/**
|
|
1027
1355
|
* Own the conversation scrollport's bottom-follow while `active` is true.
|
|
1028
1356
|
*
|
|
@@ -1036,15 +1364,19 @@ window.__ModuleLoader__.load({
|
|
|
1036
1364
|
* @param predictiveRef - Optional live visibility gate for predictive runway.
|
|
1037
1365
|
* @param entranceExtentRef - Optional measured growth delta for a generic row.
|
|
1038
1366
|
* @param revealedCharsRef - Committed code-point count for feed-forward phase.
|
|
1367
|
+
* @param controlScroll - When false, leave the scrollport entirely to the Host.
|
|
1039
1368
|
*/
|
|
1040
|
-
function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef) {
|
|
1369
|
+
function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef, controlScroll = true) {
|
|
1041
1370
|
const activeRef = (0, react.useRef)(active);
|
|
1042
1371
|
const entranceRef = (0, react.useRef)(entrance);
|
|
1043
1372
|
const onEntranceSettledRef = (0, react.useRef)(onEntranceSettled);
|
|
1044
1373
|
entranceRef.current = entrance;
|
|
1045
1374
|
onEntranceSettledRef.current = onEntranceSettled;
|
|
1046
1375
|
activeRef.current = active;
|
|
1376
|
+
const controlScrollRef = (0, react.useRef)(controlScroll);
|
|
1377
|
+
controlScrollRef.current = controlScroll;
|
|
1047
1378
|
(0, react.useLayoutEffect)(() => {
|
|
1379
|
+
if (!controlScroll) return;
|
|
1048
1380
|
if (!active) return;
|
|
1049
1381
|
const startedAsEntrance = entrance;
|
|
1050
1382
|
const owner = {};
|
|
@@ -1063,8 +1395,10 @@ window.__ModuleLoader__.load({
|
|
|
1063
1395
|
let interactTimer = null;
|
|
1064
1396
|
let port = null;
|
|
1065
1397
|
let resize = null;
|
|
1398
|
+
let mutations = null;
|
|
1066
1399
|
let observedTail = null;
|
|
1067
1400
|
let statusWasPresent = null;
|
|
1401
|
+
let lastStatusHeightPx = 0;
|
|
1068
1402
|
let trajectoryPositionPx = null;
|
|
1069
1403
|
let trajectoryVelocityPxPerMs = 0;
|
|
1070
1404
|
let trajectoryTargetVelocityPxPerMs = 0;
|
|
@@ -1120,10 +1454,43 @@ window.__ModuleLoader__.load({
|
|
|
1120
1454
|
if (holding === next && isLeader(next)) return;
|
|
1121
1455
|
holding = next;
|
|
1122
1456
|
const leader = followLeaders.get(next);
|
|
1123
|
-
if (leader === void 0 || generation > leader.generation)
|
|
1124
|
-
|
|
1125
|
-
|
|
1457
|
+
if ((leader === void 0 || generation > leader.generation) && !completionSettleGuardsPort(next)) {
|
|
1458
|
+
followCompletionSettle.delete(next);
|
|
1459
|
+
followLeaders.set(next, {
|
|
1460
|
+
generation,
|
|
1461
|
+
owner
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
const yieldScrollOwnership = (next, floor) => {
|
|
1466
|
+
if (hostOwnsScroll) return;
|
|
1467
|
+
hostOwnsScroll = true;
|
|
1468
|
+
followHostScrollPorts.set(next, { userRows: countUserRows(next) });
|
|
1469
|
+
followTrace("yield-scroll", {
|
|
1470
|
+
from: Math.round(next.scrollTop),
|
|
1471
|
+
to: Math.round(floor)
|
|
1126
1472
|
});
|
|
1473
|
+
clearVisual(next);
|
|
1474
|
+
setFollowScrollTop(next, Math.max(0, next.scrollHeight - next.clientHeight));
|
|
1475
|
+
next.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1476
|
+
followLeaders.delete(next);
|
|
1477
|
+
releaseRevealScale();
|
|
1478
|
+
debugRuntime.reportFollow(next, null);
|
|
1479
|
+
};
|
|
1480
|
+
const detectHostScroll = (next, floor) => {
|
|
1481
|
+
if (followHostScrollPorts.has(next)) {
|
|
1482
|
+
hostOwnsScroll = true;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const ledger = followScrollLedgers.get(next);
|
|
1486
|
+
if (ledger === void 0 || Math.abs(next.scrollTop - ledger) <= 1) return;
|
|
1487
|
+
if (Math.abs(next.scrollTop - floor) <= 1) {
|
|
1488
|
+
followScrollLedgers.set(next, next.scrollTop);
|
|
1489
|
+
externalScrollStrikes = 0;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
externalScrollStrikes += 1;
|
|
1493
|
+
if (externalScrollStrikes >= 2) yieldScrollOwnership(next, floor);
|
|
1127
1494
|
};
|
|
1128
1495
|
const drop = (next) => {
|
|
1129
1496
|
if (holding === next) holding = null;
|
|
@@ -1164,16 +1531,102 @@ window.__ModuleLoader__.load({
|
|
|
1164
1531
|
interactTimer = null;
|
|
1165
1532
|
}, 800);
|
|
1166
1533
|
};
|
|
1534
|
+
/**
|
|
1535
|
+
* Last observed scroll extent, shared by the pre-paint correction and the
|
|
1536
|
+
* settle loop so a host layout shrink is re-opened exactly once no matter
|
|
1537
|
+
* which observer sees it first. `-1` until the first owned observation.
|
|
1538
|
+
*/
|
|
1539
|
+
let settleRetiring = false;
|
|
1540
|
+
let hostOwnsScroll = false;
|
|
1541
|
+
let externalScrollStrikes = 0;
|
|
1542
|
+
Number.NEGATIVE_INFINITY;
|
|
1543
|
+
let handedOff = false;
|
|
1544
|
+
/**
|
|
1545
|
+
* Bring the reading surface back down toward its held position after an
|
|
1546
|
+
* upward host push (clamp jump, live→settled swap): spend persistent pad
|
|
1547
|
+
* first — lowering the floor lets the pin carry the text back down —
|
|
1548
|
+
* then raise the compositor shift for whatever pad cannot cover, and
|
|
1549
|
+
* rebase the spring extent so the raise survives the next applyVisual
|
|
1550
|
+
* (which otherwise recomputes the shift from lag and undoes it).
|
|
1551
|
+
*/
|
|
1552
|
+
const pullReadingAnchorBack = (host, measured, trace = false) => {
|
|
1553
|
+
holdGuardAnchor(host, measured, measured.top);
|
|
1554
|
+
};
|
|
1555
|
+
/**
|
|
1556
|
+
* THE screen-space anchor hold, shared by every entry point: the
|
|
1557
|
+
* structural-observer path (pre-paint cascade correction), the settle
|
|
1558
|
+
* loop's per-frame poll, and the ACTIVE loop's per-frame poll. The
|
|
1559
|
+
* shift-excluded delta filters the engine's own motion (reveal glide,
|
|
1560
|
+
* wrap lockstep) to ~0, so a live poll may compensate safely — which is
|
|
1561
|
+
* what saves the completion swap: the settled-side arm primes in its
|
|
1562
|
+
* layout effect and steals leadership IN the cascade frame, its own
|
|
1563
|
+
* observers miss the mutations (armed after the fact), and only this
|
|
1564
|
+
* poll sees the jump while it can still be corrected before paint.
|
|
1565
|
+
* Returns the measured delta (null when nothing was comparable).
|
|
1566
|
+
*/
|
|
1567
|
+
const enforceReadingAnchor = (host, trace = false) => {
|
|
1568
|
+
const measured = measureReadingAnchor(host);
|
|
1569
|
+
if (measured === null) return null;
|
|
1570
|
+
if (measured.delta > .5) {
|
|
1571
|
+
if (trace) followTrace("anchor-hold", {
|
|
1572
|
+
screenDelta: Math.round(measured.delta),
|
|
1573
|
+
st: Math.round(host.scrollTop)
|
|
1574
|
+
});
|
|
1575
|
+
if (pruneDeadRunway(host)) reservePx = 0;
|
|
1576
|
+
holdGuardAnchor(host, measured, measured.top);
|
|
1577
|
+
} else if (measured.delta < -.5) {
|
|
1578
|
+
if (pruneDeadRunway(host)) reservePx = 0;
|
|
1579
|
+
pullReadingAnchorBack(host, measured, trace);
|
|
1580
|
+
} else holdGuardAnchor(host, measured, measured.top);
|
|
1581
|
+
return measured.delta;
|
|
1582
|
+
};
|
|
1583
|
+
/** Pre-paint correction (observers, commit subscription). */
|
|
1167
1584
|
const restoreBeforePaint = () => {
|
|
1168
|
-
if (!following || port === null
|
|
1585
|
+
if (!following || port === null) return;
|
|
1586
|
+
if (hostOwnsScroll || followHostScrollPorts.has(port)) {
|
|
1587
|
+
hostOwnsScroll = true;
|
|
1588
|
+
followScrollLedgers.set(port, port.scrollTop);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (!activeRef.current) detectHostScroll(port, Math.max(0, port.scrollHeight - port.clientHeight));
|
|
1592
|
+
const leaderless = !isLeader(port);
|
|
1593
|
+
if (!activeRef.current && followActivePorts.has(port)) return;
|
|
1594
|
+
if (leaderless && (followLeaders.has(port) || !handedOff)) return;
|
|
1595
|
+
if (leaderless) {
|
|
1596
|
+
const sharedMotion = followMotionStates.get(port);
|
|
1597
|
+
if (sharedMotion !== void 0) {
|
|
1598
|
+
animatedH = Math.min(port.scrollHeight, Math.max(0, sharedMotion.extent));
|
|
1599
|
+
reservePx = sharedMotion.reservePx;
|
|
1600
|
+
velocityPxPerSec = sharedMotion.velocityPxPerSec;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
pruneDeadRunway(port);
|
|
1604
|
+
if (!settleRetiring) {
|
|
1605
|
+
const measured = measureReadingAnchor(port);
|
|
1606
|
+
if (measured !== null && measured.delta > .5) {
|
|
1607
|
+
if (pruneDeadRunway(port)) reservePx = 0;
|
|
1608
|
+
holdGuardAnchor(port, measured, measured.top);
|
|
1609
|
+
} else if (measured !== null && measured.delta < -.5) {
|
|
1610
|
+
if (pruneDeadRunway(port)) reservePx = 0;
|
|
1611
|
+
if (!activeRef.current) pullReadingAnchorBack(port, measured);
|
|
1612
|
+
else {
|
|
1613
|
+
const released = Math.min(flowPadOf(port), -measured.delta);
|
|
1614
|
+
if (released > .25) {
|
|
1615
|
+
setFlowPad(port, flowPadOf(port) - released);
|
|
1616
|
+
animatedH = Math.max(0, animatedH - released);
|
|
1617
|
+
}
|
|
1618
|
+
holdGuardAnchor(port, measured, measured.top + released);
|
|
1619
|
+
}
|
|
1620
|
+
} else if (measured !== null) holdGuardAnchor(port, measured, measured.top);
|
|
1621
|
+
}
|
|
1169
1622
|
const tuning = debugRuntime.activeTuning();
|
|
1170
1623
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1171
1624
|
const floor = Math.max(0, port.scrollHeight - port.clientHeight);
|
|
1172
1625
|
if (followFloorHistory.get(port) !== floor) invalidatePaintLimit(port);
|
|
1173
1626
|
const isReasoningSurface = rootRef.current?.querySelector("[data-variant=\"think\"]") !== null;
|
|
1174
1627
|
const trajectoryShift = predictive && !isReasoningSurface && runwayOffsetOf(port) > 0 && trajectoryPositionPx !== null ? floor - trajectoryPositionPx : void 0;
|
|
1175
|
-
animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift);
|
|
1176
|
-
if (trajectoryShift !== void 0) trajectoryPositionPx = floor -
|
|
1628
|
+
animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, activeRef.current ? Number.POSITIVE_INFINITY : followLastShiftPx.get(port) ?? Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift, 0, activeRef.current && !hostOwnsScroll);
|
|
1629
|
+
if (trajectoryShift !== void 0) trajectoryPositionPx = floor - (followLastShiftPx.get(port) ?? floor - (trajectoryPositionPx ?? floor));
|
|
1177
1630
|
updateRevealScale(port, 0, true);
|
|
1178
1631
|
reportFollow(port, activeRef.current);
|
|
1179
1632
|
};
|
|
@@ -1183,6 +1636,7 @@ window.__ModuleLoader__.load({
|
|
|
1183
1636
|
if (port !== null) {
|
|
1184
1637
|
for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
1185
1638
|
resize?.disconnect();
|
|
1639
|
+
mutations?.disconnect();
|
|
1186
1640
|
}
|
|
1187
1641
|
unsubscribeCommit?.();
|
|
1188
1642
|
port = next;
|
|
@@ -1192,11 +1646,23 @@ window.__ModuleLoader__.load({
|
|
|
1192
1646
|
});
|
|
1193
1647
|
for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
|
|
1194
1648
|
if (typeof ResizeObserver !== "undefined") {
|
|
1195
|
-
resize = new ResizeObserver(restoreBeforePaint);
|
|
1649
|
+
resize = new ResizeObserver(() => restoreBeforePaint());
|
|
1196
1650
|
resize.observe(port);
|
|
1197
1651
|
const proxy = resizeProxyOf(port);
|
|
1198
1652
|
if (proxy !== null) resize.observe(proxy);
|
|
1199
1653
|
}
|
|
1654
|
+
if (typeof MutationObserver !== "undefined") {
|
|
1655
|
+
const flow = flowElementOf(port);
|
|
1656
|
+
if (flow !== null) {
|
|
1657
|
+
mutations = new MutationObserver(() => {
|
|
1658
|
+
restoreBeforePaint();
|
|
1659
|
+
});
|
|
1660
|
+
mutations.observe(flow, {
|
|
1661
|
+
childList: true,
|
|
1662
|
+
subtree: true
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1200
1666
|
};
|
|
1201
1667
|
/**
|
|
1202
1668
|
* Keep the observer on the flow's TAIL surface. A flow locked to the
|
|
@@ -1224,17 +1690,28 @@ window.__ModuleLoader__.load({
|
|
|
1224
1690
|
if (nextPort === null) return;
|
|
1225
1691
|
bindPort(nextPort);
|
|
1226
1692
|
observeTailSurface();
|
|
1693
|
+
resetHostScrollOwnershipForNewTurn(nextPort);
|
|
1694
|
+
hostOwnsScroll = followHostScrollPorts.has(nextPort);
|
|
1695
|
+
if (activeRef.current) followActivePorts.add(nextPort);
|
|
1696
|
+
else followActivePorts.delete(nextPort);
|
|
1227
1697
|
if (nextPort.clientHeight <= 0) return;
|
|
1228
|
-
const
|
|
1698
|
+
const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
|
|
1699
|
+
const reportedLag = floor - nextPort.scrollTop;
|
|
1229
1700
|
const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
|
|
1230
1701
|
if (!primed) {
|
|
1702
|
+
if (completionSettleGuardsPort(nextPort)) {
|
|
1703
|
+
primed = true;
|
|
1704
|
+
following = false;
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1231
1707
|
const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
|
|
1232
1708
|
if (inherited === void 0) {
|
|
1233
1709
|
const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
|
|
1234
1710
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1235
1711
|
animatedH = entrancePending ? Math.max(0, nextPort.scrollHeight - entranceExtent) : nextPort.scrollHeight;
|
|
1236
1712
|
const hasStatus = turnStatusOf(nextPort) !== null;
|
|
1237
|
-
reservePx = predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0;
|
|
1713
|
+
reservePx = Math.max(ownedBottomSpaceOf(nextPort), predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0);
|
|
1714
|
+
if (!completionSettleGuardsPort(nextPort)) setFlowPad(nextPort, 0);
|
|
1238
1715
|
statusWasPresent = hasStatus;
|
|
1239
1716
|
velocityPxPerSec = 0;
|
|
1240
1717
|
following = !readerScrolledUp(nextPort) && !followReaderHolds.has(nextPort);
|
|
@@ -1247,7 +1724,7 @@ window.__ModuleLoader__.load({
|
|
|
1247
1724
|
if (following) {
|
|
1248
1725
|
hold(nextPort);
|
|
1249
1726
|
if (isLeader(nextPort)) {
|
|
1250
|
-
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !(predictiveRef?.current ?? predictive));
|
|
1727
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !(predictiveRef?.current ?? predictive), void 0, 0, !hostOwnsScroll);
|
|
1251
1728
|
if (predictive && root.querySelector("[data-variant=\"think\"]") === null && runwayOffsetOf(nextPort) > 0) {
|
|
1252
1729
|
const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
|
|
1253
1730
|
const requestedMinLagPx = Math.max(20, runwayOffsetOf(nextPort) - 32);
|
|
@@ -1299,20 +1776,27 @@ window.__ModuleLoader__.load({
|
|
|
1299
1776
|
reportFollow(nextPort, activeRef.current);
|
|
1300
1777
|
return;
|
|
1301
1778
|
}
|
|
1779
|
+
detectHostScroll(nextPort, floor);
|
|
1780
|
+
if (hostOwnsScroll) {
|
|
1781
|
+
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
1782
|
+
reportFollow(nextPort, false);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1302
1785
|
hold(nextPort);
|
|
1303
1786
|
if (!isLeader(nextPort)) {
|
|
1304
1787
|
finishEntrance();
|
|
1305
1788
|
return;
|
|
1306
1789
|
}
|
|
1307
1790
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1308
|
-
const
|
|
1791
|
+
const statusElement = turnStatusOf(nextPort);
|
|
1792
|
+
const hasStatus = statusElement !== null;
|
|
1793
|
+
if (statusElement !== null) lastStatusHeightPx = statusElement.offsetHeight;
|
|
1309
1794
|
const statusJustRemoved = predictGrowth && statusWasPresent === true && !hasStatus;
|
|
1310
|
-
if (statusJustRemoved) reservePx = tuning.runwayPx;
|
|
1795
|
+
if (statusJustRemoved) reservePx = Math.max(reservePx, tuning.runwayPx) + lastStatusHeightPx;
|
|
1311
1796
|
statusWasPresent = hasStatus;
|
|
1312
1797
|
const reserveEnabled = hasStatus || statusJustRemoved || reservePx > .25 || speedCpsRef.current > 90;
|
|
1313
1798
|
const pressureReserveTarget = predictGrowth && reserveEnabled ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0;
|
|
1314
|
-
const
|
|
1315
|
-
const effectiveReserveTarget = !predictGrowth ? 0 : statusJustRemoved ? tuning.runwayPx : heldReserveTarget;
|
|
1799
|
+
const effectiveReserveTarget = Math.max(reservePx, pressureReserveTarget);
|
|
1316
1800
|
const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
|
|
1317
1801
|
reservePx += (effectiveReserveTarget - reservePx) * reserveStep;
|
|
1318
1802
|
if (predictGrowth || runwayOffsetOf(nextPort) > .5) ensureRunway(nextPort, shiftSurfacesOf(nextPort), reservePx);
|
|
@@ -1386,23 +1870,45 @@ window.__ModuleLoader__.load({
|
|
|
1386
1870
|
animatedH = contentHeight - runwayOffset;
|
|
1387
1871
|
velocityPxPerSec = 0;
|
|
1388
1872
|
} else {
|
|
1389
|
-
|
|
1390
|
-
animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
|
|
1873
|
+
animatedH = Math.min(contentHeight - runwayOffset, animatedH + step.advancePx);
|
|
1391
1874
|
velocityPxPerSec = step.velocityPxPerSec;
|
|
1392
1875
|
}
|
|
1393
1876
|
}
|
|
1394
|
-
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift);
|
|
1395
|
-
if (trajectoryActive) trajectoryPositionPx = floorNow -
|
|
1877
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift, elapsedMs, !hostOwnsScroll);
|
|
1878
|
+
if (trajectoryActive) trajectoryPositionPx = floorNow - (followLastShiftPx.get(nextPort) ?? trajectoryShift ?? 0);
|
|
1396
1879
|
updateRevealScale(nextPort, elapsedMs);
|
|
1397
1880
|
reportFollow(nextPort, true);
|
|
1881
|
+
if (isLeader(nextPort)) {
|
|
1882
|
+
if (!activeRef.current) enforceReadingAnchor(nextPort);
|
|
1883
|
+
else measureReadingAnchor(nextPort);
|
|
1884
|
+
}
|
|
1398
1885
|
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
|
|
1399
1886
|
};
|
|
1400
1887
|
frame(performance.now());
|
|
1401
1888
|
return () => {
|
|
1889
|
+
if (!controlScrollRef.current) {
|
|
1890
|
+
cancelAnimationFrame(rafId);
|
|
1891
|
+
if (port !== null) followActivePorts.delete(port);
|
|
1892
|
+
unsubscribeCommit?.();
|
|
1893
|
+
resize?.disconnect();
|
|
1894
|
+
mutations?.disconnect();
|
|
1895
|
+
if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
1896
|
+
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
1897
|
+
const disabledHost = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
|
|
1898
|
+
if (disabledHost !== null) {
|
|
1899
|
+
clearVisual(disabledHost);
|
|
1900
|
+
followLeaders.delete(disabledHost);
|
|
1901
|
+
debugRuntime.reportFollow(disabledHost, null);
|
|
1902
|
+
}
|
|
1903
|
+
releaseRevealScale();
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1402
1906
|
cancelAnimationFrame(rafId);
|
|
1907
|
+
if (port !== null) followActivePorts.delete(port);
|
|
1403
1908
|
unsubscribeCommit?.();
|
|
1404
1909
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
1405
1910
|
resize?.disconnect();
|
|
1911
|
+
mutations?.disconnect();
|
|
1406
1912
|
if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
1407
1913
|
const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
|
|
1408
1914
|
if (host === null) return;
|
|
@@ -1426,8 +1932,18 @@ window.__ModuleLoader__.load({
|
|
|
1426
1932
|
debugRuntime.reportFollow(host, null);
|
|
1427
1933
|
return;
|
|
1428
1934
|
}
|
|
1935
|
+
let settleQuietMs = 0;
|
|
1936
|
+
let settleSig = "";
|
|
1429
1937
|
const lagBeforeCompletionPaint = Math.max(0, host.scrollHeight - animatedH - runwayOffsetOf(host));
|
|
1430
|
-
|
|
1938
|
+
const currentRunway = runwayOffsetOf(host);
|
|
1939
|
+
const currentShift = Math.abs(currentShiftOf(shiftSurfacesOf(host).at(-1) ?? host));
|
|
1940
|
+
if (!activeRef.current && lagBeforeCompletionPaint <= 25 && currentRunway <= .25 && currentShift <= .25 && flowPadOf(host) <= .25) {
|
|
1941
|
+
followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
|
|
1942
|
+
followTrace("fast-gate", {
|
|
1943
|
+
sh: host.scrollHeight,
|
|
1944
|
+
st: Math.round(host.scrollTop),
|
|
1945
|
+
pad: Math.round(flowPadOf(host))
|
|
1946
|
+
});
|
|
1431
1947
|
finishAtNaturalFloor(host, !startedAsEntrance);
|
|
1432
1948
|
followLeaders.delete(host);
|
|
1433
1949
|
releaseRevealScale();
|
|
@@ -1435,19 +1951,37 @@ window.__ModuleLoader__.load({
|
|
|
1435
1951
|
return;
|
|
1436
1952
|
}
|
|
1437
1953
|
const completionShift = currentShiftOf(shiftSurfacesOf(host).at(-1) ?? host);
|
|
1438
|
-
const completionShiftCeiling = completionShift
|
|
1954
|
+
const completionShiftCeiling = startedAsEntrance && completionShift <= .25 ? Number.POSITIVE_INFINITY : completionShift;
|
|
1955
|
+
if (hostOwnsScroll) {
|
|
1956
|
+
clearVisual(host);
|
|
1957
|
+
setFollowScrollTop(host, Math.max(0, host.scrollHeight - host.clientHeight));
|
|
1958
|
+
host.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1959
|
+
followLeaders.delete(host);
|
|
1960
|
+
releaseRevealScale();
|
|
1961
|
+
debugRuntime.reportFollow(host, null);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
followTraceUntilMs = performance.now() + 15e3;
|
|
1965
|
+
if (pruneDeadRunway(host)) {
|
|
1966
|
+
followTrace("cleanup-dead-margin", {
|
|
1967
|
+
sh: host.scrollHeight,
|
|
1968
|
+
st: Math.round(host.scrollTop),
|
|
1969
|
+
pad: Math.round(flowPadOf(host))
|
|
1970
|
+
});
|
|
1971
|
+
reservePx = 0;
|
|
1972
|
+
}
|
|
1439
1973
|
const completionTuning = debugRuntime.activeTuning();
|
|
1440
1974
|
const previousCompletionRunway = runwayOffsetOf(host);
|
|
1441
|
-
ensureRunway(host, shiftSurfacesOf(host), Math.max(reservePx, completionTuning.runwayPx));
|
|
1975
|
+
ensureRunway(host, shiftSurfacesOf(host), Math.max(reservePx, Math.max(0, completionTuning.runwayPx - flowPadOf(host))));
|
|
1442
1976
|
const completionRunway = runwayOffsetOf(host);
|
|
1977
|
+
measureReadingAnchor(host);
|
|
1443
1978
|
animatedH = Math.max(0, animatedH - (completionRunway - previousCompletionRunway));
|
|
1444
|
-
settleAtFloor(host);
|
|
1445
|
-
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionRunway, completionShiftCeiling);
|
|
1979
|
+
if (!hostOwnsScroll) settleAtFloor(host);
|
|
1980
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionRunway, completionShiftCeiling, false, void 0, 0, !hostOwnsScroll);
|
|
1446
1981
|
reportFollow(host, false);
|
|
1447
1982
|
const runwayOffset = runwayOffsetOf(host);
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
finishAtNaturalFloor(host, !startedAsEntrance);
|
|
1983
|
+
if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && runwayOffset <= .25 && reservePx <= .25) {
|
|
1984
|
+
finishAtNaturalFloor(host, !startedAsEntrance, !hostOwnsScroll);
|
|
1451
1985
|
followLeaders.delete(host);
|
|
1452
1986
|
releaseRevealScale();
|
|
1453
1987
|
debugRuntime.reportFollow(host, null);
|
|
@@ -1456,14 +1990,35 @@ window.__ModuleLoader__.load({
|
|
|
1456
1990
|
for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
|
|
1457
1991
|
const stopSettleListeners = () => {
|
|
1458
1992
|
for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
|
|
1993
|
+
resize?.disconnect();
|
|
1994
|
+
mutations?.disconnect();
|
|
1459
1995
|
if (interactTimer !== null) {
|
|
1460
1996
|
clearTimeout(interactTimer);
|
|
1461
1997
|
interactTimer = null;
|
|
1462
1998
|
}
|
|
1463
1999
|
};
|
|
2000
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2001
|
+
resize = new ResizeObserver(() => restoreBeforePaint());
|
|
2002
|
+
resize.observe(host);
|
|
2003
|
+
const proxy = resizeProxyOf(host);
|
|
2004
|
+
if (proxy !== null) resize.observe(proxy);
|
|
2005
|
+
}
|
|
2006
|
+
if (typeof MutationObserver !== "undefined") {
|
|
2007
|
+
const flow = flowElementOf(host);
|
|
2008
|
+
if (flow !== null) {
|
|
2009
|
+
mutations = new MutationObserver(() => {
|
|
2010
|
+
restoreBeforePaint();
|
|
2011
|
+
});
|
|
2012
|
+
mutations.observe(flow, {
|
|
2013
|
+
childList: true,
|
|
2014
|
+
subtree: true
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
followCompletionSettle.add(host);
|
|
2019
|
+
followCompletionSettleRows.set(host, countUserRows(host));
|
|
2020
|
+
handedOff = true;
|
|
1464
2021
|
let settleLast = performance.now();
|
|
1465
|
-
let settleMarginPx = Math.max(runwayOffsetOf(host), reservePx);
|
|
1466
|
-
let settleMarginRate = settleMarginPx / 160;
|
|
1467
2022
|
const settleFrame = (now) => {
|
|
1468
2023
|
if (!isLeader(host)) {
|
|
1469
2024
|
stopSettleListeners();
|
|
@@ -1473,6 +2028,7 @@ window.__ModuleLoader__.load({
|
|
|
1473
2028
|
readerGestureIntent = false;
|
|
1474
2029
|
handBackVisual(host);
|
|
1475
2030
|
clearVisual(host);
|
|
2031
|
+
followCompletionSettle.delete(host);
|
|
1476
2032
|
followLeaders.delete(host);
|
|
1477
2033
|
releaseRevealScale();
|
|
1478
2034
|
debugRuntime.reportFollow(host, null);
|
|
@@ -1482,19 +2038,56 @@ window.__ModuleLoader__.load({
|
|
|
1482
2038
|
const dt = Math.min(32, Math.max(0, now - settleLast));
|
|
1483
2039
|
const tuning = debugRuntime.activeTuning();
|
|
1484
2040
|
settleLast = now;
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
2041
|
+
detectHostScroll(host, Math.max(0, host.scrollHeight - host.clientHeight));
|
|
2042
|
+
if (hostOwnsScroll) {
|
|
2043
|
+
clearVisual(host);
|
|
2044
|
+
setFollowScrollTop(host, Math.max(0, host.scrollHeight - host.clientHeight));
|
|
2045
|
+
host.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
2046
|
+
followCompletionSettle.delete(host);
|
|
2047
|
+
followLeaders.delete(host);
|
|
2048
|
+
releaseRevealScale();
|
|
2049
|
+
debugRuntime.reportFollow(host, null);
|
|
2050
|
+
stopSettleListeners();
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const guardDelta = !settleRetiring && !followActivePorts.has(host) ? enforceReadingAnchor(host, true) : null;
|
|
2054
|
+
settleQuietMs = !settleRetiring && (guardDelta === null || Math.abs(guardDelta) <= .5) ? settleQuietMs + dt : 0;
|
|
2055
|
+
const settleStatus = turnStatusOf(host);
|
|
2056
|
+
const ownedRunwayPx = runwayOffsetOf(host);
|
|
2057
|
+
if (ownedRunwayPx > .25) {
|
|
2058
|
+
const requestedTransferPx = settleStatus === null ? ownedRunwayPx : Math.min(reservePx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
|
|
2059
|
+
const transferredPx = transferRunwayToFlowPad(host, requestedTransferPx);
|
|
2060
|
+
if (transferredPx > 0) setFlowPad(host, Math.max(0, flowPadOf(host) - transferredPx));
|
|
2061
|
+
reservePx = Math.max(0, reservePx - transferredPx);
|
|
2062
|
+
animatedH += transferredPx;
|
|
1489
2063
|
}
|
|
1490
2064
|
const runwayOffset = runwayOffsetOf(host);
|
|
1491
2065
|
const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
|
|
1492
|
-
if (lag <= .25 &&
|
|
2066
|
+
if (!settleRetiring && lag <= .25 && reservePx <= .25 && settleQuietMs >= 240 && flowPadOf(host) > .25) {
|
|
2067
|
+
followTrace("retire-start", {
|
|
2068
|
+
pad: Math.round(flowPadOf(host)),
|
|
2069
|
+
sh: host.scrollHeight,
|
|
2070
|
+
st: Math.round(host.scrollTop)
|
|
2071
|
+
});
|
|
2072
|
+
settleRetiring = true;
|
|
2073
|
+
}
|
|
2074
|
+
if (settleRetiring) {
|
|
2075
|
+
const padPx = flowPadOf(host);
|
|
2076
|
+
const retirePx = Math.min(padPx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
|
|
2077
|
+
if (padPx - retirePx <= .25) {
|
|
2078
|
+
setFlowPad(host, 0);
|
|
2079
|
+
settleRetiring = false;
|
|
2080
|
+
} else setFlowPad(host, padPx - retirePx);
|
|
2081
|
+
}
|
|
2082
|
+
if (lag <= .25 && (reservePx <= .25 || settleStatus === null) && flowPadOf(host) <= .25 && settleQuietMs >= 240) {
|
|
2083
|
+
followTrace("finish", {
|
|
2084
|
+
st: Math.round(host.scrollTop),
|
|
2085
|
+
sh: host.scrollHeight,
|
|
2086
|
+
pad: Math.round(flowPadOf(host))
|
|
2087
|
+
});
|
|
1493
2088
|
animatedH = host.scrollHeight;
|
|
1494
|
-
reservePx = 0;
|
|
1495
2089
|
velocityPxPerSec = 0;
|
|
1496
|
-
|
|
1497
|
-
finishAtNaturalFloor(host, !startedAsEntrance);
|
|
2090
|
+
finishAtNaturalFloor(host, !startedAsEntrance, !hostOwnsScroll);
|
|
1498
2091
|
followLeaders.delete(host);
|
|
1499
2092
|
releaseRevealScale();
|
|
1500
2093
|
debugRuntime.reportFollow(host, null);
|
|
@@ -1508,8 +2101,22 @@ window.__ModuleLoader__.load({
|
|
|
1508
2101
|
}, tuning);
|
|
1509
2102
|
animatedH = Math.min(host.scrollHeight - runwayOffset, animatedH + step.advancePx);
|
|
1510
2103
|
velocityPxPerSec = step.velocityPxPerSec;
|
|
1511
|
-
settleAtFloor(host);
|
|
1512
|
-
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec,
|
|
2104
|
+
if (!hostOwnsScroll) settleAtFloor(host);
|
|
2105
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, runwayOffset, Number.POSITIVE_INFINITY, false, void 0, 0, !hostOwnsScroll);
|
|
2106
|
+
if (traceActive()) {
|
|
2107
|
+
const sig = `${Math.round(host.scrollTop)}|${host.scrollHeight}|${Math.round(flowPadOf(host))}|${Math.round(reservePx)}|${settleRetiring}`;
|
|
2108
|
+
if (sig !== settleSig) {
|
|
2109
|
+
followTrace("settle", {
|
|
2110
|
+
st: Math.round(host.scrollTop),
|
|
2111
|
+
sh: host.scrollHeight,
|
|
2112
|
+
pad: Math.round(flowPadOf(host)),
|
|
2113
|
+
reserve: Math.round(reservePx),
|
|
2114
|
+
lag: Math.round(lag * 10) / 10,
|
|
2115
|
+
retiring: settleRetiring
|
|
2116
|
+
});
|
|
2117
|
+
settleSig = sig;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
1513
2120
|
reportFollow(host, false);
|
|
1514
2121
|
requestAnimationFrame(settleFrame);
|
|
1515
2122
|
};
|
|
@@ -1521,7 +2128,8 @@ window.__ModuleLoader__.load({
|
|
|
1521
2128
|
speedCpsRef,
|
|
1522
2129
|
revealScaleRef,
|
|
1523
2130
|
predictive,
|
|
1524
|
-
predictiveRef
|
|
2131
|
+
predictiveRef,
|
|
2132
|
+
controlScroll
|
|
1525
2133
|
]);
|
|
1526
2134
|
(0, react.useLayoutEffect)(() => {
|
|
1527
2135
|
const host = rootRef.current?.closest("[data-conversation-scroll]") ?? null;
|
|
@@ -2003,10 +2611,10 @@ window.__ModuleLoader__.load({
|
|
|
2003
2611
|
* generic wrapped renderers re-arm one glide when their DOM grows without
|
|
2004
2612
|
* requiring a business-kind-specific lifecycle predicate.
|
|
2005
2613
|
*/
|
|
2006
|
-
function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealedCharsRef, revealScaleRef, predictive = true, predictiveRef, hostRef, children }) {
|
|
2614
|
+
function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealedCharsRef, revealScaleRef, predictive = true, predictiveRef, controlScroll = true, hostRef, className, entranceActive, children }) {
|
|
2007
2615
|
const localRootRef = (0, react.useRef)(null);
|
|
2008
2616
|
const rootRef = hostRef ?? localRootRef;
|
|
2009
|
-
useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef);
|
|
2617
|
+
useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef, controlScroll);
|
|
2010
2618
|
(0, react.useEffect)(() => {
|
|
2011
2619
|
if (onGrowth === void 0 || typeof ResizeObserver === "undefined") return;
|
|
2012
2620
|
const root = rootRef.current;
|
|
@@ -2039,7 +2647,8 @@ window.__ModuleLoader__.load({
|
|
|
2039
2647
|
}, [onGrowth]);
|
|
2040
2648
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2041
2649
|
ref: rootRef,
|
|
2042
|
-
className: TypewriterAssistantNodeView_module_css_default.follow
|
|
2650
|
+
className: className === void 0 ? TypewriterAssistantNodeView_module_css_default.follow : `${TypewriterAssistantNodeView_module_css_default.follow} ${className}`,
|
|
2651
|
+
"data-entrance": entranceActive === void 0 ? void 0 : entranceActive ? "active" : "idle",
|
|
2043
2652
|
children
|
|
2044
2653
|
});
|
|
2045
2654
|
}
|
|
@@ -2277,7 +2886,7 @@ window.__ModuleLoader__.load({
|
|
|
2277
2886
|
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
2278
2887
|
* mentions) swaps in exactly once.
|
|
2279
2888
|
*/
|
|
2280
|
-
function AnimatedMarkdownText({ text,
|
|
2889
|
+
function AnimatedMarkdownText({ text, labels, fileMentions, streaming, ownFollow, followSpeedCpsRef, followRevealedCharsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack, controlScroll = true }) {
|
|
2281
2890
|
const reduced = usePrefersReducedMotion();
|
|
2282
2891
|
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
2283
2892
|
const localSpeedCpsRef = (0, react.useRef)(35);
|
|
@@ -2340,11 +2949,12 @@ window.__ModuleLoader__.load({
|
|
|
2340
2949
|
revealedCharsRef: followRevealedCharsRef,
|
|
2341
2950
|
revealScaleRef: followRevealScaleRef,
|
|
2342
2951
|
predictive: streaming,
|
|
2952
|
+
controlScroll,
|
|
2343
2953
|
hostRef: followRootRef,
|
|
2344
2954
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
2345
2955
|
text: live ? shown : text,
|
|
2346
2956
|
streaming: live,
|
|
2347
|
-
|
|
2957
|
+
labels,
|
|
2348
2958
|
fileMentions: live ? void 0 : fileMentions
|
|
2349
2959
|
})
|
|
2350
2960
|
});
|
|
@@ -2460,7 +3070,7 @@ window.__ModuleLoader__.load({
|
|
|
2460
3070
|
* holds offscreen reveals when the frame rate is degraded. Settled text
|
|
2461
3071
|
* renders with the full Markdown pipeline.
|
|
2462
3072
|
*/
|
|
2463
|
-
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 }) {
|
|
3073
|
+
const TypewriterAssistantNodeView = (0, react.memo)(function TypewriterAssistantNodeView({ mode: _mode = DEFAULT_STREAM_CONFIG.mode, preset = DEFAULT_STREAM_CONFIG.preset, revealCharsPerSec: _revealCharsPerSec = DEFAULT_STREAM_CONFIG.revealCharsPerSec, scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec, maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec, thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand, controlScroll = true, node, useTurnData, openFile, loadImage, fileMentions, t }) {
|
|
2464
3074
|
const data = node.data;
|
|
2465
3075
|
const streaming = data.status === "running";
|
|
2466
3076
|
const reduced = usePrefersReducedMotion();
|
|
@@ -2497,9 +3107,12 @@ window.__ModuleLoader__.load({
|
|
|
2497
3107
|
turn
|
|
2498
3108
|
]);
|
|
2499
3109
|
const mentions = (0, react.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2502
|
-
|
|
3110
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
3111
|
+
code: {
|
|
3112
|
+
copyLabel: t("copy"),
|
|
3113
|
+
copiedLabel: t("copied")
|
|
3114
|
+
},
|
|
3115
|
+
footnotes: t("markdown.footnotes")
|
|
2503
3116
|
}), [t]);
|
|
2504
3117
|
const imageLoader = loadImage ?? (async () => {
|
|
2505
3118
|
throw new Error(t("image.serviceUnavailable"));
|
|
@@ -2520,7 +3133,7 @@ window.__ModuleLoader__.load({
|
|
|
2520
3133
|
case "text":
|
|
2521
3134
|
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
|
|
2522
3135
|
text: block.text,
|
|
2523
|
-
|
|
3136
|
+
labels: markdownLabels,
|
|
2524
3137
|
fileMentions: mentions,
|
|
2525
3138
|
streaming,
|
|
2526
3139
|
ownFollow: !streaming && index === lastFollow,
|
|
@@ -2529,7 +3142,8 @@ window.__ModuleLoader__.load({
|
|
|
2529
3142
|
followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
|
|
2530
3143
|
onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
|
|
2531
3144
|
preset,
|
|
2532
|
-
shouldHoldBack
|
|
3145
|
+
shouldHoldBack,
|
|
3146
|
+
controlScroll
|
|
2533
3147
|
}, index));
|
|
2534
3148
|
break;
|
|
2535
3149
|
case "reasoning":
|
|
@@ -2582,6 +3196,7 @@ window.__ModuleLoader__.load({
|
|
|
2582
3196
|
revealedCharsRef: rootRevealedCharsRef,
|
|
2583
3197
|
revealScaleRef: rootRevealScaleRef,
|
|
2584
3198
|
predictiveRef: rootPredictiveRef,
|
|
3199
|
+
controlScroll,
|
|
2585
3200
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2586
3201
|
className: TypewriterAssistantNodeView_module_css_default.body,
|
|
2587
3202
|
children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2593,867 +3208,6 @@ window.__ModuleLoader__.load({
|
|
|
2593
3208
|
});
|
|
2594
3209
|
});
|
|
2595
3210
|
//#endregion
|
|
2596
|
-
//#region src/client/auto-collapse-controller.ts
|
|
2597
|
-
/**
|
|
2598
|
-
* Auto-collapse coordinator: folds the work process of finished turns behind
|
|
2599
|
-
* one summary row so only the model's final answer stays visible.
|
|
2600
|
-
*
|
|
2601
|
-
* The Harness ChatView lays a conversation out as a flat list of flow items
|
|
2602
|
-
* (`[data-chat-flow] > [data-chat-flow-kind=…]`). This controller watches that
|
|
2603
|
-
* list, splits it into turn segments at `user`/`steering` boundaries closed by
|
|
2604
|
-
* `turn-tail`, and — once a segment has settled — hides its thinking rows,
|
|
2605
|
-
* tool/command/context seats and intermediate replies, leaving one clickable
|
|
2606
|
-
* `已处理 {时长}` row plus the final answer. Clicking the row expands the full
|
|
2607
|
-
* process again; the choice survives later renders because it lives in the
|
|
2608
|
-
* controller, not the DOM.
|
|
2609
|
-
*
|
|
2610
|
-
* Adapted from the standalone `dsh-auto-collapse` plugin (level-1 folding
|
|
2611
|
-
* only): no second-level chips, no merged thinking rows, no status-text
|
|
2612
|
-
* rewrite, no transition animation — the collapse decision itself stays
|
|
2613
|
-
* instantaneous and reversible.
|
|
2614
|
-
*
|
|
2615
|
-
* React coexistence follows the upstream design: injected rows are plain
|
|
2616
|
-
* siblings/prepend children the vdom diff never claims, native rows are only
|
|
2617
|
-
* ever touched through an inline-`display` ledger that records the precise
|
|
2618
|
-
* original value and restores it on stop, session switch, or foreign takeover
|
|
2619
|
-
* (a written-value comparison plus an ownership sentinel catches another
|
|
2620
|
-
* script rewriting styles behind our back). A MutationObserver batches real
|
|
2621
|
-
* structure changes into rAF passes; a low-frequency self-rearming audit pass
|
|
2622
|
-
* reconciles unobservable style writes; a setTimeout fallback keeps passes
|
|
2623
|
-
* flowing when background tabs suspend rAF.
|
|
2624
|
-
*/
|
|
2625
|
-
const STYLE_ID = "dshss-auto-collapse-style";
|
|
2626
|
-
/** Class of the injected per-turn summary row; the follow engine's generalized
|
|
2627
|
-
* surface walk picks it up as an ordinary foreign flow sibling. */
|
|
2628
|
-
const PROCESSED_CLASS = "dshss-processed";
|
|
2629
|
-
/** Inline custom property marking elements whose display this plugin owns. */
|
|
2630
|
-
const DISPLAY_OWNED_PROP = "--dshss-display-owned";
|
|
2631
|
-
/** Unobservable external style writes are reconciled by this audit cadence. */
|
|
2632
|
-
const AUDIT_TICK_MS = 1e3;
|
|
2633
|
-
/** Background-tab fallback that flushes a scheduled pass when rAF is frozen. */
|
|
2634
|
-
const PASS_FALLBACK_MS = 60;
|
|
2635
|
-
/**
|
|
2636
|
-
* Fold motion, deliberately minimal: layout changes land synchronously (the
|
|
2637
|
-
* display toggle happens in the same tick as the decision) and nothing
|
|
2638
|
-
* animates geometry at all — the earlier container-height squeeze was removed
|
|
2639
|
-
* because real sessions showed residual drift after the pin released (other
|
|
2640
|
-
* layout participants keep settling around the fold). What remains is one
|
|
2641
|
-
* paint-only flourish: the summary row fades its opacity in. A fold is
|
|
2642
|
-
* therefore a single reflow followed by a fade — the most stable shape
|
|
2643
|
-
* possible.
|
|
2644
|
-
*/
|
|
2645
|
-
const FOLD_ROW_FADE_MS = 160;
|
|
2646
|
-
const FOLD_EASE_OUT = "cubic-bezier(0.22, 0.61, 0.36, 1)";
|
|
2647
|
-
const ZH_COPY = {
|
|
2648
|
-
processedLabel: "已处理",
|
|
2649
|
-
expandTitle: "展开工作过程",
|
|
2650
|
-
collapseTitle: "收起工作过程",
|
|
2651
|
-
formatDuration: formatZhDuration
|
|
2652
|
-
};
|
|
2653
|
-
const EN_COPY = {
|
|
2654
|
-
processedLabel: "Processed",
|
|
2655
|
-
expandTitle: "Show the work process",
|
|
2656
|
-
collapseTitle: "Hide the work process",
|
|
2657
|
-
formatDuration: formatEnDuration
|
|
2658
|
-
};
|
|
2659
|
-
function resolveFoldCopy() {
|
|
2660
|
-
let lang = "";
|
|
2661
|
-
try {
|
|
2662
|
-
lang = document.documentElement.lang || navigator.language || "";
|
|
2663
|
-
} catch {
|
|
2664
|
-
lang = "";
|
|
2665
|
-
}
|
|
2666
|
-
return lang.toLowerCase().startsWith("zh") ? ZH_COPY : EN_COPY;
|
|
2667
|
-
}
|
|
2668
|
-
const FOLD_CSS = `
|
|
2669
|
-
.${PROCESSED_CLASS} {
|
|
2670
|
-
box-sizing: border-box;
|
|
2671
|
-
display: inline-flex;
|
|
2672
|
-
align-self: flex-start;
|
|
2673
|
-
width: fit-content;
|
|
2674
|
-
max-width: 100%;
|
|
2675
|
-
align-items: center;
|
|
2676
|
-
gap: 6px;
|
|
2677
|
-
padding: 2px 0;
|
|
2678
|
-
border: none;
|
|
2679
|
-
background: none;
|
|
2680
|
-
font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
2681
|
-
color: var(--dsw-alias-label-tertiary, rgba(127, 127, 127, 0.9));
|
|
2682
|
-
cursor: pointer;
|
|
2683
|
-
user-select: none;
|
|
2684
|
-
border-radius: 4px;
|
|
2685
|
-
transition: color 0.15s ease;
|
|
2686
|
-
}
|
|
2687
|
-
.${PROCESSED_CLASS}:hover {
|
|
2688
|
-
color: var(--dsw-alias-label-primary, inherit);
|
|
2689
|
-
background: transparent;
|
|
2690
|
-
}
|
|
2691
|
-
.${PROCESSED_CLASS}:focus-visible {
|
|
2692
|
-
outline: 2px solid var(--dsw-alias-state-focus-ring, rgba(77, 107, 254, 0.8));
|
|
2693
|
-
outline-offset: 2px;
|
|
2694
|
-
}
|
|
2695
|
-
.${PROCESSED_CLASS}-chevron {
|
|
2696
|
-
display: inline-flex;
|
|
2697
|
-
flex: none;
|
|
2698
|
-
width: 14px;
|
|
2699
|
-
height: 14px;
|
|
2700
|
-
opacity: 0.55;
|
|
2701
|
-
transform: rotate(-90deg);
|
|
2702
|
-
transition: transform 0.12s ease, opacity 0.1s ease;
|
|
2703
|
-
}
|
|
2704
|
-
.${PROCESSED_CLASS}:hover .${PROCESSED_CLASS}-chevron {
|
|
2705
|
-
opacity: 0.9;
|
|
2706
|
-
}
|
|
2707
|
-
.${PROCESSED_CLASS}[aria-expanded="true"] .${PROCESSED_CLASS}-chevron {
|
|
2708
|
-
transform: rotate(0deg);
|
|
2709
|
-
}
|
|
2710
|
-
@media (prefers-reduced-motion: reduce) {
|
|
2711
|
-
.${PROCESSED_CLASS},
|
|
2712
|
-
.${PROCESSED_CLASS}-chevron {
|
|
2713
|
-
transition: none;
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
`;
|
|
2717
|
-
var AutoCollapseController = class {
|
|
2718
|
-
observer = null;
|
|
2719
|
-
raf = 0;
|
|
2720
|
-
timer = 0;
|
|
2721
|
-
auditTimer = 0;
|
|
2722
|
-
/** True between start() and stop(); the preference toggle re-enters both. */
|
|
2723
|
-
running = false;
|
|
2724
|
-
lastPassError = "";
|
|
2725
|
-
followDeferredAt = null;
|
|
2726
|
-
flow = null;
|
|
2727
|
-
/** Stable segment key → summary row + gesture state. */
|
|
2728
|
-
segmentStates = /* @__PURE__ */ new Map();
|
|
2729
|
-
/** First sighting of a running segment, for turns without a host duration. */
|
|
2730
|
-
runningSince = /* @__PURE__ */ new Map();
|
|
2731
|
-
/** Segments that settled once: a resumed run restarts local timing. */
|
|
2732
|
-
completedOnce = /* @__PURE__ */ new Set();
|
|
2733
|
-
/** Precise pre-plugin display values, restored on stop/switch/takeover. */
|
|
2734
|
-
originalDisplay = /* @__PURE__ */ new WeakMap();
|
|
2735
|
-
writtenDisplay = /* @__PURE__ */ new WeakMap();
|
|
2736
|
-
controlledDisplay = /* @__PURE__ */ new Set();
|
|
2737
|
-
/** Reply-presence verdicts cached per message until a mutation hits it. */
|
|
2738
|
-
bodyTextCache = /* @__PURE__ */ new WeakMap();
|
|
2739
|
-
dirtyMessages = /* @__PURE__ */ new Set();
|
|
2740
|
-
onVisibilityChange = () => {
|
|
2741
|
-
if (typeof document === "undefined" || document.hidden !== true) this.schedule();
|
|
2742
|
-
};
|
|
2743
|
-
/** Re-run a pass immediately (settings changes, diagnostics, tests).
|
|
2744
|
-
* Reply-presence verdicts are dropped wholesale: manual refreshes are rare,
|
|
2745
|
-
* and a conservative rebuild beats a stale classification. */
|
|
2746
|
-
refresh() {
|
|
2747
|
-
this.bodyTextCache = /* @__PURE__ */ new WeakMap();
|
|
2748
|
-
this.dirtyMessages.clear();
|
|
2749
|
-
this.schedule();
|
|
2750
|
-
}
|
|
2751
|
-
start() {
|
|
2752
|
-
if (this.running) return;
|
|
2753
|
-
this.running = true;
|
|
2754
|
-
injectStyle();
|
|
2755
|
-
try {
|
|
2756
|
-
this.observer = new MutationObserver((records) => {
|
|
2757
|
-
if (this.shouldSchedule(records)) {
|
|
2758
|
-
this.markDirty(records);
|
|
2759
|
-
this.schedule();
|
|
2760
|
-
}
|
|
2761
|
-
});
|
|
2762
|
-
this.observer.observe(document.body, {
|
|
2763
|
-
childList: true,
|
|
2764
|
-
subtree: true,
|
|
2765
|
-
attributes: true,
|
|
2766
|
-
attributeFilter: ["data-state"],
|
|
2767
|
-
characterData: true
|
|
2768
|
-
});
|
|
2769
|
-
this.armAuditLoop();
|
|
2770
|
-
this.schedule();
|
|
2771
|
-
} catch (error) {
|
|
2772
|
-
this.reportError(error);
|
|
2773
|
-
throw error;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
stop() {
|
|
2777
|
-
this.running = false;
|
|
2778
|
-
this.followDeferredAt = null;
|
|
2779
|
-
if (this.raf !== 0) cancelAnimationFrame(this.raf);
|
|
2780
|
-
this.raf = 0;
|
|
2781
|
-
if (this.timer !== 0) clearTimeout(this.timer);
|
|
2782
|
-
this.timer = 0;
|
|
2783
|
-
if (this.auditTimer !== 0) clearTimeout(this.auditTimer);
|
|
2784
|
-
this.auditTimer = 0;
|
|
2785
|
-
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
2786
|
-
this.observer?.disconnect();
|
|
2787
|
-
this.observer = null;
|
|
2788
|
-
this.switchFlow(null);
|
|
2789
|
-
removeStyle();
|
|
2790
|
-
}
|
|
2791
|
-
/** Low-frequency reconciliation for style writes no observer can see. */
|
|
2792
|
-
armAuditLoop() {
|
|
2793
|
-
if (typeof document !== "undefined" && typeof document.addEventListener === "function") document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
2794
|
-
this.rearmAudit();
|
|
2795
|
-
}
|
|
2796
|
-
rearmAudit() {
|
|
2797
|
-
if (!this.running || this.auditTimer !== 0) return;
|
|
2798
|
-
this.auditTimer = setTimeout(() => {
|
|
2799
|
-
this.auditTimer = 0;
|
|
2800
|
-
if (!this.running) return;
|
|
2801
|
-
if (typeof document !== "undefined" && document.hidden === true) {
|
|
2802
|
-
this.rearmAudit();
|
|
2803
|
-
return;
|
|
2804
|
-
}
|
|
2805
|
-
this.schedule();
|
|
2806
|
-
this.rearmAudit();
|
|
2807
|
-
}, AUDIT_TICK_MS);
|
|
2808
|
-
}
|
|
2809
|
-
/** Only mutations inside the active flow can affect collapse geometry. */
|
|
2810
|
-
shouldSchedule(records) {
|
|
2811
|
-
if (records.length === 0 || this.flow === null || !this.flow.isConnected) return true;
|
|
2812
|
-
const flow = this.flow;
|
|
2813
|
-
return records.some((record) => {
|
|
2814
|
-
const changed = [...record.addedNodes, ...record.removedNodes];
|
|
2815
|
-
if (changed.length > 0 && changed.every((node) => isFloatingOverlayNode(node))) return false;
|
|
2816
|
-
if (record.target === flow || nodeWithin(record.target, flow)) return true;
|
|
2817
|
-
return changed.some((node) => nodeWithin(node, flow));
|
|
2818
|
-
});
|
|
2819
|
-
}
|
|
2820
|
-
/** Walk each record to its flow child so reply-cache invalidation stays
|
|
2821
|
-
* targeted; anything unattributable invalidates conservatively. */
|
|
2822
|
-
markDirty(records) {
|
|
2823
|
-
const flow = this.flow;
|
|
2824
|
-
if (flow === null || !flow.isConnected) return;
|
|
2825
|
-
if (records.length === 0) {
|
|
2826
|
-
this.bodyTextCache = /* @__PURE__ */ new WeakMap();
|
|
2827
|
-
this.dirtyMessages.clear();
|
|
2828
|
-
return;
|
|
2829
|
-
}
|
|
2830
|
-
for (const record of records) {
|
|
2831
|
-
let current = record.target;
|
|
2832
|
-
while (current !== null && current.parentNode !== flow) current = current.parentNode;
|
|
2833
|
-
if (!(current instanceof HTMLElement)) {
|
|
2834
|
-
this.bodyTextCache = /* @__PURE__ */ new WeakMap();
|
|
2835
|
-
this.dirtyMessages.clear();
|
|
2836
|
-
return;
|
|
2837
|
-
}
|
|
2838
|
-
this.dirtyMessages.add(current);
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
schedule() {
|
|
2842
|
-
if (!this.running || this.raf !== 0) return;
|
|
2843
|
-
this.raf = requestAnimationFrame(() => {
|
|
2844
|
-
this.raf = 0;
|
|
2845
|
-
if (this.timer !== 0) {
|
|
2846
|
-
clearTimeout(this.timer);
|
|
2847
|
-
this.timer = 0;
|
|
2848
|
-
}
|
|
2849
|
-
this.runPass();
|
|
2850
|
-
});
|
|
2851
|
-
if (this.timer !== 0) clearTimeout(this.timer);
|
|
2852
|
-
this.timer = setTimeout(() => {
|
|
2853
|
-
this.timer = 0;
|
|
2854
|
-
if (this.raf !== 0) {
|
|
2855
|
-
cancelAnimationFrame(this.raf);
|
|
2856
|
-
this.raf = 0;
|
|
2857
|
-
this.runPass();
|
|
2858
|
-
}
|
|
2859
|
-
}, PASS_FALLBACK_MS);
|
|
2860
|
-
}
|
|
2861
|
-
runPass() {
|
|
2862
|
-
try {
|
|
2863
|
-
this.pass();
|
|
2864
|
-
this.lastPassError = "";
|
|
2865
|
-
} catch (error) {
|
|
2866
|
-
this.reportError(error);
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
reportError(error) {
|
|
2870
|
-
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2871
|
-
if (message === this.lastPassError) return;
|
|
2872
|
-
this.lastPassError = message;
|
|
2873
|
-
console.error("[dsh-smooth-stream] auto-collapse pass failed", error);
|
|
2874
|
-
}
|
|
2875
|
-
pass() {
|
|
2876
|
-
if (!this.running) return;
|
|
2877
|
-
const nextFlow = findFlow();
|
|
2878
|
-
if (nextFlow !== this.flow) this.switchFlow(nextFlow);
|
|
2879
|
-
const flow = this.flow;
|
|
2880
|
-
if (flow === null) return;
|
|
2881
|
-
for (const el of this.dirtyMessages) this.bodyTextCache.delete(el);
|
|
2882
|
-
this.dirtyMessages.clear();
|
|
2883
|
-
const segments = buildSegments(flow, (el) => this.hasBodyCached(el));
|
|
2884
|
-
const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
|
|
2885
|
-
for (const segment of segments) {
|
|
2886
|
-
if (!segment.running) continue;
|
|
2887
|
-
if (this.completedOnce.has(segment.key)) {
|
|
2888
|
-
this.completedOnce.delete(segment.key);
|
|
2889
|
-
this.runningSince.delete(segment.key);
|
|
2890
|
-
}
|
|
2891
|
-
if (!this.runningSince.has(segment.key)) this.runningSince.set(segment.key, Date.now());
|
|
2892
|
-
}
|
|
2893
|
-
const completedKeys = /* @__PURE__ */ new Set();
|
|
2894
|
-
for (const segment of segments) {
|
|
2895
|
-
if (!segment.closed || segment.running || !segment.hasWork) continue;
|
|
2896
|
-
completedKeys.add(segment.key);
|
|
2897
|
-
this.completedOnce.add(segment.key);
|
|
2898
|
-
let state = this.segmentStates.get(segment.key);
|
|
2899
|
-
if (state === void 0) {
|
|
2900
|
-
state = {
|
|
2901
|
-
key: segment.key,
|
|
2902
|
-
row: null,
|
|
2903
|
-
expanded: false,
|
|
2904
|
-
snapshot: segment
|
|
2905
|
-
};
|
|
2906
|
-
this.segmentStates.set(segment.key, state);
|
|
2907
|
-
} else state.snapshot = segment;
|
|
2908
|
-
const started = this.runningSince.get(segment.key);
|
|
2909
|
-
const parsed = segment.boundary === null ? void 0 : parseTurnDuration(segment.boundary);
|
|
2910
|
-
if (parsed !== void 0) state.duration = parsed;
|
|
2911
|
-
else if (state.duration === void 0 && started !== void 0) state.duration = Date.now() - started;
|
|
2912
|
-
if (state.row === null || !state.row.isConnected) state.row = this.createProcessedRow(state);
|
|
2913
|
-
this.syncProcessedRow(state);
|
|
2914
|
-
}
|
|
2915
|
-
const portNow = flow.closest("[data-conversation-scroll]");
|
|
2916
|
-
if (completedKeys.size > 0 && portNow !== null && hasRecentConversationFollow(portNow, 140)) {
|
|
2917
|
-
const now = Date.now();
|
|
2918
|
-
this.followDeferredAt ??= now;
|
|
2919
|
-
if (now - this.followDeferredAt < 2e3) {
|
|
2920
|
-
this.schedule();
|
|
2921
|
-
return;
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
this.followDeferredAt = null;
|
|
2925
|
-
const port = flow.closest("[data-conversation-scroll]");
|
|
2926
|
-
const beforeFloor = Math.max(0, (port?.scrollHeight ?? 0) - (port?.clientHeight ?? 0));
|
|
2927
|
-
const pinnedToFloor = port !== null && beforeFloor - port.scrollTop <= 30;
|
|
2928
|
-
const anchorBefore = lastVisibleAnchorTop(flow);
|
|
2929
|
-
const applyMutations = () => {
|
|
2930
|
-
for (const [key, state] of [...this.segmentStates]) {
|
|
2931
|
-
if (completedKeys.has(key)) continue;
|
|
2932
|
-
state.row?.remove();
|
|
2933
|
-
this.segmentStates.delete(key);
|
|
2934
|
-
}
|
|
2935
|
-
const desiredHidden = /* @__PURE__ */ new Set();
|
|
2936
|
-
for (const segment of segments) {
|
|
2937
|
-
const state = this.segmentStates.get(segment.key);
|
|
2938
|
-
if (state !== void 0 && !state.expanded) {
|
|
2939
|
-
for (const seat of segment.hideSeats) this.hideElement(seat, desiredHidden);
|
|
2940
|
-
for (const row of segment.finalThinkRows) this.hideElement(row, desiredHidden);
|
|
2941
|
-
if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
|
|
2942
|
-
} else {
|
|
2943
|
-
for (const seat of segment.hideSeats) this.restoreElement(seat);
|
|
2944
|
-
for (const row of segment.finalThinkRows) this.restoreElement(row);
|
|
2945
|
-
if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
for (const segment of segments) {
|
|
2949
|
-
if (!hasVisibleSegmentWork(segment)) {
|
|
2950
|
-
const state = this.segmentStates.get(segment.key);
|
|
2951
|
-
if (state !== void 0 && state.row !== null) {
|
|
2952
|
-
state.row.remove();
|
|
2953
|
-
state.row = null;
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
for (const seat of segment.hideSeats) this.retainDisplayControl(seat, desiredHidden);
|
|
2957
|
-
for (const row of segment.finalThinkRows) this.retainDisplayControl(row, desiredHidden);
|
|
2958
|
-
if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
|
|
2959
|
-
}
|
|
2960
|
-
this.restoreUnusedDisplays(desiredHidden);
|
|
2961
|
-
for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
|
|
2962
|
-
};
|
|
2963
|
-
applyMutations();
|
|
2964
|
-
if (port !== null && pinnedToFloor && anchorBefore !== null) {
|
|
2965
|
-
const anchorAfter = lastVisibleAnchorTop(flow);
|
|
2966
|
-
if (anchorAfter !== null) {
|
|
2967
|
-
const delta = anchorAfter - anchorBefore;
|
|
2968
|
-
if (Math.abs(delta) > .5) port.scrollTop += delta;
|
|
2969
|
-
}
|
|
2970
|
-
}
|
|
2971
|
-
for (const key of [...this.runningSince.keys()]) if (!liveSegmentKeys.has(key)) this.runningSince.delete(key);
|
|
2972
|
-
for (const key of [...this.completedOnce]) if (!liveSegmentKeys.has(key)) this.completedOnce.delete(key);
|
|
2973
|
-
}
|
|
2974
|
-
/** Flow swap means a session switch: fully restore the old tree, rebuild. */
|
|
2975
|
-
switchFlow(next) {
|
|
2976
|
-
if (next === this.flow) return;
|
|
2977
|
-
for (const state of this.segmentStates.values()) state.row?.remove();
|
|
2978
|
-
this.segmentStates.clear();
|
|
2979
|
-
this.runningSince.clear();
|
|
2980
|
-
this.completedOnce.clear();
|
|
2981
|
-
this.bodyTextCache = /* @__PURE__ */ new WeakMap();
|
|
2982
|
-
this.dirtyMessages.clear();
|
|
2983
|
-
this.restoreAllDisplays();
|
|
2984
|
-
this.flow = next;
|
|
2985
|
-
}
|
|
2986
|
-
/** Keep already-controlled elements in the desired-hidden set so the sweep
|
|
2987
|
-
* does not flip them back mid-flight of an external change. */
|
|
2988
|
-
retainDisplayControl(el, desiredHidden) {
|
|
2989
|
-
if (this.controlledDisplay.has(el)) desiredHidden.add(el);
|
|
2990
|
-
}
|
|
2991
|
-
/** Restore every element the ledger controls that this pass no longer wants
|
|
2992
|
-
* hidden; elements absent from `desired` come back to their true display. */
|
|
2993
|
-
restoreUnusedDisplays(desired) {
|
|
2994
|
-
for (const el of [...this.controlledDisplay]) if (!desired.has(el)) this.restoreElement(el);
|
|
2995
|
-
}
|
|
2996
|
-
/** Restore every element still under the display ledger, then retire the
|
|
2997
|
-
* ledger itself (stop / session switch). */
|
|
2998
|
-
restoreAllDisplays() {
|
|
2999
|
-
for (const el of [...this.controlledDisplay]) this.restoreElement(el);
|
|
3000
|
-
this.controlledDisplay.clear();
|
|
3001
|
-
this.originalDisplay = /* @__PURE__ */ new WeakMap();
|
|
3002
|
-
this.writtenDisplay = /* @__PURE__ */ new WeakMap();
|
|
3003
|
-
}
|
|
3004
|
-
createProcessedRow(state) {
|
|
3005
|
-
const row = createProcessedRowElement(state.duration);
|
|
3006
|
-
row.addEventListener("click", () => {
|
|
3007
|
-
state.expanded = !state.expanded;
|
|
3008
|
-
this.syncProcessedRow(state);
|
|
3009
|
-
this.schedule();
|
|
3010
|
-
});
|
|
3011
|
-
return row;
|
|
3012
|
-
}
|
|
3013
|
-
syncProcessedRow(state) {
|
|
3014
|
-
const row = state.row;
|
|
3015
|
-
if (row === null) return;
|
|
3016
|
-
const copy = resolveFoldCopy();
|
|
3017
|
-
const text = row.firstElementChild;
|
|
3018
|
-
const label = state.duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(state.duration)}`;
|
|
3019
|
-
if (text !== null && text.textContent !== label) text.textContent = label;
|
|
3020
|
-
const expanded = String(state.expanded);
|
|
3021
|
-
if (row.getAttribute("aria-expanded") !== expanded) row.setAttribute("aria-expanded", expanded);
|
|
3022
|
-
const title = state.expanded ? copy.collapseTitle : copy.expandTitle;
|
|
3023
|
-
if (row.title !== title) row.title = title;
|
|
3024
|
-
}
|
|
3025
|
-
placeProcessedRow(flow, state) {
|
|
3026
|
-
const row = state.row;
|
|
3027
|
-
if (row === null) return;
|
|
3028
|
-
if (!state.snapshot.hasWork || !hasVisibleSegmentWork(state.snapshot)) {
|
|
3029
|
-
row.remove();
|
|
3030
|
-
state.row = null;
|
|
3031
|
-
return;
|
|
3032
|
-
}
|
|
3033
|
-
let target = state.snapshot.firstWork ?? state.snapshot.finalStep ?? state.snapshot.boundary;
|
|
3034
|
-
if (target === null || target.parentElement !== flow) {
|
|
3035
|
-
row.remove();
|
|
3036
|
-
state.row = null;
|
|
3037
|
-
return;
|
|
3038
|
-
}
|
|
3039
|
-
if (row.parentElement !== flow || row.nextElementSibling !== target) {
|
|
3040
|
-
target.before(row);
|
|
3041
|
-
this.revealVisualOnce(row);
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
|
-
hasBodyCached(el) {
|
|
3045
|
-
const cached = this.bodyTextCache.get(el);
|
|
3046
|
-
if (cached !== void 0) return cached;
|
|
3047
|
-
const value = hasBodyContent(el);
|
|
3048
|
-
this.bodyTextCache.set(el, value);
|
|
3049
|
-
return value;
|
|
3050
|
-
}
|
|
3051
|
-
/** True when the recorded display was changed (or the ownership sentinel
|
|
3052
|
-
* wiped) behind the ledger's back; callers hand control back to reality. */
|
|
3053
|
-
displayForeign(el) {
|
|
3054
|
-
const written = this.writtenDisplay.get(el);
|
|
3055
|
-
if (written === void 0) return false;
|
|
3056
|
-
return el.style.getPropertyValue(DISPLAY_OWNED_PROP) === "" || el.style.display !== written;
|
|
3057
|
-
}
|
|
3058
|
-
releaseDisplayLedger(el) {
|
|
3059
|
-
this.originalDisplay.delete(el);
|
|
3060
|
-
this.writtenDisplay.delete(el);
|
|
3061
|
-
this.controlledDisplay.delete(el);
|
|
3062
|
-
el.style.removeProperty(DISPLAY_OWNED_PROP);
|
|
3063
|
-
}
|
|
3064
|
-
hideElement(el, desired) {
|
|
3065
|
-
desired.add(el);
|
|
3066
|
-
if (!this.originalDisplay.has(el) && !isDisplayed(el)) return;
|
|
3067
|
-
if (!this.originalDisplay.has(el) || this.displayForeign(el)) {
|
|
3068
|
-
this.originalDisplay.set(el, el.style.display);
|
|
3069
|
-
this.writtenDisplay.set(el, el.style.display);
|
|
3070
|
-
el.style.setProperty(DISPLAY_OWNED_PROP, "1");
|
|
3071
|
-
}
|
|
3072
|
-
this.controlledDisplay.add(el);
|
|
3073
|
-
if (el.style.display === "none") return;
|
|
3074
|
-
el.style.display = "none";
|
|
3075
|
-
this.writtenDisplay.set(el, "none");
|
|
3076
|
-
}
|
|
3077
|
-
restoreElement(el) {
|
|
3078
|
-
if (!this.originalDisplay.has(el)) return;
|
|
3079
|
-
if (this.displayForeign(el)) {
|
|
3080
|
-
this.releaseDisplayLedger(el);
|
|
3081
|
-
return;
|
|
3082
|
-
}
|
|
3083
|
-
const original = this.originalDisplay.get(el);
|
|
3084
|
-
if (el.style.display !== original) el.style.display = original;
|
|
3085
|
-
this.releaseDisplayLedger(el);
|
|
3086
|
-
}
|
|
3087
|
-
/** Whether WAAPI exists and the user has not asked for reduced motion. */
|
|
3088
|
-
canAnimate(el) {
|
|
3089
|
-
if (typeof el.animate !== "function") return false;
|
|
3090
|
-
try {
|
|
3091
|
-
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
|
|
3092
|
-
} catch {
|
|
3093
|
-
return false;
|
|
3094
|
-
}
|
|
3095
|
-
return true;
|
|
3096
|
-
}
|
|
3097
|
-
/** First-appearance flourish for the plugin-owned summary row: a pure
|
|
3098
|
-
* opacity fade. Paint-only, so it cannot shift anyone's layout. */
|
|
3099
|
-
revealVisualOnce(row) {
|
|
3100
|
-
if (row.dataset.dshssShown === "1") return;
|
|
3101
|
-
row.dataset.dshssShown = "1";
|
|
3102
|
-
if (!this.canAnimate(row)) return;
|
|
3103
|
-
row.animate([{ opacity: "0" }, { opacity: "1" }], {
|
|
3104
|
-
duration: FOLD_ROW_FADE_MS,
|
|
3105
|
-
easing: FOLD_EASE_OUT
|
|
3106
|
-
});
|
|
3107
|
-
}
|
|
3108
|
-
};
|
|
3109
|
-
/** Visible chat flow container, falling back to the first one rendered. */
|
|
3110
|
-
function findFlow() {
|
|
3111
|
-
const flows = document.querySelectorAll("[data-chat-flow]");
|
|
3112
|
-
for (const flow of flows) if (flow.offsetParent !== null || flow.getBoundingClientRect().width > 0) return flow;
|
|
3113
|
-
return flows[0] ?? null;
|
|
3114
|
-
}
|
|
3115
|
-
/** parentNode walk that also accepts Text mutation targets. */
|
|
3116
|
-
function nodeWithin(node, ancestor) {
|
|
3117
|
-
for (let current = node; current !== null; current = current.parentNode) if (current === ancestor) return true;
|
|
3118
|
-
return false;
|
|
3119
|
-
}
|
|
3120
|
-
/**
|
|
3121
|
-
* Whether a mutation-added/removed node is a `position: fixed` overlay (the
|
|
3122
|
-
* harness Tooltip bubble / a popover) that never affects the flow's collapse
|
|
3123
|
-
* geometry. The primitives Tooltip mounts its fixed bubble as a Fragment
|
|
3124
|
-
* sibling inside the message row — `position: fixed` arrives from its CSS
|
|
3125
|
-
* module class, not inline style — so it appears within `[data-chat-flow]`
|
|
3126
|
-
* even though it is viewport-fixed and layout-neutral. Matching on the
|
|
3127
|
-
* tooltip's structural role instead of computed style keeps the filter
|
|
3128
|
-
* reliable before the node has been laid out.
|
|
3129
|
-
*/
|
|
3130
|
-
function isFloatingOverlayNode(node) {
|
|
3131
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
3132
|
-
if (node.getAttribute("role") === "tooltip") return true;
|
|
3133
|
-
if (node.style.position === "fixed") return true;
|
|
3134
|
-
if (node.style.position === "absolute" && node.parentElement?.getAttribute("role") === "tooltip") return true;
|
|
3135
|
-
return false;
|
|
3136
|
-
}
|
|
3137
|
-
/** Top-level message order, excluding this plugin's own summary rows. */
|
|
3138
|
-
function flowItems(flow) {
|
|
3139
|
-
return [...flow.children].filter((el) => el instanceof HTMLElement && !el.classList.contains(PROCESSED_CLASS));
|
|
3140
|
-
}
|
|
3141
|
-
function isDisplayed(el) {
|
|
3142
|
-
if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
|
|
3143
|
-
return el.style.display !== "none";
|
|
3144
|
-
}
|
|
3145
|
-
function stableElementKey(el, fallbackIndex) {
|
|
3146
|
-
const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
|
|
3147
|
-
return `${kind}:${el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`}`;
|
|
3148
|
-
}
|
|
3149
|
-
/** Kinds that can open a history stretch lacking an explicit user marker. */
|
|
3150
|
-
function opensTurnWork(el) {
|
|
3151
|
-
const kind = el.getAttribute("data-chat-flow-kind");
|
|
3152
|
-
return kind === "assistant-step" || kind === "assistant" || kind === "tool-call" || kind === "command" || kind === "manual-compaction";
|
|
3153
|
-
}
|
|
3154
|
-
/**
|
|
3155
|
-
* Split the flow into turn segments. `user`/`steering` rows close the previous
|
|
3156
|
-
* segment and open the next; `turn-tail` closes and resets the opener. Leading
|
|
3157
|
-
* context before the first user row joins that user's segment, matching how
|
|
3158
|
-
* the host presents context injection.
|
|
3159
|
-
*/
|
|
3160
|
-
function buildSegments(flow, hasBody) {
|
|
3161
|
-
const items = flowItems(flow);
|
|
3162
|
-
const itemIndex = new Map(items.map((el, index) => [el, index]));
|
|
3163
|
-
const snapshots = [];
|
|
3164
|
-
let contentStart = 0;
|
|
3165
|
-
let startMarker = null;
|
|
3166
|
-
const append = (end, boundary, closed) => {
|
|
3167
|
-
if (end < contentStart) return;
|
|
3168
|
-
const range = items.slice(contentStart, end);
|
|
3169
|
-
const hideSeats = /* @__PURE__ */ new Set();
|
|
3170
|
-
const bodySteps = [];
|
|
3171
|
-
let running = false;
|
|
3172
|
-
for (const el of range) {
|
|
3173
|
-
const classified = classifyItem(el, hasBody);
|
|
3174
|
-
if (classified.fold === "body") bodySteps.push(el);
|
|
3175
|
-
else if (classified.fold === "seat") hideSeats.add(el);
|
|
3176
|
-
if (classified.running) running = true;
|
|
3177
|
-
}
|
|
3178
|
-
const finalStep = bodySteps.at(-1) ?? null;
|
|
3179
|
-
const middleSteps = new Set(bodySteps.slice(0, -1));
|
|
3180
|
-
for (const step of middleSteps) hideSeats.add(step);
|
|
3181
|
-
const finalThinkRows = finalStep === null ? [] : thinkRowsIn(finalStep);
|
|
3182
|
-
const firstWork = range.find((el) => hideSeats.has(el)) ?? finalStep;
|
|
3183
|
-
const identity = startMarker ?? range.find((el) => opensTurnWork(el)) ?? boundary;
|
|
3184
|
-
const identityIndex = identity === null ? contentStart : itemIndex.get(identity) ?? contentStart;
|
|
3185
|
-
const key = `${startMarker === null ? "leading" : "segment"}:${identity === null ? `open:${contentStart}` : stableElementKey(identity, identityIndex)}`;
|
|
3186
|
-
snapshots.push({
|
|
3187
|
-
key,
|
|
3188
|
-
boundary,
|
|
3189
|
-
startMarker,
|
|
3190
|
-
hideSeats,
|
|
3191
|
-
middleSteps,
|
|
3192
|
-
finalStep,
|
|
3193
|
-
finalThinkRows,
|
|
3194
|
-
firstWork,
|
|
3195
|
-
closed,
|
|
3196
|
-
running,
|
|
3197
|
-
hasWork: hideSeats.size > 0 || finalThinkRows.length > 0
|
|
3198
|
-
});
|
|
3199
|
-
};
|
|
3200
|
-
items.forEach((el, index) => {
|
|
3201
|
-
const kind = el.getAttribute("data-chat-flow-kind");
|
|
3202
|
-
if (kind === "user" || kind === "steering") {
|
|
3203
|
-
if (startMarker !== null) {
|
|
3204
|
-
append(index, el, true);
|
|
3205
|
-
contentStart = index + 1;
|
|
3206
|
-
} else if (items.slice(contentStart, index).some((el) => opensTurnWork(el))) {
|
|
3207
|
-
append(index, el, true);
|
|
3208
|
-
contentStart = index + 1;
|
|
3209
|
-
}
|
|
3210
|
-
startMarker = el;
|
|
3211
|
-
return;
|
|
3212
|
-
}
|
|
3213
|
-
if (kind === "turn-tail") {
|
|
3214
|
-
append(index, el, true);
|
|
3215
|
-
contentStart = index + 1;
|
|
3216
|
-
startMarker = null;
|
|
3217
|
-
}
|
|
3218
|
-
});
|
|
3219
|
-
if (contentStart < items.length) append(items.length, null, false);
|
|
3220
|
-
return snapshots;
|
|
3221
|
-
}
|
|
3222
|
-
/**
|
|
3223
|
-
* Classify one top-level flow item against the Harness's own node vocabulary.
|
|
3224
|
-
* Reply-bearing assistant messages become `body` candidates (last one per
|
|
3225
|
-
* segment wins as the final answer); core work process — tool-call seats,
|
|
3226
|
-
* command/compaction cards, context injection, thinking-only messages —
|
|
3227
|
-
* becomes a hideable `seat`. Everything else is skipped untouched: boundaries,
|
|
3228
|
-
* empty decorations, and crucially any kind this module does not own, so rows
|
|
3229
|
-
* contributed by other plugins' slots are never folded or hidden.
|
|
3230
|
-
*/
|
|
3231
|
-
function classifyItem(el, hasBody) {
|
|
3232
|
-
const kind = el.getAttribute("data-chat-flow-kind");
|
|
3233
|
-
if (kind === "user" || kind === "steering" || kind === "turn-tail") return {
|
|
3234
|
-
fold: "skip",
|
|
3235
|
-
running: false
|
|
3236
|
-
};
|
|
3237
|
-
if (kind === "assistant-step" || kind === "assistant") {
|
|
3238
|
-
if (hasBody(el)) return {
|
|
3239
|
-
fold: "body",
|
|
3240
|
-
running: subtreeRunning(el)
|
|
3241
|
-
};
|
|
3242
|
-
if (thinkRowsIn(el).length > 0) return {
|
|
3243
|
-
fold: "seat",
|
|
3244
|
-
running: subtreeRunning(el)
|
|
3245
|
-
};
|
|
3246
|
-
return {
|
|
3247
|
-
fold: "skip",
|
|
3248
|
-
running: false
|
|
3249
|
-
};
|
|
3250
|
-
}
|
|
3251
|
-
if (kind === "tool-call") {
|
|
3252
|
-
const rows = topCallRowsIn(el);
|
|
3253
|
-
if (rows.length === 0 || !rows.every((row) => hasNativeCardChrome(row))) return {
|
|
3254
|
-
fold: "skip",
|
|
3255
|
-
running: subtreeRunning(el)
|
|
3256
|
-
};
|
|
3257
|
-
return {
|
|
3258
|
-
fold: "seat",
|
|
3259
|
-
running: subtreeRunning(el)
|
|
3260
|
-
};
|
|
3261
|
-
}
|
|
3262
|
-
if (kind === "context" || kind === "command" || kind === "manual-compaction") return {
|
|
3263
|
-
fold: "seat",
|
|
3264
|
-
running: subtreeRunning(el)
|
|
3265
|
-
};
|
|
3266
|
-
return {
|
|
3267
|
-
fold: "skip",
|
|
3268
|
-
running: false
|
|
3269
|
-
};
|
|
3270
|
-
}
|
|
3271
|
-
/** Native work-card shape: every core card family stamps `data-variant`
|
|
3272
|
-
* together with `data-state`, whether composed through ToolRow or hand-rolled
|
|
3273
|
-
* (Bash). Custom toolviews use their own namespaced attributes instead. */
|
|
3274
|
-
function hasNativeCardChrome(row) {
|
|
3275
|
-
return row.querySelector("[data-variant][data-state]") !== null;
|
|
3276
|
-
}
|
|
3277
|
-
/** Whether anything inside the item reports a running lifecycle. Deliberately
|
|
3278
|
-
* broad: staying "running" too long only delays a collapse, collapsing early
|
|
3279
|
-
* would cut off live output. Native cards stamp `data-state` next to
|
|
3280
|
-
* `data-tool`/`data-variant`; custom views use their own vocabulary and are
|
|
3281
|
-
* intentionally invisible here. */
|
|
3282
|
-
function subtreeRunning(el) {
|
|
3283
|
-
if (el.getAttribute("data-state") === "running") return true;
|
|
3284
|
-
for (const root of el.querySelectorAll("[data-tool][data-state], [data-variant][data-state]")) if (root.getAttribute("data-state") === "running") return true;
|
|
3285
|
-
return false;
|
|
3286
|
-
}
|
|
3287
|
-
function hasVisibleSegmentWork(segment) {
|
|
3288
|
-
const candidates = [...segment.hideSeats, ...segment.middleSteps];
|
|
3289
|
-
if (segment.startMarker !== null) candidates.push(segment.startMarker);
|
|
3290
|
-
if (segment.finalStep !== null) candidates.push(segment.finalStep);
|
|
3291
|
-
return candidates.some(isDisplayed);
|
|
3292
|
-
}
|
|
3293
|
-
/** Top-level tool card rows of a seat (excluding sub-dispatch nests). */
|
|
3294
|
-
function topCallRowsIn(el) {
|
|
3295
|
-
const rows = [];
|
|
3296
|
-
for (const row of el.querySelectorAll("[data-chat-call-id]")) {
|
|
3297
|
-
if (row.closest("[data-subcalls]") !== null) continue;
|
|
3298
|
-
if (row.closest("[data-chat-call-id]") !== row) continue;
|
|
3299
|
-
rows.push(row);
|
|
3300
|
-
}
|
|
3301
|
-
return rows;
|
|
3302
|
-
}
|
|
3303
|
-
/** Reasoning rows of a message: native think variants outside tool cards. */
|
|
3304
|
-
function thinkRowsIn(el) {
|
|
3305
|
-
const rows = [];
|
|
3306
|
-
for (const row of el.querySelectorAll("[data-variant=\"think\"]:not([data-tool])")) {
|
|
3307
|
-
if (row.closest("[data-chat-call-id]") !== null) continue;
|
|
3308
|
-
if (row.closest("[data-subcalls]") !== null) continue;
|
|
3309
|
-
rows.push(row);
|
|
3310
|
-
}
|
|
3311
|
-
return rows;
|
|
3312
|
-
}
|
|
3313
|
-
/**
|
|
3314
|
-
* Reply detection beyond reasoning rows and tool cards: any non-empty text or
|
|
3315
|
-
* media counts, because Markdown paragraphs necessarily carry text nodes.
|
|
3316
|
-
* Rendered markdown class names are build-time hashes, so structure-based
|
|
3317
|
-
* detection is the only stable contract.
|
|
3318
|
-
*/
|
|
3319
|
-
function hasBodyContent(el) {
|
|
3320
|
-
const kind = el.getAttribute("data-chat-flow-kind");
|
|
3321
|
-
if (kind === "command" || kind === "manual-compaction") return false;
|
|
3322
|
-
if (hasBodyText(el)) return true;
|
|
3323
|
-
const excluded = "[data-variant=\"think\"], [data-chat-call-id]";
|
|
3324
|
-
for (const media of el.querySelectorAll("img, video, audio, canvas")) if (media.closest(excluded) === null) return true;
|
|
3325
|
-
return false;
|
|
3326
|
-
}
|
|
3327
|
-
function hasBodyText(el) {
|
|
3328
|
-
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
3329
|
-
let node;
|
|
3330
|
-
while ((node = walker.nextNode()) !== null) {
|
|
3331
|
-
if (node.data.trim() === "") continue;
|
|
3332
|
-
const parent = node.parentElement;
|
|
3333
|
-
if (parent !== null && parent.closest("[data-variant=\"think\"], [data-chat-call-id], [data-variant=\"others\"][data-state]") !== null) continue;
|
|
3334
|
-
return true;
|
|
3335
|
-
}
|
|
3336
|
-
return false;
|
|
3337
|
-
}
|
|
3338
|
-
/**
|
|
3339
|
-
* Official turn duration from the closing element: localized tails read
|
|
3340
|
-
* `用时 33秒` / `用时 2分05秒` or `Ran for 33s` / `Ran for 2m 05s`; newer
|
|
3341
|
-
* tails only carry an end timestamp, which is diffed against the opening user
|
|
3342
|
-
* row's timestamp.
|
|
3343
|
-
*/
|
|
3344
|
-
/**
|
|
3345
|
-
* Viewport-top of the last visible anchored message row, or null when none is
|
|
3346
|
-
* rendered. Used to pin the reader's visual anchor across a fold commit.
|
|
3347
|
-
*/
|
|
3348
|
-
function lastVisibleAnchorTop(flow) {
|
|
3349
|
-
const rows = flow.querySelectorAll("[data-chat-anchor-key]");
|
|
3350
|
-
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
3351
|
-
const row = rows[index];
|
|
3352
|
-
if (row.getClientRects().length === 0) continue;
|
|
3353
|
-
return row.getBoundingClientRect().top;
|
|
3354
|
-
}
|
|
3355
|
-
return null;
|
|
3356
|
-
}
|
|
3357
|
-
function parseTurnDuration(boundary) {
|
|
3358
|
-
const text = boundary.textContent ?? "";
|
|
3359
|
-
const zh = text.match(/用时\s*(\d+)分(\d+)秒|用时\s*(\d+)秒/);
|
|
3360
|
-
if (zh !== null) {
|
|
3361
|
-
if (zh[1] !== void 0 && zh[2] !== void 0) return Number(zh[1]) * 6e4 + Number(zh[2]) * 1e3;
|
|
3362
|
-
if (zh[3] !== void 0) return Number(zh[3]) * 1e3;
|
|
3363
|
-
return;
|
|
3364
|
-
}
|
|
3365
|
-
const en = text.match(/Ran for\s*(?:(\d+)m\s*)?(\d+)s/);
|
|
3366
|
-
if (en !== null) return ((en[1] === void 0 ? 0 : Number(en[1])) * 60 + Number(en[2])) * 1e3;
|
|
3367
|
-
const end = parseTimeText(text);
|
|
3368
|
-
const start = findTurnStart(boundary);
|
|
3369
|
-
if (end !== void 0 && start !== void 0 && end > start) return end - start;
|
|
3370
|
-
}
|
|
3371
|
-
/** DSH timestamp text (`8月14日 21:56` / `2026年8月14日 22:11`) → epoch ms. */
|
|
3372
|
-
function parseTimeText(text) {
|
|
3373
|
-
const m = text.match(/(?:(\d{4})年)?(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})/);
|
|
3374
|
-
if (m === null) return void 0;
|
|
3375
|
-
const year = m[1] !== void 0 ? Number(m[1]) : (/* @__PURE__ */ new Date()).getFullYear();
|
|
3376
|
-
const t = new Date(year, Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5])).getTime();
|
|
3377
|
-
return Number.isNaN(t) ? void 0 : t;
|
|
3378
|
-
}
|
|
3379
|
-
/** Latest turn-opening timestamp at or before the boundary element. */
|
|
3380
|
-
function findTurnStart(boundary) {
|
|
3381
|
-
const flow = boundary.parentElement;
|
|
3382
|
-
if (flow === null) return void 0;
|
|
3383
|
-
let best = null;
|
|
3384
|
-
for (const s of flow.querySelectorAll("[class*=\"timeStart\"]")) {
|
|
3385
|
-
const pos = s.compareDocumentPosition(boundary);
|
|
3386
|
-
if ((pos & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 || (pos & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0 || s === boundary) best = s;
|
|
3387
|
-
else break;
|
|
3388
|
-
}
|
|
3389
|
-
if (best === null) return void 0;
|
|
3390
|
-
return parseTimeText(best.textContent ?? "");
|
|
3391
|
-
}
|
|
3392
|
-
/** Compact duration pieces shared by both locales: seconds under a minute,
|
|
3393
|
-
* whole minutes drop the seconds, hours drop to minute granularity. */
|
|
3394
|
-
function durationParts(ms) {
|
|
3395
|
-
const s = Math.round(ms / 1e3);
|
|
3396
|
-
return {
|
|
3397
|
-
h: Math.floor(s / 3600),
|
|
3398
|
-
m: Math.floor(s % 3600 / 60),
|
|
3399
|
-
s: s % 60
|
|
3400
|
-
};
|
|
3401
|
-
}
|
|
3402
|
-
/** 毫秒 → 中文紧凑时长(14秒 / 2分05秒 / 15分 / 3小时2分)。 */
|
|
3403
|
-
function formatZhDuration(ms) {
|
|
3404
|
-
const { h, m, s } = durationParts(ms);
|
|
3405
|
-
if (h > 0) return m > 0 ? `${h}小时${m}分` : `${h}小时`;
|
|
3406
|
-
if (s < 60 && m === 0 && h === 0) return `${s}秒`;
|
|
3407
|
-
if (s === 0) return `${m}分`;
|
|
3408
|
-
return `${m}分${String(s).padStart(2, "0")}秒`;
|
|
3409
|
-
}
|
|
3410
|
-
/** Milliseconds → compact English duration (14s / 2m05s / 15m / 3h2m). */
|
|
3411
|
-
function formatEnDuration(ms) {
|
|
3412
|
-
const { h, m, s } = durationParts(ms);
|
|
3413
|
-
if (h > 0) return m > 0 ? `${h}h${m}m` : `${h}h`;
|
|
3414
|
-
if (m === 0 && h === 0) return `${s}s`;
|
|
3415
|
-
if (s === 0) return `${m}m`;
|
|
3416
|
-
return `${m}m${String(s).padStart(2, "0")}s`;
|
|
3417
|
-
}
|
|
3418
|
-
/** Native disclosure chevron (IconChevronDownOutline14, 14x14). */
|
|
3419
|
-
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";
|
|
3420
|
-
function createChevronIcon(className) {
|
|
3421
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3422
|
-
svg.setAttribute("width", "14");
|
|
3423
|
-
svg.setAttribute("height", "14");
|
|
3424
|
-
svg.setAttribute("class", className);
|
|
3425
|
-
svg.setAttribute("viewBox", "0 0 14 14");
|
|
3426
|
-
svg.setAttribute("fill", "none");
|
|
3427
|
-
svg.setAttribute("aria-hidden", "true");
|
|
3428
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
3429
|
-
path.setAttribute("d", CHEVRON_PATH);
|
|
3430
|
-
path.setAttribute("fill", "currentColor");
|
|
3431
|
-
svg.appendChild(path);
|
|
3432
|
-
return svg;
|
|
3433
|
-
}
|
|
3434
|
-
function createProcessedRowElement(duration) {
|
|
3435
|
-
const copy = resolveFoldCopy();
|
|
3436
|
-
const btn = document.createElement("button");
|
|
3437
|
-
btn.type = "button";
|
|
3438
|
-
btn.className = PROCESSED_CLASS;
|
|
3439
|
-
btn.setAttribute("aria-expanded", "false");
|
|
3440
|
-
const text = document.createElement("span");
|
|
3441
|
-
text.textContent = duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(duration)}`;
|
|
3442
|
-
btn.append(text, createChevronIcon(`${PROCESSED_CLASS}-chevron`));
|
|
3443
|
-
btn.title = copy.expandTitle;
|
|
3444
|
-
return btn;
|
|
3445
|
-
}
|
|
3446
|
-
function injectStyle() {
|
|
3447
|
-
if (document.getElementById(STYLE_ID) !== null) return;
|
|
3448
|
-
const style = document.createElement("style");
|
|
3449
|
-
style.id = STYLE_ID;
|
|
3450
|
-
style.textContent = FOLD_CSS;
|
|
3451
|
-
document.head.appendChild(style);
|
|
3452
|
-
}
|
|
3453
|
-
function removeStyle() {
|
|
3454
|
-
document.getElementById(STYLE_ID)?.remove();
|
|
3455
|
-
}
|
|
3456
|
-
//#endregion
|
|
3457
3211
|
//#region src/client/useProgressiveDomText.ts
|
|
3458
3212
|
/**
|
|
3459
3213
|
* Progressive text reveal for opaque Agent renderers.
|
|
@@ -3714,6 +3468,24 @@ window.__ModuleLoader__.load({
|
|
|
3714
3468
|
]);
|
|
3715
3469
|
}
|
|
3716
3470
|
//#endregion
|
|
3471
|
+
//#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/AgentRowEntrance.module.css.mjs
|
|
3472
|
+
const css$2 = "._7P9_Ta_surface{min-width:0}._7P9_Ta_surface[data-entrance=active]{will-change:opacity, transform;animation:.22s cubic-bezier(.2,.8,.2,1) both _7P9_Ta_dsh-smooth-stream-agent-row-in}@keyframes _7P9_Ta_dsh-smooth-stream-agent-row-in{0%{opacity:0;clip-path:inset(0 100% 0 0);transform:translateY(4px)}55%{opacity:1;clip-path:inset(0 18% 0 0)}to{opacity:1;clip-path:inset(0);transform:translate(0,0)}}@media (prefers-reduced-motion:reduce){._7P9_Ta_surface[data-entrance=active]{opacity:1;clip-path:none;will-change:auto;animation:none;transform:none}}";
|
|
3473
|
+
const tagId$2 = "dsh-smooth-stream/AgentRowEntrance.module.css";
|
|
3474
|
+
if (typeof document !== "undefined") {
|
|
3475
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
|
|
3476
|
+
if (tag === null) {
|
|
3477
|
+
tag = document.createElement("style");
|
|
3478
|
+
tag.dataset.plugin = "dsh-smooth-stream";
|
|
3479
|
+
tag.dataset.pluginCss = tagId$2;
|
|
3480
|
+
document.head.appendChild(tag);
|
|
3481
|
+
}
|
|
3482
|
+
tag.textContent = css$2;
|
|
3483
|
+
}
|
|
3484
|
+
var AgentRowEntrance_module_css_default = {
|
|
3485
|
+
"surface": "_7P9_Ta_surface",
|
|
3486
|
+
"dsh-smooth-stream-agent-row-in": "_7P9_Ta_dsh-smooth-stream-agent-row-in"
|
|
3487
|
+
};
|
|
3488
|
+
//#endregion
|
|
3717
3489
|
//#region src/client/TypewriterToolNodeView.tsx
|
|
3718
3490
|
function openAgentLocation(node) {
|
|
3719
3491
|
if (node === null || typeof node !== "object" || !("location" in node)) return false;
|
|
@@ -3797,8 +3569,9 @@ window.__ModuleLoader__.load({
|
|
|
3797
3569
|
* @param Inner - The already-registered row component.
|
|
3798
3570
|
* @returns A follow-hosted row.
|
|
3799
3571
|
*/
|
|
3800
|
-
function wrapFollowNodeView(Inner) {
|
|
3572
|
+
function wrapFollowNodeView(Inner, useControlScroll) {
|
|
3801
3573
|
return function TypewriterFollowNodeView(props) {
|
|
3574
|
+
const controlScroll = useControlScroll?.() ?? true;
|
|
3802
3575
|
const speedCpsRef = (0, react.useRef)(35);
|
|
3803
3576
|
const hostRef = (0, react.useRef)(null);
|
|
3804
3577
|
const growing = isGrowingChatNode(props.node);
|
|
@@ -3878,8 +3651,11 @@ window.__ModuleLoader__.load({
|
|
|
3878
3651
|
onGrowth: followable ? onGrowth : void 0,
|
|
3879
3652
|
entranceExtentRef: growthExtentRef,
|
|
3880
3653
|
speedCpsRef,
|
|
3654
|
+
controlScroll,
|
|
3881
3655
|
predictive: false,
|
|
3882
3656
|
hostRef,
|
|
3657
|
+
className: AgentRowEntrance_module_css_default.surface,
|
|
3658
|
+
entranceActive: entering || growthPulse,
|
|
3883
3659
|
children: (0, react.createElement)(Inner, props)
|
|
3884
3660
|
});
|
|
3885
3661
|
};
|
|
@@ -3899,32 +3675,32 @@ window.__ModuleLoader__.load({
|
|
|
3899
3675
|
tag.textContent = css$1;
|
|
3900
3676
|
}
|
|
3901
3677
|
var SmoothStreamCard_module_css_default = {
|
|
3902
|
-
"
|
|
3678
|
+
"updateCopy": "XNGeRG_updateCopy",
|
|
3679
|
+
"footer": "XNGeRG_footer",
|
|
3680
|
+
"name": "XNGeRG_name",
|
|
3681
|
+
"update": "XNGeRG_update",
|
|
3682
|
+
"updateRow": "XNGeRG_updateRow",
|
|
3683
|
+
"headText": "XNGeRG_headText",
|
|
3903
3684
|
"label": "XNGeRG_label",
|
|
3904
|
-
"
|
|
3685
|
+
"description": "XNGeRG_description",
|
|
3686
|
+
"discard": "XNGeRG_discard",
|
|
3687
|
+
"save": "XNGeRG_save",
|
|
3905
3688
|
"cardOpen": "XNGeRG_cardOpen",
|
|
3689
|
+
"failed": "XNGeRG_failed",
|
|
3690
|
+
"fieldHead": "XNGeRG_fieldHead",
|
|
3691
|
+
"toggle": "XNGeRG_toggle",
|
|
3906
3692
|
"chevron": "XNGeRG_chevron",
|
|
3693
|
+
"hint": "XNGeRG_hint",
|
|
3694
|
+
"card": "XNGeRG_card",
|
|
3907
3695
|
"header": "XNGeRG_header",
|
|
3908
|
-
"field": "XNGeRG_field",
|
|
3909
|
-
"failure": "XNGeRG_failure",
|
|
3910
|
-
"discard": "XNGeRG_discard",
|
|
3911
|
-
"headText": "XNGeRG_headText",
|
|
3912
|
-
"updateRow": "XNGeRG_updateRow",
|
|
3913
|
-
"footer": "XNGeRG_footer",
|
|
3914
|
-
"fieldDisabled": "XNGeRG_fieldDisabled",
|
|
3915
3696
|
"pending": "XNGeRG_pending",
|
|
3916
|
-
"body": "XNGeRG_body",
|
|
3917
|
-
"save": "XNGeRG_save",
|
|
3918
|
-
"update": "XNGeRG_update",
|
|
3919
3697
|
"chevronOpen": "XNGeRG_chevronOpen",
|
|
3920
|
-
"hint": "XNGeRG_hint",
|
|
3921
|
-
"failed": "XNGeRG_failed",
|
|
3922
|
-
"updateCopy": "XNGeRG_updateCopy",
|
|
3923
|
-
"description": "XNGeRG_description",
|
|
3924
|
-
"card": "XNGeRG_card",
|
|
3925
|
-
"version": "XNGeRG_version",
|
|
3926
3698
|
"readOnly": "XNGeRG_readOnly",
|
|
3927
|
-
"
|
|
3699
|
+
"field": "XNGeRG_field",
|
|
3700
|
+
"version": "XNGeRG_version",
|
|
3701
|
+
"failure": "XNGeRG_failure",
|
|
3702
|
+
"fieldDisabled": "XNGeRG_fieldDisabled",
|
|
3703
|
+
"body": "XNGeRG_body"
|
|
3928
3704
|
};
|
|
3929
3705
|
//#endregion
|
|
3930
3706
|
//#region src/client/SmoothStreamCard.tsx
|
|
@@ -4025,40 +3801,40 @@ window.__ModuleLoader__.load({
|
|
|
4025
3801
|
className: SmoothStreamCard_module_css_default.fieldHead,
|
|
4026
3802
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4027
3803
|
className: SmoothStreamCard_module_css_default.label,
|
|
4028
|
-
children: t("
|
|
3804
|
+
children: t("controlScroll")
|
|
4029
3805
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4030
3806
|
type: "checkbox",
|
|
4031
3807
|
className: SmoothStreamCard_module_css_default.toggle,
|
|
4032
|
-
checked: state.
|
|
3808
|
+
checked: state.controlScroll,
|
|
4033
3809
|
disabled: !state.writable || state.saving || !state.enabled,
|
|
4034
3810
|
onChange: (event) => {
|
|
4035
|
-
props.edit({
|
|
3811
|
+
props.edit({ controlScroll: event.target.checked });
|
|
4036
3812
|
}
|
|
4037
3813
|
})]
|
|
4038
3814
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4039
3815
|
className: SmoothStreamCard_module_css_default.hint,
|
|
4040
|
-
children: t("
|
|
3816
|
+
children: t("controlScrollHint")
|
|
4041
3817
|
})]
|
|
4042
3818
|
}),
|
|
4043
3819
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4044
|
-
className: SmoothStreamCard_module_css_default.field
|
|
3820
|
+
className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
|
|
4045
3821
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4046
3822
|
className: SmoothStreamCard_module_css_default.fieldHead,
|
|
4047
3823
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4048
3824
|
className: SmoothStreamCard_module_css_default.label,
|
|
4049
|
-
children: t("
|
|
3825
|
+
children: t("thinkAutoExpand")
|
|
4050
3826
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4051
3827
|
type: "checkbox",
|
|
4052
3828
|
className: SmoothStreamCard_module_css_default.toggle,
|
|
4053
|
-
checked: state.
|
|
4054
|
-
disabled: !state.writable || state.saving,
|
|
3829
|
+
checked: state.thinkAutoExpand,
|
|
3830
|
+
disabled: !state.writable || state.saving || !state.enabled,
|
|
4055
3831
|
onChange: (event) => {
|
|
4056
|
-
props.edit({
|
|
3832
|
+
props.edit({ thinkAutoExpand: event.target.checked });
|
|
4057
3833
|
}
|
|
4058
3834
|
})]
|
|
4059
3835
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4060
3836
|
className: SmoothStreamCard_module_css_default.hint,
|
|
4061
|
-
children: t("
|
|
3837
|
+
children: t("thinkAutoExpandHint")
|
|
4062
3838
|
})]
|
|
4063
3839
|
}),
|
|
4064
3840
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -4182,11 +3958,11 @@ window.__ModuleLoader__.load({
|
|
|
4182
3958
|
hooks: { smoothStreamCard: this.store },
|
|
4183
3959
|
edit: (patch) => {
|
|
4184
3960
|
if (this.saving) return;
|
|
4185
|
-
if (patch.enabled !== void 0 || patch.
|
|
3961
|
+
if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.thinkAutoExpand !== void 0) this.stagedBase = {
|
|
4186
3962
|
...this.baseValues(),
|
|
4187
3963
|
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
4188
|
-
...patch.
|
|
4189
|
-
...patch.
|
|
3964
|
+
...patch.controlScroll === void 0 ? {} : { controlScroll: patch.controlScroll },
|
|
3965
|
+
...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand }
|
|
4190
3966
|
};
|
|
4191
3967
|
if (this.loadedDebug !== void 0 && (patch.debugEnabled !== void 0 || patch.debugTuning !== void 0)) this.stagedDebug = {
|
|
4192
3968
|
...this.debugValues(),
|
|
@@ -4238,8 +4014,8 @@ window.__ModuleLoader__.load({
|
|
|
4238
4014
|
baseValues() {
|
|
4239
4015
|
return this.stagedBase ?? {
|
|
4240
4016
|
enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
|
|
4241
|
-
|
|
4242
|
-
|
|
4017
|
+
controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
|
|
4018
|
+
thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand
|
|
4243
4019
|
};
|
|
4244
4020
|
}
|
|
4245
4021
|
debugValues() {
|
|
@@ -4359,7 +4135,7 @@ window.__ModuleLoader__.load({
|
|
|
4359
4135
|
"npm",
|
|
4360
4136
|
"development",
|
|
4361
4137
|
"unmanaged"
|
|
4362
|
-
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.
|
|
4138
|
+
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.controlScroll !== "boolean" || typeof data.thinkAutoExpand !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
|
|
4363
4139
|
return data;
|
|
4364
4140
|
}
|
|
4365
4141
|
function upgradeView(value) {
|
|
@@ -4388,8 +4164,8 @@ window.__ModuleLoader__.load({
|
|
|
4388
4164
|
async write(settings) {
|
|
4389
4165
|
return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, {
|
|
4390
4166
|
enabled: settings.enabled,
|
|
4167
|
+
controlScroll: settings.controlScroll,
|
|
4391
4168
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
4392
|
-
autoCollapse: settings.autoCollapse,
|
|
4393
4169
|
...settings.debugEnabled === void 0 || settings.debugTuning === void 0 ? {} : {
|
|
4394
4170
|
debugEnabled: settings.debugEnabled,
|
|
4395
4171
|
debugTuning: settings.debugTuning
|
|
@@ -4425,34 +4201,34 @@ window.__ModuleLoader__.load({
|
|
|
4425
4201
|
tag.textContent = css;
|
|
4426
4202
|
}
|
|
4427
4203
|
var DebugPanel_module_css_default = {
|
|
4428
|
-
"triggerActive": "m4_tdG_triggerActive",
|
|
4429
|
-
"guide": "m4_tdG_guide",
|
|
4430
4204
|
"metric": "m4_tdG_metric",
|
|
4431
|
-
"range": "m4_tdG_range",
|
|
4432
|
-
"iconButton": "m4_tdG_iconButton",
|
|
4433
|
-
"unsaved": "m4_tdG_unsaved",
|
|
4434
|
-
"section": "m4_tdG_section",
|
|
4435
|
-
"metrics": "m4_tdG_metrics",
|
|
4436
|
-
"panel": "m4_tdG_panel",
|
|
4437
|
-
"unit": "m4_tdG_unit",
|
|
4438
4205
|
"trigger": "m4_tdG_trigger",
|
|
4439
|
-
"
|
|
4206
|
+
"panel": "m4_tdG_panel",
|
|
4207
|
+
"statusLive": "m4_tdG_statusLive",
|
|
4208
|
+
"guide": "m4_tdG_guide",
|
|
4209
|
+
"triggerActive": "m4_tdG_triggerActive",
|
|
4440
4210
|
"secondaryButton": "m4_tdG_secondaryButton",
|
|
4211
|
+
"number": "m4_tdG_number",
|
|
4441
4212
|
"title": "m4_tdG_title",
|
|
4442
4213
|
"controlHead": "m4_tdG_controlHead",
|
|
4443
|
-
"
|
|
4444
|
-
"
|
|
4445
|
-
"primaryButton": "m4_tdG_primaryButton",
|
|
4214
|
+
"unit": "m4_tdG_unit",
|
|
4215
|
+
"visuallyHidden": "m4_tdG_visuallyHidden",
|
|
4446
4216
|
"panelHeader": "m4_tdG_panelHeader",
|
|
4447
|
-
"
|
|
4448
|
-
"
|
|
4217
|
+
"section": "m4_tdG_section",
|
|
4218
|
+
"range": "m4_tdG_range",
|
|
4219
|
+
"unsaved": "m4_tdG_unsaved",
|
|
4220
|
+
"state": "m4_tdG_state",
|
|
4221
|
+
"control": "m4_tdG_control",
|
|
4222
|
+
"numberWrap": "m4_tdG_numberWrap",
|
|
4223
|
+
"metrics": "m4_tdG_metrics",
|
|
4449
4224
|
"footerSpacer": "m4_tdG_footerSpacer",
|
|
4450
|
-
"visuallyHidden": "m4_tdG_visuallyHidden",
|
|
4451
4225
|
"statusDot": "m4_tdG_statusDot",
|
|
4452
|
-
"
|
|
4226
|
+
"controlLabel": "m4_tdG_controlLabel",
|
|
4453
4227
|
"footer": "m4_tdG_footer",
|
|
4454
|
-
"
|
|
4455
|
-
"
|
|
4228
|
+
"iconButton": "m4_tdG_iconButton",
|
|
4229
|
+
"infoButton": "m4_tdG_infoButton",
|
|
4230
|
+
"primaryButton": "m4_tdG_primaryButton",
|
|
4231
|
+
"scrollArea": "m4_tdG_scrollArea"
|
|
4456
4232
|
};
|
|
4457
4233
|
//#endregion
|
|
4458
4234
|
//#region src/client/DebugPanel.tsx
|
|
@@ -4848,10 +4624,10 @@ window.__ModuleLoader__.load({
|
|
|
4848
4624
|
description: "How replies are revealed while they stream.",
|
|
4849
4625
|
enabled: "Enable smooth streaming",
|
|
4850
4626
|
enabledHint: "Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.",
|
|
4627
|
+
controlScroll: "Take over scrolling",
|
|
4628
|
+
controlScrollHint: "On by default; smooth-stream writes the conversation scroll position. Turn off to leave bottom-follow to Harness.",
|
|
4851
4629
|
thinkAutoExpand: "Auto-expand thinking",
|
|
4852
4630
|
thinkAutoExpandHint: "Open the thinking block while it streams. Turn off to keep it collapsed.",
|
|
4853
|
-
autoCollapse: "Collapse finished work",
|
|
4854
|
-
autoCollapseHint: "When a reply finishes, fold its thinking, tools, and intermediate output behind one “Processed” summary so only the final answer shows. Click the summary to expand again; works with streaming on or off.",
|
|
4855
4631
|
debugEnabled: "Show render diagnostics",
|
|
4856
4632
|
debugEnabledHint: "Show live streaming and scroll metrics on the right side of the chat. Tune values there, then save them here.",
|
|
4857
4633
|
debugUnavailable: "Live diagnostics require a newer plugin Host.",
|
|
@@ -4927,10 +4703,10 @@ window.__ModuleLoader__.load({
|
|
|
4927
4703
|
description: "回复在流式输出时如何逐字展现。",
|
|
4928
4704
|
enabled: "启用丝滑流式渲染",
|
|
4929
4705
|
enabledHint: "由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。",
|
|
4706
|
+
controlScroll: "接管滚动",
|
|
4707
|
+
controlScrollHint: "默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。",
|
|
4930
4708
|
thinkAutoExpand: "自动展开思考",
|
|
4931
4709
|
thinkAutoExpandHint: "思考块在流式时自动展开;关闭后保持折叠,可手动展开。",
|
|
4932
|
-
autoCollapse: "完成后自动折叠",
|
|
4933
|
-
autoCollapseHint: "回复处理完成后,把思考、工具与过程输出折叠为一行“已处理”摘要,只展示最终回复;点击摘要可再次展开。开启或关闭流式渲染均可用,默认开启。",
|
|
4934
4710
|
debugEnabled: "显示渲染调试面板",
|
|
4935
4711
|
debugEnabledHint: "在聊天右侧显示流式渲染和滚动的实时参数,可在面板中调节并在这里保存。",
|
|
4936
4712
|
debugUnavailable: "当前 Host 版本不支持实时调试,请先更新插件 Host。",
|
|
@@ -5064,7 +4840,7 @@ window.__ModuleLoader__.load({
|
|
|
5064
4840
|
* @param ctx - Browser context carrying the slot registry.
|
|
5065
4841
|
* @returns Restorer that puts the original components back.
|
|
5066
4842
|
*/
|
|
5067
|
-
function wrapAgentChatRows(ctx) {
|
|
4843
|
+
function wrapAgentChatRows(ctx, useControlScroll) {
|
|
5068
4844
|
const restores = [];
|
|
5069
4845
|
const wrapped = /* @__PURE__ */ new WeakSet();
|
|
5070
4846
|
const wrapAll = () => {
|
|
@@ -5074,7 +4850,7 @@ window.__ModuleLoader__.load({
|
|
|
5074
4850
|
const current = entry.component;
|
|
5075
4851
|
if (!isWrappableComponent(current) || wrapped.has(current)) continue;
|
|
5076
4852
|
const inner = current;
|
|
5077
|
-
const next = wrapFollowNodeView(inner);
|
|
4853
|
+
const next = wrapFollowNodeView(inner, useControlScroll);
|
|
5078
4854
|
wrapped.add(next);
|
|
5079
4855
|
entry.component = next;
|
|
5080
4856
|
restores.push(() => {
|
|
@@ -5123,7 +4899,7 @@ window.__ModuleLoader__.load({
|
|
|
5123
4899
|
refresh() {
|
|
5124
4900
|
const next = this.read();
|
|
5125
4901
|
const pending = this.card?.getSnapshot().status === "loading";
|
|
5126
|
-
if (pending === this.pending && next.enabled === this.value.enabled && next.
|
|
4902
|
+
if (pending === this.pending && next.enabled === this.value.enabled && next.controlScroll === this.value.controlScroll && next.thinkAutoExpand === this.value.thinkAutoExpand && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
|
|
5127
4903
|
this.pending = pending;
|
|
5128
4904
|
this.value = next;
|
|
5129
4905
|
for (const listener of this.listeners) listener();
|
|
@@ -5132,10 +4908,6 @@ window.__ModuleLoader__.load({
|
|
|
5132
4908
|
takeoverEnabled() {
|
|
5133
4909
|
return !this.pending && this.value.enabled;
|
|
5134
4910
|
}
|
|
5135
|
-
/** Whether finished turns should fold behind a summary row right now. */
|
|
5136
|
-
autoCollapseActive() {
|
|
5137
|
-
return !this.pending && this.value.autoCollapse;
|
|
5138
|
-
}
|
|
5139
4911
|
getSnapshot = () => this.value;
|
|
5140
4912
|
subscribe = (listener) => {
|
|
5141
4913
|
this.listeners.add(listener);
|
|
@@ -5158,19 +4930,7 @@ window.__ModuleLoader__.load({
|
|
|
5158
4930
|
function apply(ctx) {
|
|
5159
4931
|
const config = readBootConfig();
|
|
5160
4932
|
const settings = new SettingsCell();
|
|
5161
|
-
|
|
5162
|
-
const autoCollapse = new AutoCollapseController();
|
|
5163
|
-
const syncAutoCollapse = () => {
|
|
5164
|
-
if (settings.autoCollapseActive()) autoCollapse.start();
|
|
5165
|
-
else autoCollapse.stop();
|
|
5166
|
-
};
|
|
5167
|
-
const unsubscribe = settings.subscribe(syncAutoCollapse);
|
|
5168
|
-
syncAutoCollapse();
|
|
5169
|
-
return () => {
|
|
5170
|
-
unsubscribe();
|
|
5171
|
-
autoCollapse.stop();
|
|
5172
|
-
};
|
|
5173
|
-
});
|
|
4933
|
+
const useControlScroll = () => (0, react.useSyncExternalStore)(settings.subscribe, () => settings.getSnapshot().controlScroll, () => settings.getSnapshot().controlScroll);
|
|
5174
4934
|
ctx.inject([
|
|
5175
4935
|
"slots",
|
|
5176
4936
|
"locale",
|
|
@@ -5238,7 +4998,8 @@ window.__ModuleLoader__.load({
|
|
|
5238
4998
|
revealCharsPerSec: config.revealCharsPerSec,
|
|
5239
4999
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
5240
5000
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
5241
|
-
thinkAutoExpand: preferences.thinkAutoExpand
|
|
5001
|
+
thinkAutoExpand: preferences.thinkAutoExpand,
|
|
5002
|
+
controlScroll: preferences.controlScroll
|
|
5242
5003
|
});
|
|
5243
5004
|
};
|
|
5244
5005
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
@@ -5250,7 +5011,7 @@ window.__ModuleLoader__.load({
|
|
|
5250
5011
|
return;
|
|
5251
5012
|
}
|
|
5252
5013
|
if (releaseTakeover !== void 0) return;
|
|
5253
|
-
const unwrap = wrapAgentChatRows(ctx);
|
|
5014
|
+
const unwrap = wrapAgentChatRows(ctx, useControlScroll);
|
|
5254
5015
|
const unshadow = ctx.slots.register({
|
|
5255
5016
|
name: "conversation.chat.node",
|
|
5256
5017
|
key: "assistant-step",
|