dsh-smooth-stream 0.3.2 → 0.3.4
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 +10 -13
- package/README.md +10 -13
- package/lib/client.js +1315 -357
- package/lib/client.js.map +1 -1
- package/package.json +9 -2
- package/src/client/FollowHost.tsx +49 -4
- package/src/client/TypewriterAssistantNodeView.module.css +1 -0
- package/src/client/TypewriterAssistantNodeView.tsx +328 -21
- package/src/client/TypewriterToolNodeView.tsx +183 -8
- package/src/client/index.ts +25 -8
- package/src/client/teleprompterGlide.ts +814 -231
- package/src/client/useProgressiveDomText.ts +206 -0
- package/src/client/useSmoothStreamContent.ts +139 -73
- package/src/config.ts +3 -6
package/lib/client.js
CHANGED
|
@@ -10,7 +10,7 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
11
|
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
12
12
|
//#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
|
|
13
|
-
const css$1 = ".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>*{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{min-width:0}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,li,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
|
|
13
|
+
const css$1 = ".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>*{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}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,li,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
|
|
14
14
|
const tagId$1 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
|
|
15
15
|
if (typeof document !== "undefined") {
|
|
16
16
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
|
|
@@ -23,27 +23,27 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
tag.textContent = css$1;
|
|
24
24
|
}
|
|
25
25
|
var TypewriterAssistantNodeView_module_css_default = {
|
|
26
|
+
"thinkBody": "I17U7q_thinkBody",
|
|
26
27
|
"body": "I17U7q_body",
|
|
27
|
-
"disclosureRow": "I17U7q_disclosureRow",
|
|
28
|
-
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
29
|
-
"stopped": "I17U7q_stopped",
|
|
30
|
-
"root": "I17U7q_root",
|
|
31
|
-
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
32
28
|
"thinkRow": "I17U7q_thinkRow",
|
|
33
29
|
"thinkLeading": "I17U7q_thinkLeading",
|
|
34
|
-
"
|
|
30
|
+
"disclosureLeading": "I17U7q_disclosureLeading",
|
|
35
31
|
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
32
|
+
"follow": "I17U7q_follow",
|
|
33
|
+
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
34
|
+
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
35
|
+
"thinkTitle": "I17U7q_thinkTitle",
|
|
36
|
+
"disclosureRow": "I17U7q_disclosureRow",
|
|
37
|
+
"disclosureChevronHover": "I17U7q_disclosureChevronHover",
|
|
36
38
|
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
37
39
|
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
38
|
-
"
|
|
39
|
-
"disclosureContent": "I17U7q_disclosureContent",
|
|
40
|
+
"root": "I17U7q_root",
|
|
40
41
|
"think": "I17U7q_think",
|
|
41
|
-
"thinkBody": "I17U7q_thinkBody",
|
|
42
|
-
"disclosureChevronHover": "I17U7q_disclosureChevronHover",
|
|
43
|
-
"thinkChevron": "I17U7q_thinkChevron",
|
|
44
42
|
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
43
|
+
"thinkChevron": "I17U7q_thinkChevron",
|
|
44
|
+
"stopped": "I17U7q_stopped",
|
|
45
|
+
"thinkSummary": "I17U7q_thinkSummary",
|
|
46
|
+
"disclosureContent": "I17U7q_disclosureContent"
|
|
47
47
|
};
|
|
48
48
|
//#endregion
|
|
49
49
|
//#region src/client/AnimatedDisclosure.tsx
|
|
@@ -123,7 +123,6 @@ window.__ModuleLoader__.load({
|
|
|
123
123
|
maxActiveCps: 360,
|
|
124
124
|
maxCps: 240,
|
|
125
125
|
maxFlushCps: 480,
|
|
126
|
-
minCommitIntervalMs: 16,
|
|
127
126
|
minCps: 24,
|
|
128
127
|
settleAfterMs: 280,
|
|
129
128
|
settleDrainMaxMs: 420,
|
|
@@ -139,7 +138,6 @@ window.__ModuleLoader__.load({
|
|
|
139
138
|
maxActiveCps: 480,
|
|
140
139
|
maxCps: 320,
|
|
141
140
|
maxFlushCps: 640,
|
|
142
|
-
minCommitIntervalMs: 16,
|
|
143
141
|
minCps: 32,
|
|
144
142
|
settleAfterMs: 200,
|
|
145
143
|
settleDrainMaxMs: 280,
|
|
@@ -155,7 +153,6 @@ window.__ModuleLoader__.load({
|
|
|
155
153
|
maxActiveCps: 280,
|
|
156
154
|
maxCps: 180,
|
|
157
155
|
maxFlushCps: 400,
|
|
158
|
-
minCommitIntervalMs: 16,
|
|
159
156
|
minCps: 20,
|
|
160
157
|
settleAfterMs: 360,
|
|
161
158
|
settleDrainMaxMs: 520,
|
|
@@ -163,23 +160,28 @@ window.__ModuleLoader__.load({
|
|
|
163
160
|
targetBufferMs: 56
|
|
164
161
|
}
|
|
165
162
|
};
|
|
166
|
-
const
|
|
167
|
-
const
|
|
163
|
+
const QUEUE_ACCEL_EXPONENT = 1.25;
|
|
164
|
+
const QUEUE_PRESSURE_FACTOR = .85;
|
|
168
165
|
const CATCHUP_SECONDS = .15;
|
|
169
|
-
const clamp
|
|
166
|
+
const clamp = (value, min, max) => {
|
|
170
167
|
return Math.min(max, Math.max(min, value));
|
|
171
168
|
};
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
/** Float-debt queue integration from `ultimate_stream_physics_scroller.html`. */
|
|
170
|
+
function computeAdaptiveQueueStep(backlog, dtMs, debt, revealScale = 1) {
|
|
171
|
+
if (backlog <= 0 || dtMs <= 0) return {
|
|
172
|
+
revealChars: 0,
|
|
173
|
+
debt: 0,
|
|
174
|
+
speedCps: 0
|
|
175
|
+
};
|
|
176
|
+
const speedCps = Math.min(600, 90 + Math.pow(backlog, QUEUE_ACCEL_EXPONENT) * QUEUE_PRESSURE_FACTOR);
|
|
177
|
+
const effectiveScale = clamp(revealScale, .05, 1);
|
|
178
|
+
const accumulated = Math.max(0, debt) + speedCps * effectiveScale * (dtMs / 1e3);
|
|
179
|
+
const revealChars = Math.min(backlog, Math.floor(accumulated));
|
|
180
|
+
return {
|
|
181
|
+
revealChars,
|
|
182
|
+
debt: revealChars >= backlog ? 0 : accumulated - revealChars,
|
|
183
|
+
speedCps
|
|
184
|
+
};
|
|
183
185
|
}
|
|
184
186
|
/** Counts user-perceived characters (code points), not UTF-16 units. */
|
|
185
187
|
const countChars = (text) => {
|
|
@@ -191,9 +193,20 @@ window.__ModuleLoader__.load({
|
|
|
191
193
|
function computeSettleDrain(config, input) {
|
|
192
194
|
if (input.inputActive || !input.settling) return 0;
|
|
193
195
|
const overflowCps = Math.max(0, input.backlog - 300) * 1e3 / 2;
|
|
194
|
-
const drainTargetMs = clamp
|
|
196
|
+
const drainTargetMs = clamp(input.backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);
|
|
195
197
|
const settleCps = input.backlog * 1e3 / drainTargetMs;
|
|
196
|
-
return clamp
|
|
198
|
+
return clamp(Math.max(settleCps, overflowCps), config.flushCps, config.maxFlushCps);
|
|
199
|
+
}
|
|
200
|
+
/** Fixed velocity that closes a producer-complete queue within its deadline. */
|
|
201
|
+
function computeCompletionDrain(config, backlog) {
|
|
202
|
+
if (backlog <= 0) return 0;
|
|
203
|
+
const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);
|
|
204
|
+
const deadlineCps = backlog * 1e3 / drainTargetMs;
|
|
205
|
+
return Math.max(deadlineCps, computeSettleDrain(config, {
|
|
206
|
+
backlog,
|
|
207
|
+
inputActive: false,
|
|
208
|
+
settling: true
|
|
209
|
+
}));
|
|
197
210
|
}
|
|
198
211
|
/**
|
|
199
212
|
* Drain rate multiplier once the input ends: leftover backlog reveals at
|
|
@@ -203,18 +216,18 @@ window.__ModuleLoader__.load({
|
|
|
203
216
|
/** Pure per-frame reveal decision shared by the loop and its tests. */
|
|
204
217
|
function computeRevealStep(config, input, dtSeconds) {
|
|
205
218
|
const trackedCps = Math.max(input.emaCps, input.arrivalCpsEma);
|
|
206
|
-
const baseCps = clamp
|
|
219
|
+
const baseCps = clamp(trackedCps, config.minCps, config.maxFlushCps);
|
|
207
220
|
const targetLagChars = input.inputActive ? Math.max(2, Math.round(baseCps * config.targetBufferMs / 1e3)) : 0;
|
|
208
221
|
let currentCps;
|
|
209
|
-
if (input.steadyCps !== void 0) currentCps = input.inputActive || input.settling ? clamp
|
|
222
|
+
if (input.steadyCps !== void 0) currentCps = input.inputActive || input.settling ? clamp(input.steadyCps * (input.inputActive ? 1 : SETTLE_DRAIN_MULTIPLIER), config.minCps, config.maxFlushCps) : 0;
|
|
210
223
|
else if (input.inputActive) {
|
|
211
224
|
const overflow = Math.max(0, input.backlog - 32);
|
|
212
225
|
const catchup = overflow > 0 ? overflow / CATCHUP_SECONDS : 0;
|
|
213
|
-
currentCps = clamp
|
|
226
|
+
currentCps = clamp(baseCps * 1.08 + catchup, config.minCps, config.maxFlushCps);
|
|
214
227
|
} else if (input.settling) currentCps = computeSettleDrain(config, input);
|
|
215
228
|
else {
|
|
216
229
|
const idleFlushCps = Math.max(config.flushCps, baseCps * 1.8, input.arrivalCpsEma * .8);
|
|
217
|
-
currentCps = clamp
|
|
230
|
+
currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);
|
|
218
231
|
}
|
|
219
232
|
const minRevealChars = input.inputActive ? 1 : 2;
|
|
220
233
|
return {
|
|
@@ -229,35 +242,35 @@ window.__ModuleLoader__.load({
|
|
|
229
242
|
* @param options - Preset, guard, and steady-rate wiring.
|
|
230
243
|
* @returns The displayed content, revealed at the smoothed cadence.
|
|
231
244
|
*/
|
|
232
|
-
function useSmoothStreamContent(content, { enabled = true, preset = "balanced", shouldHoldBack, steadyCps, defaultCps, speedCpsRef } = {}) {
|
|
245
|
+
function useSmoothStreamContent(content, { enabled = true, inputComplete = false, preset = "balanced", shouldHoldBack, steadyCps, defaultCps, speedCpsRef, revealScaleRef } = {}) {
|
|
233
246
|
const config = PRESET_CONFIG[preset];
|
|
234
247
|
const seedCps = defaultCps ?? config.defaultCps;
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
248
|
+
const initialContent = enabled ? "" : content;
|
|
249
|
+
const [displayedContent, setDisplayedContent] = (0, react.useState)(initialContent);
|
|
250
|
+
const displayedContentRef = (0, react.useRef)(initialContent);
|
|
251
|
+
const displayedCountRef = (0, react.useRef)(countChars(initialContent));
|
|
252
|
+
const targetContentRef = (0, react.useRef)(initialContent);
|
|
253
|
+
const targetCharsRef = (0, react.useRef)([...initialContent]);
|
|
254
|
+
const targetCountRef = (0, react.useRef)(countChars(initialContent));
|
|
241
255
|
const emaCpsRef = (0, react.useRef)(seedCps);
|
|
242
256
|
const lastInputTsRef = (0, react.useRef)(0);
|
|
243
|
-
const lastInputCountRef = (0, react.useRef)(countChars(
|
|
257
|
+
const lastInputCountRef = (0, react.useRef)(countChars(initialContent));
|
|
244
258
|
const chunkSizeEmaRef = (0, react.useRef)(1);
|
|
245
259
|
const arrivalCpsEmaRef = (0, react.useRef)(seedCps);
|
|
246
260
|
const rafRef = (0, react.useRef)(null);
|
|
247
261
|
const lastFrameTsRef = (0, react.useRef)(null);
|
|
248
|
-
const
|
|
262
|
+
const queueDebtRef = (0, react.useRef)(0);
|
|
263
|
+
const settleCpsRef = (0, react.useRef)(null);
|
|
249
264
|
const holdBackRef = (0, react.useRef)(shouldHoldBack);
|
|
250
265
|
const speedOutRef = (0, react.useRef)(speedCpsRef);
|
|
251
266
|
speedOutRef.current = speedCpsRef;
|
|
267
|
+
const revealScaleOutRef = (0, react.useRef)(revealScaleRef);
|
|
268
|
+
revealScaleOutRef.current = revealScaleRef;
|
|
269
|
+
const inputCompleteRef = (0, react.useRef)(inputComplete);
|
|
270
|
+
inputCompleteRef.current = inputComplete;
|
|
252
271
|
(0, react.useEffect)(() => {
|
|
253
272
|
holdBackRef.current = shouldHoldBack;
|
|
254
273
|
}, [shouldHoldBack]);
|
|
255
|
-
const clearWakeTimer = (0, react.useCallback)(() => {
|
|
256
|
-
if (wakeTimerRef.current !== null) {
|
|
257
|
-
clearTimeout(wakeTimerRef.current);
|
|
258
|
-
wakeTimerRef.current = null;
|
|
259
|
-
}
|
|
260
|
-
}, []);
|
|
261
274
|
const stopFrameLoop = (0, react.useCallback)(() => {
|
|
262
275
|
if (rafRef.current !== null) {
|
|
263
276
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -265,20 +278,9 @@ window.__ModuleLoader__.load({
|
|
|
265
278
|
}
|
|
266
279
|
lastFrameTsRef.current = null;
|
|
267
280
|
}, []);
|
|
268
|
-
const stopScheduling = (0, react.useCallback)(() => {
|
|
269
|
-
stopFrameLoop();
|
|
270
|
-
clearWakeTimer();
|
|
271
|
-
}, [clearWakeTimer, stopFrameLoop]);
|
|
272
281
|
const startFrameLoopRef = (0, react.useRef)(() => {});
|
|
273
|
-
const scheduleFrameWake = (0, react.useCallback)((delayMs) => {
|
|
274
|
-
clearWakeTimer();
|
|
275
|
-
wakeTimerRef.current = setTimeout(() => {
|
|
276
|
-
wakeTimerRef.current = null;
|
|
277
|
-
startFrameLoopRef.current();
|
|
278
|
-
}, Math.max(1, Math.ceil(delayMs)));
|
|
279
|
-
}, [clearWakeTimer]);
|
|
280
282
|
const syncImmediate = (0, react.useCallback)((nextContent) => {
|
|
281
|
-
|
|
283
|
+
stopFrameLoop();
|
|
282
284
|
const chars = [...nextContent];
|
|
283
285
|
const now = performance.now();
|
|
284
286
|
targetContentRef.current = nextContent;
|
|
@@ -286,43 +288,64 @@ window.__ModuleLoader__.load({
|
|
|
286
288
|
targetCountRef.current = chars.length;
|
|
287
289
|
displayedContentRef.current = nextContent;
|
|
288
290
|
displayedCountRef.current = chars.length;
|
|
291
|
+
queueDebtRef.current = 0;
|
|
292
|
+
settleCpsRef.current = null;
|
|
293
|
+
const speedOut = speedOutRef.current;
|
|
294
|
+
if (speedOut !== void 0) speedOut.current = seedCps;
|
|
289
295
|
setDisplayedContent(nextContent);
|
|
290
296
|
emaCpsRef.current = seedCps;
|
|
291
297
|
chunkSizeEmaRef.current = 1;
|
|
292
298
|
arrivalCpsEmaRef.current = seedCps;
|
|
293
299
|
lastInputTsRef.current = now;
|
|
294
300
|
lastInputCountRef.current = chars.length;
|
|
295
|
-
}, [seedCps,
|
|
301
|
+
}, [seedCps, stopFrameLoop]);
|
|
302
|
+
(0, react.useLayoutEffect)(() => {
|
|
303
|
+
if (!enabled || !inputComplete || displayedContentRef.current === content) return;
|
|
304
|
+
syncImmediate(content);
|
|
305
|
+
}, [
|
|
306
|
+
content,
|
|
307
|
+
enabled,
|
|
308
|
+
inputComplete,
|
|
309
|
+
syncImmediate
|
|
310
|
+
]);
|
|
296
311
|
const startFrameLoop = (0, react.useCallback)(() => {
|
|
297
|
-
clearWakeTimer();
|
|
298
312
|
if (rafRef.current !== null) return;
|
|
299
|
-
const tick = () => {
|
|
313
|
+
const tick = (now) => {
|
|
300
314
|
const targetCount = targetCountRef.current;
|
|
301
315
|
const displayedCount = displayedCountRef.current;
|
|
302
316
|
const backlog = targetCount - displayedCount;
|
|
303
317
|
if (backlog <= 0) {
|
|
318
|
+
queueDebtRef.current = 0;
|
|
319
|
+
settleCpsRef.current = null;
|
|
320
|
+
const speedOut = speedOutRef.current;
|
|
321
|
+
if (speedOut !== void 0) speedOut.current = seedCps;
|
|
304
322
|
stopFrameLoop();
|
|
305
323
|
return;
|
|
306
324
|
}
|
|
307
|
-
const now = performance.now();
|
|
308
325
|
if (lastFrameTsRef.current === null) {
|
|
309
326
|
lastFrameTsRef.current = now;
|
|
310
327
|
rafRef.current = requestAnimationFrame(tick);
|
|
311
328
|
return;
|
|
312
329
|
}
|
|
313
|
-
const commitIntervalMs = Math.min(MAX_COMMIT_INTERVAL_MS, config.minCommitIntervalMs);
|
|
314
330
|
const frameIntervalMs = Math.max(0, now - lastFrameTsRef.current);
|
|
315
|
-
if (frameIntervalMs < commitIntervalMs) {
|
|
316
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
331
|
const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .12));
|
|
320
332
|
lastFrameTsRef.current = now;
|
|
321
333
|
const idleMs = now - lastInputTsRef.current;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
334
|
+
const producerComplete = inputCompleteRef.current;
|
|
335
|
+
const inputActive = !producerComplete && idleMs <= config.activeInputWindowMs;
|
|
336
|
+
const settling = producerComplete || !inputActive && idleMs >= config.settleAfterMs;
|
|
337
|
+
if (!producerComplete) settleCpsRef.current = null;
|
|
324
338
|
let revealChars;
|
|
325
|
-
|
|
339
|
+
let revealSpeedCps;
|
|
340
|
+
let nextQueueDebt = 0;
|
|
341
|
+
if (producerComplete) {
|
|
342
|
+
const settleCps = settleCpsRef.current ?? computeCompletionDrain(config, backlog);
|
|
343
|
+
settleCpsRef.current = settleCps;
|
|
344
|
+
const accumulated = Math.max(0, queueDebtRef.current) + settleCps * dtSeconds;
|
|
345
|
+
revealChars = Math.min(backlog, Math.floor(accumulated));
|
|
346
|
+
revealSpeedCps = settleCps;
|
|
347
|
+
nextQueueDebt = revealChars >= backlog ? 0 : accumulated - revealChars;
|
|
348
|
+
} else if (steadyCps !== void 0) {
|
|
326
349
|
const step = computeRevealStep(config, {
|
|
327
350
|
backlog,
|
|
328
351
|
chunkSizeEma: chunkSizeEmaRef.current,
|
|
@@ -333,15 +356,23 @@ window.__ModuleLoader__.load({
|
|
|
333
356
|
steadyCps
|
|
334
357
|
}, dtSeconds);
|
|
335
358
|
revealChars = Math.min(step.revealChars, backlog);
|
|
336
|
-
|
|
359
|
+
revealSpeedCps = frameIntervalMs > 0 ? revealChars * 1e3 / frameIntervalMs : 0;
|
|
360
|
+
} else {
|
|
361
|
+
const step = computeAdaptiveQueueStep(backlog, frameIntervalMs, queueDebtRef.current, revealScaleOutRef.current?.current ?? 1);
|
|
362
|
+
revealChars = step.revealChars;
|
|
363
|
+
revealSpeedCps = step.speedCps;
|
|
364
|
+
nextQueueDebt = step.debt;
|
|
365
|
+
}
|
|
337
366
|
if (holdBackRef.current?.() === true) {
|
|
338
367
|
rafRef.current = requestAnimationFrame(tick);
|
|
339
368
|
return;
|
|
340
369
|
}
|
|
370
|
+
queueDebtRef.current = nextQueueDebt;
|
|
341
371
|
const speedOut = speedOutRef.current;
|
|
342
|
-
if (speedOut !== void 0
|
|
343
|
-
|
|
344
|
-
|
|
372
|
+
if (speedOut !== void 0) speedOut.current = revealSpeedCps;
|
|
373
|
+
if (revealChars <= 0) {
|
|
374
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
375
|
+
return;
|
|
345
376
|
}
|
|
346
377
|
const nextCount = displayedCount + revealChars;
|
|
347
378
|
const segment = targetCharsRef.current.slice(displayedCount, nextCount).join("");
|
|
@@ -360,9 +391,7 @@ window.__ModuleLoader__.load({
|
|
|
360
391
|
rafRef.current = requestAnimationFrame(tick);
|
|
361
392
|
}, [
|
|
362
393
|
config,
|
|
363
|
-
scheduleFrameWake,
|
|
364
394
|
stopFrameLoop,
|
|
365
|
-
clearWakeTimer,
|
|
366
395
|
steadyCps
|
|
367
396
|
]);
|
|
368
397
|
(0, react.useEffect)(() => {
|
|
@@ -385,12 +414,13 @@ window.__ModuleLoader__.load({
|
|
|
385
414
|
targetContentRef.current = content;
|
|
386
415
|
targetCharsRef.current.push(...appendedChars);
|
|
387
416
|
targetCountRef.current += appendedCount;
|
|
417
|
+
settleCpsRef.current = null;
|
|
388
418
|
const hadSample = lastInputTsRef.current > 0;
|
|
389
419
|
const deltaChars = targetCountRef.current - lastInputCountRef.current;
|
|
390
420
|
const deltaMs = Math.max(1, now - lastInputTsRef.current);
|
|
391
421
|
if (hadSample && deltaChars > 0) {
|
|
392
422
|
const instantCps = deltaChars * 1e3 / deltaMs;
|
|
393
|
-
const normalizedInstantCps = clamp
|
|
423
|
+
const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 3);
|
|
394
424
|
const chunkEmaAlpha = .45;
|
|
395
425
|
chunkSizeEmaRef.current = chunkSizeEmaRef.current * .55 + appendedCount * chunkEmaAlpha;
|
|
396
426
|
arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * .55 + normalizedInstantCps * chunkEmaAlpha;
|
|
@@ -408,9 +438,9 @@ window.__ModuleLoader__.load({
|
|
|
408
438
|
]);
|
|
409
439
|
(0, react.useEffect)(() => {
|
|
410
440
|
return () => {
|
|
411
|
-
|
|
441
|
+
stopFrameLoop();
|
|
412
442
|
};
|
|
413
|
-
}, [
|
|
443
|
+
}, [stopFrameLoop]);
|
|
414
444
|
return displayedContent;
|
|
415
445
|
}
|
|
416
446
|
//#endregion
|
|
@@ -493,289 +523,503 @@ window.__ModuleLoader__.load({
|
|
|
493
523
|
/**
|
|
494
524
|
* Conversation-port follow while an assistant reply streams.
|
|
495
525
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
526
|
+
* A sub-stepped spring physics engine drives a float `animatedH`, rather
|
|
527
|
+
* than restarting native smooth-scroll animations as every glyph lands.
|
|
528
|
+
* Remaining lag rides a small compositor transform while the real scrollport
|
|
529
|
+
* stays at its floor. The transform is bounded by the measured paint gap
|
|
530
|
+
* before conversation chrome. This follower:
|
|
501
531
|
*
|
|
502
|
-
* -
|
|
532
|
+
* - marks programmatic writes via `data-follow-owned` for compatible hosts;
|
|
503
533
|
* - sets `overflow-anchor: none` so CSS scroll-anchoring does not snap;
|
|
504
534
|
* - restores `animatedH` in a ResizeObserver (before paint) so a layout
|
|
505
535
|
* pass cannot flash a snapped frame;
|
|
506
|
-
* -
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* -
|
|
510
|
-
*
|
|
511
|
-
* -
|
|
512
|
-
* layout gap cannot be consumed by an interpolated transform.
|
|
536
|
+
* - expresses safe lag as a compositor transform on message rows;
|
|
537
|
+
* - opens a speed-adaptive layout runway before fast output wraps, preserving
|
|
538
|
+
* the reference spring constants at every reveal speed;
|
|
539
|
+
* - catches up any lag that cannot fit before turn status / composer chrome,
|
|
540
|
+
* so fixed chrome never has to counter-shift and the host stays at-bottom;
|
|
541
|
+
* - never clips or overlays streamed text.
|
|
513
542
|
*
|
|
514
|
-
* A real reader
|
|
515
|
-
*
|
|
516
|
-
* continuous. Lifecycle completion is different: without a reader gesture,
|
|
517
|
-
* the last owner settles at the floor and preserves bottom-follow.
|
|
543
|
+
* A real reader gesture receives the effective visual position before the
|
|
544
|
+
* transform clears. Lifecycle completion instead settles at the floor.
|
|
518
545
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
546
|
+
* Directional wheel/touch intent unpins immediately; pointer/key input falls
|
|
547
|
+
* back to an upward scroll delta from the engine's own written position. A
|
|
548
|
+
* reader release re-acquires only after returning to the real floor.
|
|
521
549
|
*/
|
|
522
550
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* treat the write as reader input.
|
|
551
|
+
* Programmatic follow marker retained for hosts that recognize external
|
|
552
|
+
* scroll ownership. Current Harness also sees the write land at the floor.
|
|
526
553
|
*/
|
|
527
554
|
const FOLLOW_OWNED_ATTR = "data-follow-owned";
|
|
528
|
-
/**
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
555
|
+
/** Sub-pixel settle threshold; clearing below this cannot produce a visible rebound. */
|
|
556
|
+
const FOLLOW_SETTLE_EPSILON_PX = .25;
|
|
557
|
+
/** Lowest reveal rate retained while the spring is short on paint room. */
|
|
558
|
+
const FOLLOW_REVEAL_MIN_SCALE = .55;
|
|
559
|
+
/** Safe-lag occupancy band over which reveal pressure is progressively reduced. */
|
|
560
|
+
const FOLLOW_BACKPRESSURE_START_RATIO = .25;
|
|
533
561
|
const GESTURE_EVENTS = [
|
|
534
562
|
"wheel",
|
|
563
|
+
"touchstart",
|
|
535
564
|
"touchmove",
|
|
565
|
+
"touchend",
|
|
566
|
+
"touchcancel",
|
|
536
567
|
"pointerdown",
|
|
537
568
|
"keydown"
|
|
538
569
|
];
|
|
539
|
-
|
|
540
|
-
|
|
570
|
+
/** Visible runway needed for the current reveal pressure. */
|
|
571
|
+
function computeFollowReserve(speedCps, runwayPx = 48) {
|
|
572
|
+
const available = Math.max(0, runwayPx);
|
|
573
|
+
if (available <= 0) return 0;
|
|
574
|
+
if (speedCps <= 90) return 0;
|
|
575
|
+
const normalized = Math.min(1, Math.max(0, (speedCps - 90) / 510));
|
|
576
|
+
const minimum = Math.min(available, 16);
|
|
577
|
+
return minimum + normalized * (available - minimum);
|
|
541
578
|
}
|
|
542
579
|
/**
|
|
543
|
-
*
|
|
580
|
+
* Reveal-rate multiplier needed to retain one-wrap headroom for the spring.
|
|
581
|
+
* Throttling starts only after a quarter of the safe transform is occupied;
|
|
582
|
+
* a constrained paint lands at the minimum immediately so the next reveal
|
|
583
|
+
* commit cannot keep feeding an already-full visual buffer.
|
|
584
|
+
*/
|
|
585
|
+
function computeFollowRevealScale(lagPx, capacityPx, constrained = false) {
|
|
586
|
+
if (constrained) return FOLLOW_REVEAL_MIN_SCALE;
|
|
587
|
+
if (!Number.isFinite(capacityPx)) return 1;
|
|
588
|
+
if (capacityPx <= 0) return lagPx > 0 ? FOLLOW_REVEAL_MIN_SCALE : 1;
|
|
589
|
+
const ratio = Math.min(1, Math.max(0, lagPx / capacityPx));
|
|
590
|
+
if (ratio >= .75) return FOLLOW_REVEAL_MIN_SCALE;
|
|
591
|
+
const progress = Math.min(1, Math.max(0, (ratio - FOLLOW_BACKPRESSURE_START_RATIO) / .5));
|
|
592
|
+
return 1 - .44999999999999996 * (progress * progress * (3 - 2 * progress));
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Semi-implicit spring integration with four substeps per <=32ms slice.
|
|
544
596
|
* @param dtMs - Frame delta in ms.
|
|
545
|
-
* @param input - Current lag
|
|
546
|
-
* @returns The
|
|
597
|
+
* @param input - Current visible lag and carried physics velocity.
|
|
598
|
+
* @returns The position advance, its fraction, and next velocity.
|
|
547
599
|
*/
|
|
548
600
|
function computeFollowStep(dtMs, input) {
|
|
549
601
|
if (input.lag <= .1 || dtMs <= 0) return {
|
|
550
602
|
advancePx: 0,
|
|
551
|
-
lerpStep: 0
|
|
603
|
+
lerpStep: 0,
|
|
604
|
+
velocityPxPerSec: 0
|
|
552
605
|
};
|
|
553
|
-
|
|
554
|
-
|
|
606
|
+
let lag = input.lag;
|
|
607
|
+
let velocity = Math.max(0, input.velocityPxPerSec ?? 0);
|
|
608
|
+
const elapsedMs = Math.min(32, dtMs);
|
|
609
|
+
const slices = Math.max(1, Math.ceil(elapsedMs / 32));
|
|
610
|
+
const subDt = elapsedMs / 1e3 / slices / 4;
|
|
611
|
+
for (let slice = 0; slice < slices; slice += 1) for (let substep = 0; substep < 4; substep += 1) {
|
|
612
|
+
const acceleration = (130 * lag - 24 * velocity) / 1;
|
|
613
|
+
velocity = Math.max(0, velocity + acceleration * subDt);
|
|
614
|
+
const advance = velocity * subDt;
|
|
615
|
+
if (advance >= lag) return {
|
|
616
|
+
advancePx: input.lag,
|
|
617
|
+
lerpStep: 1,
|
|
618
|
+
velocityPxPerSec: 0
|
|
619
|
+
};
|
|
620
|
+
lag -= advance;
|
|
621
|
+
}
|
|
622
|
+
const advancePx = input.lag - lag;
|
|
555
623
|
return {
|
|
556
|
-
advancePx
|
|
557
|
-
lerpStep
|
|
624
|
+
advancePx,
|
|
625
|
+
lerpStep: advancePx / input.lag,
|
|
626
|
+
velocityPxPerSec: velocity
|
|
558
627
|
};
|
|
559
628
|
}
|
|
560
|
-
/** The message-rows box the lag transform rides on, when the host has one. */
|
|
561
|
-
function shiftRootOf(port) {
|
|
562
|
-
return port.querySelector("[data-chat-transcript]");
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Row wrappers that carry only messages/tools. Hosts without a transcript
|
|
566
|
-
* box keep the turn-status chrome as a sibling of the rows inside
|
|
567
|
-
* `[data-chat-flow]`, so shifting that whole flow would drag the chrome
|
|
568
|
-
* along; shifting the rows individually leaves every non-message sibling
|
|
569
|
-
* (turn status, steering bubbles) pinned while the text glides.
|
|
570
|
-
*
|
|
571
|
-
* Only the outermost rows are shifted: a tool call nests its subcalls as
|
|
572
|
-
* descendant `[data-chat-anchor-key]` rows, and writing the lag on each of
|
|
573
|
-
* them would double (or further multiply) the shift, tearing the subcalls
|
|
574
|
-
* away from their parent every frame. Descendants ride the parent's
|
|
575
|
-
* transform instead.
|
|
576
|
-
*/
|
|
577
|
-
function shiftRowsOf(port) {
|
|
578
|
-
return [...port.querySelectorAll("[data-chat-anchor-key]")].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
|
|
579
|
-
}
|
|
580
629
|
/** Element whose resize signals flow growth for the before-paint restore. */
|
|
581
630
|
function resizeProxyOf(port) {
|
|
582
631
|
return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
|
|
583
632
|
}
|
|
584
|
-
/**
|
|
585
|
-
function
|
|
586
|
-
|
|
633
|
+
/** Outermost message surfaces; nested tool rows ride their parent. */
|
|
634
|
+
function shiftSurfacesOf(port) {
|
|
635
|
+
const transcript = port.querySelector("[data-chat-transcript]");
|
|
636
|
+
if (transcript !== null) return [transcript];
|
|
637
|
+
return [...port.querySelectorAll("[data-chat-anchor-key]")].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
|
|
587
638
|
}
|
|
588
|
-
/** Cached natural gap from a shifted surface's bottom to the status line. */
|
|
589
|
-
const statusGapCache = /* @__PURE__ */ new WeakMap();
|
|
590
639
|
function currentShiftOf(element) {
|
|
591
|
-
return Number(/translate3d\(0
|
|
592
|
-
}
|
|
593
|
-
function statusGapBelow(surface, chrome) {
|
|
594
|
-
const cached = statusGapCache.get(surface);
|
|
595
|
-
if (cached?.chrome === chrome) return cached.px;
|
|
596
|
-
const naturalBottom = surface.getBoundingClientRect().bottom - currentShiftOf(surface);
|
|
597
|
-
const px = Math.max(0, chrome.getBoundingClientRect().top - naturalBottom);
|
|
598
|
-
statusGapCache.set(surface, {
|
|
599
|
-
chrome,
|
|
600
|
-
px
|
|
601
|
-
});
|
|
602
|
-
return px;
|
|
640
|
+
return Number(/translate3d\(0(?:px)?,\s*(-?[\d.]+)px,\s*0(?:px)?\)/.exec(element.style.transform)?.[1] ?? 0);
|
|
603
641
|
}
|
|
604
|
-
function setShift(element, px
|
|
642
|
+
function setShift(element, px) {
|
|
605
643
|
if (Math.abs(px) > .01) {
|
|
644
|
+
if (Math.abs(currentShiftOf(element) - px) <= .01 && element.style.willChange === "transform" && element.style.clipPath === "") return;
|
|
606
645
|
element.style.transform = `translate3d(0, ${px}px, 0)`;
|
|
607
646
|
element.style.willChange = "transform";
|
|
608
|
-
const bottomClip = paintAllowance === void 0 ? 0 : Math.max(0, px - paintAllowance);
|
|
609
|
-
element.style.clipPath = bottomClip > .01 ? `inset(0 0 ${bottomClip}px 0)` : "";
|
|
610
647
|
} else {
|
|
648
|
+
if (element.style.transform === "" && element.style.willChange === "" && element.style.clipPath === "") return;
|
|
611
649
|
element.style.transform = "";
|
|
612
650
|
element.style.willChange = "";
|
|
613
|
-
element.style.clipPath = "";
|
|
614
651
|
}
|
|
652
|
+
element.style.clipPath = "";
|
|
615
653
|
}
|
|
616
|
-
|
|
617
|
-
* The running-turn label. It stays in normal flow so the host column's gap
|
|
618
|
-
* always separates it from the transcript.
|
|
619
|
-
*/
|
|
620
|
-
function statusChromeOf(port) {
|
|
654
|
+
function turnStatusOf(port) {
|
|
621
655
|
return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
|
|
622
656
|
}
|
|
657
|
+
/** Height committed by one newly mounted Chat row, including its flex gap. */
|
|
658
|
+
function entranceExtentOf(root) {
|
|
659
|
+
const row = root.closest("[data-chat-flow-key]") ?? root;
|
|
660
|
+
const rect = row.getBoundingClientRect();
|
|
661
|
+
const height = Math.max(0, rect.height, rect.bottom - rect.top, row.offsetHeight);
|
|
662
|
+
let previous = row.previousElementSibling;
|
|
663
|
+
while (previous instanceof HTMLElement) {
|
|
664
|
+
const previousRect = previous.getBoundingClientRect();
|
|
665
|
+
if (previousRect.height > 0 || previousRect.bottom > previousRect.top) return Math.max(height, rect.bottom - previousRect.bottom);
|
|
666
|
+
previous = previous.previousElementSibling;
|
|
667
|
+
}
|
|
668
|
+
return height;
|
|
669
|
+
}
|
|
623
670
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* transcript box). Before that, message rows and status chrome remain in
|
|
628
|
-
* normal flow. Without any shift surface the engine carries the interpolated
|
|
629
|
-
* top itself.
|
|
671
|
+
* The plugin can be reinjected without replacing the conversation DOM. Keep
|
|
672
|
+
* runway ownership in the page realm so a fresh bundle adopts the existing
|
|
673
|
+
* margin instead of treating it as host layout and adding another 48px.
|
|
630
674
|
*/
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
port
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
675
|
+
const FOLLOW_RUNWAYS_SYMBOL = Symbol.for("dsh-smooth-stream.follow-runways");
|
|
676
|
+
const followRunwayRegistry = globalThis;
|
|
677
|
+
const followRunways = followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] ?? /* @__PURE__ */ new WeakMap();
|
|
678
|
+
followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
|
|
679
|
+
const followPaintLimits = /* @__PURE__ */ new WeakMap();
|
|
680
|
+
function invalidatePaintLimit(port) {
|
|
681
|
+
followPaintLimits.delete(port);
|
|
682
|
+
}
|
|
683
|
+
/** Logical position and velocity survive a React owner handoff and finish. */
|
|
684
|
+
const followMotionStates = /* @__PURE__ */ new WeakMap();
|
|
685
|
+
function restoreRunway(port) {
|
|
686
|
+
const runway = followRunways.get(port);
|
|
687
|
+
if (runway === void 0) return;
|
|
688
|
+
runway.element.style[runway.property] = runway.original;
|
|
689
|
+
followRunways.delete(port);
|
|
690
|
+
invalidatePaintLimit(port);
|
|
691
|
+
}
|
|
692
|
+
function isLegacyRunway(value) {
|
|
693
|
+
if (value === "") return false;
|
|
694
|
+
const terms = [...value.matchAll(/([\d.]+)px/g)];
|
|
695
|
+
if (terms.length === 0 || value.replaceAll(/calc|px|[\d.+()\s]/g, "") !== "") return false;
|
|
696
|
+
return terms.every(([, raw]) => {
|
|
697
|
+
const px = Number(raw);
|
|
698
|
+
return Number.isFinite(px) && px >= 48 && Math.abs(px % 48) <= Number.EPSILON;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/** Remove unowned runway residue written by v0.3.3 and earlier bundles. */
|
|
702
|
+
function migrateLegacyRunway(port, surfaces, status, composer) {
|
|
703
|
+
if (followRunways.has(port)) return;
|
|
704
|
+
let migrated = false;
|
|
705
|
+
if (status !== null && isLegacyRunway(status.style.marginTop)) {
|
|
706
|
+
status.style.marginTop = "";
|
|
707
|
+
migrated = true;
|
|
708
|
+
}
|
|
709
|
+
const last = surfaces.at(-1);
|
|
710
|
+
if (status === null && composer !== null && last !== void 0 && isLegacyRunway(last.style.marginBottom)) {
|
|
711
|
+
last.style.marginBottom = "";
|
|
712
|
+
migrated = true;
|
|
713
|
+
}
|
|
714
|
+
if (migrated) invalidatePaintLimit(port);
|
|
715
|
+
}
|
|
716
|
+
function ensureRunway(port, surfaces) {
|
|
717
|
+
const status = turnStatusOf(port);
|
|
718
|
+
const composer = port.querySelector("[data-composer-seat]");
|
|
719
|
+
migrateLegacyRunway(port, surfaces, status, composer);
|
|
720
|
+
const naturalHeight = Math.max(0, port.scrollHeight - runwayOffsetOf(port));
|
|
721
|
+
if (port.clientHeight <= 0 || naturalHeight <= port.clientHeight) {
|
|
722
|
+
restoreRunway(port);
|
|
651
723
|
return;
|
|
652
724
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
port
|
|
662
|
-
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
663
|
-
const last = rows.length - 1;
|
|
664
|
-
for (const [index, row] of rows.entries()) setShift(row, lag, index === last && chrome !== null ? statusGapBelow(row, chrome) : void 0);
|
|
725
|
+
const target = status === null ? {
|
|
726
|
+
element: composer === null ? void 0 : surfaces.at(-1),
|
|
727
|
+
property: "marginBottom"
|
|
728
|
+
} : {
|
|
729
|
+
element: status,
|
|
730
|
+
property: "marginTop"
|
|
731
|
+
};
|
|
732
|
+
if (target.element === void 0) {
|
|
733
|
+
restoreRunway(port);
|
|
665
734
|
return;
|
|
666
735
|
}
|
|
667
|
-
|
|
668
|
-
|
|
736
|
+
const element = target.element;
|
|
737
|
+
const current = followRunways.get(port);
|
|
738
|
+
if (current?.element === element && current.property === target.property) return;
|
|
739
|
+
restoreRunway(port);
|
|
740
|
+
const beforeHeight = port.scrollHeight;
|
|
741
|
+
const original = element.style[target.property];
|
|
742
|
+
element.style[target.property] = original === "" ? `48px` : `calc(${original} + 48px)`;
|
|
743
|
+
const offset = Math.max(0, port.scrollHeight - beforeHeight);
|
|
744
|
+
followRunways.set(port, {
|
|
745
|
+
element,
|
|
746
|
+
offset,
|
|
747
|
+
property: target.property,
|
|
748
|
+
original
|
|
749
|
+
});
|
|
750
|
+
invalidatePaintLimit(port);
|
|
751
|
+
}
|
|
752
|
+
function runwayOffsetOf(port) {
|
|
753
|
+
return followRunways.get(port)?.offset ?? 0;
|
|
754
|
+
}
|
|
755
|
+
/** Available paint room below the last message before fixed conversation chrome. */
|
|
756
|
+
function safeShiftLimit(port, surfaces) {
|
|
757
|
+
const last = surfaces.at(-1);
|
|
758
|
+
if (last === void 0) return 0;
|
|
759
|
+
const status = turnStatusOf(port);
|
|
760
|
+
const composer = port.querySelector("[data-composer-seat]");
|
|
761
|
+
const cached = followPaintLimits.get(port);
|
|
762
|
+
if (cached !== void 0 && performance.now() - cached.measuredAtMs <= 250 && cached.clientHeight === port.clientHeight && cached.surface === last && cached.status === status && cached.composer === composer) return cached.limit;
|
|
763
|
+
const ceiling = [status, composer].filter((element) => element !== null).map((element) => ({
|
|
764
|
+
element,
|
|
765
|
+
rect: element.getBoundingClientRect()
|
|
766
|
+
})).filter(({ rect }) => Number.isFinite(rect.top) && Number.isFinite(rect.bottom) && rect.bottom > rect.top).sort((first, second) => first.rect.top - second.rect.top)[0];
|
|
767
|
+
if (ceiling === void 0) return status === null && composer === null ? Number.POSITIVE_INFINITY : runwayOffsetOf(port);
|
|
768
|
+
const ceilingTop = ceiling.rect.top - currentShiftOf(ceiling.element);
|
|
769
|
+
const naturalBottom = last.getBoundingClientRect().bottom - currentShiftOf(last);
|
|
770
|
+
const limit = Math.max(0, ceilingTop - naturalBottom - 1);
|
|
771
|
+
followPaintLimits.set(port, {
|
|
772
|
+
clientHeight: port.clientHeight,
|
|
773
|
+
limit,
|
|
774
|
+
measuredAtMs: performance.now(),
|
|
775
|
+
composer,
|
|
776
|
+
status,
|
|
777
|
+
surface: last
|
|
778
|
+
});
|
|
779
|
+
return limit;
|
|
780
|
+
}
|
|
781
|
+
function setFollowScrollTop(port, nextTop) {
|
|
782
|
+
if (Math.abs(port.scrollTop - nextTop) > .01) port.scrollTop = nextTop;
|
|
783
|
+
followScrollLedgers.set(port, port.scrollTop);
|
|
784
|
+
const ownedTop = String(port.scrollTop);
|
|
785
|
+
if (port.getAttribute(FOLLOW_OWNED_ATTR) !== ownedTop) port.setAttribute(FOLLOW_OWNED_ATTR, ownedTop);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Last scrollTop this engine wrote or accepted, per port. Reader intent is a
|
|
789
|
+
* real upward delta from this ledger; a key press or touch while pinned
|
|
790
|
+
* (typing in the composer) must not release the pin, because a released pin
|
|
791
|
+
* can never re-acquire while content streams away from the reader position.
|
|
792
|
+
*/
|
|
793
|
+
const followScrollLedgers = /* @__PURE__ */ new WeakMap();
|
|
794
|
+
const followActivityAt = /* @__PURE__ */ new WeakMap();
|
|
795
|
+
/** Whether this port was owned recently enough to identify a closing tail row. */
|
|
796
|
+
function hasRecentConversationFollow(port, windowMs = 250) {
|
|
797
|
+
const last = followActivityAt.get(port);
|
|
798
|
+
return last !== void 0 && performance.now() - last <= windowMs;
|
|
799
|
+
}
|
|
800
|
+
function readerScrolledUp(port) {
|
|
801
|
+
return port.scrollTop < (followScrollLedgers.get(port) ?? 0) - 8;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Paint a bounded visual lag and return the effective logical extent.
|
|
805
|
+
*
|
|
806
|
+
* This is the final geometry invariant, not merely an animation preference:
|
|
807
|
+
* any lag beyond the real gap to status/composer chrome is caught up in the
|
|
808
|
+
* same frame. Carrying that excess in `scrollTop` would move the transcript
|
|
809
|
+
* toward fixed chrome and also make the host expose jump-to-bottom.
|
|
810
|
+
*/
|
|
811
|
+
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0) {
|
|
812
|
+
const surfaces = shiftSurfacesOf(port);
|
|
813
|
+
ensureRunway(port, surfaces);
|
|
814
|
+
const contentHeight = Math.max(0, port.scrollHeight);
|
|
815
|
+
const runwayOffset = runwayOffsetOf(port);
|
|
816
|
+
const targetHeight = Math.max(0, contentHeight - runwayOffset);
|
|
817
|
+
const floor = Math.max(0, contentHeight - port.clientHeight);
|
|
818
|
+
const extent = Math.min(targetHeight, Math.max(0, animatedH));
|
|
819
|
+
if (port.style.overflowAnchor !== "none") port.style.overflowAnchor = "none";
|
|
820
|
+
if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
|
|
821
|
+
if (floor <= 0) {
|
|
822
|
+
setFollowScrollTop(port, 0);
|
|
823
|
+
followMotionStates.set(port, {
|
|
824
|
+
capacityPx: Number.POSITIVE_INFINITY,
|
|
825
|
+
constrained: false,
|
|
826
|
+
extent: targetHeight,
|
|
827
|
+
lagPx: 0,
|
|
828
|
+
reservePx: 0,
|
|
829
|
+
velocityPxPerSec: 0
|
|
830
|
+
});
|
|
831
|
+
for (const surface of surfaces) setShift(surface, 0);
|
|
832
|
+
const status = turnStatusOf(port);
|
|
833
|
+
if (status !== null) setShift(status, 0);
|
|
834
|
+
return targetHeight;
|
|
835
|
+
}
|
|
836
|
+
setFollowScrollTop(port, floor);
|
|
837
|
+
const limit = floor > 0 ? safeShiftLimit(port, surfaces) : 0;
|
|
838
|
+
const visibleReserve = Math.min(runwayOffset, Math.max(0, reservePx));
|
|
839
|
+
const baselineShift = runwayOffset - visibleReserve;
|
|
840
|
+
const requestedLag = Math.max(0, targetHeight - extent);
|
|
841
|
+
const shift = Math.min(baselineShift + requestedLag, Math.max(0, limit));
|
|
842
|
+
const effectiveLag = Math.max(0, shift - baselineShift);
|
|
843
|
+
const capacityPx = Math.max(0, limit - baselineShift);
|
|
844
|
+
const effectiveExtent = targetHeight - effectiveLag;
|
|
845
|
+
followMotionStates.set(port, {
|
|
846
|
+
capacityPx,
|
|
847
|
+
constrained: requestedLag > effectiveLag + FOLLOW_SETTLE_EPSILON_PX,
|
|
848
|
+
extent: effectiveExtent,
|
|
849
|
+
lagPx: effectiveLag,
|
|
850
|
+
reservePx: visibleReserve,
|
|
851
|
+
velocityPxPerSec
|
|
852
|
+
});
|
|
853
|
+
for (const surface of surfaces) setShift(surface, shift);
|
|
854
|
+
const status = turnStatusOf(port);
|
|
855
|
+
if (status !== null) setShift(status, 0);
|
|
856
|
+
return effectiveExtent;
|
|
857
|
+
}
|
|
858
|
+
function clearMotion(port) {
|
|
859
|
+
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
860
|
+
port.style.overflowAnchor = "";
|
|
861
|
+
port.style.scrollBehavior = "";
|
|
862
|
+
for (const surface of shiftSurfacesOf(port)) setShift(surface, 0);
|
|
863
|
+
const status = turnStatusOf(port);
|
|
864
|
+
if (status !== null) setShift(status, 0);
|
|
669
865
|
}
|
|
670
866
|
function clearVisual(port) {
|
|
867
|
+
clearMotion(port);
|
|
868
|
+
restoreRunway(port);
|
|
869
|
+
followMotionStates.delete(port);
|
|
870
|
+
invalidatePaintLimit(port);
|
|
871
|
+
}
|
|
872
|
+
/** Keep an already-promoted surface at zero until one stable final paint lands. */
|
|
873
|
+
function holdCompositorAtRest(element) {
|
|
874
|
+
element.style.transform = "translate3d(0, 0px, 0)";
|
|
875
|
+
element.style.willChange = "transform";
|
|
876
|
+
element.style.clipPath = "";
|
|
877
|
+
}
|
|
878
|
+
/** Remove equal offsets, land on the floor, then retire the compositor quietly. */
|
|
879
|
+
function finishAtNaturalFloor(port) {
|
|
880
|
+
const surfaces = shiftSurfacesOf(port);
|
|
881
|
+
const status = turnStatusOf(port);
|
|
882
|
+
const promoted = [...surfaces, ...status === null ? [] : [status]].filter((element) => element.style.transform !== "" || element.style.willChange === "transform");
|
|
883
|
+
const promotedSet = new Set(promoted);
|
|
884
|
+
restoreRunway(port);
|
|
885
|
+
settleAtFloor(port);
|
|
671
886
|
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
672
887
|
port.style.overflowAnchor = "";
|
|
673
888
|
port.style.scrollBehavior = "";
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
shiftRoot.style.clipPath = "";
|
|
680
|
-
} else for (const row of shiftRowsOf(port)) {
|
|
681
|
-
statusGapCache.delete(row);
|
|
682
|
-
row.style.transform = "";
|
|
683
|
-
row.style.willChange = "";
|
|
684
|
-
row.style.clipPath = "";
|
|
685
|
-
}
|
|
686
|
-
const chrome = statusChromeOf(port);
|
|
687
|
-
if (chrome !== null) {
|
|
688
|
-
chrome.style.transform = "";
|
|
689
|
-
chrome.style.willChange = "";
|
|
690
|
-
chrome.style.clipPath = "";
|
|
889
|
+
for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
|
|
890
|
+
else setShift(surface, 0);
|
|
891
|
+
if (status !== null) {
|
|
892
|
+
if (promotedSet.has(status)) holdCompositorAtRest(status);
|
|
893
|
+
else setShift(status, 0);
|
|
691
894
|
}
|
|
895
|
+
followMotionStates.delete(port);
|
|
896
|
+
if (promoted.length === 0) return;
|
|
897
|
+
requestAnimationFrame(() => {
|
|
898
|
+
requestAnimationFrame(() => {
|
|
899
|
+
if (port.hasAttribute(FOLLOW_OWNED_ATTR)) return;
|
|
900
|
+
for (const element of promoted) if (Math.abs(currentShiftOf(element)) <= .01) setShift(element, 0);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
692
903
|
}
|
|
693
904
|
function settleAtFloor(port) {
|
|
694
|
-
port
|
|
695
|
-
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
696
|
-
}
|
|
697
|
-
/** Live follow hosts per conversation port so one unmount does not clear another. */
|
|
698
|
-
const followOwners = /* @__PURE__ */ new WeakMap();
|
|
699
|
-
function acquireFollow(port) {
|
|
700
|
-
followOwners.set(port, (followOwners.get(port) ?? 0) + 1);
|
|
701
|
-
}
|
|
702
|
-
function releaseFollow(port) {
|
|
703
|
-
const next = (followOwners.get(port) ?? 1) - 1;
|
|
704
|
-
if (next <= 0) {
|
|
705
|
-
followOwners.delete(port);
|
|
706
|
-
return 0;
|
|
707
|
-
}
|
|
708
|
-
followOwners.set(port, next);
|
|
709
|
-
return next;
|
|
905
|
+
setFollowScrollTop(port, Math.max(0, port.scrollHeight - port.clientHeight));
|
|
710
906
|
}
|
|
907
|
+
/** Only the newest active follower may write one port's shared visual state. */
|
|
908
|
+
const followLeaders = /* @__PURE__ */ new WeakMap();
|
|
909
|
+
let followGeneration = 0;
|
|
711
910
|
/**
|
|
712
911
|
* Own the conversation scrollport's bottom-follow while `active` is true.
|
|
713
912
|
*
|
|
714
913
|
* @param rootRef - An element inside the conversation scrollport.
|
|
715
914
|
* @param active - True while the reply is still revealing.
|
|
716
915
|
* @param speedCpsRef - Live reveal-rate EMA from the smoother.
|
|
916
|
+
* @param revealScaleRef - Optional backpressure control for text reveal.
|
|
917
|
+
* @param predictive - Whether to reserve paint room ahead of growth.
|
|
918
|
+
* @param entrance - Whether the first committed row height should glide in.
|
|
919
|
+
* @param onEntranceSettled - Releases a one-shot entrance owner after catch-up.
|
|
920
|
+
* @param predictiveRef - Optional live visibility gate for predictive runway.
|
|
921
|
+
* @param entranceExtentRef - Optional measured growth delta for a generic row.
|
|
717
922
|
*/
|
|
718
|
-
function useConversationFollow(rootRef, active, speedCpsRef) {
|
|
923
|
+
function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef) {
|
|
719
924
|
const activeRef = (0, react.useRef)(active);
|
|
925
|
+
const entranceRef = (0, react.useRef)(entrance);
|
|
926
|
+
const onEntranceSettledRef = (0, react.useRef)(onEntranceSettled);
|
|
927
|
+
entranceRef.current = entrance;
|
|
928
|
+
onEntranceSettledRef.current = onEntranceSettled;
|
|
720
929
|
(0, react.useEffect)(() => {
|
|
721
930
|
activeRef.current = active;
|
|
722
931
|
}, [active]);
|
|
723
|
-
(0, react.
|
|
932
|
+
(0, react.useLayoutEffect)(() => {
|
|
724
933
|
if (!active) return;
|
|
934
|
+
const owner = {};
|
|
935
|
+
const generation = ++followGeneration;
|
|
725
936
|
let rafId = 0;
|
|
726
937
|
let last = performance.now();
|
|
727
938
|
let following = true;
|
|
728
939
|
let primed = false;
|
|
729
940
|
let animatedH = 0;
|
|
941
|
+
let reservePx = 0;
|
|
942
|
+
let velocityPxPerSec = 0;
|
|
730
943
|
let interacting = false;
|
|
944
|
+
let readerGestureIntent = false;
|
|
945
|
+
let readerReleased = false;
|
|
946
|
+
let touchStartY = null;
|
|
731
947
|
let interactTimer = null;
|
|
732
948
|
let port = null;
|
|
733
949
|
let resize = null;
|
|
734
950
|
let holding = null;
|
|
735
|
-
let
|
|
951
|
+
let entrancePending = entranceRef.current;
|
|
952
|
+
const finishEntrance = () => {
|
|
953
|
+
if (!entrancePending) return;
|
|
954
|
+
entrancePending = false;
|
|
955
|
+
onEntranceSettledRef.current?.();
|
|
956
|
+
};
|
|
957
|
+
const updateRevealScale = (next, elapsedMs, urgent = false) => {
|
|
958
|
+
if (revealScaleRef === void 0) return;
|
|
959
|
+
const state = followMotionStates.get(next);
|
|
960
|
+
const target = state === void 0 ? 1 : computeFollowRevealScale(state.lagPx, state.capacityPx, state.constrained);
|
|
961
|
+
const current = Math.min(1, Math.max(FOLLOW_REVEAL_MIN_SCALE, revealScaleRef.current));
|
|
962
|
+
if (target < current || urgent) {
|
|
963
|
+
revealScaleRef.current = Math.min(current, target);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const releaseStep = 1 - Math.exp(-Math.max(0, elapsedMs) / 240);
|
|
967
|
+
revealScaleRef.current = current + (target - current) * releaseStep;
|
|
968
|
+
};
|
|
969
|
+
const releaseRevealScale = () => {
|
|
970
|
+
if (revealScaleRef !== void 0) revealScaleRef.current = 1;
|
|
971
|
+
};
|
|
972
|
+
const isLeader = (next) => followLeaders.get(next)?.owner === owner;
|
|
736
973
|
const hold = (next) => {
|
|
737
|
-
|
|
738
|
-
if (holding
|
|
739
|
-
acquireFollow(next);
|
|
974
|
+
followActivityAt.set(next, performance.now());
|
|
975
|
+
if (holding === next && isLeader(next)) return;
|
|
740
976
|
holding = next;
|
|
977
|
+
const leader = followLeaders.get(next);
|
|
978
|
+
if (leader === void 0 || generation > leader.generation) followLeaders.set(next, {
|
|
979
|
+
generation,
|
|
980
|
+
owner
|
|
981
|
+
});
|
|
741
982
|
};
|
|
742
983
|
const drop = (next) => {
|
|
743
|
-
if (holding === next)
|
|
744
|
-
|
|
745
|
-
|
|
984
|
+
if (holding === next) holding = null;
|
|
985
|
+
if (isLeader(next)) {
|
|
986
|
+
clearMotion(next);
|
|
987
|
+
followMotionStates.delete(next);
|
|
988
|
+
releaseRevealScale();
|
|
746
989
|
}
|
|
747
|
-
clearVisual(next);
|
|
748
990
|
};
|
|
749
|
-
/**
|
|
750
|
-
* Give the reader the visual position the glide was showing before the
|
|
751
|
-
* transforms go away. While transforms carry the lag, the engine sits at
|
|
752
|
-
* the floor and the effective visual top is `engine - lag`; writing that
|
|
753
|
-
* keeps the handover frame-continuous, and the write shows up in
|
|
754
|
-
* ChatView's ledger as reader movement, disarming its snap-follow.
|
|
755
|
-
* Without a shift surface the engine already holds the interpolated top,
|
|
756
|
-
* so there is nothing to compensate.
|
|
757
|
-
*/
|
|
758
991
|
const handBackVisual = (next) => {
|
|
759
|
-
|
|
992
|
+
const shift = currentShiftOf(shiftSurfacesOf(next).at(-1) ?? next);
|
|
993
|
+
const visualTop = Math.max(0, next.scrollTop - Math.max(0, shift));
|
|
760
994
|
const floor = Math.max(0, next.scrollHeight - next.clientHeight);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
next.scrollTop = Math.min(floor, Math.max(0, next.scrollTop - lag));
|
|
995
|
+
next.scrollTop = Math.min(visualTop, Math.max(0, floor - 26));
|
|
996
|
+
followScrollLedgers.set(next, next.scrollTop);
|
|
764
997
|
};
|
|
765
998
|
const markGesture = (event) => {
|
|
766
999
|
interacting = true;
|
|
767
|
-
if (event
|
|
768
|
-
|
|
1000
|
+
if (event.type === "wheel") {
|
|
1001
|
+
const deltaY = event.deltaY;
|
|
1002
|
+
if (Number.isFinite(deltaY) && deltaY < 0) readerGestureIntent = true;
|
|
1003
|
+
} else if (event.type === "touchstart") touchStartY = event.touches[0]?.clientY ?? null;
|
|
1004
|
+
else if (event.type === "touchmove") {
|
|
1005
|
+
const touch = event.touches[0];
|
|
1006
|
+
if (touch !== void 0) {
|
|
1007
|
+
if (touchStartY === null) touchStartY = touch.clientY;
|
|
1008
|
+
if (touch.clientY - touchStartY > 1) readerGestureIntent = true;
|
|
1009
|
+
}
|
|
1010
|
+
} else if (event.type === "touchend" || event.type === "touchcancel") touchStartY = null;
|
|
769
1011
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
770
1012
|
interactTimer = setTimeout(() => {
|
|
771
1013
|
interacting = false;
|
|
1014
|
+
readerGestureIntent = false;
|
|
772
1015
|
interactTimer = null;
|
|
773
|
-
awayPx = 0;
|
|
774
1016
|
}, 800);
|
|
775
1017
|
};
|
|
776
1018
|
const restoreBeforePaint = () => {
|
|
777
|
-
if (!following || port === null) return;
|
|
778
|
-
|
|
1019
|
+
if (!following || port === null || !isLeader(port)) return;
|
|
1020
|
+
invalidatePaintLimit(port);
|
|
1021
|
+
animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec);
|
|
1022
|
+
updateRevealScale(port, 0, true);
|
|
779
1023
|
};
|
|
780
1024
|
const bindPort = (next) => {
|
|
781
1025
|
if (port === next) return;
|
|
@@ -784,6 +1028,7 @@ window.__ModuleLoader__.load({
|
|
|
784
1028
|
resize?.disconnect();
|
|
785
1029
|
}
|
|
786
1030
|
port = next;
|
|
1031
|
+
invalidatePaintLimit(port);
|
|
787
1032
|
for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
|
|
788
1033
|
if (typeof ResizeObserver !== "undefined") {
|
|
789
1034
|
resize = new ResizeObserver(restoreBeforePaint);
|
|
@@ -794,48 +1039,96 @@ window.__ModuleLoader__.load({
|
|
|
794
1039
|
};
|
|
795
1040
|
const frame = (now) => {
|
|
796
1041
|
rafId = requestAnimationFrame(frame);
|
|
797
|
-
const
|
|
1042
|
+
const elapsedMs = Math.max(.001, now - last);
|
|
1043
|
+
const dt = Math.min(32, elapsedMs);
|
|
798
1044
|
last = now;
|
|
799
1045
|
const root = rootRef.current;
|
|
800
1046
|
if (root === null) return;
|
|
801
1047
|
const nextPort = root.closest("[data-conversation-scroll]");
|
|
802
1048
|
if (nextPort === null) return;
|
|
803
1049
|
bindPort(nextPort);
|
|
1050
|
+
if (nextPort.clientHeight <= 0) return;
|
|
804
1051
|
const reportedLag = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight) - nextPort.scrollTop;
|
|
805
1052
|
const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
|
|
806
1053
|
if (!primed) {
|
|
807
|
-
|
|
808
|
-
|
|
1054
|
+
const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
|
|
1055
|
+
if (inherited === void 0) {
|
|
1056
|
+
const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
|
|
1057
|
+
animatedH = Math.max(0, nextPort.scrollHeight - entranceExtent);
|
|
1058
|
+
reservePx = 0;
|
|
1059
|
+
velocityPxPerSec = 0;
|
|
1060
|
+
following = Math.max(0, reportedLag - entranceExtent) <= 25;
|
|
1061
|
+
} else {
|
|
1062
|
+
animatedH = Math.min(nextPort.scrollHeight, inherited.extent);
|
|
1063
|
+
reservePx = inherited.reservePx;
|
|
1064
|
+
velocityPxPerSec = inherited.velocityPxPerSec;
|
|
1065
|
+
following = true;
|
|
1066
|
+
}
|
|
809
1067
|
if (following) {
|
|
810
1068
|
hold(nextPort);
|
|
811
|
-
|
|
812
|
-
|
|
1069
|
+
if (isLeader(nextPort)) {
|
|
1070
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec);
|
|
1071
|
+
updateRevealScale(nextPort, elapsedMs);
|
|
1072
|
+
const runwayOffset = runwayOffsetOf(nextPort);
|
|
1073
|
+
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
|
|
1074
|
+
} else finishEntrance();
|
|
1075
|
+
} else finishEntrance();
|
|
813
1076
|
primed = true;
|
|
814
1077
|
return;
|
|
815
1078
|
}
|
|
816
|
-
if (!following && !interacting && reportedLag <= 25) {
|
|
1079
|
+
if (!following && !interacting && reportedLag <= (readerReleased ? 1 : 25)) {
|
|
817
1080
|
following = true;
|
|
1081
|
+
readerReleased = false;
|
|
818
1082
|
animatedH = extent;
|
|
1083
|
+
reservePx = 0;
|
|
1084
|
+
velocityPxPerSec = 0;
|
|
1085
|
+
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
819
1086
|
hold(nextPort);
|
|
820
|
-
} else if (following && interacting &&
|
|
1087
|
+
} else if (following && interacting && (readerGestureIntent || readerScrolledUp(nextPort))) {
|
|
821
1088
|
following = false;
|
|
822
|
-
|
|
1089
|
+
readerGestureIntent = false;
|
|
1090
|
+
readerReleased = true;
|
|
823
1091
|
handBackVisual(nextPort);
|
|
824
1092
|
animatedH = nextPort.scrollHeight;
|
|
1093
|
+
reservePx = 0;
|
|
1094
|
+
velocityPxPerSec = 0;
|
|
825
1095
|
drop(nextPort);
|
|
1096
|
+
finishEntrance();
|
|
1097
|
+
}
|
|
1098
|
+
if (!activeRef.current || !following) {
|
|
1099
|
+
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
1100
|
+
return;
|
|
826
1101
|
}
|
|
827
|
-
if (!activeRef.current || !following) return;
|
|
828
1102
|
hold(nextPort);
|
|
829
|
-
|
|
1103
|
+
if (!isLeader(nextPort)) {
|
|
1104
|
+
finishEntrance();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const runwayOffset = runwayOffsetOf(nextPort);
|
|
1108
|
+
const contentHeight = nextPort.scrollHeight;
|
|
1109
|
+
const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
|
|
1110
|
+
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1111
|
+
const reserveTarget = !predictGrowth ? 0 : computeFollowReserve(speedCpsRef.current, runwayOffset);
|
|
1112
|
+
const reserveStep = 1 - Math.exp(-elapsedMs / 180);
|
|
1113
|
+
reservePx += (reserveTarget - reservePx) * reserveStep;
|
|
830
1114
|
const step = computeFollowStep(dt, {
|
|
831
1115
|
lag,
|
|
832
|
-
speedEma: speedCpsRef.current
|
|
1116
|
+
speedEma: speedCpsRef.current,
|
|
1117
|
+
velocityPxPerSec
|
|
833
1118
|
});
|
|
834
|
-
if (lag <= .1)
|
|
835
|
-
|
|
836
|
-
|
|
1119
|
+
if (lag <= .1) {
|
|
1120
|
+
animatedH = contentHeight - runwayOffset;
|
|
1121
|
+
velocityPxPerSec = 0;
|
|
1122
|
+
} else {
|
|
1123
|
+
const minimumLag = predictGrowth ? 0 : Math.max(0, reservePx);
|
|
1124
|
+
animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
|
|
1125
|
+
velocityPxPerSec = step.velocityPxPerSec;
|
|
1126
|
+
}
|
|
1127
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec);
|
|
1128
|
+
updateRevealScale(nextPort, elapsedMs);
|
|
1129
|
+
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
|
|
837
1130
|
};
|
|
838
|
-
|
|
1131
|
+
frame(performance.now());
|
|
839
1132
|
return () => {
|
|
840
1133
|
cancelAnimationFrame(rafId);
|
|
841
1134
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
@@ -843,31 +1136,125 @@ window.__ModuleLoader__.load({
|
|
|
843
1136
|
if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
844
1137
|
const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
|
|
845
1138
|
if (host === null) return;
|
|
846
|
-
const remaining = holding === host ? releaseFollow(host) : followOwners.get(host) ?? 0;
|
|
847
1139
|
holding = null;
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
else settleAtFloor(host);
|
|
852
|
-
}
|
|
1140
|
+
if (!isLeader(host)) return;
|
|
1141
|
+
const preserveReader = interacting && (readerGestureIntent || readerScrolledUp(host));
|
|
1142
|
+
if (!following || !primed) {
|
|
853
1143
|
clearVisual(host);
|
|
1144
|
+
followLeaders.delete(host);
|
|
1145
|
+
releaseRevealScale();
|
|
1146
|
+
return;
|
|
854
1147
|
}
|
|
1148
|
+
if (preserveReader) {
|
|
1149
|
+
handBackVisual(host);
|
|
1150
|
+
clearVisual(host);
|
|
1151
|
+
followLeaders.delete(host);
|
|
1152
|
+
releaseRevealScale();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
ensureRunway(host, shiftSurfacesOf(host));
|
|
1156
|
+
const completionRunway = runwayOffsetOf(host);
|
|
1157
|
+
const completionMinimumLag = Math.max(0, reservePx);
|
|
1158
|
+
animatedH = Math.min(animatedH, host.scrollHeight - completionRunway - completionMinimumLag);
|
|
1159
|
+
settleAtFloor(host);
|
|
1160
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec);
|
|
1161
|
+
const runwayOffset = runwayOffsetOf(host);
|
|
1162
|
+
if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && reservePx <= .25) {
|
|
1163
|
+
finishAtNaturalFloor(host);
|
|
1164
|
+
followLeaders.delete(host);
|
|
1165
|
+
releaseRevealScale();
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
|
|
1169
|
+
const stopSettleListeners = () => {
|
|
1170
|
+
for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
|
|
1171
|
+
if (interactTimer !== null) {
|
|
1172
|
+
clearTimeout(interactTimer);
|
|
1173
|
+
interactTimer = null;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
let settleLast = performance.now();
|
|
1177
|
+
const settleFrame = (now) => {
|
|
1178
|
+
if (!isLeader(host)) {
|
|
1179
|
+
stopSettleListeners();
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
|
|
1183
|
+
readerGestureIntent = false;
|
|
1184
|
+
handBackVisual(host);
|
|
1185
|
+
clearVisual(host);
|
|
1186
|
+
followLeaders.delete(host);
|
|
1187
|
+
releaseRevealScale();
|
|
1188
|
+
stopSettleListeners();
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const dt = Math.min(32, Math.max(0, now - settleLast));
|
|
1192
|
+
settleLast = now;
|
|
1193
|
+
const runwayOffset = runwayOffsetOf(host);
|
|
1194
|
+
const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
|
|
1195
|
+
const reserveStep = 1 - Math.exp(-dt / 180);
|
|
1196
|
+
reservePx += (0 - reservePx) * reserveStep;
|
|
1197
|
+
if (lag <= .25 && reservePx <= .25) {
|
|
1198
|
+
animatedH = host.scrollHeight - runwayOffset;
|
|
1199
|
+
reservePx = 0;
|
|
1200
|
+
velocityPxPerSec = 0;
|
|
1201
|
+
finishAtNaturalFloor(host);
|
|
1202
|
+
followLeaders.delete(host);
|
|
1203
|
+
releaseRevealScale();
|
|
1204
|
+
stopSettleListeners();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const step = computeFollowStep(dt, {
|
|
1208
|
+
lag,
|
|
1209
|
+
speedEma: speedCpsRef.current,
|
|
1210
|
+
velocityPxPerSec
|
|
1211
|
+
});
|
|
1212
|
+
const minimumLag = Math.max(0, reservePx);
|
|
1213
|
+
animatedH = Math.min(host.scrollHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
|
|
1214
|
+
velocityPxPerSec = step.velocityPxPerSec;
|
|
1215
|
+
settleAtFloor(host);
|
|
1216
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec);
|
|
1217
|
+
requestAnimationFrame(settleFrame);
|
|
1218
|
+
};
|
|
1219
|
+
requestAnimationFrame(settleFrame);
|
|
855
1220
|
};
|
|
856
1221
|
}, [
|
|
857
1222
|
active,
|
|
858
1223
|
rootRef,
|
|
859
|
-
speedCpsRef
|
|
1224
|
+
speedCpsRef,
|
|
1225
|
+
revealScaleRef,
|
|
1226
|
+
predictive,
|
|
1227
|
+
predictiveRef
|
|
860
1228
|
]);
|
|
861
1229
|
}
|
|
862
1230
|
//#endregion
|
|
863
1231
|
//#region src/client/FollowHost.tsx
|
|
864
1232
|
/**
|
|
865
1233
|
* Document-flow host that owns conversation-port follow while `active`.
|
|
866
|
-
* Shared by assistant blocks and every other
|
|
1234
|
+
* Shared by assistant blocks and every other Agent Chat row. `onGrowth` lets
|
|
1235
|
+
* generic wrapped renderers re-arm one glide when their DOM grows without
|
|
1236
|
+
* requiring a business-kind-specific lifecycle predicate.
|
|
867
1237
|
*/
|
|
868
|
-
function FollowHost({ active, speedCpsRef, children }) {
|
|
869
|
-
const
|
|
870
|
-
|
|
1238
|
+
function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealScaleRef, predictive = true, predictiveRef, hostRef, children }) {
|
|
1239
|
+
const localRootRef = (0, react.useRef)(null);
|
|
1240
|
+
const rootRef = hostRef ?? localRootRef;
|
|
1241
|
+
useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef);
|
|
1242
|
+
(0, react.useEffect)(() => {
|
|
1243
|
+
if (onGrowth === void 0 || typeof ResizeObserver === "undefined") return;
|
|
1244
|
+
const root = rootRef.current;
|
|
1245
|
+
if (root === null) return;
|
|
1246
|
+
let previousHeight = null;
|
|
1247
|
+
const observer = new ResizeObserver((entries) => {
|
|
1248
|
+
const nextHeight = entries[0]?.contentRect.height ?? root.getBoundingClientRect().height;
|
|
1249
|
+
if (!Number.isFinite(nextHeight)) return;
|
|
1250
|
+
if (previousHeight !== null && nextHeight > previousHeight + .5) onGrowth(nextHeight - previousHeight);
|
|
1251
|
+
previousHeight = nextHeight;
|
|
1252
|
+
});
|
|
1253
|
+
observer.observe(root);
|
|
1254
|
+
return () => {
|
|
1255
|
+
observer.disconnect();
|
|
1256
|
+
};
|
|
1257
|
+
}, [onGrowth]);
|
|
871
1258
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
872
1259
|
ref: rootRef,
|
|
873
1260
|
className: TypewriterAssistantNodeView_module_css_default.follow,
|
|
@@ -907,6 +1294,200 @@ window.__ModuleLoader__.load({
|
|
|
907
1294
|
}, []);
|
|
908
1295
|
return reduced;
|
|
909
1296
|
}
|
|
1297
|
+
/** Conservative fallback before the streaming Markdown tail has geometry. */
|
|
1298
|
+
const PREDICTIVE_WRAP_FALLBACK_CHARS = 32;
|
|
1299
|
+
const STREAM_ANNOUNCEMENT_INTERVAL_MS = 800;
|
|
1300
|
+
const STREAM_ANNOUNCEMENT_MAX_CHARS = 320;
|
|
1301
|
+
function approximateInlineWidth(text, emPx) {
|
|
1302
|
+
let width = 0;
|
|
1303
|
+
for (const char of text) if (/\s/u.test(char)) width += emPx * .33;
|
|
1304
|
+
else if (/^[\x00-\x7f]$/u.test(char)) width += emPx * .56;
|
|
1305
|
+
else width += emPx;
|
|
1306
|
+
return width;
|
|
1307
|
+
}
|
|
1308
|
+
function measurePendingTextGeometry(root, visibleText) {
|
|
1309
|
+
if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
|
|
1310
|
+
root,
|
|
1311
|
+
visibleText,
|
|
1312
|
+
fontSize: 14,
|
|
1313
|
+
wrapThresholdWidth: null
|
|
1314
|
+
};
|
|
1315
|
+
const rootRect = root.getBoundingClientRect();
|
|
1316
|
+
const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
|
|
1317
|
+
if (rootWidth <= 0) return {
|
|
1318
|
+
root,
|
|
1319
|
+
visibleText,
|
|
1320
|
+
fontSize: 14,
|
|
1321
|
+
wrapThresholdWidth: null
|
|
1322
|
+
};
|
|
1323
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1324
|
+
let tail = null;
|
|
1325
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
|
|
1326
|
+
const parent = tail?.parentElement ?? root;
|
|
1327
|
+
const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
|
|
1328
|
+
if (tail === null || typeof document.createRange !== "function") return {
|
|
1329
|
+
root,
|
|
1330
|
+
visibleText,
|
|
1331
|
+
fontSize,
|
|
1332
|
+
wrapThresholdWidth: rootWidth
|
|
1333
|
+
};
|
|
1334
|
+
try {
|
|
1335
|
+
const length = tail.textContent?.length ?? 0;
|
|
1336
|
+
if (length <= 0) return {
|
|
1337
|
+
root,
|
|
1338
|
+
visibleText,
|
|
1339
|
+
fontSize,
|
|
1340
|
+
wrapThresholdWidth: rootWidth
|
|
1341
|
+
};
|
|
1342
|
+
const range = document.createRange();
|
|
1343
|
+
range.setStart(tail, Math.max(0, length - 1));
|
|
1344
|
+
range.setEnd(tail, length);
|
|
1345
|
+
const tailRect = range.getBoundingClientRect();
|
|
1346
|
+
const contentRight = rootRect.right;
|
|
1347
|
+
if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
|
|
1348
|
+
root,
|
|
1349
|
+
visibleText,
|
|
1350
|
+
fontSize,
|
|
1351
|
+
wrapThresholdWidth: rootWidth
|
|
1352
|
+
};
|
|
1353
|
+
return {
|
|
1354
|
+
root,
|
|
1355
|
+
visibleText,
|
|
1356
|
+
fontSize,
|
|
1357
|
+
wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
|
|
1358
|
+
};
|
|
1359
|
+
} catch {
|
|
1360
|
+
return {
|
|
1361
|
+
root,
|
|
1362
|
+
visibleText,
|
|
1363
|
+
fontSize,
|
|
1364
|
+
wrapThresholdWidth: rootWidth
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/** Whether buffered source can reach a new visual line before it drains. */
|
|
1369
|
+
function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
|
|
1370
|
+
if (pending === "") return false;
|
|
1371
|
+
if (/[\r\n]/u.test(pending)) return true;
|
|
1372
|
+
const pendingChars = [...pending];
|
|
1373
|
+
if (root === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
|
|
1374
|
+
let geometry = geometryRef.current;
|
|
1375
|
+
if (geometry?.root !== root || geometry.visibleText !== visibleText) {
|
|
1376
|
+
geometry = measurePendingTextGeometry(root, visibleText);
|
|
1377
|
+
geometryRef.current = geometry;
|
|
1378
|
+
}
|
|
1379
|
+
if (geometry.wrapThresholdWidth === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
|
|
1380
|
+
return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
|
|
1381
|
+
}
|
|
1382
|
+
function announcementChunkEnd(source, start) {
|
|
1383
|
+
const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
|
|
1384
|
+
if (hardEnd === source.length) return hardEnd;
|
|
1385
|
+
const softStart = start + Math.floor(STREAM_ANNOUNCEMENT_MAX_CHARS * .6);
|
|
1386
|
+
for (let index = hardEnd - 1; index >= softStart; index -= 1) if (/[\s.,;:!?]/u.test(source[index] ?? "")) return index + 1;
|
|
1387
|
+
return hardEnd;
|
|
1388
|
+
}
|
|
1389
|
+
/** A commit-driven live region isolated from the visible Markdown subtree. */
|
|
1390
|
+
const StreamAnnouncement = (0, react.memo)(function StreamAnnouncement({ text, active }) {
|
|
1391
|
+
const [announcement, setAnnouncement] = (0, react.useState)({
|
|
1392
|
+
text: "",
|
|
1393
|
+
revision: 0,
|
|
1394
|
+
present: active
|
|
1395
|
+
});
|
|
1396
|
+
const sourceRef = (0, react.useRef)(text);
|
|
1397
|
+
const activeRef = (0, react.useRef)(active);
|
|
1398
|
+
const announcedOffsetRef = (0, react.useRef)(active ? 0 : text.length);
|
|
1399
|
+
const drainSourceRef = (0, react.useRef)(null);
|
|
1400
|
+
const timerRef = (0, react.useRef)(null);
|
|
1401
|
+
(0, react.useEffect)(() => {
|
|
1402
|
+
const clearTimer = () => {
|
|
1403
|
+
if (timerRef.current === null) return;
|
|
1404
|
+
clearTimeout(timerRef.current);
|
|
1405
|
+
timerRef.current = null;
|
|
1406
|
+
};
|
|
1407
|
+
const publishNext = (source) => {
|
|
1408
|
+
const start = Math.min(announcedOffsetRef.current, source.length);
|
|
1409
|
+
const end = announcementChunkEnd(source, start);
|
|
1410
|
+
if (end <= start) return false;
|
|
1411
|
+
announcedOffsetRef.current = end;
|
|
1412
|
+
setAnnouncement((previous) => ({
|
|
1413
|
+
text: source.slice(start, end),
|
|
1414
|
+
revision: previous.revision + 1,
|
|
1415
|
+
present: true
|
|
1416
|
+
}));
|
|
1417
|
+
return end < source.length;
|
|
1418
|
+
};
|
|
1419
|
+
const hideAfterLinger = () => {
|
|
1420
|
+
timerRef.current = setTimeout(() => {
|
|
1421
|
+
timerRef.current = null;
|
|
1422
|
+
if (activeRef.current) return;
|
|
1423
|
+
drainSourceRef.current = null;
|
|
1424
|
+
setAnnouncement((previous) => ({
|
|
1425
|
+
...previous,
|
|
1426
|
+
present: false
|
|
1427
|
+
}));
|
|
1428
|
+
}, STREAM_ANNOUNCEMENT_INTERVAL_MS);
|
|
1429
|
+
};
|
|
1430
|
+
const drainNext = () => {
|
|
1431
|
+
const source = drainSourceRef.current;
|
|
1432
|
+
if (source === null) return;
|
|
1433
|
+
if (!publishNext(source)) {
|
|
1434
|
+
hideAfterLinger();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
timerRef.current = setTimeout(() => {
|
|
1438
|
+
timerRef.current = null;
|
|
1439
|
+
if (activeRef.current) return;
|
|
1440
|
+
drainNext();
|
|
1441
|
+
}, STREAM_ANNOUNCEMENT_INTERVAL_MS);
|
|
1442
|
+
};
|
|
1443
|
+
const scheduleLive = () => {
|
|
1444
|
+
if (timerRef.current !== null || announcedOffsetRef.current >= sourceRef.current.length) return;
|
|
1445
|
+
timerRef.current = setTimeout(() => {
|
|
1446
|
+
timerRef.current = null;
|
|
1447
|
+
if (!activeRef.current) return;
|
|
1448
|
+
const source = sourceRef.current;
|
|
1449
|
+
if (publishNext(source)) scheduleLive();
|
|
1450
|
+
}, STREAM_ANNOUNCEMENT_INTERVAL_MS);
|
|
1451
|
+
};
|
|
1452
|
+
const wasActive = activeRef.current;
|
|
1453
|
+
const previousSource = sourceRef.current;
|
|
1454
|
+
if (!(!active && !wasActive && drainSourceRef.current !== null) && (!text.startsWith(previousSource) || announcedOffsetRef.current > text.length)) announcedOffsetRef.current = 0;
|
|
1455
|
+
sourceRef.current = text;
|
|
1456
|
+
activeRef.current = active;
|
|
1457
|
+
if (active) {
|
|
1458
|
+
if (!wasActive) {
|
|
1459
|
+
clearTimer();
|
|
1460
|
+
drainSourceRef.current = null;
|
|
1461
|
+
setAnnouncement((previous) => ({
|
|
1462
|
+
text: "",
|
|
1463
|
+
revision: previous.revision + 1,
|
|
1464
|
+
present: true
|
|
1465
|
+
}));
|
|
1466
|
+
}
|
|
1467
|
+
scheduleLive();
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (!wasActive) {
|
|
1471
|
+
if (drainSourceRef.current === null) announcedOffsetRef.current = text.length;
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
clearTimer();
|
|
1475
|
+
drainSourceRef.current = text;
|
|
1476
|
+
drainNext();
|
|
1477
|
+
}, [active, text]);
|
|
1478
|
+
(0, react.useEffect)(() => () => {
|
|
1479
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
1480
|
+
timerRef.current = null;
|
|
1481
|
+
}, []);
|
|
1482
|
+
if (!active && !announcement.present) return null;
|
|
1483
|
+
const activating = active && !activeRef.current;
|
|
1484
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1485
|
+
className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
|
|
1486
|
+
"aria-live": "polite",
|
|
1487
|
+
"aria-atomic": "true",
|
|
1488
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activating ? "" : announcement.text }, announcement.revision)
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
910
1491
|
/**
|
|
911
1492
|
* Smooth streaming text arm. While the reply runs, the accumulated source is
|
|
912
1493
|
* revealed through the smoother at a rate that tracks the model's arrival
|
|
@@ -918,18 +1499,51 @@ window.__ModuleLoader__.load({
|
|
|
918
1499
|
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
919
1500
|
* mentions) swaps in exactly once.
|
|
920
1501
|
*/
|
|
921
|
-
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming,
|
|
1502
|
+
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, ownFollow, followSpeedCpsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack }) {
|
|
922
1503
|
const reduced = usePrefersReducedMotion();
|
|
923
1504
|
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
924
|
-
const
|
|
1505
|
+
const localSpeedCpsRef = (0, react.useRef)(35);
|
|
1506
|
+
const followRootRef = (0, react.useRef)(null);
|
|
1507
|
+
const predictionSourceRef = (0, react.useRef)(null);
|
|
1508
|
+
const predictionStateRef = (0, react.useRef)(false);
|
|
1509
|
+
const predictionGeometryRef = (0, react.useRef)(null);
|
|
1510
|
+
const speedCpsRef = followSpeedCpsRef ?? localSpeedCpsRef;
|
|
925
1511
|
const displayed = useSmoothStreamContent(text, {
|
|
926
1512
|
enabled: typing && !reduced,
|
|
1513
|
+
inputComplete: !streaming,
|
|
927
1514
|
preset,
|
|
928
1515
|
shouldHoldBack,
|
|
929
|
-
speedCpsRef
|
|
1516
|
+
speedCpsRef,
|
|
1517
|
+
revealScaleRef: followRevealScaleRef
|
|
930
1518
|
});
|
|
931
1519
|
const shown = reduced ? text : displayed;
|
|
932
1520
|
const live = typing && !reduced;
|
|
1521
|
+
(0, react.useEffect)(() => {
|
|
1522
|
+
const root = followRootRef.current;
|
|
1523
|
+
if (root === null || typeof ResizeObserver === "undefined") return;
|
|
1524
|
+
const observer = new ResizeObserver(() => {
|
|
1525
|
+
if (predictionGeometryRef.current?.root === root) predictionGeometryRef.current = null;
|
|
1526
|
+
});
|
|
1527
|
+
observer.observe(root);
|
|
1528
|
+
return () => {
|
|
1529
|
+
observer.disconnect();
|
|
1530
|
+
};
|
|
1531
|
+
}, []);
|
|
1532
|
+
(0, react.useLayoutEffect)(() => {
|
|
1533
|
+
if (onPredictiveChange === void 0) return;
|
|
1534
|
+
const pending = text.slice(shown.length);
|
|
1535
|
+
const sourceChanged = predictionSourceRef.current !== text;
|
|
1536
|
+
const next = !live || !streaming || pending === "" ? false : sourceChanged ? pendingTextCanGrow(followRootRef.current, shown, pending, predictionGeometryRef) : predictionStateRef.current;
|
|
1537
|
+
predictionSourceRef.current = text;
|
|
1538
|
+
predictionStateRef.current = next;
|
|
1539
|
+
onPredictiveChange(next);
|
|
1540
|
+
}, [
|
|
1541
|
+
live,
|
|
1542
|
+
onPredictiveChange,
|
|
1543
|
+
shown,
|
|
1544
|
+
streaming,
|
|
1545
|
+
text
|
|
1546
|
+
]);
|
|
933
1547
|
(0, react.useEffect)(() => {
|
|
934
1548
|
if (typing && !streaming && shown.length === text.length) setTyping(false);
|
|
935
1549
|
}, [
|
|
@@ -938,23 +1552,18 @@ window.__ModuleLoader__.load({
|
|
|
938
1552
|
text,
|
|
939
1553
|
typing
|
|
940
1554
|
]);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
"aria-live": "polite",
|
|
944
|
-
children: text
|
|
945
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
946
|
-
active: ownFollow,
|
|
1555
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1556
|
+
active: live && ownFollow,
|
|
947
1557
|
speedCpsRef,
|
|
1558
|
+
revealScaleRef: followRevealScaleRef,
|
|
1559
|
+
predictive: streaming,
|
|
1560
|
+
hostRef: followRootRef,
|
|
948
1561
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
949
|
-
text: shown,
|
|
950
|
-
streaming:
|
|
951
|
-
codeLabels
|
|
1562
|
+
text: live ? shown : text,
|
|
1563
|
+
streaming: live,
|
|
1564
|
+
codeLabels,
|
|
1565
|
+
fileMentions: live ? void 0 : fileMentions
|
|
952
1566
|
})
|
|
953
|
-
})] });
|
|
954
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
955
|
-
text,
|
|
956
|
-
codeLabels,
|
|
957
|
-
fileMentions
|
|
958
1567
|
});
|
|
959
1568
|
}
|
|
960
1569
|
function imageLabels(t) {
|
|
@@ -988,7 +1597,7 @@ window.__ModuleLoader__.load({
|
|
|
988
1597
|
* or the assistant node settling — not when the rest of the reply is
|
|
989
1598
|
* still streaming.
|
|
990
1599
|
*/
|
|
991
|
-
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, t }) {
|
|
1600
|
+
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
|
|
992
1601
|
const reduced = usePrefersReducedMotion();
|
|
993
1602
|
const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
|
|
994
1603
|
const summaryRef = (0, react.useRef)(null);
|
|
@@ -996,11 +1605,12 @@ window.__ModuleLoader__.load({
|
|
|
996
1605
|
enabled: running && !reduced,
|
|
997
1606
|
preset,
|
|
998
1607
|
shouldHoldBack,
|
|
999
|
-
speedCpsRef: followSpeedCpsRef
|
|
1608
|
+
speedCpsRef: followSpeedCpsRef,
|
|
1609
|
+
revealScaleRef: followRevealScaleRef
|
|
1000
1610
|
});
|
|
1001
1611
|
const shown = running && !reduced ? displayed : text;
|
|
1002
1612
|
const summary = running ? latestLine(shown) : firstLine(text);
|
|
1003
|
-
(0, react.
|
|
1613
|
+
(0, react.useLayoutEffect)(() => {
|
|
1004
1614
|
if (thinkAutoExpand) setExpanded(running);
|
|
1005
1615
|
}, [running, thinkAutoExpand]);
|
|
1006
1616
|
(0, react.useEffect)(() => {
|
|
@@ -1058,12 +1668,22 @@ window.__ModuleLoader__.load({
|
|
|
1058
1668
|
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 }) {
|
|
1059
1669
|
const data = node.data;
|
|
1060
1670
|
const streaming = data.status === "running";
|
|
1671
|
+
const reduced = usePrefersReducedMotion();
|
|
1061
1672
|
const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
|
|
1062
1673
|
const rootSpeedRef = (0, react.useRef)(35);
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1674
|
+
const rootRevealScaleRef = (0, react.useRef)(1);
|
|
1675
|
+
const reasoningTailIndex = streaming && data.blocks[data.blocks.length - 1]?.kind === "reasoning" ? data.blocks.length - 1 : -1;
|
|
1676
|
+
const reasoningOwnsSpeed = reasoningTailIndex !== -1;
|
|
1677
|
+
const rootPredictiveRef = (0, react.useRef)(false);
|
|
1678
|
+
const previousReasoningTailRef = (0, react.useRef)(-1);
|
|
1679
|
+
if (reasoningTailIndex !== previousReasoningTailRef.current) {
|
|
1680
|
+
rootPredictiveRef.current = false;
|
|
1065
1681
|
if (!reasoningOwnsSpeed) rootSpeedRef.current = 35;
|
|
1066
|
-
|
|
1682
|
+
previousReasoningTailRef.current = reasoningTailIndex;
|
|
1683
|
+
}
|
|
1684
|
+
const updateTextPrediction = (0, react.useMemo)(() => (predictive) => {
|
|
1685
|
+
rootPredictiveRef.current = predictive;
|
|
1686
|
+
}, []);
|
|
1067
1687
|
const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
|
|
1068
1688
|
const tail = useTurnData("turn-tail");
|
|
1069
1689
|
const owner = (0, react.useMemo)(() => {
|
|
@@ -1089,6 +1709,7 @@ window.__ModuleLoader__.load({
|
|
|
1089
1709
|
throw new Error(t("image.serviceUnavailable"));
|
|
1090
1710
|
});
|
|
1091
1711
|
if (!(streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call"))) return null;
|
|
1712
|
+
const announcementText = data.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
|
|
1092
1713
|
const rendered = [];
|
|
1093
1714
|
const last = data.blocks.length - 1;
|
|
1094
1715
|
let lastFollow = -1;
|
|
@@ -1106,8 +1727,10 @@ window.__ModuleLoader__.load({
|
|
|
1106
1727
|
codeLabels,
|
|
1107
1728
|
fileMentions: mentions,
|
|
1108
1729
|
streaming,
|
|
1109
|
-
announce: index === last,
|
|
1110
1730
|
ownFollow: !streaming && index === lastFollow,
|
|
1731
|
+
followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
|
|
1732
|
+
followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
|
|
1733
|
+
onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
|
|
1111
1734
|
preset,
|
|
1112
1735
|
shouldHoldBack
|
|
1113
1736
|
}, index));
|
|
@@ -1120,6 +1743,7 @@ window.__ModuleLoader__.load({
|
|
|
1120
1743
|
thinkAutoExpand,
|
|
1121
1744
|
shouldHoldBack,
|
|
1122
1745
|
followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
|
|
1746
|
+
followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
|
|
1123
1747
|
t
|
|
1124
1748
|
}, index));
|
|
1125
1749
|
break;
|
|
@@ -1148,13 +1772,18 @@ window.__ModuleLoader__.load({
|
|
|
1148
1772
|
}, index));
|
|
1149
1773
|
}
|
|
1150
1774
|
}
|
|
1151
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1775
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1152
1776
|
ref: guardRef,
|
|
1153
1777
|
className: TypewriterAssistantNodeView_module_css_default.root,
|
|
1154
1778
|
"data-streaming": streaming || void 0,
|
|
1155
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1156
|
-
|
|
1779
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StreamAnnouncement, {
|
|
1780
|
+
text: announcementText,
|
|
1781
|
+
active: streaming && !reduced
|
|
1782
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1783
|
+
active: streaming && !reduced,
|
|
1157
1784
|
speedCpsRef: rootSpeedRef,
|
|
1785
|
+
revealScaleRef: rootRevealScaleRef,
|
|
1786
|
+
predictiveRef: rootPredictiveRef,
|
|
1158
1787
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1159
1788
|
className: TypewriterAssistantNodeView_module_css_default.body,
|
|
1160
1789
|
children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1162,15 +1791,214 @@ window.__ModuleLoader__.load({
|
|
|
1162
1791
|
children: t("message.stopped")
|
|
1163
1792
|
})]
|
|
1164
1793
|
})
|
|
1165
|
-
})
|
|
1794
|
+
})]
|
|
1166
1795
|
});
|
|
1167
1796
|
});
|
|
1168
1797
|
//#endregion
|
|
1798
|
+
//#region src/client/useProgressiveDomText.ts
|
|
1799
|
+
/**
|
|
1800
|
+
* Progressive text reveal for opaque Agent renderers.
|
|
1801
|
+
*
|
|
1802
|
+
* Slot renderers own arbitrary React trees, so the generic integration cannot
|
|
1803
|
+
* clone or classify their business components. This hook leaves that tree and
|
|
1804
|
+
* all of its event handlers in place, and only paces visible Text node data
|
|
1805
|
+
* while the row belongs to the live Agent turn. No clip, mask, overlay, or
|
|
1806
|
+
* duplicate accessibility tree is introduced.
|
|
1807
|
+
*/
|
|
1808
|
+
const SKIP_TEXT_SELECTOR = [
|
|
1809
|
+
"[aria-hidden=\"true\"]",
|
|
1810
|
+
"[aria-live]",
|
|
1811
|
+
"[contenteditable=\"true\"]",
|
|
1812
|
+
"script",
|
|
1813
|
+
"style",
|
|
1814
|
+
"textarea"
|
|
1815
|
+
].join(",");
|
|
1816
|
+
function revealable(node, root) {
|
|
1817
|
+
if (node.data.trim() === "") return false;
|
|
1818
|
+
const parent = node.parentElement;
|
|
1819
|
+
return parent !== null && root.contains(parent) && parent.closest(SKIP_TEXT_SELECTOR) === null;
|
|
1820
|
+
}
|
|
1821
|
+
function commonPrefix(left, right) {
|
|
1822
|
+
const limit = Math.min(left.length, right.length);
|
|
1823
|
+
let index = 0;
|
|
1824
|
+
while (index < limit && left[index] === right[index]) index += 1;
|
|
1825
|
+
return index;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Pace text inside a renderer whose React component is intentionally opaque.
|
|
1829
|
+
* Initial content is revealed only for a genuinely new row; later mutations
|
|
1830
|
+
* stay paced until `enabled` becomes false, at which point the full renderer
|
|
1831
|
+
* content is restored synchronously before paint.
|
|
1832
|
+
*/
|
|
1833
|
+
function useProgressiveDomText(rootRef, enabled, revealInitial, speedCpsRef, onSettled) {
|
|
1834
|
+
(0, react.useLayoutEffect)(() => {
|
|
1835
|
+
if (!enabled) return;
|
|
1836
|
+
const root = rootRef.current;
|
|
1837
|
+
if (root === null || typeof document === "undefined") return;
|
|
1838
|
+
const records = /* @__PURE__ */ new Map();
|
|
1839
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1840
|
+
const internalWrites = /* @__PURE__ */ new WeakMap();
|
|
1841
|
+
let rafId = 0;
|
|
1842
|
+
let lastFrame = null;
|
|
1843
|
+
let debt = 0;
|
|
1844
|
+
let stopped = false;
|
|
1845
|
+
let announcedSettled = false;
|
|
1846
|
+
const announceSettled = () => {
|
|
1847
|
+
lastFrame = null;
|
|
1848
|
+
debt = 0;
|
|
1849
|
+
speedCpsRef.current = 35;
|
|
1850
|
+
if (announcedSettled) return;
|
|
1851
|
+
announcedSettled = true;
|
|
1852
|
+
onSettled?.();
|
|
1853
|
+
};
|
|
1854
|
+
const write = (node, value) => {
|
|
1855
|
+
if (node.data === value) return;
|
|
1856
|
+
internalWrites.set(node, value);
|
|
1857
|
+
node.data = value;
|
|
1858
|
+
};
|
|
1859
|
+
const enqueue = (node, full, preserve) => {
|
|
1860
|
+
if (!revealable(node, root)) return;
|
|
1861
|
+
const chars = [...full];
|
|
1862
|
+
const preserved = preserve === void 0 ? 0 : Math.min(preserve.shown, commonPrefix(preserve.chars, chars));
|
|
1863
|
+
const record = {
|
|
1864
|
+
chars,
|
|
1865
|
+
full,
|
|
1866
|
+
shown: preserved
|
|
1867
|
+
};
|
|
1868
|
+
records.set(node, record);
|
|
1869
|
+
if (preserved < chars.length) {
|
|
1870
|
+
pending.add(node);
|
|
1871
|
+
announcedSettled = false;
|
|
1872
|
+
} else pending.delete(node);
|
|
1873
|
+
write(node, chars.slice(0, preserved).join(""));
|
|
1874
|
+
};
|
|
1875
|
+
const visit = (from, reveal) => {
|
|
1876
|
+
if (from.nodeType === Node.TEXT_NODE) {
|
|
1877
|
+
const text = from;
|
|
1878
|
+
if (reveal) enqueue(text, text.data, records.get(text));
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const walker = document.createTreeWalker(from, NodeFilter.SHOW_TEXT);
|
|
1882
|
+
let current = walker.nextNode();
|
|
1883
|
+
while (current !== null) {
|
|
1884
|
+
const text = current;
|
|
1885
|
+
if (reveal) enqueue(text, text.data, records.get(text));
|
|
1886
|
+
current = walker.nextNode();
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
const scheduleFrame = () => {
|
|
1890
|
+
if (stopped || pending.size === 0 || rafId !== 0) return;
|
|
1891
|
+
rafId = requestAnimationFrame(frame);
|
|
1892
|
+
};
|
|
1893
|
+
const frame = (now) => {
|
|
1894
|
+
rafId = 0;
|
|
1895
|
+
if (stopped) return;
|
|
1896
|
+
if (pending.size === 0) {
|
|
1897
|
+
announceSettled();
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
announcedSettled = false;
|
|
1901
|
+
if (lastFrame === null) {
|
|
1902
|
+
lastFrame = now;
|
|
1903
|
+
scheduleFrame();
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const elapsed = Math.max(0, now - lastFrame);
|
|
1907
|
+
lastFrame = now;
|
|
1908
|
+
let backlog = 0;
|
|
1909
|
+
for (const node of pending) {
|
|
1910
|
+
const record = records.get(node);
|
|
1911
|
+
if (record !== void 0) backlog += record.chars.length - record.shown;
|
|
1912
|
+
}
|
|
1913
|
+
const step = computeAdaptiveQueueStep(backlog, elapsed, debt);
|
|
1914
|
+
debt = step.debt;
|
|
1915
|
+
speedCpsRef.current = step.speedCps;
|
|
1916
|
+
let remaining = step.revealChars;
|
|
1917
|
+
for (const node of [...pending]) {
|
|
1918
|
+
if (remaining <= 0) break;
|
|
1919
|
+
const record = records.get(node);
|
|
1920
|
+
if (record === void 0 || !node.isConnected) {
|
|
1921
|
+
pending.delete(node);
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
const amount = Math.min(remaining, record.chars.length - record.shown);
|
|
1925
|
+
const shown = record.shown + amount;
|
|
1926
|
+
const next = {
|
|
1927
|
+
...record,
|
|
1928
|
+
shown
|
|
1929
|
+
};
|
|
1930
|
+
records.set(node, next);
|
|
1931
|
+
write(node, next.chars.slice(0, shown).join(""));
|
|
1932
|
+
remaining -= amount;
|
|
1933
|
+
if (shown >= next.chars.length) pending.delete(node);
|
|
1934
|
+
}
|
|
1935
|
+
if (pending.size === 0) announceSettled();
|
|
1936
|
+
else scheduleFrame();
|
|
1937
|
+
};
|
|
1938
|
+
if (revealInitial) visit(root, true);
|
|
1939
|
+
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver((mutations) => {
|
|
1940
|
+
for (const mutation of mutations) {
|
|
1941
|
+
if (mutation.type === "characterData") {
|
|
1942
|
+
const node = mutation.target;
|
|
1943
|
+
if (internalWrites.get(node) === node.data) {
|
|
1944
|
+
internalWrites.delete(node);
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
enqueue(node, node.data, records.get(node));
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
for (const added of mutation.addedNodes) visit(added, true);
|
|
1951
|
+
}
|
|
1952
|
+
if (pending.size === 0) announceSettled();
|
|
1953
|
+
else scheduleFrame();
|
|
1954
|
+
});
|
|
1955
|
+
observer?.observe(root, {
|
|
1956
|
+
childList: true,
|
|
1957
|
+
characterData: true,
|
|
1958
|
+
subtree: true
|
|
1959
|
+
});
|
|
1960
|
+
if (pending.size === 0) announceSettled();
|
|
1961
|
+
else scheduleFrame();
|
|
1962
|
+
return () => {
|
|
1963
|
+
stopped = true;
|
|
1964
|
+
cancelAnimationFrame(rafId);
|
|
1965
|
+
observer?.disconnect();
|
|
1966
|
+
for (const [node, record] of records) {
|
|
1967
|
+
const controlled = record.chars.slice(0, record.shown).join("");
|
|
1968
|
+
if (node.isConnected && node.data === controlled) write(node, record.full);
|
|
1969
|
+
}
|
|
1970
|
+
pending.clear();
|
|
1971
|
+
speedCpsRef.current = 35;
|
|
1972
|
+
};
|
|
1973
|
+
}, [
|
|
1974
|
+
enabled,
|
|
1975
|
+
onSettled,
|
|
1976
|
+
revealInitial,
|
|
1977
|
+
rootRef,
|
|
1978
|
+
speedCpsRef
|
|
1979
|
+
]);
|
|
1980
|
+
}
|
|
1981
|
+
//#endregion
|
|
1169
1982
|
//#region src/client/TypewriterToolNodeView.tsx
|
|
1983
|
+
function openAgentLocation(node) {
|
|
1984
|
+
if (node === null || typeof node !== "object" || !("location" in node)) return false;
|
|
1985
|
+
const location = node.location;
|
|
1986
|
+
if (location === null || typeof location !== "object" || !("kind" in location)) return false;
|
|
1987
|
+
const kind = location.kind;
|
|
1988
|
+
if (!("turn" in location)) return false;
|
|
1989
|
+
const turn = location.turn;
|
|
1990
|
+
if (turn === null || typeof turn !== "object" || !("status" in turn)) return false;
|
|
1991
|
+
if (kind === "turn") return turn.status === "open";
|
|
1992
|
+
if (kind !== "step" || !("step" in location)) return false;
|
|
1993
|
+
const step = location.step;
|
|
1994
|
+
return step !== null && typeof step === "object" && "status" in step && step.status === "open";
|
|
1995
|
+
}
|
|
1170
1996
|
/**
|
|
1171
|
-
* True while a Chat node
|
|
1172
|
-
* 'running'` payload, a Tool root that has not
|
|
1173
|
-
* model-retry whose current attempt is still
|
|
1997
|
+
* True while a Chat node has an explicitly unfinished lifecycle: an
|
|
1998
|
+
* assistant/workflow `status: 'running'` payload, a Tool root that has not
|
|
1999
|
+
* settled (`kind` absent), or a model-retry whose current attempt is still
|
|
2000
|
+
* `scheduled`. Open-but-otherwise-unknown rows are covered by
|
|
2001
|
+
* `isFollowableChatNode` below.
|
|
1174
2002
|
* @param node - The Chat node's view `node` prop.
|
|
1175
2003
|
* @returns whether this row should own conversation follow.
|
|
1176
2004
|
*/
|
|
@@ -1179,6 +2007,11 @@ window.__ModuleLoader__.load({
|
|
|
1179
2007
|
const data = node.data;
|
|
1180
2008
|
if (data === null || typeof data !== "object") return false;
|
|
1181
2009
|
if ("status" in data && data.status === "running") return true;
|
|
2010
|
+
if ("kind" in data && data.kind === "command" && "outcome" in data && data.outcome === null) return true;
|
|
2011
|
+
if ("command" in data) {
|
|
2012
|
+
const command = data.command;
|
|
2013
|
+
if (command !== null && typeof command === "object" && "outcome" in command && command.outcome === null) return true;
|
|
2014
|
+
}
|
|
1182
2015
|
if ("root" in data) {
|
|
1183
2016
|
const root = data.root;
|
|
1184
2017
|
if (root !== null && typeof root === "object" && !("kind" in root)) return true;
|
|
@@ -1190,18 +2023,128 @@ window.__ModuleLoader__.load({
|
|
|
1190
2023
|
return false;
|
|
1191
2024
|
}
|
|
1192
2025
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
2026
|
+
* True for any Agent-owned Chat row in the currently open turn/step. This is
|
|
2027
|
+
* the extensibility boundary: a newly registered Context, Command, Tool, or
|
|
2028
|
+
* workflow renderer is followed without adding another kind-specific branch.
|
|
2029
|
+
*/
|
|
2030
|
+
function isFollowableChatNode(node) {
|
|
2031
|
+
return isGrowingChatNode(node) || openAgentLocation(node);
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* True when a newly mounted Agent-owned row belongs to the current open
|
|
2035
|
+
* Turn/Step, or is itself an unresolved growing lifecycle.
|
|
2036
|
+
* @param node - The Chat node's view `node` prop.
|
|
2037
|
+
* @returns whether its initial height should enter through conversation follow.
|
|
2038
|
+
*/
|
|
2039
|
+
function shouldAnimateChatNodeEntrance(node) {
|
|
2040
|
+
return isFollowableChatNode(node);
|
|
2041
|
+
}
|
|
2042
|
+
/** Runtime-only fallback for unknown or terminal rows at the active flow tip. */
|
|
2043
|
+
function liveAgentTailMode(root) {
|
|
2044
|
+
const port = root.closest("[data-conversation-scroll]");
|
|
2045
|
+
if (port === null) return null;
|
|
2046
|
+
const row = root.closest("[data-chat-flow-key]");
|
|
2047
|
+
if (row !== null) {
|
|
2048
|
+
let sibling = row.nextElementSibling;
|
|
2049
|
+
while (sibling !== null) {
|
|
2050
|
+
if (sibling instanceof HTMLElement && sibling.hasAttribute("data-chat-flow-key")) return null;
|
|
2051
|
+
sibling = sibling.nextElementSibling;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const flow = root.closest("[data-chat-flow]");
|
|
2055
|
+
if (flow !== null && [...flow.children].some((child) => child instanceof HTMLElement && child.getAttribute("role") === "status" && !child.hasAttribute("data-chat-flow-key"))) return "turn";
|
|
2056
|
+
return hasRecentConversationFollow(port) ? "handoff" : null;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Wrap a prior Agent Chat renderer so its entrance and later growth share
|
|
2060
|
+
* conversation follow. Presentation stays with the wrapped component; kit
|
|
2061
|
+
* seats (`renderSlot`, locale, inject) pass through unchanged.
|
|
1196
2062
|
* @param Inner - The already-registered row component.
|
|
1197
2063
|
* @returns A follow-hosted row.
|
|
1198
2064
|
*/
|
|
1199
2065
|
function wrapFollowNodeView(Inner) {
|
|
1200
2066
|
return function TypewriterFollowNodeView(props) {
|
|
1201
2067
|
const speedCpsRef = (0, react.useRef)(35);
|
|
2068
|
+
const hostRef = (0, react.useRef)(null);
|
|
2069
|
+
const growing = isGrowingChatNode(props.node);
|
|
2070
|
+
const structurallyFollowable = isFollowableChatNode(props.node);
|
|
2071
|
+
const structuralRef = (0, react.useRef)(structurallyFollowable);
|
|
2072
|
+
const [runtimeFollowable, setRuntimeFollowable] = (0, react.useState)(false);
|
|
2073
|
+
const runtimePersistentRef = (0, react.useRef)(false);
|
|
2074
|
+
const runtimeHandledRef = (0, react.useRef)(false);
|
|
2075
|
+
const followable = structurallyFollowable || runtimeFollowable;
|
|
2076
|
+
const revealInitialRef = (0, react.useRef)(true);
|
|
2077
|
+
const [entering, setEntering] = (0, react.useState)(() => shouldAnimateChatNodeEntrance(props.node));
|
|
2078
|
+
const [growthPulse, setGrowthPulse] = (0, react.useState)(false);
|
|
2079
|
+
const followableRef = (0, react.useRef)(false);
|
|
2080
|
+
const growingRef = (0, react.useRef)(growing);
|
|
2081
|
+
const entranceActiveRef = (0, react.useRef)(entering || growthPulse);
|
|
2082
|
+
const growthExtentRef = (0, react.useRef)(null);
|
|
2083
|
+
const mountedRef = (0, react.useRef)(true);
|
|
2084
|
+
const pulseTimerRef = (0, react.useRef)(null);
|
|
2085
|
+
structuralRef.current = structurallyFollowable;
|
|
2086
|
+
followableRef.current = followable;
|
|
2087
|
+
growingRef.current = growing;
|
|
2088
|
+
entranceActiveRef.current = entering || growthPulse;
|
|
2089
|
+
const finishRuntimeReveal = (0, react.useCallback)(() => {
|
|
2090
|
+
if (structuralRef.current || runtimePersistentRef.current) return;
|
|
2091
|
+
runtimeHandledRef.current = true;
|
|
2092
|
+
setRuntimeFollowable(false);
|
|
2093
|
+
}, []);
|
|
2094
|
+
useProgressiveDomText(hostRef, followable, revealInitialRef.current, speedCpsRef, runtimeFollowable ? finishRuntimeReveal : void 0);
|
|
2095
|
+
(0, react.useLayoutEffect)(() => {
|
|
2096
|
+
if (structurallyFollowable) return;
|
|
2097
|
+
const root = hostRef.current;
|
|
2098
|
+
if (root === null) return;
|
|
2099
|
+
const mode = liveAgentTailMode(root);
|
|
2100
|
+
if (runtimeFollowable) {
|
|
2101
|
+
if (runtimePersistentRef.current && mode !== "turn") {
|
|
2102
|
+
runtimePersistentRef.current = false;
|
|
2103
|
+
runtimeHandledRef.current = true;
|
|
2104
|
+
setRuntimeFollowable(false);
|
|
2105
|
+
}
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (runtimeHandledRef.current || mode === null) return;
|
|
2109
|
+
runtimePersistentRef.current = mode === "turn";
|
|
2110
|
+
setRuntimeFollowable(true);
|
|
2111
|
+
setEntering(true);
|
|
2112
|
+
}, [runtimeFollowable, structurallyFollowable]);
|
|
2113
|
+
const finishEntrance = (0, react.useCallback)(() => {
|
|
2114
|
+
growthExtentRef.current = null;
|
|
2115
|
+
if (pulseTimerRef.current !== null) {
|
|
2116
|
+
clearTimeout(pulseTimerRef.current);
|
|
2117
|
+
pulseTimerRef.current = null;
|
|
2118
|
+
}
|
|
2119
|
+
setEntering(false);
|
|
2120
|
+
setGrowthPulse(false);
|
|
2121
|
+
}, []);
|
|
2122
|
+
const onGrowth = (0, react.useCallback)((deltaPx) => {
|
|
2123
|
+
if (!mountedRef.current || !followableRef.current || growingRef.current || entranceActiveRef.current) return;
|
|
2124
|
+
growthExtentRef.current = deltaPx;
|
|
2125
|
+
setGrowthPulse(true);
|
|
2126
|
+
if (pulseTimerRef.current !== null) clearTimeout(pulseTimerRef.current);
|
|
2127
|
+
pulseTimerRef.current = setTimeout(() => {
|
|
2128
|
+
pulseTimerRef.current = null;
|
|
2129
|
+
setGrowthPulse(false);
|
|
2130
|
+
}, 1200);
|
|
2131
|
+
}, []);
|
|
2132
|
+
(0, react.useEffect)(() => {
|
|
2133
|
+
mountedRef.current = true;
|
|
2134
|
+
return () => {
|
|
2135
|
+
mountedRef.current = false;
|
|
2136
|
+
if (pulseTimerRef.current !== null) clearTimeout(pulseTimerRef.current);
|
|
2137
|
+
};
|
|
2138
|
+
}, []);
|
|
1202
2139
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1203
|
-
active:
|
|
2140
|
+
active: growing,
|
|
2141
|
+
entrance: entering || growthPulse,
|
|
2142
|
+
onEntranceSettled: finishEntrance,
|
|
2143
|
+
onGrowth: followable ? onGrowth : void 0,
|
|
2144
|
+
entranceExtentRef: growthExtentRef,
|
|
1204
2145
|
speedCpsRef,
|
|
2146
|
+
predictive: false,
|
|
2147
|
+
hostRef,
|
|
1205
2148
|
children: (0, react.createElement)(Inner, props)
|
|
1206
2149
|
});
|
|
1207
2150
|
};
|
|
@@ -1221,31 +2164,31 @@ window.__ModuleLoader__.load({
|
|
|
1221
2164
|
tag.textContent = css;
|
|
1222
2165
|
}
|
|
1223
2166
|
var SmoothStreamCard_module_css_default = {
|
|
1224
|
-
"
|
|
1225
|
-
"
|
|
1226
|
-
"
|
|
2167
|
+
"header": "XNGeRG_header",
|
|
2168
|
+
"version": "XNGeRG_version",
|
|
2169
|
+
"readOnly": "XNGeRG_readOnly",
|
|
1227
2170
|
"footer": "XNGeRG_footer",
|
|
1228
|
-
"
|
|
1229
|
-
"
|
|
2171
|
+
"label": "XNGeRG_label",
|
|
2172
|
+
"hint": "XNGeRG_hint",
|
|
1230
2173
|
"fieldHead": "XNGeRG_fieldHead",
|
|
1231
|
-
"
|
|
1232
|
-
"
|
|
1233
|
-
"
|
|
1234
|
-
"
|
|
2174
|
+
"failure": "XNGeRG_failure",
|
|
2175
|
+
"update": "XNGeRG_update",
|
|
2176
|
+
"field": "XNGeRG_field",
|
|
2177
|
+
"save": "XNGeRG_save",
|
|
1235
2178
|
"pending": "XNGeRG_pending",
|
|
2179
|
+
"card": "XNGeRG_card",
|
|
2180
|
+
"cardOpen": "XNGeRG_cardOpen",
|
|
2181
|
+
"chevron": "XNGeRG_chevron",
|
|
2182
|
+
"toggle": "XNGeRG_toggle",
|
|
1236
2183
|
"body": "XNGeRG_body",
|
|
1237
|
-
"
|
|
1238
|
-
"updateRow": "XNGeRG_updateRow",
|
|
2184
|
+
"name": "XNGeRG_name",
|
|
1239
2185
|
"failed": "XNGeRG_failed",
|
|
1240
|
-
"
|
|
1241
|
-
"version": "XNGeRG_version",
|
|
1242
|
-
"hint": "XNGeRG_hint",
|
|
1243
|
-
"chevron": "XNGeRG_chevron",
|
|
1244
|
-
"description": "XNGeRG_description",
|
|
1245
|
-
"header": "XNGeRG_header",
|
|
2186
|
+
"updateRow": "XNGeRG_updateRow",
|
|
1246
2187
|
"discard": "XNGeRG_discard",
|
|
1247
|
-
"
|
|
1248
|
-
"
|
|
2188
|
+
"description": "XNGeRG_description",
|
|
2189
|
+
"headText": "XNGeRG_headText",
|
|
2190
|
+
"updateCopy": "XNGeRG_updateCopy",
|
|
2191
|
+
"chevronOpen": "XNGeRG_chevronOpen"
|
|
1249
2192
|
};
|
|
1250
2193
|
//#endregion
|
|
1251
2194
|
//#region src/client/SmoothStreamCard.tsx
|
|
@@ -1644,13 +2587,24 @@ window.__ModuleLoader__.load({
|
|
|
1644
2587
|
"balanced",
|
|
1645
2588
|
"silky"
|
|
1646
2589
|
];
|
|
1647
|
-
/**
|
|
2590
|
+
/**
|
|
2591
|
+
* The assistant renderer owns its own character queue and conversation
|
|
2592
|
+
* follower, so wrapping it again would create two scroll owners. Human input
|
|
2593
|
+
* stays immediate; every Agent-owned output renderer goes through the same
|
|
2594
|
+
* generic follow boundary. This is deliberately keyed by the owner that
|
|
2595
|
+
* provides the renderer, not by individual tool names, so new Context,
|
|
2596
|
+
* Command, and Tool rows are covered automatically.
|
|
2597
|
+
*/
|
|
1648
2598
|
const SKIP_WRAP = /* @__PURE__ */ new Set([
|
|
1649
2599
|
"assistant-step",
|
|
1650
2600
|
"user",
|
|
1651
2601
|
"steering",
|
|
1652
2602
|
"command-input"
|
|
1653
2603
|
]);
|
|
2604
|
+
/** React function/class or an exotic component such as memo/forwardRef/lazy. */
|
|
2605
|
+
function isWrappableComponent(value) {
|
|
2606
|
+
return typeof value === "function" || value !== null && typeof value === "object" && "$$typeof" in value;
|
|
2607
|
+
}
|
|
1654
2608
|
/**
|
|
1655
2609
|
* Read the Host-bridged boot config. The inline script is produced by this
|
|
1656
2610
|
* plugin's Host half from a schema-validated value, so only the structural
|
|
@@ -1670,16 +2624,19 @@ window.__ModuleLoader__.load({
|
|
|
1670
2624
|
return raw;
|
|
1671
2625
|
}
|
|
1672
2626
|
/**
|
|
1673
|
-
* Wrap every keyed Chat row except
|
|
2627
|
+
* Wrap every Agent-owned keyed Chat row except the assistant renderer in
|
|
2628
|
+
* place. A second
|
|
1674
2629
|
* register with the same `children` table throws because the child slot is
|
|
1675
2630
|
* already declared, and only the winning entry receives `renderSlot`;
|
|
1676
2631
|
* swapping `entry.component` keeps the original children, locale, and inject
|
|
1677
2632
|
* seats. `assistant-step` is replaced below so text and Think use the
|
|
1678
|
-
* typewriter reveal.
|
|
2633
|
+
* typewriter reveal. The wrapper owns only the shared layout-growth/follow
|
|
2634
|
+
* lifecycle; the Harness keeps each renderer's controls, disclosures, and
|
|
2635
|
+
* cards intact.
|
|
1679
2636
|
* @param ctx - Browser context carrying the slot registry.
|
|
1680
2637
|
* @returns Restorer that puts the original components back.
|
|
1681
2638
|
*/
|
|
1682
|
-
function
|
|
2639
|
+
function wrapAgentChatRows(ctx) {
|
|
1683
2640
|
const restores = [];
|
|
1684
2641
|
const wrapped = /* @__PURE__ */ new WeakSet();
|
|
1685
2642
|
const wrapAll = () => {
|
|
@@ -1687,7 +2644,7 @@ window.__ModuleLoader__.load({
|
|
|
1687
2644
|
const key = entry.options.key;
|
|
1688
2645
|
if (key === void 0 || SKIP_WRAP.has(key)) continue;
|
|
1689
2646
|
const current = entry.component;
|
|
1690
|
-
if (
|
|
2647
|
+
if (!isWrappableComponent(current) || wrapped.has(current)) continue;
|
|
1691
2648
|
const inner = current;
|
|
1692
2649
|
const next = wrapFollowNodeView(inner);
|
|
1693
2650
|
wrapped.add(next);
|
|
@@ -1750,8 +2707,9 @@ window.__ModuleLoader__.load({
|
|
|
1750
2707
|
/**
|
|
1751
2708
|
* Register the typewriter renderer after the conversation package declares the
|
|
1752
2709
|
* keyed Chat node seat. A lower priority shadows the built-in assistant row;
|
|
1753
|
-
* every other
|
|
1754
|
-
* workflow runs share
|
|
2710
|
+
* every other keyed renderer is wrapped in place so Context, commands, Tool
|
|
2711
|
+
* cards, retries, and workflow runs share one extensible follow boundary. The
|
|
2712
|
+
* Host-bridged configuration
|
|
1755
2713
|
* selects the render direction, smoothing preset, and glide speed; the
|
|
1756
2714
|
* plugin-owned settings RPC supplies the live auto-expand preference when the
|
|
1757
2715
|
* settings surface is composed.
|
|
@@ -1797,7 +2755,7 @@ window.__ModuleLoader__.load({
|
|
|
1797
2755
|
});
|
|
1798
2756
|
};
|
|
1799
2757
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
1800
|
-
const unwrap =
|
|
2758
|
+
const unwrap = wrapAgentChatRows(ctx);
|
|
1801
2759
|
const unshadow = ctx.slots.register({
|
|
1802
2760
|
name: "conversation.chat.node",
|
|
1803
2761
|
key: "assistant-step",
|