dsh-smooth-stream 0.4.3 → 0.6.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 +962 -1051
- package/lib/client.js.map +1 -1
- package/lib/index.js +22 -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.module.css +34 -0
- package/src/client/SmoothStreamCard.tsx +39 -10
- package/src/client/TypewriterAssistantNodeView.tsx +130 -21
- package/src/client/TypewriterToolNodeView.tsx +9 -1
- package/src/client/clientStore.ts +71 -12
- package/src/client/index.ts +15 -29
- package/src/client/locales.ts +25 -5
- package/src/client/smooth-stream-card-controller.ts +10 -6
- package/src/client/smooth-stream-settings-api.ts +6 -4
- package/src/client/teleprompterGlide.ts +1021 -75
- package/src/client/turn-process-face.d.ts +27 -0
- package/src/plugin.ts +30 -6
- package/src/settings-api.ts +4 -2
- package/src/settings.ts +26 -8
- package/src/client/auto-collapse-controller.ts +0 -1072
package/lib/client.js
CHANGED
|
@@ -10,40 +10,40 @@ 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
|
+
"stopped": "I17U7q_stopped",
|
|
27
|
+
"thinkChevron": "I17U7q_thinkChevron",
|
|
26
28
|
"thinkTitle": "I17U7q_thinkTitle",
|
|
27
29
|
"thinkLeading": "I17U7q_thinkLeading",
|
|
28
30
|
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
31
|
+
"body": "I17U7q_body",
|
|
32
|
+
"thinkBody": "I17U7q_thinkBody",
|
|
33
|
+
"disclosureContent": "I17U7q_disclosureContent",
|
|
33
34
|
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
35
|
+
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
36
|
+
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
37
|
+
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
34
38
|
"follow": "I17U7q_follow",
|
|
35
|
-
"
|
|
36
|
-
"stopped": "I17U7q_stopped",
|
|
39
|
+
"think": "I17U7q_think",
|
|
37
40
|
"thinkSummary": "I17U7q_thinkSummary",
|
|
38
41
|
"disclosureRow": "I17U7q_disclosureRow",
|
|
39
|
-
"
|
|
40
|
-
"thinkRow": "I17U7q_thinkRow",
|
|
41
|
-
"thinkChevron": "I17U7q_thinkChevron",
|
|
42
|
-
"think": "I17U7q_think",
|
|
43
|
-
"body": "I17U7q_body",
|
|
42
|
+
"root": "I17U7q_root",
|
|
44
43
|
"disclosureLeading": "I17U7q_disclosureLeading",
|
|
44
|
+
"thinkRow": "I17U7q_thinkRow",
|
|
45
45
|
"disclosureChevronHover": "I17U7q_disclosureChevronHover",
|
|
46
|
-
"
|
|
46
|
+
"disclosureRoot": "I17U7q_disclosureRoot"
|
|
47
47
|
};
|
|
48
48
|
//#endregion
|
|
49
49
|
//#region src/client/AnimatedDisclosure.tsx
|
|
@@ -125,33 +125,68 @@ 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,
|
|
129
|
+
motionPreference: "auto",
|
|
128
130
|
thinkAutoExpand: true,
|
|
129
|
-
autoCollapse: true,
|
|
130
131
|
debugEnabled: false,
|
|
131
132
|
debugTuning: DEFAULT_STREAM_DEBUG_TUNING
|
|
132
133
|
};
|
|
133
134
|
//#endregion
|
|
134
135
|
//#region src/client/clientStore.ts
|
|
136
|
+
function fallbackSnapshotStore(init) {
|
|
137
|
+
let state = init;
|
|
138
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
139
|
+
return {
|
|
140
|
+
getSnapshot: () => state,
|
|
141
|
+
subscribe: (listener) => {
|
|
142
|
+
listeners.add(listener);
|
|
143
|
+
return () => {
|
|
144
|
+
listeners.delete(listener);
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
update: (mutator) => {
|
|
148
|
+
const draft = Object.create(Object.getPrototypeOf(state), Object.getOwnPropertyDescriptors(state));
|
|
149
|
+
mutator(draft);
|
|
150
|
+
state = draft;
|
|
151
|
+
for (const listener of [...listeners]) listener(state);
|
|
152
|
+
},
|
|
153
|
+
set: (next) => {
|
|
154
|
+
state = next;
|
|
155
|
+
for (const listener of [...listeners]) listener(state);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
135
159
|
/**
|
|
136
160
|
* Runtime require: the module-loader factory scopes its own resolver over the
|
|
137
161
|
* booting kernel's table; under Node (vitest) there is no global require, so
|
|
138
162
|
* fall back to `createRequire`.
|
|
139
163
|
*/
|
|
140
164
|
function pickRequire() {
|
|
141
|
-
if (typeof require === "function")
|
|
142
|
-
|
|
165
|
+
if (typeof require === "function") try {
|
|
166
|
+
return require;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (typeof process !== "undefined" && typeof process.getBuiltinModule === "function") try {
|
|
143
171
|
const { createRequire } = process.getBuiltinModule("node:module");
|
|
144
172
|
return createRequire(process.cwd() + "/package.json");
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
145
175
|
}
|
|
146
|
-
|
|
176
|
+
return null;
|
|
147
177
|
}
|
|
148
178
|
let clientStore;
|
|
149
179
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
const req = pickRequire();
|
|
181
|
+
if (req !== null) try {
|
|
182
|
+
clientStore = req("@deepseek-ai/dsh-client-store");
|
|
183
|
+
} catch {
|
|
184
|
+
try {
|
|
185
|
+
clientStore = req("@deepseek-ai/dsh-client-runtime/client");
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
const createSnapshotStore = clientStore?.createSnapshotStore ?? fallbackSnapshotStore;
|
|
155
190
|
//#endregion
|
|
156
191
|
//#region src/client/debugRuntime.ts
|
|
157
192
|
/**
|
|
@@ -433,8 +468,15 @@ window.__ModuleLoader__.load({
|
|
|
433
468
|
* scroll ownership. Current Harness also sees the write land at the floor.
|
|
434
469
|
*/
|
|
435
470
|
const FOLLOW_OWNED_ATTR = "data-follow-owned";
|
|
436
|
-
/**
|
|
437
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Duration of completion runway retirement. The final pad is visible motion:
|
|
473
|
+
* its shrinking floor brings the transcript down to its natural resting
|
|
474
|
+
* position. 1.5s keeps the default 72px runway at 48px/s (0.8px per 60Hz
|
|
475
|
+
* frame), matching the configured default reading-follow velocity instead of
|
|
476
|
+
* the old 160ms / 450px/s staircase that looked like repeated completion
|
|
477
|
+
* jumps.
|
|
478
|
+
*/
|
|
479
|
+
const FOLLOW_RUNWAY_RETIRE_MS = 1500;
|
|
438
480
|
/** Runway size emitted by bundles before the 72px predictive runway. */
|
|
439
481
|
const LEGACY_RUNWAY_PX = 48;
|
|
440
482
|
/** Safe-lag occupancy band over which reveal pressure is progressively reduced. */
|
|
@@ -591,7 +633,7 @@ window.__ModuleLoader__.load({
|
|
|
591
633
|
* Outermost message surfaces; nested tool rows ride their parent.
|
|
592
634
|
*
|
|
593
635
|
* Another plugin may insert its own element as a flow sibling of the Chat rows
|
|
594
|
-
* (meow-memory's fold bar is one
|
|
636
|
+
* (meow-memory's fold bar is one).
|
|
595
637
|
* Such a row carries no `data-chat-anchor-key`, so selecting only anchored
|
|
596
638
|
* rows would shift the conversation while leaving the foreign row at its
|
|
597
639
|
* natural offset, letting the shifted rows paint over it. Every direct flow
|
|
@@ -705,12 +747,237 @@ window.__ModuleLoader__.load({
|
|
|
705
747
|
const followHadChrome = /* @__PURE__ */ new WeakSet();
|
|
706
748
|
/** Last painted shift per port, to spread a wrap's one-line step over frames. */
|
|
707
749
|
const followLastShiftPx = /* @__PURE__ */ new WeakMap();
|
|
750
|
+
/** Last settled floor per port, to size the shift decay bound against extent collapse. */
|
|
751
|
+
const followLastFloorPx = /* @__PURE__ */ new WeakMap();
|
|
752
|
+
const followGuardAnchors = /* @__PURE__ */ new WeakMap();
|
|
753
|
+
/**
|
|
754
|
+
* Ports whose completion settle loop owns the follow. The settle drains the
|
|
755
|
+
* reveal, retires the pad and guards the cascade; the swap that ends the
|
|
756
|
+
* turn REMOUNTS the follower arms in the same frame cluster, and a freshly
|
|
757
|
+
* mounted arm primes with a higher generation and would otherwise steal the
|
|
758
|
+
* port mid-drain — its observers miss the cascade mutations (armed after
|
|
759
|
+
* the fact) and its state initialization re-materializes engine space.
|
|
760
|
+
* Ownership is released only when the settle finishes, the reader gestures,
|
|
761
|
+
* or a genuinely NEW turn arrives (a user row joined since the handoff).
|
|
762
|
+
*/
|
|
763
|
+
const followCompletionSettle = /* @__PURE__ */ new WeakSet();
|
|
764
|
+
/** User-row count at handoff, for the new-turn check above. */
|
|
765
|
+
const followCompletionSettleRows = /* @__PURE__ */ new WeakMap();
|
|
766
|
+
/** User rows below the flow, or -1 when the host does not label rows with
|
|
767
|
+
* `data-chat-flow-kind` (the engine's own audit benches): the new-turn
|
|
768
|
+
* check is unsupported there and ownership guarding must stay OFF. */
|
|
769
|
+
function countUserRows(port) {
|
|
770
|
+
const flow = flowElementOf(port);
|
|
771
|
+
if (flow === null) return -1;
|
|
772
|
+
let count = 0;
|
|
773
|
+
let sawKind = false;
|
|
774
|
+
for (const child of flow.children) {
|
|
775
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
776
|
+
const kind = child.getAttribute("data-chat-flow-kind");
|
|
777
|
+
if (kind !== null) sawKind = true;
|
|
778
|
+
if (kind === "user") count++;
|
|
779
|
+
}
|
|
780
|
+
return sawKind ? count : -1;
|
|
781
|
+
}
|
|
782
|
+
/** True while this port's completion settle owns the follow and no new turn
|
|
783
|
+
* has arrived since the handoff. */
|
|
784
|
+
function completionSettleGuardsPort(port) {
|
|
785
|
+
if (!followCompletionSettle.has(port)) return false;
|
|
786
|
+
const baseline = followCompletionSettleRows.get(port) ?? -1;
|
|
787
|
+
const current = countUserRows(port);
|
|
788
|
+
return baseline >= 0 && current >= 0 && current <= baseline;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Ports whose completion settle loop owns the follow. A settle loop drains,
|
|
792
|
+
* retires the pad and guards the cascade; an arm that primes while this is
|
|
793
|
+
* set with `active === false` (the settled-side static arm mounting in the
|
|
794
|
+
* very swap frame) must NOT seize leadership — the swap re-renders the node
|
|
795
|
+
* view, a fresh arm would otherwise steal the port mid-drain with
|
|
796
|
+
* `reservePx = ownedBottomSpace` (the pad!), re-materialize an equal runway
|
|
797
|
+
* through applyVisual, double-count the extent and fight the settle's own
|
|
798
|
+
* guard for the rest of the window. A streaming arm (active === true, the
|
|
799
|
+
* next turn) still takes over normally and the settle yields.
|
|
800
|
+
*/
|
|
801
|
+
function readingAnchorOf(port) {
|
|
802
|
+
const flow = flowElementOf(port);
|
|
803
|
+
if (flow === null) return null;
|
|
804
|
+
let anchor = null;
|
|
805
|
+
for (const child of flow.children) {
|
|
806
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
807
|
+
if (child.getAttribute("data-chat-flow-kind") === "assistant" || child.querySelector("[data-variant=\"think\"]") !== null) anchor = child;
|
|
808
|
+
}
|
|
809
|
+
return anchor ?? shiftSurfacesOf(port).at(-1) ?? null;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Measure the reading surface's screen delta since the last guard pass.
|
|
813
|
+
* Returns null when there is nothing comparable yet (first observation), or
|
|
814
|
+
* when the anchor changed identity in a way that is NOT the in-place
|
|
815
|
+
* live→settled replacement (a new turn's row joined — nothing jumped,
|
|
816
|
+
* re-seed). Compensation sites must store the POST-compensation held
|
|
817
|
+
* position via `holdGuardAnchor`, or the guard would read its own
|
|
818
|
+
* correction as a fresh jump and oscillate.
|
|
819
|
+
*/
|
|
820
|
+
function measureReadingAnchor(port) {
|
|
821
|
+
const anchor = readingAnchorOf(port);
|
|
822
|
+
if (anchor === null) {
|
|
823
|
+
followGuardAnchors.delete(port);
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
const rect = anchor.getBoundingClientRect();
|
|
827
|
+
if (!(rect.width > 0 || rect.height > 0)) return null;
|
|
828
|
+
const top = rect.top;
|
|
829
|
+
const shift = currentShiftOf(anchor);
|
|
830
|
+
const pad = flowPadOf(port);
|
|
831
|
+
const scrollTop = port.scrollTop;
|
|
832
|
+
const scrollHeight = port.scrollHeight;
|
|
833
|
+
const flow = flowElementOf(port);
|
|
834
|
+
const index = flow === null ? -1 : [...flow.children].indexOf(anchor);
|
|
835
|
+
const stored = followGuardAnchors.get(port);
|
|
836
|
+
if (stored === void 0) {
|
|
837
|
+
followGuardAnchors.set(port, {
|
|
838
|
+
element: anchor,
|
|
839
|
+
top,
|
|
840
|
+
index,
|
|
841
|
+
shift,
|
|
842
|
+
pad,
|
|
843
|
+
scrollTop,
|
|
844
|
+
scrollHeight
|
|
845
|
+
});
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
if (Math.abs(scrollHeight - stored.scrollHeight) <= .5 && Math.abs(scrollTop - stored.scrollTop) > .5) {
|
|
849
|
+
followScrollLedgers.set(port, scrollTop);
|
|
850
|
+
followGuardAnchors.set(port, {
|
|
851
|
+
element: anchor,
|
|
852
|
+
top,
|
|
853
|
+
index,
|
|
854
|
+
shift,
|
|
855
|
+
pad,
|
|
856
|
+
scrollTop,
|
|
857
|
+
scrollHeight
|
|
858
|
+
});
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const delta = top - stored.top - (shift - stored.shift) + (pad - stored.pad);
|
|
862
|
+
if (stored.element === anchor) {
|
|
863
|
+
followGuardAnchors.set(port, {
|
|
864
|
+
element: anchor,
|
|
865
|
+
top,
|
|
866
|
+
index,
|
|
867
|
+
shift,
|
|
868
|
+
pad,
|
|
869
|
+
scrollTop,
|
|
870
|
+
scrollHeight
|
|
871
|
+
});
|
|
872
|
+
return {
|
|
873
|
+
anchor,
|
|
874
|
+
index,
|
|
875
|
+
top,
|
|
876
|
+
delta
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (!stored.element.isConnected && stored.index === index) return {
|
|
880
|
+
anchor,
|
|
881
|
+
index,
|
|
882
|
+
top,
|
|
883
|
+
delta
|
|
884
|
+
};
|
|
885
|
+
followGuardAnchors.set(port, {
|
|
886
|
+
element: anchor,
|
|
887
|
+
top,
|
|
888
|
+
index,
|
|
889
|
+
shift,
|
|
890
|
+
pad,
|
|
891
|
+
scrollTop,
|
|
892
|
+
scrollHeight
|
|
893
|
+
});
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
/** Record the position the reader should keep seeing after a correction. */
|
|
897
|
+
function holdGuardAnchor(port, measured, heldTop) {
|
|
898
|
+
followGuardAnchors.set(port, {
|
|
899
|
+
element: measured.anchor,
|
|
900
|
+
index: measured.index,
|
|
901
|
+
top: heldTop,
|
|
902
|
+
shift: currentShiftOf(measured.anchor),
|
|
903
|
+
pad: flowPadOf(port),
|
|
904
|
+
scrollTop: port.scrollTop,
|
|
905
|
+
scrollHeight: port.scrollHeight
|
|
906
|
+
});
|
|
907
|
+
}
|
|
708
908
|
/** Last runway offset seen per port, to rebase the extent when the margin size changes. */
|
|
709
909
|
const followRunwayOffsetHistory = /* @__PURE__ */ new WeakMap();
|
|
710
910
|
/** Last observed scroll floor per port, for the slack→overflow runway re-measure. */
|
|
711
911
|
const followFloorHistory = /* @__PURE__ */ new WeakMap();
|
|
712
912
|
/** One-shot flag: the transition frame must paint the full runway as baseline. */
|
|
713
913
|
const followSlackTransition = /* @__PURE__ */ new WeakSet();
|
|
914
|
+
const followSettlePads = /* @__PURE__ */ new WeakMap();
|
|
915
|
+
/**
|
|
916
|
+
* Retired follower space lives as `padding-bottom` on the FLOW element — the
|
|
917
|
+
* one node the engine already owns styles on (flow-fill min-height) and the
|
|
918
|
+
* host never rewrites. Host completion commits routinely REPLACE row elements
|
|
919
|
+
* (live→settled swap re-keys the assistant row, the status row unmounts), and
|
|
920
|
+
* any engine space written on those rows dies with them, sinking the floor
|
|
921
|
+
* and slamming the pinned transcript for a frame. The flow survives.
|
|
922
|
+
*/
|
|
923
|
+
function flowPadOf(port) {
|
|
924
|
+
return followSettlePads.get(port)?.px ?? 0;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* After a loss is re-opened as pad, a registry entry whose element is no
|
|
928
|
+
* longer connected claims extent that no longer exists; drop it so the next
|
|
929
|
+
* `ensureRunway` re-measures fresh instead of double-counting.
|
|
930
|
+
*/
|
|
931
|
+
function pruneDeadRunway(port) {
|
|
932
|
+
const runway = followRunways.get(port);
|
|
933
|
+
if (runway !== void 0 && !runway.element.isConnected) {
|
|
934
|
+
restoreRunway(port);
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
function setFlowPad(port, px) {
|
|
940
|
+
const flow = flowElementOf(port);
|
|
941
|
+
if (flow === null) return;
|
|
942
|
+
const existing = followSettlePads.get(port);
|
|
943
|
+
const original = existing?.original ?? flow.style.paddingBottom;
|
|
944
|
+
if (px <= .25) {
|
|
945
|
+
if (existing !== void 0) {
|
|
946
|
+
flow.style.paddingBottom = existing.original;
|
|
947
|
+
followSettlePads.delete(port);
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
flow.style.paddingBottom = original === "" ? `${px}px` : `calc(${original} + ${px}px)`;
|
|
952
|
+
followSettlePads.set(port, {
|
|
953
|
+
element: flow,
|
|
954
|
+
original,
|
|
955
|
+
px
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
/** Extent the follower owns below the content: the live runway margin plus
|
|
959
|
+
* the retired completion pad. At adopt the pad is reclaimed into the fresh
|
|
960
|
+
* reservation (same frame, pre-paint), so the floor never steps. */
|
|
961
|
+
function ownedBottomSpaceOf(port) {
|
|
962
|
+
return runwayOffsetOf(port) + flowPadOf(port);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Completion-window diagnostics. Armed when a follower hands off to its
|
|
966
|
+
* settle loop; every engine decision and every externally-written scroll
|
|
967
|
+
* position inside the window prints one compact console line, so a live
|
|
968
|
+
* host session can be diffed against the lab without a debugger.
|
|
969
|
+
*/
|
|
970
|
+
let followTraceUntilMs = 0;
|
|
971
|
+
function traceActive() {
|
|
972
|
+
return debugRuntime.isEnabled() && performance.now() < followTraceUntilMs;
|
|
973
|
+
}
|
|
974
|
+
function followTrace(event, detail) {
|
|
975
|
+
if (!traceActive()) return;
|
|
976
|
+
console.log(`[dsh-follow] ${event}`, JSON.stringify(detail));
|
|
977
|
+
}
|
|
978
|
+
function hostShOf(port) {
|
|
979
|
+
return port.scrollHeight;
|
|
980
|
+
}
|
|
714
981
|
function invalidatePaintLimit(port) {
|
|
715
982
|
followPaintLimits.delete(port);
|
|
716
983
|
}
|
|
@@ -831,6 +1098,31 @@ window.__ModuleLoader__.load({
|
|
|
831
1098
|
function runwayOffsetOf(port) {
|
|
832
1099
|
return followRunways.get(port)?.offset ?? 0;
|
|
833
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Move owned runway margin into the persistent flow pad without changing the
|
|
1103
|
+
* scroll extent. Returns the actual layout pixels removed from the margin.
|
|
1104
|
+
*/
|
|
1105
|
+
function transferRunwayToFlowPad(port, requestedPx) {
|
|
1106
|
+
const runway = followRunways.get(port);
|
|
1107
|
+
if (runway === void 0 || requestedPx <= 0 || !runway.element.isConnected) return 0;
|
|
1108
|
+
const nextRequestedPx = Math.max(0, runway.requestedPx - requestedPx);
|
|
1109
|
+
const beforeOffset = runway.offset;
|
|
1110
|
+
const beforeHeight = port.scrollHeight;
|
|
1111
|
+
runway.element.style[runway.property] = nextRequestedPx <= .25 ? runway.original : runway.original === "" ? `${nextRequestedPx}px` : `calc(${runway.original} + ${nextRequestedPx}px)`;
|
|
1112
|
+
const nextOffset = Math.max(0, beforeOffset + port.scrollHeight - beforeHeight);
|
|
1113
|
+
const transferredPx = Math.max(0, beforeOffset - nextOffset);
|
|
1114
|
+
if (nextRequestedPx <= .25 || nextOffset <= .25) followRunways.delete(port);
|
|
1115
|
+
else followRunways.set(port, {
|
|
1116
|
+
...runway,
|
|
1117
|
+
offset: nextOffset,
|
|
1118
|
+
requestedPx: nextRequestedPx
|
|
1119
|
+
});
|
|
1120
|
+
if (transferredPx > 0) {
|
|
1121
|
+
setFlowPad(port, flowPadOf(port) + transferredPx);
|
|
1122
|
+
invalidatePaintLimit(port);
|
|
1123
|
+
}
|
|
1124
|
+
return transferredPx;
|
|
1125
|
+
}
|
|
834
1126
|
/** Available paint room below the last message before fixed conversation chrome. */
|
|
835
1127
|
function safeShiftLimit(port, surfaces) {
|
|
836
1128
|
const last = surfaces.at(-1);
|
|
@@ -859,6 +1151,13 @@ window.__ModuleLoader__.load({
|
|
|
859
1151
|
return limit;
|
|
860
1152
|
}
|
|
861
1153
|
function setFollowScrollTop(port, nextTop) {
|
|
1154
|
+
const ledger = followScrollLedgers.get(port);
|
|
1155
|
+
if (port.getAttribute(FOLLOW_OWNED_ATTR) === null) port.setAttribute(FOLLOW_OWNED_ATTR, "active");
|
|
1156
|
+
if (ledger !== void 0 && traceActive() && Math.abs(port.scrollTop - ledger) > 1) followTrace("external-scroll", {
|
|
1157
|
+
from: Math.round(port.scrollTop),
|
|
1158
|
+
to: Math.round(nextTop),
|
|
1159
|
+
ledger: Math.round(ledger)
|
|
1160
|
+
});
|
|
862
1161
|
if (Math.abs(port.scrollTop - nextTop) > .01) port.scrollTop = nextTop;
|
|
863
1162
|
followScrollLedgers.set(port, port.scrollTop);
|
|
864
1163
|
const ownedTop = String(port.scrollTop);
|
|
@@ -872,6 +1171,19 @@ window.__ModuleLoader__.load({
|
|
|
872
1171
|
*/
|
|
873
1172
|
const followScrollLedgers = /* @__PURE__ */ new WeakMap();
|
|
874
1173
|
const followActivityAt = /* @__PURE__ */ new WeakMap();
|
|
1174
|
+
/**
|
|
1175
|
+
* Scroll ownership must survive follower-arm remounts. Per-closure state let a
|
|
1176
|
+
* new text/tool arm reset the strike counter, so the same host write kept
|
|
1177
|
+
* triggering another engine write and repainting the visible up/down fight.
|
|
1178
|
+
*/
|
|
1179
|
+
const followHostScrollPorts = /* @__PURE__ */ new WeakMap();
|
|
1180
|
+
/** Clear host ownership only when a new user turn actually starts. */
|
|
1181
|
+
function resetHostScrollOwnershipForNewTurn(port) {
|
|
1182
|
+
const ownership = followHostScrollPorts.get(port);
|
|
1183
|
+
if (ownership === void 0) return;
|
|
1184
|
+
const userRows = countUserRows(port);
|
|
1185
|
+
if (userRows >= 0 && userRows > ownership.userRows) followHostScrollPorts.delete(port);
|
|
1186
|
+
}
|
|
875
1187
|
/** Whether this port was owned recently enough to identify a closing tail row. */
|
|
876
1188
|
function hasRecentConversationFollow(port, windowMs = 250) {
|
|
877
1189
|
const last = followActivityAt.get(port);
|
|
@@ -888,7 +1200,7 @@ window.__ModuleLoader__.load({
|
|
|
888
1200
|
* same frame. Carrying that excess in `scrollTop` would move the transcript
|
|
889
1201
|
* toward fixed chrome and also make the host expose jump-to-bottom.
|
|
890
1202
|
*/
|
|
891
|
-
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx) {
|
|
1203
|
+
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx, dtMs = 16.7, writeScrollTop = true) {
|
|
892
1204
|
const surfaces = shiftSurfacesOf(port);
|
|
893
1205
|
ensureFlowFillsPort(port);
|
|
894
1206
|
{
|
|
@@ -900,10 +1212,7 @@ window.__ModuleLoader__.load({
|
|
|
900
1212
|
ensureRunway(port, surfaces, owned?.requestedPx ?? runwayPx);
|
|
901
1213
|
followSlackTransition.add(port);
|
|
902
1214
|
}
|
|
903
|
-
if (preFloor !== lastFloorSeen)
|
|
904
|
-
invalidatePaintLimit(port);
|
|
905
|
-
followFloorHistory.set(port, preFloor);
|
|
906
|
-
}
|
|
1215
|
+
if (preFloor !== lastFloorSeen) followFloorHistory.set(port, preFloor);
|
|
907
1216
|
}
|
|
908
1217
|
ensureRunway(port, surfaces, reservePx);
|
|
909
1218
|
const contentHeight2 = Math.max(0, port.scrollHeight);
|
|
@@ -920,7 +1229,7 @@ window.__ModuleLoader__.load({
|
|
|
920
1229
|
if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
|
|
921
1230
|
if (floor <= 0) {
|
|
922
1231
|
followRunwayOffsetHistory.set(port, 0);
|
|
923
|
-
setFollowScrollTop(port, 0);
|
|
1232
|
+
if (writeScrollTop) setFollowScrollTop(port, 0);
|
|
924
1233
|
followMotionStates.set(port, {
|
|
925
1234
|
capacityPx: Number.POSITIVE_INFINITY,
|
|
926
1235
|
constrained: false,
|
|
@@ -944,15 +1253,25 @@ window.__ModuleLoader__.load({
|
|
|
944
1253
|
const motionShift = Math.min(trajectoryShiftPx ?? baselineShift + requestedLag, availableShift);
|
|
945
1254
|
let shift = Math.max(motionShift, promoteAtRest && motionShift <= .01 && availableShift > 0 ? .1 : 0);
|
|
946
1255
|
const previousShift = followLastShiftPx.get(port);
|
|
947
|
-
|
|
1256
|
+
const previousFloor = followLastFloorPx.get(port);
|
|
1257
|
+
const floorDropPx = Math.max(0, (previousFloor ?? floor) - floor);
|
|
1258
|
+
const maxDecayPx = Math.max(dtMs <= 0 ? 8 : Math.max(1, 8 / 16.67 * dtMs), floorDropPx);
|
|
1259
|
+
followLastFloorPx.set(port, floor);
|
|
1260
|
+
if (previousShift !== void 0 && shift < previousShift - maxDecayPx) shift = previousShift - maxDecayPx;
|
|
1261
|
+
if (followCompletionSettle.has(port) && previousShift !== void 0 && previousFloor !== void 0) {
|
|
1262
|
+
const confirmedGrowthPx = Math.max(0, floor - previousFloor);
|
|
1263
|
+
if (shift > previousShift + confirmedGrowthPx) shift = previousShift + confirmedGrowthPx;
|
|
1264
|
+
}
|
|
948
1265
|
followLastShiftPx.set(port, shift);
|
|
949
|
-
const
|
|
1266
|
+
const requestedShift = trajectoryShiftPx ?? baselineShift + requestedLag;
|
|
1267
|
+
const effectiveLag = Math.max(0, shift - baselineShift);
|
|
950
1268
|
const capacityPx = Math.max(0, limit - baselineShift);
|
|
951
1269
|
const effectiveExtent = targetHeight - effectiveLag;
|
|
952
|
-
|
|
1270
|
+
const isConstrained = requestedShift > availableShift + .25 || limit <= 0 && requestedShift > baselineShift;
|
|
1271
|
+
if (writeScrollTop) setFollowScrollTop(port, floor);
|
|
953
1272
|
followMotionStates.set(port, {
|
|
954
1273
|
capacityPx,
|
|
955
|
-
constrained:
|
|
1274
|
+
constrained: isConstrained,
|
|
956
1275
|
extent: effectiveExtent,
|
|
957
1276
|
lagPx: effectiveLag,
|
|
958
1277
|
reservePx: visibleReserve,
|
|
@@ -976,6 +1295,7 @@ window.__ModuleLoader__.load({
|
|
|
976
1295
|
restoreRunway(port);
|
|
977
1296
|
followMotionStates.delete(port);
|
|
978
1297
|
followLastShiftPx.delete(port);
|
|
1298
|
+
followLastFloorPx.delete(port);
|
|
979
1299
|
invalidatePaintLimit(port);
|
|
980
1300
|
}
|
|
981
1301
|
/** Keep an already-promoted surface at zero until one stable final paint lands. */
|
|
@@ -985,20 +1305,27 @@ window.__ModuleLoader__.load({
|
|
|
985
1305
|
element.style.clipPath = "";
|
|
986
1306
|
}
|
|
987
1307
|
/** Remove equal offsets, land on the floor, then retire the compositor quietly. */
|
|
988
|
-
function finishAtNaturalFloor(port, retainCompositor = true) {
|
|
1308
|
+
function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true) {
|
|
1309
|
+
followCompletionSettle.delete(port);
|
|
1310
|
+
followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
|
|
1311
|
+
followTrace("finish-enter", {
|
|
1312
|
+
sh: hostShOf(port),
|
|
1313
|
+
st: Math.round(port.scrollTop),
|
|
1314
|
+
pad: Math.round(flowPadOf(port)),
|
|
1315
|
+
retain: retainCompositor
|
|
1316
|
+
});
|
|
989
1317
|
const surfaces = shiftSurfacesOf(port);
|
|
990
1318
|
const status = turnStatusOf(port);
|
|
991
1319
|
if (!retainCompositor) {
|
|
992
1320
|
restoreRunway(port);
|
|
993
|
-
settleAtFloor(port);
|
|
1321
|
+
if (writeScrollTop) settleAtFloor(port);
|
|
994
1322
|
clearMotion(port);
|
|
995
1323
|
followMotionStates.delete(port);
|
|
996
1324
|
return;
|
|
997
1325
|
}
|
|
998
1326
|
const promoted = [...surfaces, ...status === null ? [] : [status]].filter((element) => element.style.transform !== "" || element.style.willChange === "transform");
|
|
999
1327
|
const promotedSet = new Set(promoted);
|
|
1000
|
-
|
|
1001
|
-
settleAtFloor(port);
|
|
1328
|
+
if (writeScrollTop) settleAtFloor(port);
|
|
1002
1329
|
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1003
1330
|
port.style.overflowAnchor = "";
|
|
1004
1331
|
port.style.scrollBehavior = "";
|
|
@@ -1023,6 +1350,8 @@ window.__ModuleLoader__.load({
|
|
|
1023
1350
|
/** Only the newest active follower may write one port's shared visual state. */
|
|
1024
1351
|
const followLeaders = /* @__PURE__ */ new WeakMap();
|
|
1025
1352
|
let followGeneration = 0;
|
|
1353
|
+
/** Ports with a live streaming arm; completion guards must not fight them. */
|
|
1354
|
+
const followActivePorts = /* @__PURE__ */ new WeakSet();
|
|
1026
1355
|
/**
|
|
1027
1356
|
* Own the conversation scrollport's bottom-follow while `active` is true.
|
|
1028
1357
|
*
|
|
@@ -1036,15 +1365,19 @@ window.__ModuleLoader__.load({
|
|
|
1036
1365
|
* @param predictiveRef - Optional live visibility gate for predictive runway.
|
|
1037
1366
|
* @param entranceExtentRef - Optional measured growth delta for a generic row.
|
|
1038
1367
|
* @param revealedCharsRef - Committed code-point count for feed-forward phase.
|
|
1368
|
+
* @param controlScroll - When false, leave the scrollport entirely to the Host.
|
|
1039
1369
|
*/
|
|
1040
|
-
function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef) {
|
|
1370
|
+
function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef, controlScroll = true) {
|
|
1041
1371
|
const activeRef = (0, react.useRef)(active);
|
|
1042
1372
|
const entranceRef = (0, react.useRef)(entrance);
|
|
1043
1373
|
const onEntranceSettledRef = (0, react.useRef)(onEntranceSettled);
|
|
1044
1374
|
entranceRef.current = entrance;
|
|
1045
1375
|
onEntranceSettledRef.current = onEntranceSettled;
|
|
1046
1376
|
activeRef.current = active;
|
|
1377
|
+
const controlScrollRef = (0, react.useRef)(controlScroll);
|
|
1378
|
+
controlScrollRef.current = controlScroll;
|
|
1047
1379
|
(0, react.useLayoutEffect)(() => {
|
|
1380
|
+
if (!controlScroll) return;
|
|
1048
1381
|
if (!active) return;
|
|
1049
1382
|
const startedAsEntrance = entrance;
|
|
1050
1383
|
const owner = {};
|
|
@@ -1063,8 +1396,10 @@ window.__ModuleLoader__.load({
|
|
|
1063
1396
|
let interactTimer = null;
|
|
1064
1397
|
let port = null;
|
|
1065
1398
|
let resize = null;
|
|
1399
|
+
let mutations = null;
|
|
1066
1400
|
let observedTail = null;
|
|
1067
1401
|
let statusWasPresent = null;
|
|
1402
|
+
let lastStatusHeightPx = 0;
|
|
1068
1403
|
let trajectoryPositionPx = null;
|
|
1069
1404
|
let trajectoryVelocityPxPerMs = 0;
|
|
1070
1405
|
let trajectoryTargetVelocityPxPerMs = 0;
|
|
@@ -1120,10 +1455,43 @@ window.__ModuleLoader__.load({
|
|
|
1120
1455
|
if (holding === next && isLeader(next)) return;
|
|
1121
1456
|
holding = next;
|
|
1122
1457
|
const leader = followLeaders.get(next);
|
|
1123
|
-
if (leader === void 0 || generation > leader.generation)
|
|
1124
|
-
|
|
1125
|
-
|
|
1458
|
+
if ((leader === void 0 || generation > leader.generation) && !completionSettleGuardsPort(next)) {
|
|
1459
|
+
followCompletionSettle.delete(next);
|
|
1460
|
+
followLeaders.set(next, {
|
|
1461
|
+
generation,
|
|
1462
|
+
owner
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
const yieldScrollOwnership = (next, floor) => {
|
|
1467
|
+
if (hostOwnsScroll) return;
|
|
1468
|
+
hostOwnsScroll = true;
|
|
1469
|
+
followHostScrollPorts.set(next, { userRows: countUserRows(next) });
|
|
1470
|
+
followTrace("yield-scroll", {
|
|
1471
|
+
from: Math.round(next.scrollTop),
|
|
1472
|
+
to: Math.round(floor)
|
|
1126
1473
|
});
|
|
1474
|
+
clearVisual(next);
|
|
1475
|
+
setFollowScrollTop(next, Math.max(0, next.scrollHeight - next.clientHeight));
|
|
1476
|
+
next.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1477
|
+
followLeaders.delete(next);
|
|
1478
|
+
releaseRevealScale();
|
|
1479
|
+
debugRuntime.reportFollow(next, null);
|
|
1480
|
+
};
|
|
1481
|
+
const detectHostScroll = (next, floor) => {
|
|
1482
|
+
if (followHostScrollPorts.has(next)) {
|
|
1483
|
+
hostOwnsScroll = true;
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const ledger = followScrollLedgers.get(next);
|
|
1487
|
+
if (ledger === void 0 || Math.abs(next.scrollTop - ledger) <= 1) return;
|
|
1488
|
+
if (Math.abs(next.scrollTop - floor) <= 1) {
|
|
1489
|
+
followScrollLedgers.set(next, next.scrollTop);
|
|
1490
|
+
externalScrollStrikes = 0;
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
externalScrollStrikes += 1;
|
|
1494
|
+
if (externalScrollStrikes >= 2) yieldScrollOwnership(next, floor);
|
|
1127
1495
|
};
|
|
1128
1496
|
const drop = (next) => {
|
|
1129
1497
|
if (holding === next) holding = null;
|
|
@@ -1164,16 +1532,102 @@ window.__ModuleLoader__.load({
|
|
|
1164
1532
|
interactTimer = null;
|
|
1165
1533
|
}, 800);
|
|
1166
1534
|
};
|
|
1535
|
+
/**
|
|
1536
|
+
* Last observed scroll extent, shared by the pre-paint correction and the
|
|
1537
|
+
* settle loop so a host layout shrink is re-opened exactly once no matter
|
|
1538
|
+
* which observer sees it first. `-1` until the first owned observation.
|
|
1539
|
+
*/
|
|
1540
|
+
let settleRetiring = false;
|
|
1541
|
+
let hostOwnsScroll = false;
|
|
1542
|
+
let externalScrollStrikes = 0;
|
|
1543
|
+
Number.NEGATIVE_INFINITY;
|
|
1544
|
+
let handedOff = false;
|
|
1545
|
+
/**
|
|
1546
|
+
* Bring the reading surface back down toward its held position after an
|
|
1547
|
+
* upward host push (clamp jump, live→settled swap): spend persistent pad
|
|
1548
|
+
* first — lowering the floor lets the pin carry the text back down —
|
|
1549
|
+
* then raise the compositor shift for whatever pad cannot cover, and
|
|
1550
|
+
* rebase the spring extent so the raise survives the next applyVisual
|
|
1551
|
+
* (which otherwise recomputes the shift from lag and undoes it).
|
|
1552
|
+
*/
|
|
1553
|
+
const pullReadingAnchorBack = (host, measured, trace = false) => {
|
|
1554
|
+
holdGuardAnchor(host, measured, measured.top);
|
|
1555
|
+
};
|
|
1556
|
+
/**
|
|
1557
|
+
* THE screen-space anchor hold, shared by every entry point: the
|
|
1558
|
+
* structural-observer path (pre-paint cascade correction), the settle
|
|
1559
|
+
* loop's per-frame poll, and the ACTIVE loop's per-frame poll. The
|
|
1560
|
+
* shift-excluded delta filters the engine's own motion (reveal glide,
|
|
1561
|
+
* wrap lockstep) to ~0, so a live poll may compensate safely — which is
|
|
1562
|
+
* what saves the completion swap: the settled-side arm primes in its
|
|
1563
|
+
* layout effect and steals leadership IN the cascade frame, its own
|
|
1564
|
+
* observers miss the mutations (armed after the fact), and only this
|
|
1565
|
+
* poll sees the jump while it can still be corrected before paint.
|
|
1566
|
+
* Returns the measured delta (null when nothing was comparable).
|
|
1567
|
+
*/
|
|
1568
|
+
const enforceReadingAnchor = (host, trace = false) => {
|
|
1569
|
+
const measured = measureReadingAnchor(host);
|
|
1570
|
+
if (measured === null) return null;
|
|
1571
|
+
if (measured.delta > .5) {
|
|
1572
|
+
if (trace) followTrace("anchor-hold", {
|
|
1573
|
+
screenDelta: Math.round(measured.delta),
|
|
1574
|
+
st: Math.round(host.scrollTop)
|
|
1575
|
+
});
|
|
1576
|
+
if (pruneDeadRunway(host)) reservePx = 0;
|
|
1577
|
+
holdGuardAnchor(host, measured, measured.top);
|
|
1578
|
+
} else if (measured.delta < -.5) {
|
|
1579
|
+
if (pruneDeadRunway(host)) reservePx = 0;
|
|
1580
|
+
pullReadingAnchorBack(host, measured, trace);
|
|
1581
|
+
} else holdGuardAnchor(host, measured, measured.top);
|
|
1582
|
+
return measured.delta;
|
|
1583
|
+
};
|
|
1584
|
+
/** Pre-paint correction (observers, commit subscription). */
|
|
1167
1585
|
const restoreBeforePaint = () => {
|
|
1168
|
-
if (!following || port === null
|
|
1586
|
+
if (!following || port === null) return;
|
|
1587
|
+
if (hostOwnsScroll || followHostScrollPorts.has(port)) {
|
|
1588
|
+
hostOwnsScroll = true;
|
|
1589
|
+
followScrollLedgers.set(port, port.scrollTop);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (!activeRef.current) detectHostScroll(port, Math.max(0, port.scrollHeight - port.clientHeight));
|
|
1593
|
+
const leaderless = !isLeader(port);
|
|
1594
|
+
if (!activeRef.current && followActivePorts.has(port)) return;
|
|
1595
|
+
if (leaderless && (followLeaders.has(port) || !handedOff)) return;
|
|
1596
|
+
if (leaderless) {
|
|
1597
|
+
const sharedMotion = followMotionStates.get(port);
|
|
1598
|
+
if (sharedMotion !== void 0) {
|
|
1599
|
+
animatedH = Math.min(port.scrollHeight, Math.max(0, sharedMotion.extent));
|
|
1600
|
+
reservePx = sharedMotion.reservePx;
|
|
1601
|
+
velocityPxPerSec = sharedMotion.velocityPxPerSec;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
pruneDeadRunway(port);
|
|
1605
|
+
if (!settleRetiring) {
|
|
1606
|
+
const measured = measureReadingAnchor(port);
|
|
1607
|
+
if (measured !== null && measured.delta > .5) {
|
|
1608
|
+
if (pruneDeadRunway(port)) reservePx = 0;
|
|
1609
|
+
holdGuardAnchor(port, measured, measured.top);
|
|
1610
|
+
} else if (measured !== null && measured.delta < -.5) {
|
|
1611
|
+
if (pruneDeadRunway(port)) reservePx = 0;
|
|
1612
|
+
if (!activeRef.current) pullReadingAnchorBack(port, measured);
|
|
1613
|
+
else {
|
|
1614
|
+
const released = Math.min(flowPadOf(port), -measured.delta);
|
|
1615
|
+
if (released > .25) {
|
|
1616
|
+
setFlowPad(port, flowPadOf(port) - released);
|
|
1617
|
+
animatedH = Math.max(0, animatedH - released);
|
|
1618
|
+
}
|
|
1619
|
+
holdGuardAnchor(port, measured, measured.top + released);
|
|
1620
|
+
}
|
|
1621
|
+
} else if (measured !== null) holdGuardAnchor(port, measured, measured.top);
|
|
1622
|
+
}
|
|
1169
1623
|
const tuning = debugRuntime.activeTuning();
|
|
1170
1624
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1171
1625
|
const floor = Math.max(0, port.scrollHeight - port.clientHeight);
|
|
1172
1626
|
if (followFloorHistory.get(port) !== floor) invalidatePaintLimit(port);
|
|
1173
1627
|
const isReasoningSurface = rootRef.current?.querySelector("[data-variant=\"think\"]") !== null;
|
|
1174
1628
|
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 -
|
|
1629
|
+
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);
|
|
1630
|
+
if (trajectoryShift !== void 0) trajectoryPositionPx = floor - (followLastShiftPx.get(port) ?? floor - (trajectoryPositionPx ?? floor));
|
|
1177
1631
|
updateRevealScale(port, 0, true);
|
|
1178
1632
|
reportFollow(port, activeRef.current);
|
|
1179
1633
|
};
|
|
@@ -1183,6 +1637,7 @@ window.__ModuleLoader__.load({
|
|
|
1183
1637
|
if (port !== null) {
|
|
1184
1638
|
for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
1185
1639
|
resize?.disconnect();
|
|
1640
|
+
mutations?.disconnect();
|
|
1186
1641
|
}
|
|
1187
1642
|
unsubscribeCommit?.();
|
|
1188
1643
|
port = next;
|
|
@@ -1192,11 +1647,23 @@ window.__ModuleLoader__.load({
|
|
|
1192
1647
|
});
|
|
1193
1648
|
for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
|
|
1194
1649
|
if (typeof ResizeObserver !== "undefined") {
|
|
1195
|
-
resize = new ResizeObserver(restoreBeforePaint);
|
|
1650
|
+
resize = new ResizeObserver(() => restoreBeforePaint());
|
|
1196
1651
|
resize.observe(port);
|
|
1197
1652
|
const proxy = resizeProxyOf(port);
|
|
1198
1653
|
if (proxy !== null) resize.observe(proxy);
|
|
1199
1654
|
}
|
|
1655
|
+
if (typeof MutationObserver !== "undefined") {
|
|
1656
|
+
const flow = flowElementOf(port);
|
|
1657
|
+
if (flow !== null) {
|
|
1658
|
+
mutations = new MutationObserver(() => {
|
|
1659
|
+
restoreBeforePaint();
|
|
1660
|
+
});
|
|
1661
|
+
mutations.observe(flow, {
|
|
1662
|
+
childList: true,
|
|
1663
|
+
subtree: true
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1200
1667
|
};
|
|
1201
1668
|
/**
|
|
1202
1669
|
* Keep the observer on the flow's TAIL surface. A flow locked to the
|
|
@@ -1224,17 +1691,28 @@ window.__ModuleLoader__.load({
|
|
|
1224
1691
|
if (nextPort === null) return;
|
|
1225
1692
|
bindPort(nextPort);
|
|
1226
1693
|
observeTailSurface();
|
|
1694
|
+
resetHostScrollOwnershipForNewTurn(nextPort);
|
|
1695
|
+
hostOwnsScroll = followHostScrollPorts.has(nextPort);
|
|
1696
|
+
if (activeRef.current) followActivePorts.add(nextPort);
|
|
1697
|
+
else followActivePorts.delete(nextPort);
|
|
1227
1698
|
if (nextPort.clientHeight <= 0) return;
|
|
1228
|
-
const
|
|
1699
|
+
const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
|
|
1700
|
+
const reportedLag = floor - nextPort.scrollTop;
|
|
1229
1701
|
const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
|
|
1230
1702
|
if (!primed) {
|
|
1703
|
+
if (completionSettleGuardsPort(nextPort)) {
|
|
1704
|
+
primed = true;
|
|
1705
|
+
following = false;
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1231
1708
|
const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
|
|
1232
1709
|
if (inherited === void 0) {
|
|
1233
1710
|
const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
|
|
1234
1711
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1235
1712
|
animatedH = entrancePending ? Math.max(0, nextPort.scrollHeight - entranceExtent) : nextPort.scrollHeight;
|
|
1236
1713
|
const hasStatus = turnStatusOf(nextPort) !== null;
|
|
1237
|
-
reservePx = predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0;
|
|
1714
|
+
reservePx = Math.max(ownedBottomSpaceOf(nextPort), predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0);
|
|
1715
|
+
if (!completionSettleGuardsPort(nextPort)) setFlowPad(nextPort, 0);
|
|
1238
1716
|
statusWasPresent = hasStatus;
|
|
1239
1717
|
velocityPxPerSec = 0;
|
|
1240
1718
|
following = !readerScrolledUp(nextPort) && !followReaderHolds.has(nextPort);
|
|
@@ -1247,7 +1725,7 @@ window.__ModuleLoader__.load({
|
|
|
1247
1725
|
if (following) {
|
|
1248
1726
|
hold(nextPort);
|
|
1249
1727
|
if (isLeader(nextPort)) {
|
|
1250
|
-
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !(predictiveRef?.current ?? predictive));
|
|
1728
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !(predictiveRef?.current ?? predictive), void 0, 0, !hostOwnsScroll);
|
|
1251
1729
|
if (predictive && root.querySelector("[data-variant=\"think\"]") === null && runwayOffsetOf(nextPort) > 0) {
|
|
1252
1730
|
const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
|
|
1253
1731
|
const requestedMinLagPx = Math.max(20, runwayOffsetOf(nextPort) - 32);
|
|
@@ -1299,20 +1777,27 @@ window.__ModuleLoader__.load({
|
|
|
1299
1777
|
reportFollow(nextPort, activeRef.current);
|
|
1300
1778
|
return;
|
|
1301
1779
|
}
|
|
1780
|
+
detectHostScroll(nextPort, floor);
|
|
1781
|
+
if (hostOwnsScroll) {
|
|
1782
|
+
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
1783
|
+
reportFollow(nextPort, false);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1302
1786
|
hold(nextPort);
|
|
1303
1787
|
if (!isLeader(nextPort)) {
|
|
1304
1788
|
finishEntrance();
|
|
1305
1789
|
return;
|
|
1306
1790
|
}
|
|
1307
1791
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1308
|
-
const
|
|
1792
|
+
const statusElement = turnStatusOf(nextPort);
|
|
1793
|
+
const hasStatus = statusElement !== null;
|
|
1794
|
+
if (statusElement !== null) lastStatusHeightPx = statusElement.offsetHeight;
|
|
1309
1795
|
const statusJustRemoved = predictGrowth && statusWasPresent === true && !hasStatus;
|
|
1310
|
-
if (statusJustRemoved) reservePx = tuning.runwayPx;
|
|
1796
|
+
if (statusJustRemoved) reservePx = Math.max(reservePx, tuning.runwayPx) + lastStatusHeightPx;
|
|
1311
1797
|
statusWasPresent = hasStatus;
|
|
1312
1798
|
const reserveEnabled = hasStatus || statusJustRemoved || reservePx > .25 || speedCpsRef.current > 90;
|
|
1313
1799
|
const pressureReserveTarget = predictGrowth && reserveEnabled ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0;
|
|
1314
|
-
const
|
|
1315
|
-
const effectiveReserveTarget = !predictGrowth ? 0 : statusJustRemoved ? tuning.runwayPx : heldReserveTarget;
|
|
1800
|
+
const effectiveReserveTarget = Math.max(reservePx, pressureReserveTarget);
|
|
1316
1801
|
const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
|
|
1317
1802
|
reservePx += (effectiveReserveTarget - reservePx) * reserveStep;
|
|
1318
1803
|
if (predictGrowth || runwayOffsetOf(nextPort) > .5) ensureRunway(nextPort, shiftSurfacesOf(nextPort), reservePx);
|
|
@@ -1386,23 +1871,45 @@ window.__ModuleLoader__.load({
|
|
|
1386
1871
|
animatedH = contentHeight - runwayOffset;
|
|
1387
1872
|
velocityPxPerSec = 0;
|
|
1388
1873
|
} else {
|
|
1389
|
-
|
|
1390
|
-
animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
|
|
1874
|
+
animatedH = Math.min(contentHeight - runwayOffset, animatedH + step.advancePx);
|
|
1391
1875
|
velocityPxPerSec = step.velocityPxPerSec;
|
|
1392
1876
|
}
|
|
1393
1877
|
}
|
|
1394
|
-
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift);
|
|
1395
|
-
if (trajectoryActive) trajectoryPositionPx = floorNow -
|
|
1878
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift, elapsedMs, !hostOwnsScroll);
|
|
1879
|
+
if (trajectoryActive) trajectoryPositionPx = floorNow - (followLastShiftPx.get(nextPort) ?? trajectoryShift ?? 0);
|
|
1396
1880
|
updateRevealScale(nextPort, elapsedMs);
|
|
1397
1881
|
reportFollow(nextPort, true);
|
|
1882
|
+
if (isLeader(nextPort)) {
|
|
1883
|
+
if (!activeRef.current) enforceReadingAnchor(nextPort);
|
|
1884
|
+
else measureReadingAnchor(nextPort);
|
|
1885
|
+
}
|
|
1398
1886
|
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
|
|
1399
1887
|
};
|
|
1400
1888
|
frame(performance.now());
|
|
1401
1889
|
return () => {
|
|
1890
|
+
if (!controlScrollRef.current) {
|
|
1891
|
+
cancelAnimationFrame(rafId);
|
|
1892
|
+
if (port !== null) followActivePorts.delete(port);
|
|
1893
|
+
unsubscribeCommit?.();
|
|
1894
|
+
resize?.disconnect();
|
|
1895
|
+
mutations?.disconnect();
|
|
1896
|
+
if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
1897
|
+
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
1898
|
+
const disabledHost = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
|
|
1899
|
+
if (disabledHost !== null) {
|
|
1900
|
+
clearVisual(disabledHost);
|
|
1901
|
+
followLeaders.delete(disabledHost);
|
|
1902
|
+
debugRuntime.reportFollow(disabledHost, null);
|
|
1903
|
+
}
|
|
1904
|
+
releaseRevealScale();
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1402
1907
|
cancelAnimationFrame(rafId);
|
|
1908
|
+
if (port !== null) followActivePorts.delete(port);
|
|
1403
1909
|
unsubscribeCommit?.();
|
|
1404
1910
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
1405
1911
|
resize?.disconnect();
|
|
1912
|
+
mutations?.disconnect();
|
|
1406
1913
|
if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
1407
1914
|
const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
|
|
1408
1915
|
if (host === null) return;
|
|
@@ -1426,8 +1933,18 @@ window.__ModuleLoader__.load({
|
|
|
1426
1933
|
debugRuntime.reportFollow(host, null);
|
|
1427
1934
|
return;
|
|
1428
1935
|
}
|
|
1936
|
+
let settleQuietMs = 0;
|
|
1937
|
+
let settleSig = "";
|
|
1429
1938
|
const lagBeforeCompletionPaint = Math.max(0, host.scrollHeight - animatedH - runwayOffsetOf(host));
|
|
1430
|
-
|
|
1939
|
+
const currentRunway = runwayOffsetOf(host);
|
|
1940
|
+
const currentShift = Math.abs(currentShiftOf(shiftSurfacesOf(host).at(-1) ?? host));
|
|
1941
|
+
if (!activeRef.current && lagBeforeCompletionPaint <= 25 && currentRunway <= .25 && currentShift <= .25 && flowPadOf(host) <= .25) {
|
|
1942
|
+
followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
|
|
1943
|
+
followTrace("fast-gate", {
|
|
1944
|
+
sh: host.scrollHeight,
|
|
1945
|
+
st: Math.round(host.scrollTop),
|
|
1946
|
+
pad: Math.round(flowPadOf(host))
|
|
1947
|
+
});
|
|
1431
1948
|
finishAtNaturalFloor(host, !startedAsEntrance);
|
|
1432
1949
|
followLeaders.delete(host);
|
|
1433
1950
|
releaseRevealScale();
|
|
@@ -1435,19 +1952,37 @@ window.__ModuleLoader__.load({
|
|
|
1435
1952
|
return;
|
|
1436
1953
|
}
|
|
1437
1954
|
const completionShift = currentShiftOf(shiftSurfacesOf(host).at(-1) ?? host);
|
|
1438
|
-
const completionShiftCeiling = completionShift
|
|
1955
|
+
const completionShiftCeiling = startedAsEntrance && completionShift <= .25 ? Number.POSITIVE_INFINITY : completionShift;
|
|
1956
|
+
if (hostOwnsScroll) {
|
|
1957
|
+
clearVisual(host);
|
|
1958
|
+
setFollowScrollTop(host, Math.max(0, host.scrollHeight - host.clientHeight));
|
|
1959
|
+
host.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1960
|
+
followLeaders.delete(host);
|
|
1961
|
+
releaseRevealScale();
|
|
1962
|
+
debugRuntime.reportFollow(host, null);
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
followTraceUntilMs = performance.now() + 15e3;
|
|
1966
|
+
if (pruneDeadRunway(host)) {
|
|
1967
|
+
followTrace("cleanup-dead-margin", {
|
|
1968
|
+
sh: host.scrollHeight,
|
|
1969
|
+
st: Math.round(host.scrollTop),
|
|
1970
|
+
pad: Math.round(flowPadOf(host))
|
|
1971
|
+
});
|
|
1972
|
+
reservePx = 0;
|
|
1973
|
+
}
|
|
1439
1974
|
const completionTuning = debugRuntime.activeTuning();
|
|
1440
1975
|
const previousCompletionRunway = runwayOffsetOf(host);
|
|
1441
|
-
ensureRunway(host, shiftSurfacesOf(host), Math.max(reservePx, completionTuning.runwayPx));
|
|
1976
|
+
ensureRunway(host, shiftSurfacesOf(host), Math.max(reservePx, Math.max(0, completionTuning.runwayPx - flowPadOf(host))));
|
|
1442
1977
|
const completionRunway = runwayOffsetOf(host);
|
|
1978
|
+
measureReadingAnchor(host);
|
|
1443
1979
|
animatedH = Math.max(0, animatedH - (completionRunway - previousCompletionRunway));
|
|
1444
|
-
settleAtFloor(host);
|
|
1445
|
-
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionRunway, completionShiftCeiling);
|
|
1980
|
+
if (!hostOwnsScroll) settleAtFloor(host);
|
|
1981
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionRunway, completionShiftCeiling, false, void 0, 0, !hostOwnsScroll);
|
|
1446
1982
|
reportFollow(host, false);
|
|
1447
1983
|
const runwayOffset = runwayOffsetOf(host);
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
finishAtNaturalFloor(host, !startedAsEntrance);
|
|
1984
|
+
if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && runwayOffset <= .25 && reservePx <= .25) {
|
|
1985
|
+
finishAtNaturalFloor(host, !startedAsEntrance, !hostOwnsScroll);
|
|
1451
1986
|
followLeaders.delete(host);
|
|
1452
1987
|
releaseRevealScale();
|
|
1453
1988
|
debugRuntime.reportFollow(host, null);
|
|
@@ -1456,14 +1991,35 @@ window.__ModuleLoader__.load({
|
|
|
1456
1991
|
for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
|
|
1457
1992
|
const stopSettleListeners = () => {
|
|
1458
1993
|
for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
|
|
1994
|
+
resize?.disconnect();
|
|
1995
|
+
mutations?.disconnect();
|
|
1459
1996
|
if (interactTimer !== null) {
|
|
1460
1997
|
clearTimeout(interactTimer);
|
|
1461
1998
|
interactTimer = null;
|
|
1462
1999
|
}
|
|
1463
2000
|
};
|
|
2001
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2002
|
+
resize = new ResizeObserver(() => restoreBeforePaint());
|
|
2003
|
+
resize.observe(host);
|
|
2004
|
+
const proxy = resizeProxyOf(host);
|
|
2005
|
+
if (proxy !== null) resize.observe(proxy);
|
|
2006
|
+
}
|
|
2007
|
+
if (typeof MutationObserver !== "undefined") {
|
|
2008
|
+
const flow = flowElementOf(host);
|
|
2009
|
+
if (flow !== null) {
|
|
2010
|
+
mutations = new MutationObserver(() => {
|
|
2011
|
+
restoreBeforePaint();
|
|
2012
|
+
});
|
|
2013
|
+
mutations.observe(flow, {
|
|
2014
|
+
childList: true,
|
|
2015
|
+
subtree: true
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
followCompletionSettle.add(host);
|
|
2020
|
+
followCompletionSettleRows.set(host, countUserRows(host));
|
|
2021
|
+
handedOff = true;
|
|
1464
2022
|
let settleLast = performance.now();
|
|
1465
|
-
let settleMarginPx = Math.max(runwayOffsetOf(host), reservePx);
|
|
1466
|
-
let settleMarginRate = settleMarginPx / 160;
|
|
1467
2023
|
const settleFrame = (now) => {
|
|
1468
2024
|
if (!isLeader(host)) {
|
|
1469
2025
|
stopSettleListeners();
|
|
@@ -1473,6 +2029,7 @@ window.__ModuleLoader__.load({
|
|
|
1473
2029
|
readerGestureIntent = false;
|
|
1474
2030
|
handBackVisual(host);
|
|
1475
2031
|
clearVisual(host);
|
|
2032
|
+
followCompletionSettle.delete(host);
|
|
1476
2033
|
followLeaders.delete(host);
|
|
1477
2034
|
releaseRevealScale();
|
|
1478
2035
|
debugRuntime.reportFollow(host, null);
|
|
@@ -1482,19 +2039,56 @@ window.__ModuleLoader__.load({
|
|
|
1482
2039
|
const dt = Math.min(32, Math.max(0, now - settleLast));
|
|
1483
2040
|
const tuning = debugRuntime.activeTuning();
|
|
1484
2041
|
settleLast = now;
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
2042
|
+
detectHostScroll(host, Math.max(0, host.scrollHeight - host.clientHeight));
|
|
2043
|
+
if (hostOwnsScroll) {
|
|
2044
|
+
clearVisual(host);
|
|
2045
|
+
setFollowScrollTop(host, Math.max(0, host.scrollHeight - host.clientHeight));
|
|
2046
|
+
host.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
2047
|
+
followCompletionSettle.delete(host);
|
|
2048
|
+
followLeaders.delete(host);
|
|
2049
|
+
releaseRevealScale();
|
|
2050
|
+
debugRuntime.reportFollow(host, null);
|
|
2051
|
+
stopSettleListeners();
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
const guardDelta = !settleRetiring && !followActivePorts.has(host) ? enforceReadingAnchor(host, true) : null;
|
|
2055
|
+
settleQuietMs = !settleRetiring && (guardDelta === null || Math.abs(guardDelta) <= .5) ? settleQuietMs + dt : 0;
|
|
2056
|
+
const settleStatus = turnStatusOf(host);
|
|
2057
|
+
const ownedRunwayPx = runwayOffsetOf(host);
|
|
2058
|
+
if (ownedRunwayPx > .25) {
|
|
2059
|
+
const requestedTransferPx = settleStatus === null ? ownedRunwayPx : Math.min(reservePx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
|
|
2060
|
+
const transferredPx = transferRunwayToFlowPad(host, requestedTransferPx);
|
|
2061
|
+
if (transferredPx > 0) setFlowPad(host, Math.max(0, flowPadOf(host) - transferredPx));
|
|
2062
|
+
reservePx = Math.max(0, reservePx - transferredPx);
|
|
2063
|
+
animatedH += transferredPx;
|
|
1489
2064
|
}
|
|
1490
2065
|
const runwayOffset = runwayOffsetOf(host);
|
|
1491
2066
|
const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
|
|
1492
|
-
if (lag <= .25 &&
|
|
2067
|
+
if (!settleRetiring && lag <= .25 && reservePx <= .25 && settleQuietMs >= 240 && flowPadOf(host) > .25) {
|
|
2068
|
+
followTrace("retire-start", {
|
|
2069
|
+
pad: Math.round(flowPadOf(host)),
|
|
2070
|
+
sh: host.scrollHeight,
|
|
2071
|
+
st: Math.round(host.scrollTop)
|
|
2072
|
+
});
|
|
2073
|
+
settleRetiring = true;
|
|
2074
|
+
}
|
|
2075
|
+
if (settleRetiring) {
|
|
2076
|
+
const padPx = flowPadOf(host);
|
|
2077
|
+
const retirePx = Math.min(padPx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
|
|
2078
|
+
if (padPx - retirePx <= .25) {
|
|
2079
|
+
setFlowPad(host, 0);
|
|
2080
|
+
settleRetiring = false;
|
|
2081
|
+
} else setFlowPad(host, padPx - retirePx);
|
|
2082
|
+
}
|
|
2083
|
+
if (lag <= .25 && (reservePx <= .25 || settleStatus === null) && flowPadOf(host) <= .25 && settleQuietMs >= 240) {
|
|
2084
|
+
followTrace("finish", {
|
|
2085
|
+
st: Math.round(host.scrollTop),
|
|
2086
|
+
sh: host.scrollHeight,
|
|
2087
|
+
pad: Math.round(flowPadOf(host))
|
|
2088
|
+
});
|
|
1493
2089
|
animatedH = host.scrollHeight;
|
|
1494
|
-
reservePx = 0;
|
|
1495
2090
|
velocityPxPerSec = 0;
|
|
1496
|
-
|
|
1497
|
-
finishAtNaturalFloor(host, !startedAsEntrance);
|
|
2091
|
+
finishAtNaturalFloor(host, !startedAsEntrance, !hostOwnsScroll);
|
|
1498
2092
|
followLeaders.delete(host);
|
|
1499
2093
|
releaseRevealScale();
|
|
1500
2094
|
debugRuntime.reportFollow(host, null);
|
|
@@ -1508,8 +2102,22 @@ window.__ModuleLoader__.load({
|
|
|
1508
2102
|
}, tuning);
|
|
1509
2103
|
animatedH = Math.min(host.scrollHeight - runwayOffset, animatedH + step.advancePx);
|
|
1510
2104
|
velocityPxPerSec = step.velocityPxPerSec;
|
|
1511
|
-
settleAtFloor(host);
|
|
1512
|
-
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec,
|
|
2105
|
+
if (!hostOwnsScroll) settleAtFloor(host);
|
|
2106
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, runwayOffset, Number.POSITIVE_INFINITY, false, void 0, 0, !hostOwnsScroll);
|
|
2107
|
+
if (traceActive()) {
|
|
2108
|
+
const sig = `${Math.round(host.scrollTop)}|${host.scrollHeight}|${Math.round(flowPadOf(host))}|${Math.round(reservePx)}|${settleRetiring}`;
|
|
2109
|
+
if (sig !== settleSig) {
|
|
2110
|
+
followTrace("settle", {
|
|
2111
|
+
st: Math.round(host.scrollTop),
|
|
2112
|
+
sh: host.scrollHeight,
|
|
2113
|
+
pad: Math.round(flowPadOf(host)),
|
|
2114
|
+
reserve: Math.round(reservePx),
|
|
2115
|
+
lag: Math.round(lag * 10) / 10,
|
|
2116
|
+
retiring: settleRetiring
|
|
2117
|
+
});
|
|
2118
|
+
settleSig = sig;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
1513
2121
|
reportFollow(host, false);
|
|
1514
2122
|
requestAnimationFrame(settleFrame);
|
|
1515
2123
|
};
|
|
@@ -1521,7 +2129,8 @@ window.__ModuleLoader__.load({
|
|
|
1521
2129
|
speedCpsRef,
|
|
1522
2130
|
revealScaleRef,
|
|
1523
2131
|
predictive,
|
|
1524
|
-
predictiveRef
|
|
2132
|
+
predictiveRef,
|
|
2133
|
+
controlScroll
|
|
1525
2134
|
]);
|
|
1526
2135
|
(0, react.useLayoutEffect)(() => {
|
|
1527
2136
|
const host = rootRef.current?.closest("[data-conversation-scroll]") ?? null;
|
|
@@ -2003,10 +2612,10 @@ window.__ModuleLoader__.load({
|
|
|
2003
2612
|
* generic wrapped renderers re-arm one glide when their DOM grows without
|
|
2004
2613
|
* requiring a business-kind-specific lifecycle predicate.
|
|
2005
2614
|
*/
|
|
2006
|
-
function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealedCharsRef, revealScaleRef, predictive = true, predictiveRef, hostRef, children }) {
|
|
2615
|
+
function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealedCharsRef, revealScaleRef, predictive = true, predictiveRef, controlScroll = true, hostRef, className, entranceActive, children }) {
|
|
2007
2616
|
const localRootRef = (0, react.useRef)(null);
|
|
2008
2617
|
const rootRef = hostRef ?? localRootRef;
|
|
2009
|
-
useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef);
|
|
2618
|
+
useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef, revealedCharsRef, controlScroll);
|
|
2010
2619
|
(0, react.useEffect)(() => {
|
|
2011
2620
|
if (onGrowth === void 0 || typeof ResizeObserver === "undefined") return;
|
|
2012
2621
|
const root = rootRef.current;
|
|
@@ -2039,7 +2648,8 @@ window.__ModuleLoader__.load({
|
|
|
2039
2648
|
}, [onGrowth]);
|
|
2040
2649
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2041
2650
|
ref: rootRef,
|
|
2042
|
-
className: TypewriterAssistantNodeView_module_css_default.follow
|
|
2651
|
+
className: className === void 0 ? TypewriterAssistantNodeView_module_css_default.follow : `${TypewriterAssistantNodeView_module_css_default.follow} ${className}`,
|
|
2652
|
+
"data-entrance": entranceActive === void 0 ? void 0 : entranceActive ? "active" : "idle",
|
|
2043
2653
|
children
|
|
2044
2654
|
});
|
|
2045
2655
|
}
|
|
@@ -2072,6 +2682,20 @@ window.__ModuleLoader__.load({
|
|
|
2072
2682
|
}, []);
|
|
2073
2683
|
return reduced;
|
|
2074
2684
|
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Resolve whether the reveal engine should stay off for this view. The OS
|
|
2687
|
+
* preference wins only in `auto` mode: `force-smooth` keeps the engine on
|
|
2688
|
+
* machines where a system-wide reduce-motion switch (or a forced browser
|
|
2689
|
+
* flag) would otherwise silently bypass smoothing, and `force-reduced`
|
|
2690
|
+
* disables it even when the OS asks for motion. Credit: three-state design
|
|
2691
|
+
* proposed by @Zn-Dk in #21/#22.
|
|
2692
|
+
*/
|
|
2693
|
+
function useMotionReduced(preference) {
|
|
2694
|
+
const system = usePrefersReducedMotion();
|
|
2695
|
+
if (preference === "force-smooth") return false;
|
|
2696
|
+
if (preference === "force-reduced") return true;
|
|
2697
|
+
return system;
|
|
2698
|
+
}
|
|
2075
2699
|
/** Conservative fallback before the streaming Markdown tail has geometry. */
|
|
2076
2700
|
const PREDICTIVE_WRAP_FALLBACK_CHARS = 32;
|
|
2077
2701
|
const STREAM_ANNOUNCEMENT_INTERVAL_MS = 800;
|
|
@@ -2277,8 +2901,8 @@ window.__ModuleLoader__.load({
|
|
|
2277
2901
|
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
2278
2902
|
* mentions) swaps in exactly once.
|
|
2279
2903
|
*/
|
|
2280
|
-
function AnimatedMarkdownText({ text,
|
|
2281
|
-
const reduced =
|
|
2904
|
+
function AnimatedMarkdownText({ text, labels, fileMentions, streaming, motionReduced, ownFollow, followSpeedCpsRef, followRevealedCharsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack, controlScroll = true }) {
|
|
2905
|
+
const reduced = motionReduced;
|
|
2282
2906
|
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
2283
2907
|
const localSpeedCpsRef = (0, react.useRef)(35);
|
|
2284
2908
|
const followRootRef = (0, react.useRef)(null);
|
|
@@ -2334,17 +2958,21 @@ window.__ModuleLoader__.load({
|
|
|
2334
2958
|
text,
|
|
2335
2959
|
typing
|
|
2336
2960
|
]);
|
|
2961
|
+
(0, react.useEffect)(() => {
|
|
2962
|
+
if (streaming) setTyping(true);
|
|
2963
|
+
}, [streaming]);
|
|
2337
2964
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
2338
2965
|
active: live && ownFollow,
|
|
2339
2966
|
speedCpsRef,
|
|
2340
2967
|
revealedCharsRef: followRevealedCharsRef,
|
|
2341
2968
|
revealScaleRef: followRevealScaleRef,
|
|
2342
2969
|
predictive: streaming,
|
|
2970
|
+
controlScroll,
|
|
2343
2971
|
hostRef: followRootRef,
|
|
2344
2972
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
2345
2973
|
text: live ? shown : text,
|
|
2346
2974
|
streaming: live,
|
|
2347
|
-
|
|
2975
|
+
labels,
|
|
2348
2976
|
fileMentions: live ? void 0 : fileMentions
|
|
2349
2977
|
})
|
|
2350
2978
|
});
|
|
@@ -2362,6 +2990,50 @@ window.__ModuleLoader__.load({
|
|
|
2362
2990
|
}
|
|
2363
2991
|
};
|
|
2364
2992
|
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Apply searchable hidden state without unmounting a stable subtree — the
|
|
2995
|
+
* Host's completion fold hides the answer-inline reasoning this way, so the
|
|
2996
|
+
* takeover renderer must reproduce it to stay inside the contract: the row
|
|
2997
|
+
* disappears into the Host's process summary and comes back on find-in-page.
|
|
2998
|
+
* (Mirrors the Harness chat kit's `useSearchableHidden`.)
|
|
2999
|
+
*/
|
|
3000
|
+
function useSearchableHidden(hidden, reveal) {
|
|
3001
|
+
const ref = (0, react.useRef)(null);
|
|
3002
|
+
(0, react.useLayoutEffect)(() => {
|
|
3003
|
+
const element = ref.current;
|
|
3004
|
+
if (element === null) return;
|
|
3005
|
+
if (hidden && element.contains(element.ownerDocument.activeElement)) {
|
|
3006
|
+
reveal();
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
if (hidden) element.setAttribute("hidden", "until-found");
|
|
3010
|
+
else element.removeAttribute("hidden");
|
|
3011
|
+
}, [hidden, reveal]);
|
|
3012
|
+
(0, react.useEffect)(() => {
|
|
3013
|
+
const element = ref.current;
|
|
3014
|
+
if (element === null) return;
|
|
3015
|
+
element.addEventListener("beforematch", reveal);
|
|
3016
|
+
return () => {
|
|
3017
|
+
element.removeEventListener("beforematch", reveal);
|
|
3018
|
+
};
|
|
3019
|
+
}, [reveal]);
|
|
3020
|
+
return ref;
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* One answer-inline reasoning block wrapped in the Host's fold contract. The
|
|
3024
|
+
* Host computes the fold decision (turn closed, compact transcript, this node
|
|
3025
|
+
* is the answer) and delivers it through the `turnProcess` owner prop; when
|
|
3026
|
+
* folded the block hides into the Host's "thought" summary row instead of
|
|
3027
|
+
* staying mounted above the answer.
|
|
3028
|
+
*/
|
|
3029
|
+
function FoldableReasoning({ hidden, reveal, children }) {
|
|
3030
|
+
const ref = useSearchableHidden(hidden, reveal);
|
|
3031
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3032
|
+
ref,
|
|
3033
|
+
"data-turn-process-inline": hidden || void 0,
|
|
3034
|
+
children
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
2365
3037
|
function firstLine(text) {
|
|
2366
3038
|
const newline = text.indexOf("\n");
|
|
2367
3039
|
return newline === -1 ? text : text.slice(0, newline);
|
|
@@ -2380,8 +3052,8 @@ window.__ModuleLoader__.load({
|
|
|
2380
3052
|
* or the assistant node settling — not when the rest of the reply is
|
|
2381
3053
|
* still streaming.
|
|
2382
3054
|
*/
|
|
2383
|
-
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
|
|
2384
|
-
const reduced =
|
|
3055
|
+
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
|
|
3056
|
+
const reduced = motionReduced;
|
|
2385
3057
|
const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
|
|
2386
3058
|
const [autoClosed, setAutoClosed] = (0, react.useState)(false);
|
|
2387
3059
|
const summaryRef = (0, react.useRef)(null);
|
|
@@ -2460,10 +3132,14 @@ window.__ModuleLoader__.load({
|
|
|
2460
3132
|
* holds offscreen reveals when the frame rate is degraded. Settled text
|
|
2461
3133
|
* renders with the full Markdown pipeline.
|
|
2462
3134
|
*/
|
|
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 }) {
|
|
3135
|
+
const TypewriterAssistantNodeView = (0, react.memo)(function TypewriterAssistantNodeView({ mode: _mode = DEFAULT_STREAM_CONFIG.mode, preset = DEFAULT_STREAM_CONFIG.preset, revealCharsPerSec: _revealCharsPerSec = DEFAULT_STREAM_CONFIG.revealCharsPerSec, scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec, maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec, thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand, controlScroll = true, motionPreference = DEFAULT_STREAM_SETTINGS.motionPreference, node, useTurnData, openFile, loadImage, fileMentions, turnProcess, t }) {
|
|
2464
3136
|
const data = node.data;
|
|
2465
3137
|
const streaming = data.status === "running";
|
|
2466
|
-
const reduced =
|
|
3138
|
+
const reduced = useMotionReduced(motionPreference);
|
|
3139
|
+
const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
|
|
3140
|
+
const revealProcess = (0, react.useCallback)(() => {
|
|
3141
|
+
turnProcess?.setOpen(true);
|
|
3142
|
+
}, [turnProcess]);
|
|
2467
3143
|
const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
|
|
2468
3144
|
const rootSpeedRef = (0, react.useRef)(35);
|
|
2469
3145
|
const rootRevealedCharsRef = (0, react.useRef)(0);
|
|
@@ -2497,9 +3173,12 @@ window.__ModuleLoader__.load({
|
|
|
2497
3173
|
turn
|
|
2498
3174
|
]);
|
|
2499
3175
|
const mentions = (0, react.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2502
|
-
|
|
3176
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
3177
|
+
code: {
|
|
3178
|
+
copyLabel: t("copy"),
|
|
3179
|
+
copiedLabel: t("copied")
|
|
3180
|
+
},
|
|
3181
|
+
footnotes: t("markdown.footnotes")
|
|
2503
3182
|
}), [t]);
|
|
2504
3183
|
const imageLoader = loadImage ?? (async () => {
|
|
2505
3184
|
throw new Error(t("image.serviceUnavailable"));
|
|
@@ -2520,28 +3199,35 @@ window.__ModuleLoader__.load({
|
|
|
2520
3199
|
case "text":
|
|
2521
3200
|
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
|
|
2522
3201
|
text: block.text,
|
|
2523
|
-
|
|
3202
|
+
labels: markdownLabels,
|
|
2524
3203
|
fileMentions: mentions,
|
|
2525
3204
|
streaming,
|
|
3205
|
+
motionReduced: reduced,
|
|
2526
3206
|
ownFollow: !streaming && index === lastFollow,
|
|
2527
3207
|
followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
|
|
2528
3208
|
followRevealedCharsRef: index === lastFollow ? rootRevealedCharsRef : void 0,
|
|
2529
3209
|
followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
|
|
2530
3210
|
onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
|
|
2531
3211
|
preset,
|
|
2532
|
-
shouldHoldBack
|
|
3212
|
+
shouldHoldBack,
|
|
3213
|
+
controlScroll
|
|
2533
3214
|
}, index));
|
|
2534
3215
|
break;
|
|
2535
3216
|
case "reasoning":
|
|
2536
|
-
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
3217
|
+
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FoldableReasoning, {
|
|
3218
|
+
hidden: reasoningHidden,
|
|
3219
|
+
reveal: revealProcess,
|
|
3220
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
|
|
3221
|
+
text: block.text,
|
|
3222
|
+
running: streaming && index === last,
|
|
3223
|
+
preset,
|
|
3224
|
+
thinkAutoExpand,
|
|
3225
|
+
motionReduced: reduced,
|
|
3226
|
+
shouldHoldBack,
|
|
3227
|
+
followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
|
|
3228
|
+
followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
|
|
3229
|
+
t
|
|
3230
|
+
})
|
|
2545
3231
|
}, index));
|
|
2546
3232
|
break;
|
|
2547
3233
|
case "image": {
|
|
@@ -2582,6 +3268,7 @@ window.__ModuleLoader__.load({
|
|
|
2582
3268
|
revealedCharsRef: rootRevealedCharsRef,
|
|
2583
3269
|
revealScaleRef: rootRevealScaleRef,
|
|
2584
3270
|
predictiveRef: rootPredictiveRef,
|
|
3271
|
+
controlScroll,
|
|
2585
3272
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2586
3273
|
className: TypewriterAssistantNodeView_module_css_default.body,
|
|
2587
3274
|
children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2593,867 +3280,6 @@ window.__ModuleLoader__.load({
|
|
|
2593
3280
|
});
|
|
2594
3281
|
});
|
|
2595
3282
|
//#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
3283
|
//#region src/client/useProgressiveDomText.ts
|
|
3458
3284
|
/**
|
|
3459
3285
|
* Progressive text reveal for opaque Agent renderers.
|
|
@@ -3714,6 +3540,24 @@ window.__ModuleLoader__.load({
|
|
|
3714
3540
|
]);
|
|
3715
3541
|
}
|
|
3716
3542
|
//#endregion
|
|
3543
|
+
//#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/AgentRowEntrance.module.css.mjs
|
|
3544
|
+
const css$2 = "._7P9_Ta_surface{min-width:0}._7P9_Ta_surface[data-entrance=active]{will-change:opacity, transform;animation:.22s cubic-bezier(.2,.8,.2,1) both _7P9_Ta_dsh-smooth-stream-agent-row-in}@keyframes _7P9_Ta_dsh-smooth-stream-agent-row-in{0%{opacity:0;clip-path:inset(0 100% 0 0);transform:translateY(4px)}55%{opacity:1;clip-path:inset(0 18% 0 0)}to{opacity:1;clip-path:inset(0);transform:translate(0,0)}}@media (prefers-reduced-motion:reduce){._7P9_Ta_surface[data-entrance=active]{opacity:1;clip-path:none;will-change:auto;animation:none;transform:none}}";
|
|
3545
|
+
const tagId$2 = "dsh-smooth-stream/AgentRowEntrance.module.css";
|
|
3546
|
+
if (typeof document !== "undefined") {
|
|
3547
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
|
|
3548
|
+
if (tag === null) {
|
|
3549
|
+
tag = document.createElement("style");
|
|
3550
|
+
tag.dataset.plugin = "dsh-smooth-stream";
|
|
3551
|
+
tag.dataset.pluginCss = tagId$2;
|
|
3552
|
+
document.head.appendChild(tag);
|
|
3553
|
+
}
|
|
3554
|
+
tag.textContent = css$2;
|
|
3555
|
+
}
|
|
3556
|
+
var AgentRowEntrance_module_css_default = {
|
|
3557
|
+
"dsh-smooth-stream-agent-row-in": "_7P9_Ta_dsh-smooth-stream-agent-row-in",
|
|
3558
|
+
"surface": "_7P9_Ta_surface"
|
|
3559
|
+
};
|
|
3560
|
+
//#endregion
|
|
3717
3561
|
//#region src/client/TypewriterToolNodeView.tsx
|
|
3718
3562
|
function openAgentLocation(node) {
|
|
3719
3563
|
if (node === null || typeof node !== "object" || !("location" in node)) return false;
|
|
@@ -3797,8 +3641,9 @@ window.__ModuleLoader__.load({
|
|
|
3797
3641
|
* @param Inner - The already-registered row component.
|
|
3798
3642
|
* @returns A follow-hosted row.
|
|
3799
3643
|
*/
|
|
3800
|
-
function wrapFollowNodeView(Inner) {
|
|
3644
|
+
function wrapFollowNodeView(Inner, useControlScroll) {
|
|
3801
3645
|
return function TypewriterFollowNodeView(props) {
|
|
3646
|
+
const controlScroll = useControlScroll?.() ?? true;
|
|
3802
3647
|
const speedCpsRef = (0, react.useRef)(35);
|
|
3803
3648
|
const hostRef = (0, react.useRef)(null);
|
|
3804
3649
|
const growing = isGrowingChatNode(props.node);
|
|
@@ -3878,15 +3723,18 @@ window.__ModuleLoader__.load({
|
|
|
3878
3723
|
onGrowth: followable ? onGrowth : void 0,
|
|
3879
3724
|
entranceExtentRef: growthExtentRef,
|
|
3880
3725
|
speedCpsRef,
|
|
3726
|
+
controlScroll,
|
|
3881
3727
|
predictive: false,
|
|
3882
3728
|
hostRef,
|
|
3729
|
+
className: AgentRowEntrance_module_css_default.surface,
|
|
3730
|
+
entranceActive: entering || growthPulse,
|
|
3883
3731
|
children: (0, react.createElement)(Inner, props)
|
|
3884
3732
|
});
|
|
3885
3733
|
};
|
|
3886
3734
|
}
|
|
3887
3735
|
//#endregion
|
|
3888
3736
|
//#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
|
|
3889
|
-
const css$1 = ".XNGeRG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.XNGeRG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.XNGeRG_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.XNGeRG_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.XNGeRG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.XNGeRG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.XNGeRG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.XNGeRG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.XNGeRG_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.XNGeRG_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.XNGeRG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.XNGeRG_chevronOpen{transform:rotate(180deg)}.XNGeRG_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.XNGeRG_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.XNGeRG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.XNGeRG_field+.XNGeRG_field{border-top:1px solid var(--dsw-alias-border-l2)}.XNGeRG_fieldDisabled{opacity:.5}.XNGeRG_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.XNGeRG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.XNGeRG_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_toggle:disabled{cursor:default}.XNGeRG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.XNGeRG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.XNGeRG_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.XNGeRG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.XNGeRG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.XNGeRG_save,.XNGeRG_discard,.XNGeRG_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.XNGeRG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.XNGeRG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.XNGeRG_discard:hover:not(:disabled),.XNGeRG_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.XNGeRG_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.XNGeRG_discard:disabled,.XNGeRG_save:disabled,.XNGeRG_update:disabled{opacity:.4;cursor:default}.XNGeRG_discard:focus-visible,.XNGeRG_save:focus-visible,.XNGeRG_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
|
|
3737
|
+
const css$1 = ".XNGeRG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.XNGeRG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.XNGeRG_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.XNGeRG_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.XNGeRG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.XNGeRG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.XNGeRG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.XNGeRG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.XNGeRG_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.XNGeRG_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.XNGeRG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.XNGeRG_chevronOpen{transform:rotate(180deg)}.XNGeRG_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.XNGeRG_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.XNGeRG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.XNGeRG_field+.XNGeRG_field{border-top:1px solid var(--dsw-alias-border-l2)}.XNGeRG_fieldDisabled{opacity:.5}.XNGeRG_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.XNGeRG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.XNGeRG_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_toggle:disabled{cursor:default}.XNGeRG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.XNGeRG_choiceRow{flex-wrap:wrap;gap:6px 14px;margin:2px 0 0;padding:0;display:flex}.XNGeRG_choice{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:5px;font-size:12px;line-height:1.5;display:flex}.XNGeRG_choiceInput{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_choice:has(.XNGeRG_choiceInput:disabled){cursor:default;opacity:.55}.XNGeRG_choiceInput:disabled{cursor:default}.XNGeRG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.XNGeRG_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.XNGeRG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.XNGeRG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.XNGeRG_save,.XNGeRG_discard,.XNGeRG_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.XNGeRG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.XNGeRG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.XNGeRG_discard:hover:not(:disabled),.XNGeRG_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.XNGeRG_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.XNGeRG_discard:disabled,.XNGeRG_save:disabled,.XNGeRG_update:disabled{opacity:.4;cursor:default}.XNGeRG_discard:focus-visible,.XNGeRG_save:focus-visible,.XNGeRG_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
|
|
3890
3738
|
const tagId$1 = "dsh-smooth-stream/SmoothStreamCard.module.css";
|
|
3891
3739
|
if (typeof document !== "undefined") {
|
|
3892
3740
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
|
|
@@ -3899,32 +3747,35 @@ window.__ModuleLoader__.load({
|
|
|
3899
3747
|
tag.textContent = css$1;
|
|
3900
3748
|
}
|
|
3901
3749
|
var SmoothStreamCard_module_css_default = {
|
|
3750
|
+
"body": "XNGeRG_body",
|
|
3751
|
+
"fieldDisabled": "XNGeRG_fieldDisabled",
|
|
3752
|
+
"readOnly": "XNGeRG_readOnly",
|
|
3753
|
+
"field": "XNGeRG_field",
|
|
3754
|
+
"choiceInput": "XNGeRG_choiceInput",
|
|
3755
|
+
"headText": "XNGeRG_headText",
|
|
3756
|
+
"version": "XNGeRG_version",
|
|
3757
|
+
"choice": "XNGeRG_choice",
|
|
3758
|
+
"description": "XNGeRG_description",
|
|
3759
|
+
"pending": "XNGeRG_pending",
|
|
3902
3760
|
"fieldHead": "XNGeRG_fieldHead",
|
|
3903
3761
|
"label": "XNGeRG_label",
|
|
3904
3762
|
"toggle": "XNGeRG_toggle",
|
|
3905
|
-
"
|
|
3763
|
+
"hint": "XNGeRG_hint",
|
|
3764
|
+
"save": "XNGeRG_save",
|
|
3765
|
+
"updateRow": "XNGeRG_updateRow",
|
|
3906
3766
|
"chevron": "XNGeRG_chevron",
|
|
3767
|
+
"cardOpen": "XNGeRG_cardOpen",
|
|
3768
|
+
"card": "XNGeRG_card",
|
|
3907
3769
|
"header": "XNGeRG_header",
|
|
3908
|
-
"field": "XNGeRG_field",
|
|
3909
3770
|
"failure": "XNGeRG_failure",
|
|
3910
|
-
"discard": "XNGeRG_discard",
|
|
3911
|
-
"headText": "XNGeRG_headText",
|
|
3912
|
-
"updateRow": "XNGeRG_updateRow",
|
|
3913
|
-
"footer": "XNGeRG_footer",
|
|
3914
|
-
"fieldDisabled": "XNGeRG_fieldDisabled",
|
|
3915
|
-
"pending": "XNGeRG_pending",
|
|
3916
|
-
"body": "XNGeRG_body",
|
|
3917
|
-
"save": "XNGeRG_save",
|
|
3918
|
-
"update": "XNGeRG_update",
|
|
3919
|
-
"chevronOpen": "XNGeRG_chevronOpen",
|
|
3920
|
-
"hint": "XNGeRG_hint",
|
|
3921
3771
|
"failed": "XNGeRG_failed",
|
|
3772
|
+
"chevronOpen": "XNGeRG_chevronOpen",
|
|
3773
|
+
"discard": "XNGeRG_discard",
|
|
3922
3774
|
"updateCopy": "XNGeRG_updateCopy",
|
|
3923
|
-
"
|
|
3924
|
-
"
|
|
3925
|
-
"
|
|
3926
|
-
"
|
|
3927
|
-
"name": "XNGeRG_name"
|
|
3775
|
+
"update": "XNGeRG_update",
|
|
3776
|
+
"footer": "XNGeRG_footer",
|
|
3777
|
+
"name": "XNGeRG_name",
|
|
3778
|
+
"choiceRow": "XNGeRG_choiceRow"
|
|
3928
3779
|
};
|
|
3929
3780
|
//#endregion
|
|
3930
3781
|
//#region src/client/SmoothStreamCard.tsx
|
|
@@ -4025,40 +3876,91 @@ window.__ModuleLoader__.load({
|
|
|
4025
3876
|
className: SmoothStreamCard_module_css_default.fieldHead,
|
|
4026
3877
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4027
3878
|
className: SmoothStreamCard_module_css_default.label,
|
|
4028
|
-
children: t("
|
|
3879
|
+
children: t("controlScroll")
|
|
4029
3880
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4030
3881
|
type: "checkbox",
|
|
4031
3882
|
className: SmoothStreamCard_module_css_default.toggle,
|
|
4032
|
-
checked: state.
|
|
3883
|
+
checked: state.controlScroll,
|
|
4033
3884
|
disabled: !state.writable || state.saving || !state.enabled,
|
|
4034
3885
|
onChange: (event) => {
|
|
4035
|
-
props.edit({
|
|
3886
|
+
props.edit({ controlScroll: event.target.checked });
|
|
4036
3887
|
}
|
|
4037
3888
|
})]
|
|
4038
3889
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4039
3890
|
className: SmoothStreamCard_module_css_default.hint,
|
|
4040
|
-
children: t("
|
|
3891
|
+
children: t("controlScrollHint")
|
|
4041
3892
|
})]
|
|
4042
3893
|
}),
|
|
3894
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3895
|
+
className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
|
|
3896
|
+
children: [
|
|
3897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3898
|
+
className: SmoothStreamCard_module_css_default.fieldHead,
|
|
3899
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3900
|
+
className: SmoothStreamCard_module_css_default.label,
|
|
3901
|
+
children: t("motionPreference")
|
|
3902
|
+
})
|
|
3903
|
+
}),
|
|
3904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3905
|
+
className: SmoothStreamCard_module_css_default.hint,
|
|
3906
|
+
children: t("motionPreferenceHint")
|
|
3907
|
+
}),
|
|
3908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3909
|
+
className: SmoothStreamCard_module_css_default.choiceRow,
|
|
3910
|
+
role: "radiogroup",
|
|
3911
|
+
"aria-label": t("motionPreference"),
|
|
3912
|
+
children: [
|
|
3913
|
+
[
|
|
3914
|
+
"auto",
|
|
3915
|
+
"motionAuto",
|
|
3916
|
+
"motionAutoHint"
|
|
3917
|
+
],
|
|
3918
|
+
[
|
|
3919
|
+
"force-smooth",
|
|
3920
|
+
"motionForceSmooth",
|
|
3921
|
+
"motionForceSmoothHint"
|
|
3922
|
+
],
|
|
3923
|
+
[
|
|
3924
|
+
"force-reduced",
|
|
3925
|
+
"motionForceReduced",
|
|
3926
|
+
"motionForceReducedHint"
|
|
3927
|
+
]
|
|
3928
|
+
].map(([value, label, hint]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3929
|
+
className: SmoothStreamCard_module_css_default.choice,
|
|
3930
|
+
title: t(hint),
|
|
3931
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3932
|
+
type: "radio",
|
|
3933
|
+
className: SmoothStreamCard_module_css_default.choiceInput,
|
|
3934
|
+
name: "smooth-stream-motion",
|
|
3935
|
+
checked: state.motionPreference === value,
|
|
3936
|
+
disabled: !state.writable || state.saving || !state.enabled,
|
|
3937
|
+
onChange: () => {
|
|
3938
|
+
props.edit({ motionPreference: value });
|
|
3939
|
+
}
|
|
3940
|
+
}), t(label)]
|
|
3941
|
+
}, value))
|
|
3942
|
+
})
|
|
3943
|
+
]
|
|
3944
|
+
}),
|
|
4043
3945
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4044
|
-
className: SmoothStreamCard_module_css_default.field
|
|
3946
|
+
className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
|
|
4045
3947
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4046
3948
|
className: SmoothStreamCard_module_css_default.fieldHead,
|
|
4047
3949
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4048
3950
|
className: SmoothStreamCard_module_css_default.label,
|
|
4049
|
-
children: t("
|
|
3951
|
+
children: t("thinkAutoExpand")
|
|
4050
3952
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4051
3953
|
type: "checkbox",
|
|
4052
3954
|
className: SmoothStreamCard_module_css_default.toggle,
|
|
4053
|
-
checked: state.
|
|
4054
|
-
disabled: !state.writable || state.saving,
|
|
3955
|
+
checked: state.thinkAutoExpand,
|
|
3956
|
+
disabled: !state.writable || state.saving || !state.enabled,
|
|
4055
3957
|
onChange: (event) => {
|
|
4056
|
-
props.edit({
|
|
3958
|
+
props.edit({ thinkAutoExpand: event.target.checked });
|
|
4057
3959
|
}
|
|
4058
3960
|
})]
|
|
4059
3961
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4060
3962
|
className: SmoothStreamCard_module_css_default.hint,
|
|
4061
|
-
children: t("
|
|
3963
|
+
children: t("thinkAutoExpandHint")
|
|
4062
3964
|
})]
|
|
4063
3965
|
}),
|
|
4064
3966
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -4182,11 +4084,12 @@ window.__ModuleLoader__.load({
|
|
|
4182
4084
|
hooks: { smoothStreamCard: this.store },
|
|
4183
4085
|
edit: (patch) => {
|
|
4184
4086
|
if (this.saving) return;
|
|
4185
|
-
if (patch.enabled !== void 0 || patch.
|
|
4087
|
+
if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.motionPreference !== void 0 || patch.thinkAutoExpand !== void 0) this.stagedBase = {
|
|
4186
4088
|
...this.baseValues(),
|
|
4187
4089
|
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
4188
|
-
...patch.
|
|
4189
|
-
...patch.
|
|
4090
|
+
...patch.controlScroll === void 0 ? {} : { controlScroll: patch.controlScroll },
|
|
4091
|
+
...patch.motionPreference === void 0 ? {} : { motionPreference: patch.motionPreference },
|
|
4092
|
+
...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand }
|
|
4190
4093
|
};
|
|
4191
4094
|
if (this.loadedDebug !== void 0 && (patch.debugEnabled !== void 0 || patch.debugTuning !== void 0)) this.stagedDebug = {
|
|
4192
4095
|
...this.debugValues(),
|
|
@@ -4238,8 +4141,9 @@ window.__ModuleLoader__.load({
|
|
|
4238
4141
|
baseValues() {
|
|
4239
4142
|
return this.stagedBase ?? {
|
|
4240
4143
|
enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
|
|
4241
|
-
|
|
4242
|
-
|
|
4144
|
+
controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
|
|
4145
|
+
motionPreference: this.loaded?.motionPreference ?? DEFAULT_STREAM_SETTINGS.motionPreference,
|
|
4146
|
+
thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand
|
|
4243
4147
|
};
|
|
4244
4148
|
}
|
|
4245
4149
|
debugValues() {
|
|
@@ -4359,7 +4263,11 @@ window.__ModuleLoader__.load({
|
|
|
4359
4263
|
"npm",
|
|
4360
4264
|
"development",
|
|
4361
4265
|
"unmanaged"
|
|
4362
|
-
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.
|
|
4266
|
+
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.controlScroll !== "boolean" || ![
|
|
4267
|
+
"auto",
|
|
4268
|
+
"force-smooth",
|
|
4269
|
+
"force-reduced"
|
|
4270
|
+
].includes(data.motionPreference) || typeof data.thinkAutoExpand !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
|
|
4363
4271
|
return data;
|
|
4364
4272
|
}
|
|
4365
4273
|
function upgradeView(value) {
|
|
@@ -4388,8 +4296,9 @@ window.__ModuleLoader__.load({
|
|
|
4388
4296
|
async write(settings) {
|
|
4389
4297
|
return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, {
|
|
4390
4298
|
enabled: settings.enabled,
|
|
4299
|
+
controlScroll: settings.controlScroll,
|
|
4300
|
+
motionPreference: settings.motionPreference,
|
|
4391
4301
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
4392
|
-
autoCollapse: settings.autoCollapse,
|
|
4393
4302
|
...settings.debugEnabled === void 0 || settings.debugTuning === void 0 ? {} : {
|
|
4394
4303
|
debugEnabled: settings.debugEnabled,
|
|
4395
4304
|
debugTuning: settings.debugTuning
|
|
@@ -4425,34 +4334,34 @@ window.__ModuleLoader__.load({
|
|
|
4425
4334
|
tag.textContent = css;
|
|
4426
4335
|
}
|
|
4427
4336
|
var DebugPanel_module_css_default = {
|
|
4337
|
+
"iconButton": "m4_tdG_iconButton",
|
|
4428
4338
|
"triggerActive": "m4_tdG_triggerActive",
|
|
4429
|
-
"
|
|
4430
|
-
"
|
|
4339
|
+
"controlHead": "m4_tdG_controlHead",
|
|
4340
|
+
"number": "m4_tdG_number",
|
|
4431
4341
|
"range": "m4_tdG_range",
|
|
4432
|
-
"
|
|
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
|
-
"trigger": "m4_tdG_trigger",
|
|
4439
|
-
"state": "m4_tdG_state",
|
|
4440
|
-
"secondaryButton": "m4_tdG_secondaryButton",
|
|
4342
|
+
"statusLive": "m4_tdG_statusLive",
|
|
4441
4343
|
"title": "m4_tdG_title",
|
|
4442
|
-
"controlHead": "m4_tdG_controlHead",
|
|
4443
4344
|
"controlLabel": "m4_tdG_controlLabel",
|
|
4345
|
+
"guide": "m4_tdG_guide",
|
|
4346
|
+
"infoButton": "m4_tdG_infoButton",
|
|
4444
4347
|
"numberWrap": "m4_tdG_numberWrap",
|
|
4445
|
-
"
|
|
4446
|
-
"
|
|
4447
|
-
"
|
|
4348
|
+
"footer": "m4_tdG_footer",
|
|
4349
|
+
"control": "m4_tdG_control",
|
|
4350
|
+
"secondaryButton": "m4_tdG_secondaryButton",
|
|
4351
|
+
"unsaved": "m4_tdG_unsaved",
|
|
4352
|
+
"metrics": "m4_tdG_metrics",
|
|
4448
4353
|
"scrollArea": "m4_tdG_scrollArea",
|
|
4354
|
+
"state": "m4_tdG_state",
|
|
4355
|
+
"metric": "m4_tdG_metric",
|
|
4356
|
+
"panel": "m4_tdG_panel",
|
|
4357
|
+
"trigger": "m4_tdG_trigger",
|
|
4358
|
+
"statusDot": "m4_tdG_statusDot",
|
|
4359
|
+
"section": "m4_tdG_section",
|
|
4360
|
+
"unit": "m4_tdG_unit",
|
|
4449
4361
|
"footerSpacer": "m4_tdG_footerSpacer",
|
|
4450
4362
|
"visuallyHidden": "m4_tdG_visuallyHidden",
|
|
4451
|
-
"
|
|
4452
|
-
"
|
|
4453
|
-
"footer": "m4_tdG_footer",
|
|
4454
|
-
"number": "m4_tdG_number",
|
|
4455
|
-
"control": "m4_tdG_control"
|
|
4363
|
+
"primaryButton": "m4_tdG_primaryButton",
|
|
4364
|
+
"panelHeader": "m4_tdG_panelHeader"
|
|
4456
4365
|
};
|
|
4457
4366
|
//#endregion
|
|
4458
4367
|
//#region src/client/DebugPanel.tsx
|
|
@@ -4848,10 +4757,18 @@ window.__ModuleLoader__.load({
|
|
|
4848
4757
|
description: "How replies are revealed while they stream.",
|
|
4849
4758
|
enabled: "Enable smooth streaming",
|
|
4850
4759
|
enabledHint: "Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.",
|
|
4760
|
+
controlScroll: "Take over scrolling",
|
|
4761
|
+
controlScrollHint: "On by default; smooth-stream writes the conversation scroll position. Turn off to leave bottom-follow to Harness.",
|
|
4762
|
+
motionPreference: "Motion",
|
|
4763
|
+
motionPreferenceHint: "How streaming responds to the system reduce-motion setting.",
|
|
4764
|
+
motionAuto: "Follow system",
|
|
4765
|
+
motionAutoHint: "Reduced-motion systems get raw text (accessibility first).",
|
|
4766
|
+
motionForceSmooth: "Always smooth",
|
|
4767
|
+
motionForceSmoothHint: "Keep the smoothing engine even when the system asks for reduced motion.",
|
|
4768
|
+
motionForceReduced: "Always raw",
|
|
4769
|
+
motionForceReducedHint: "Render raw text even when the system allows motion.",
|
|
4851
4770
|
thinkAutoExpand: "Auto-expand thinking",
|
|
4852
4771
|
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
4772
|
debugEnabled: "Show render diagnostics",
|
|
4856
4773
|
debugEnabledHint: "Show live streaming and scroll metrics on the right side of the chat. Tune values there, then save them here.",
|
|
4857
4774
|
debugUnavailable: "Live diagnostics require a newer plugin Host.",
|
|
@@ -4927,10 +4844,18 @@ window.__ModuleLoader__.load({
|
|
|
4927
4844
|
description: "回复在流式输出时如何逐字展现。",
|
|
4928
4845
|
enabled: "启用丝滑流式渲染",
|
|
4929
4846
|
enabledHint: "由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。",
|
|
4847
|
+
controlScroll: "接管滚动",
|
|
4848
|
+
controlScrollHint: "默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。",
|
|
4849
|
+
motionPreference: "动效偏好",
|
|
4850
|
+
motionPreferenceHint: "流式渲染如何响应系统的「减少动态效果」设置。",
|
|
4851
|
+
motionAuto: "跟随系统",
|
|
4852
|
+
motionAutoHint: "系统开启减少动态效果时直接呈现原始文本(优先无障碍)。",
|
|
4853
|
+
motionForceSmooth: "始终平滑",
|
|
4854
|
+
motionForceSmoothHint: "即使系统要求减少动态效果,仍保持平滑流式渲染。",
|
|
4855
|
+
motionForceReduced: "始终原始",
|
|
4856
|
+
motionForceReducedHint: "即使系统允许动效,也直接呈现原始文本。",
|
|
4930
4857
|
thinkAutoExpand: "自动展开思考",
|
|
4931
4858
|
thinkAutoExpandHint: "思考块在流式时自动展开;关闭后保持折叠,可手动展开。",
|
|
4932
|
-
autoCollapse: "完成后自动折叠",
|
|
4933
|
-
autoCollapseHint: "回复处理完成后,把思考、工具与过程输出折叠为一行“已处理”摘要,只展示最终回复;点击摘要可再次展开。开启或关闭流式渲染均可用,默认开启。",
|
|
4934
4859
|
debugEnabled: "显示渲染调试面板",
|
|
4935
4860
|
debugEnabledHint: "在聊天右侧显示流式渲染和滚动的实时参数,可在面板中调节并在这里保存。",
|
|
4936
4861
|
debugUnavailable: "当前 Host 版本不支持实时调试,请先更新插件 Host。",
|
|
@@ -5064,7 +4989,7 @@ window.__ModuleLoader__.load({
|
|
|
5064
4989
|
* @param ctx - Browser context carrying the slot registry.
|
|
5065
4990
|
* @returns Restorer that puts the original components back.
|
|
5066
4991
|
*/
|
|
5067
|
-
function wrapAgentChatRows(ctx) {
|
|
4992
|
+
function wrapAgentChatRows(ctx, useControlScroll) {
|
|
5068
4993
|
const restores = [];
|
|
5069
4994
|
const wrapped = /* @__PURE__ */ new WeakSet();
|
|
5070
4995
|
const wrapAll = () => {
|
|
@@ -5074,7 +4999,7 @@ window.__ModuleLoader__.load({
|
|
|
5074
4999
|
const current = entry.component;
|
|
5075
5000
|
if (!isWrappableComponent(current) || wrapped.has(current)) continue;
|
|
5076
5001
|
const inner = current;
|
|
5077
|
-
const next = wrapFollowNodeView(inner);
|
|
5002
|
+
const next = wrapFollowNodeView(inner, useControlScroll);
|
|
5078
5003
|
wrapped.add(next);
|
|
5079
5004
|
entry.component = next;
|
|
5080
5005
|
restores.push(() => {
|
|
@@ -5123,7 +5048,7 @@ window.__ModuleLoader__.load({
|
|
|
5123
5048
|
refresh() {
|
|
5124
5049
|
const next = this.read();
|
|
5125
5050
|
const pending = this.card?.getSnapshot().status === "loading";
|
|
5126
|
-
if (pending === this.pending && next.enabled === this.value.enabled && next.
|
|
5051
|
+
if (pending === this.pending && next.enabled === this.value.enabled && next.controlScroll === this.value.controlScroll && next.motionPreference === this.value.motionPreference && next.thinkAutoExpand === this.value.thinkAutoExpand && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
|
|
5127
5052
|
this.pending = pending;
|
|
5128
5053
|
this.value = next;
|
|
5129
5054
|
for (const listener of this.listeners) listener();
|
|
@@ -5132,10 +5057,6 @@ window.__ModuleLoader__.load({
|
|
|
5132
5057
|
takeoverEnabled() {
|
|
5133
5058
|
return !this.pending && this.value.enabled;
|
|
5134
5059
|
}
|
|
5135
|
-
/** Whether finished turns should fold behind a summary row right now. */
|
|
5136
|
-
autoCollapseActive() {
|
|
5137
|
-
return !this.pending && this.value.autoCollapse;
|
|
5138
|
-
}
|
|
5139
5060
|
getSnapshot = () => this.value;
|
|
5140
5061
|
subscribe = (listener) => {
|
|
5141
5062
|
this.listeners.add(listener);
|
|
@@ -5158,19 +5079,7 @@ window.__ModuleLoader__.load({
|
|
|
5158
5079
|
function apply(ctx) {
|
|
5159
5080
|
const config = readBootConfig();
|
|
5160
5081
|
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
|
-
});
|
|
5082
|
+
const useControlScroll = () => (0, react.useSyncExternalStore)(settings.subscribe, () => settings.getSnapshot().controlScroll, () => settings.getSnapshot().controlScroll);
|
|
5174
5083
|
ctx.inject([
|
|
5175
5084
|
"slots",
|
|
5176
5085
|
"locale",
|
|
@@ -5238,7 +5147,9 @@ window.__ModuleLoader__.load({
|
|
|
5238
5147
|
revealCharsPerSec: config.revealCharsPerSec,
|
|
5239
5148
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
5240
5149
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
5241
|
-
thinkAutoExpand: preferences.thinkAutoExpand
|
|
5150
|
+
thinkAutoExpand: preferences.thinkAutoExpand,
|
|
5151
|
+
controlScroll: preferences.controlScroll,
|
|
5152
|
+
motionPreference: preferences.motionPreference
|
|
5242
5153
|
});
|
|
5243
5154
|
};
|
|
5244
5155
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
@@ -5250,7 +5161,7 @@ window.__ModuleLoader__.load({
|
|
|
5250
5161
|
return;
|
|
5251
5162
|
}
|
|
5252
5163
|
if (releaseTakeover !== void 0) return;
|
|
5253
|
-
const unwrap = wrapAgentChatRows(ctx);
|
|
5164
|
+
const unwrap = wrapAgentChatRows(ctx, useControlScroll);
|
|
5254
5165
|
const unshadow = ctx.slots.register({
|
|
5255
5166
|
name: "conversation.chat.node",
|
|
5256
5167
|
key: "assistant-step",
|