dsh-smooth-stream 0.3.1 → 0.3.3
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 +12 -10
- package/README.md +12 -10
- package/lib/client.js +1140 -347
- package/lib/client.js.map +1 -1
- package/package.json +3 -1
- package/src/client/FollowHost.tsx +49 -4
- package/src/client/TypewriterAssistantNodeView.module.css +3 -2
- package/src/client/TypewriterAssistantNodeView.tsx +160 -23
- package/src/client/TypewriterToolNodeView.tsx +183 -8
- package/src/client/index.ts +25 -8
- package/src/client/teleprompterGlide.ts +763 -213
- 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
|
+
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
26
27
|
"root": "I17U7q_root",
|
|
27
|
-
"
|
|
28
|
+
"thinkRow": "I17U7q_thinkRow",
|
|
29
|
+
"thinkChevron": "I17U7q_thinkChevron",
|
|
30
|
+
"disclosureRow": "I17U7q_disclosureRow",
|
|
28
31
|
"disclosureLeading": "I17U7q_disclosureLeading",
|
|
32
|
+
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
33
|
+
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
34
|
+
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
35
|
+
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
36
|
+
"thinkSummary": "I17U7q_thinkSummary",
|
|
37
|
+
"follow": "I17U7q_follow",
|
|
38
|
+
"thinkTitle": "I17U7q_thinkTitle",
|
|
39
|
+
"disclosureContent": "I17U7q_disclosureContent",
|
|
29
40
|
"thinkBody": "I17U7q_thinkBody",
|
|
41
|
+
"thinkLeading": "I17U7q_thinkLeading",
|
|
30
42
|
"disclosureChevronHover": "I17U7q_disclosureChevronHover",
|
|
31
|
-
"disclosureContent": "I17U7q_disclosureContent",
|
|
32
43
|
"stopped": "I17U7q_stopped",
|
|
33
|
-
"thinkTitle": "I17U7q_thinkTitle",
|
|
34
|
-
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
35
|
-
"think": "I17U7q_think",
|
|
36
|
-
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
37
|
-
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
38
44
|
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
39
|
-
"thinkChevron": "I17U7q_thinkChevron",
|
|
40
|
-
"follow": "I17U7q_follow",
|
|
41
|
-
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
42
45
|
"body": "I17U7q_body",
|
|
43
|
-
"
|
|
44
|
-
"disclosureRow": "I17U7q_disclosureRow",
|
|
45
|
-
"thinkLeading": "I17U7q_thinkLeading",
|
|
46
|
-
"thinkRow": "I17U7q_thinkRow"
|
|
46
|
+
"think": "I17U7q_think"
|
|
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
|
|
@@ -202,16 +215,20 @@ window.__ModuleLoader__.load({
|
|
|
202
215
|
const SETTLE_DRAIN_MULTIPLIER = 1.8;
|
|
203
216
|
/** Pure per-frame reveal decision shared by the loop and its tests. */
|
|
204
217
|
function computeRevealStep(config, input, dtSeconds) {
|
|
205
|
-
const
|
|
218
|
+
const trackedCps = Math.max(input.emaCps, input.arrivalCpsEma);
|
|
219
|
+
const baseCps = clamp(trackedCps, config.minCps, config.maxFlushCps);
|
|
206
220
|
const targetLagChars = input.inputActive ? Math.max(2, Math.round(baseCps * config.targetBufferMs / 1e3)) : 0;
|
|
207
221
|
let currentCps;
|
|
208
|
-
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;
|
|
209
223
|
else if (input.inputActive) {
|
|
210
224
|
const overflow = Math.max(0, input.backlog - 32);
|
|
211
225
|
const catchup = overflow > 0 ? overflow / CATCHUP_SECONDS : 0;
|
|
212
|
-
currentCps = clamp
|
|
226
|
+
currentCps = clamp(baseCps * 1.08 + catchup, config.minCps, config.maxFlushCps);
|
|
213
227
|
} else if (input.settling) currentCps = computeSettleDrain(config, input);
|
|
214
|
-
else
|
|
228
|
+
else {
|
|
229
|
+
const idleFlushCps = Math.max(config.flushCps, baseCps * 1.8, input.arrivalCpsEma * .8);
|
|
230
|
+
currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);
|
|
231
|
+
}
|
|
215
232
|
const minRevealChars = input.inputActive ? 1 : 2;
|
|
216
233
|
return {
|
|
217
234
|
revealChars: Math.max(minRevealChars, Math.round(currentCps * dtSeconds)),
|
|
@@ -225,35 +242,35 @@ window.__ModuleLoader__.load({
|
|
|
225
242
|
* @param options - Preset, guard, and steady-rate wiring.
|
|
226
243
|
* @returns The displayed content, revealed at the smoothed cadence.
|
|
227
244
|
*/
|
|
228
|
-
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 } = {}) {
|
|
229
246
|
const config = PRESET_CONFIG[preset];
|
|
230
247
|
const seedCps = defaultCps ?? config.defaultCps;
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
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));
|
|
237
255
|
const emaCpsRef = (0, react.useRef)(seedCps);
|
|
238
256
|
const lastInputTsRef = (0, react.useRef)(0);
|
|
239
|
-
const lastInputCountRef = (0, react.useRef)(countChars(
|
|
257
|
+
const lastInputCountRef = (0, react.useRef)(countChars(initialContent));
|
|
240
258
|
const chunkSizeEmaRef = (0, react.useRef)(1);
|
|
241
259
|
const arrivalCpsEmaRef = (0, react.useRef)(seedCps);
|
|
242
260
|
const rafRef = (0, react.useRef)(null);
|
|
243
261
|
const lastFrameTsRef = (0, react.useRef)(null);
|
|
244
|
-
const
|
|
262
|
+
const queueDebtRef = (0, react.useRef)(0);
|
|
263
|
+
const settleCpsRef = (0, react.useRef)(null);
|
|
245
264
|
const holdBackRef = (0, react.useRef)(shouldHoldBack);
|
|
246
265
|
const speedOutRef = (0, react.useRef)(speedCpsRef);
|
|
247
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;
|
|
248
271
|
(0, react.useEffect)(() => {
|
|
249
272
|
holdBackRef.current = shouldHoldBack;
|
|
250
273
|
}, [shouldHoldBack]);
|
|
251
|
-
const clearWakeTimer = (0, react.useCallback)(() => {
|
|
252
|
-
if (wakeTimerRef.current !== null) {
|
|
253
|
-
clearTimeout(wakeTimerRef.current);
|
|
254
|
-
wakeTimerRef.current = null;
|
|
255
|
-
}
|
|
256
|
-
}, []);
|
|
257
274
|
const stopFrameLoop = (0, react.useCallback)(() => {
|
|
258
275
|
if (rafRef.current !== null) {
|
|
259
276
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -261,20 +278,9 @@ window.__ModuleLoader__.load({
|
|
|
261
278
|
}
|
|
262
279
|
lastFrameTsRef.current = null;
|
|
263
280
|
}, []);
|
|
264
|
-
const stopScheduling = (0, react.useCallback)(() => {
|
|
265
|
-
stopFrameLoop();
|
|
266
|
-
clearWakeTimer();
|
|
267
|
-
}, [clearWakeTimer, stopFrameLoop]);
|
|
268
281
|
const startFrameLoopRef = (0, react.useRef)(() => {});
|
|
269
|
-
const scheduleFrameWake = (0, react.useCallback)((delayMs) => {
|
|
270
|
-
clearWakeTimer();
|
|
271
|
-
wakeTimerRef.current = setTimeout(() => {
|
|
272
|
-
wakeTimerRef.current = null;
|
|
273
|
-
startFrameLoopRef.current();
|
|
274
|
-
}, Math.max(1, Math.ceil(delayMs)));
|
|
275
|
-
}, [clearWakeTimer]);
|
|
276
282
|
const syncImmediate = (0, react.useCallback)((nextContent) => {
|
|
277
|
-
|
|
283
|
+
stopFrameLoop();
|
|
278
284
|
const chars = [...nextContent];
|
|
279
285
|
const now = performance.now();
|
|
280
286
|
targetContentRef.current = nextContent;
|
|
@@ -282,43 +288,64 @@ window.__ModuleLoader__.load({
|
|
|
282
288
|
targetCountRef.current = chars.length;
|
|
283
289
|
displayedContentRef.current = nextContent;
|
|
284
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;
|
|
285
295
|
setDisplayedContent(nextContent);
|
|
286
296
|
emaCpsRef.current = seedCps;
|
|
287
297
|
chunkSizeEmaRef.current = 1;
|
|
288
298
|
arrivalCpsEmaRef.current = seedCps;
|
|
289
299
|
lastInputTsRef.current = now;
|
|
290
300
|
lastInputCountRef.current = chars.length;
|
|
291
|
-
}, [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
|
+
]);
|
|
292
311
|
const startFrameLoop = (0, react.useCallback)(() => {
|
|
293
|
-
clearWakeTimer();
|
|
294
312
|
if (rafRef.current !== null) return;
|
|
295
|
-
const tick = () => {
|
|
313
|
+
const tick = (now) => {
|
|
296
314
|
const targetCount = targetCountRef.current;
|
|
297
315
|
const displayedCount = displayedCountRef.current;
|
|
298
316
|
const backlog = targetCount - displayedCount;
|
|
299
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;
|
|
300
322
|
stopFrameLoop();
|
|
301
323
|
return;
|
|
302
324
|
}
|
|
303
|
-
const now = performance.now();
|
|
304
325
|
if (lastFrameTsRef.current === null) {
|
|
305
326
|
lastFrameTsRef.current = now;
|
|
306
327
|
rafRef.current = requestAnimationFrame(tick);
|
|
307
328
|
return;
|
|
308
329
|
}
|
|
309
|
-
const commitIntervalMs = Math.min(MAX_COMMIT_INTERVAL_MS, config.minCommitIntervalMs);
|
|
310
330
|
const frameIntervalMs = Math.max(0, now - lastFrameTsRef.current);
|
|
311
|
-
if (frameIntervalMs < commitIntervalMs) {
|
|
312
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
331
|
const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .12));
|
|
316
332
|
lastFrameTsRef.current = now;
|
|
317
333
|
const idleMs = now - lastInputTsRef.current;
|
|
318
|
-
const
|
|
319
|
-
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;
|
|
320
338
|
let revealChars;
|
|
321
|
-
|
|
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) {
|
|
322
349
|
const step = computeRevealStep(config, {
|
|
323
350
|
backlog,
|
|
324
351
|
chunkSizeEma: chunkSizeEmaRef.current,
|
|
@@ -329,15 +356,23 @@ window.__ModuleLoader__.load({
|
|
|
329
356
|
steadyCps
|
|
330
357
|
}, dtSeconds);
|
|
331
358
|
revealChars = Math.min(step.revealChars, backlog);
|
|
332
|
-
|
|
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
|
+
}
|
|
333
366
|
if (holdBackRef.current?.() === true) {
|
|
334
367
|
rafRef.current = requestAnimationFrame(tick);
|
|
335
368
|
return;
|
|
336
369
|
}
|
|
370
|
+
queueDebtRef.current = nextQueueDebt;
|
|
337
371
|
const speedOut = speedOutRef.current;
|
|
338
|
-
if (speedOut !== void 0
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
if (speedOut !== void 0) speedOut.current = revealSpeedCps;
|
|
373
|
+
if (revealChars <= 0) {
|
|
374
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
375
|
+
return;
|
|
341
376
|
}
|
|
342
377
|
const nextCount = displayedCount + revealChars;
|
|
343
378
|
const segment = targetCharsRef.current.slice(displayedCount, nextCount).join("");
|
|
@@ -356,9 +391,7 @@ window.__ModuleLoader__.load({
|
|
|
356
391
|
rafRef.current = requestAnimationFrame(tick);
|
|
357
392
|
}, [
|
|
358
393
|
config,
|
|
359
|
-
scheduleFrameWake,
|
|
360
394
|
stopFrameLoop,
|
|
361
|
-
clearWakeTimer,
|
|
362
395
|
steadyCps
|
|
363
396
|
]);
|
|
364
397
|
(0, react.useEffect)(() => {
|
|
@@ -381,14 +414,16 @@ window.__ModuleLoader__.load({
|
|
|
381
414
|
targetContentRef.current = content;
|
|
382
415
|
targetCharsRef.current.push(...appendedChars);
|
|
383
416
|
targetCountRef.current += appendedCount;
|
|
417
|
+
settleCpsRef.current = null;
|
|
384
418
|
const hadSample = lastInputTsRef.current > 0;
|
|
385
419
|
const deltaChars = targetCountRef.current - lastInputCountRef.current;
|
|
386
420
|
const deltaMs = Math.max(1, now - lastInputTsRef.current);
|
|
387
421
|
if (hadSample && deltaChars > 0) {
|
|
388
|
-
const
|
|
422
|
+
const instantCps = deltaChars * 1e3 / deltaMs;
|
|
423
|
+
const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 3);
|
|
389
424
|
const chunkEmaAlpha = .45;
|
|
390
|
-
chunkSizeEmaRef.current = chunkSizeEmaRef.current *
|
|
391
|
-
arrivalCpsEmaRef.current = arrivalCpsEmaRef.current *
|
|
425
|
+
chunkSizeEmaRef.current = chunkSizeEmaRef.current * .55 + appendedCount * chunkEmaAlpha;
|
|
426
|
+
arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * .55 + normalizedInstantCps * chunkEmaAlpha;
|
|
392
427
|
emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + normalizedInstantCps * config.emaAlpha;
|
|
393
428
|
}
|
|
394
429
|
lastInputTsRef.current = now;
|
|
@@ -403,9 +438,9 @@ window.__ModuleLoader__.load({
|
|
|
403
438
|
]);
|
|
404
439
|
(0, react.useEffect)(() => {
|
|
405
440
|
return () => {
|
|
406
|
-
|
|
441
|
+
stopFrameLoop();
|
|
407
442
|
};
|
|
408
|
-
}, [
|
|
443
|
+
}, [stopFrameLoop]);
|
|
409
444
|
return displayedContent;
|
|
410
445
|
}
|
|
411
446
|
//#endregion
|
|
@@ -488,268 +523,465 @@ window.__ModuleLoader__.load({
|
|
|
488
523
|
/**
|
|
489
524
|
* Conversation-port follow while an assistant reply streams.
|
|
490
525
|
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
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:
|
|
496
531
|
*
|
|
497
|
-
* -
|
|
532
|
+
* - marks programmatic writes via `data-follow-owned` for compatible hosts;
|
|
498
533
|
* - sets `overflow-anchor: none` so CSS scroll-anchoring does not snap;
|
|
499
534
|
* - restores `animatedH` in a ResizeObserver (before paint) so a layout
|
|
500
535
|
* pass cannot flash a snapped frame;
|
|
501
|
-
* -
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* -
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* each wrap instead of hopping a line at a time.
|
|
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.
|
|
508
542
|
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* before clearing the transform, so the frame stays continuous. That write
|
|
512
|
-
* also lands in ChatView's observed-top ledger as reader input beyond
|
|
513
|
-
* `FOLLOW_THRESHOLD`, releasing its native snap-follow instead of fighting
|
|
514
|
-
* the gesture for the port.
|
|
543
|
+
* A real reader gesture receives the effective visual position before the
|
|
544
|
+
* transform clears. Lifecycle completion instead settles at the floor.
|
|
515
545
|
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
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.
|
|
518
549
|
*/
|
|
519
550
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
* 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.
|
|
523
553
|
*/
|
|
524
554
|
const FOLLOW_OWNED_ATTR = "data-follow-owned";
|
|
525
|
-
/**
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
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;
|
|
530
561
|
const GESTURE_EVENTS = [
|
|
531
562
|
"wheel",
|
|
563
|
+
"touchstart",
|
|
532
564
|
"touchmove",
|
|
565
|
+
"touchend",
|
|
566
|
+
"touchcancel",
|
|
533
567
|
"pointerdown",
|
|
534
568
|
"keydown"
|
|
535
569
|
];
|
|
536
|
-
|
|
537
|
-
|
|
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);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
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));
|
|
538
593
|
}
|
|
539
594
|
/**
|
|
540
|
-
*
|
|
595
|
+
* Semi-implicit spring integration with four substeps per <=32ms slice.
|
|
541
596
|
* @param dtMs - Frame delta in ms.
|
|
542
|
-
* @param input - Current lag
|
|
543
|
-
* @returns The
|
|
597
|
+
* @param input - Current visible lag and carried physics velocity.
|
|
598
|
+
* @returns The position advance, its fraction, and next velocity.
|
|
544
599
|
*/
|
|
545
600
|
function computeFollowStep(dtMs, input) {
|
|
546
601
|
if (input.lag <= .1 || dtMs <= 0) return {
|
|
547
602
|
advancePx: 0,
|
|
548
|
-
lerpStep: 0
|
|
603
|
+
lerpStep: 0,
|
|
604
|
+
velocityPxPerSec: 0
|
|
549
605
|
};
|
|
550
|
-
|
|
551
|
-
|
|
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;
|
|
552
623
|
return {
|
|
553
|
-
advancePx
|
|
554
|
-
lerpStep
|
|
624
|
+
advancePx,
|
|
625
|
+
lerpStep: advancePx / input.lag,
|
|
626
|
+
velocityPxPerSec: velocity
|
|
555
627
|
};
|
|
556
628
|
}
|
|
557
|
-
/** The message-rows box the lag transform rides on, when the host has one. */
|
|
558
|
-
function shiftRootOf(port) {
|
|
559
|
-
return port.querySelector("[data-chat-transcript]");
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Row wrappers that carry only messages/tools. Hosts without a transcript
|
|
563
|
-
* box keep the turn-status chrome as a sibling of the rows inside
|
|
564
|
-
* `[data-chat-flow]`, so shifting that whole flow would drag the chrome
|
|
565
|
-
* along; shifting the rows individually leaves every non-message sibling
|
|
566
|
-
* (turn status, steering bubbles) pinned while the text glides.
|
|
567
|
-
*
|
|
568
|
-
* Only the outermost rows are shifted: a tool call nests its subcalls as
|
|
569
|
-
* descendant `[data-chat-anchor-key]` rows, and writing the lag on each of
|
|
570
|
-
* them would double (or further multiply) the shift, tearing the subcalls
|
|
571
|
-
* away from their parent every frame. Descendants ride the parent's
|
|
572
|
-
* transform instead.
|
|
573
|
-
*/
|
|
574
|
-
function shiftRowsOf(port) {
|
|
575
|
-
return [...port.querySelectorAll("[data-chat-anchor-key]")].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
|
|
576
|
-
}
|
|
577
629
|
/** Element whose resize signals flow growth for the before-paint restore. */
|
|
578
630
|
function resizeProxyOf(port) {
|
|
579
631
|
return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
|
|
580
632
|
}
|
|
581
|
-
/**
|
|
582
|
-
function
|
|
583
|
-
|
|
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);
|
|
638
|
+
}
|
|
639
|
+
function currentShiftOf(element) {
|
|
640
|
+
return Number(/translate3d\(0(?:px)?,\s*(-?[\d.]+)px,\s*0(?:px)?\)/.exec(element.style.transform)?.[1] ?? 0);
|
|
584
641
|
}
|
|
585
|
-
function setShift(element, px
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
element.style.transform = `translate3d(0, ${
|
|
642
|
+
function setShift(element, px) {
|
|
643
|
+
if (Math.abs(px) > .01) {
|
|
644
|
+
if (Math.abs(currentShiftOf(element) - px) <= .01 && element.style.willChange === "transform" && element.style.clipPath === "") return;
|
|
645
|
+
element.style.transform = `translate3d(0, ${px}px, 0)`;
|
|
589
646
|
element.style.willChange = "transform";
|
|
590
647
|
} else {
|
|
648
|
+
if (element.style.transform === "" && element.style.willChange === "" && element.style.clipPath === "") return;
|
|
591
649
|
element.style.transform = "";
|
|
592
650
|
element.style.willChange = "";
|
|
593
651
|
}
|
|
652
|
+
element.style.clipPath = "";
|
|
653
|
+
}
|
|
654
|
+
function turnStatusOf(port) {
|
|
655
|
+
return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
|
|
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
|
+
}
|
|
670
|
+
const followRunways = /* @__PURE__ */ new WeakMap();
|
|
671
|
+
const followPaintLimits = /* @__PURE__ */ new WeakMap();
|
|
672
|
+
function invalidatePaintLimit(port) {
|
|
673
|
+
followPaintLimits.delete(port);
|
|
674
|
+
}
|
|
675
|
+
/** Logical position and velocity survive a React owner handoff and finish. */
|
|
676
|
+
const followMotionStates = /* @__PURE__ */ new WeakMap();
|
|
677
|
+
function restoreRunway(port) {
|
|
678
|
+
const runway = followRunways.get(port);
|
|
679
|
+
if (runway === void 0) return;
|
|
680
|
+
runway.element.style[runway.property] = runway.original;
|
|
681
|
+
followRunways.delete(port);
|
|
682
|
+
invalidatePaintLimit(port);
|
|
683
|
+
}
|
|
684
|
+
function ensureRunway(port, surfaces) {
|
|
685
|
+
const status = turnStatusOf(port);
|
|
686
|
+
const composer = port.querySelector("[data-composer-seat]");
|
|
687
|
+
const target = status === null ? {
|
|
688
|
+
element: composer === null ? void 0 : surfaces.at(-1),
|
|
689
|
+
property: "marginBottom"
|
|
690
|
+
} : {
|
|
691
|
+
element: status,
|
|
692
|
+
property: "marginTop"
|
|
693
|
+
};
|
|
694
|
+
if (target.element === void 0) {
|
|
695
|
+
restoreRunway(port);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const element = target.element;
|
|
699
|
+
const current = followRunways.get(port);
|
|
700
|
+
if (current?.element === element && current.property === target.property) return;
|
|
701
|
+
restoreRunway(port);
|
|
702
|
+
const beforeHeight = port.scrollHeight;
|
|
703
|
+
const original = element.style[target.property];
|
|
704
|
+
element.style[target.property] = original === "" ? `48px` : `calc(${original} + 48px)`;
|
|
705
|
+
const offset = Math.max(0, port.scrollHeight - beforeHeight);
|
|
706
|
+
followRunways.set(port, {
|
|
707
|
+
element,
|
|
708
|
+
offset,
|
|
709
|
+
property: target.property,
|
|
710
|
+
original
|
|
711
|
+
});
|
|
712
|
+
invalidatePaintLimit(port);
|
|
713
|
+
}
|
|
714
|
+
function runwayOffsetOf(port) {
|
|
715
|
+
return followRunways.get(port)?.offset ?? 0;
|
|
716
|
+
}
|
|
717
|
+
/** Available paint room below the last message before fixed conversation chrome. */
|
|
718
|
+
function safeShiftLimit(port, surfaces) {
|
|
719
|
+
const last = surfaces.at(-1);
|
|
720
|
+
if (last === void 0) return 0;
|
|
721
|
+
const status = turnStatusOf(port);
|
|
722
|
+
const composer = port.querySelector("[data-composer-seat]");
|
|
723
|
+
const cached = followPaintLimits.get(port);
|
|
724
|
+
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;
|
|
725
|
+
const ceiling = [status, composer].filter((element) => element !== null).map((element) => ({
|
|
726
|
+
element,
|
|
727
|
+
rect: element.getBoundingClientRect()
|
|
728
|
+
})).filter(({ rect }) => Number.isFinite(rect.top) && Number.isFinite(rect.bottom) && rect.bottom > rect.top).sort((first, second) => first.rect.top - second.rect.top)[0];
|
|
729
|
+
if (ceiling === void 0) return status === null && composer === null ? Number.POSITIVE_INFINITY : runwayOffsetOf(port);
|
|
730
|
+
const ceilingTop = ceiling.rect.top - currentShiftOf(ceiling.element);
|
|
731
|
+
const naturalBottom = last.getBoundingClientRect().bottom - currentShiftOf(last);
|
|
732
|
+
const limit = Math.max(0, ceilingTop - naturalBottom - 1);
|
|
733
|
+
followPaintLimits.set(port, {
|
|
734
|
+
clientHeight: port.clientHeight,
|
|
735
|
+
limit,
|
|
736
|
+
measuredAtMs: performance.now(),
|
|
737
|
+
composer,
|
|
738
|
+
status,
|
|
739
|
+
surface: last
|
|
740
|
+
});
|
|
741
|
+
return limit;
|
|
742
|
+
}
|
|
743
|
+
function setFollowScrollTop(port, nextTop) {
|
|
744
|
+
if (Math.abs(port.scrollTop - nextTop) > .01) port.scrollTop = nextTop;
|
|
745
|
+
followScrollLedgers.set(port, port.scrollTop);
|
|
746
|
+
const ownedTop = String(port.scrollTop);
|
|
747
|
+
if (port.getAttribute(FOLLOW_OWNED_ATTR) !== ownedTop) port.setAttribute(FOLLOW_OWNED_ATTR, ownedTop);
|
|
594
748
|
}
|
|
595
749
|
/**
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
750
|
+
* Last scrollTop this engine wrote or accepted, per port. Reader intent is a
|
|
751
|
+
* real upward delta from this ledger; a key press or touch while pinned
|
|
752
|
+
* (typing in the composer) must not release the pin, because a released pin
|
|
753
|
+
* can never re-acquire while content streams away from the reader position.
|
|
600
754
|
*/
|
|
601
|
-
|
|
602
|
-
|
|
755
|
+
const followScrollLedgers = /* @__PURE__ */ new WeakMap();
|
|
756
|
+
const followActivityAt = /* @__PURE__ */ new WeakMap();
|
|
757
|
+
/** Whether this port was owned recently enough to identify a closing tail row. */
|
|
758
|
+
function hasRecentConversationFollow(port, windowMs = 250) {
|
|
759
|
+
const last = followActivityAt.get(port);
|
|
760
|
+
return last !== void 0 && performance.now() - last <= windowMs;
|
|
761
|
+
}
|
|
762
|
+
function readerScrolledUp(port) {
|
|
763
|
+
return port.scrollTop < (followScrollLedgers.get(port) ?? 0) - 8;
|
|
603
764
|
}
|
|
604
765
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
* shift surface the engine itself carries the interpolated top.
|
|
766
|
+
* Paint a bounded visual lag and return the effective logical extent.
|
|
767
|
+
*
|
|
768
|
+
* This is the final geometry invariant, not merely an animation preference:
|
|
769
|
+
* any lag beyond the real gap to status/composer chrome is caught up in the
|
|
770
|
+
* same frame. Carrying that excess in `scrollTop` would move the transcript
|
|
771
|
+
* toward fixed chrome and also make the host expose jump-to-bottom.
|
|
612
772
|
*/
|
|
613
|
-
function applyVisual(port, animatedH) {
|
|
773
|
+
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0) {
|
|
774
|
+
const surfaces = shiftSurfacesOf(port);
|
|
775
|
+
ensureRunway(port, surfaces);
|
|
614
776
|
const contentHeight = Math.max(0, port.scrollHeight);
|
|
777
|
+
const runwayOffset = runwayOffsetOf(port);
|
|
778
|
+
const targetHeight = Math.max(0, contentHeight - runwayOffset);
|
|
615
779
|
const floor = Math.max(0, contentHeight - port.clientHeight);
|
|
616
|
-
const extent = Math.min(
|
|
617
|
-
|
|
618
|
-
port.style.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
const rows = shiftRowsOf(port);
|
|
636
|
-
if (rows.length > 0) {
|
|
637
|
-
if (floor <= 0) {
|
|
638
|
-
port.scrollTop = 0;
|
|
639
|
-
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
640
|
-
const chrome = chromeShiftOf(port);
|
|
641
|
-
if (chrome !== null) setShift(chrome, lag, true);
|
|
642
|
-
for (const row of rows) setShift(row, 0);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
port.scrollTop = floor;
|
|
646
|
-
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
647
|
-
for (const row of rows) setShift(row, lag);
|
|
648
|
-
return;
|
|
780
|
+
const extent = Math.min(targetHeight, Math.max(0, animatedH));
|
|
781
|
+
if (port.style.overflowAnchor !== "none") port.style.overflowAnchor = "none";
|
|
782
|
+
if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
|
|
783
|
+
if (floor <= 0) {
|
|
784
|
+
setFollowScrollTop(port, 0);
|
|
785
|
+
followMotionStates.set(port, {
|
|
786
|
+
capacityPx: Number.POSITIVE_INFINITY,
|
|
787
|
+
constrained: false,
|
|
788
|
+
extent: targetHeight,
|
|
789
|
+
lagPx: 0,
|
|
790
|
+
reservePx: 0,
|
|
791
|
+
velocityPxPerSec: 0
|
|
792
|
+
});
|
|
793
|
+
for (const surface of surfaces) setShift(surface, 0);
|
|
794
|
+
const status = turnStatusOf(port);
|
|
795
|
+
if (status !== null) setShift(status, 0);
|
|
796
|
+
return targetHeight;
|
|
649
797
|
}
|
|
650
|
-
port
|
|
651
|
-
port
|
|
798
|
+
setFollowScrollTop(port, floor);
|
|
799
|
+
const limit = floor > 0 ? safeShiftLimit(port, surfaces) : 0;
|
|
800
|
+
const visibleReserve = Math.min(runwayOffset, Math.max(0, reservePx));
|
|
801
|
+
const baselineShift = runwayOffset - visibleReserve;
|
|
802
|
+
const requestedLag = Math.max(0, targetHeight - extent);
|
|
803
|
+
const shift = Math.min(baselineShift + requestedLag, Math.max(0, limit));
|
|
804
|
+
const effectiveLag = Math.max(0, shift - baselineShift);
|
|
805
|
+
const capacityPx = Math.max(0, limit - baselineShift);
|
|
806
|
+
const effectiveExtent = targetHeight - effectiveLag;
|
|
807
|
+
followMotionStates.set(port, {
|
|
808
|
+
capacityPx,
|
|
809
|
+
constrained: requestedLag > effectiveLag + FOLLOW_SETTLE_EPSILON_PX,
|
|
810
|
+
extent: effectiveExtent,
|
|
811
|
+
lagPx: effectiveLag,
|
|
812
|
+
reservePx: visibleReserve,
|
|
813
|
+
velocityPxPerSec
|
|
814
|
+
});
|
|
815
|
+
for (const surface of surfaces) setShift(surface, shift);
|
|
816
|
+
const status = turnStatusOf(port);
|
|
817
|
+
if (status !== null) setShift(status, 0);
|
|
818
|
+
return effectiveExtent;
|
|
652
819
|
}
|
|
653
|
-
function
|
|
820
|
+
function clearMotion(port) {
|
|
654
821
|
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
655
822
|
port.style.overflowAnchor = "";
|
|
656
823
|
port.style.scrollBehavior = "";
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (chrome !== null) {
|
|
667
|
-
chrome.style.transform = "";
|
|
668
|
-
chrome.style.willChange = "";
|
|
669
|
-
}
|
|
824
|
+
for (const surface of shiftSurfacesOf(port)) setShift(surface, 0);
|
|
825
|
+
const status = turnStatusOf(port);
|
|
826
|
+
if (status !== null) setShift(status, 0);
|
|
827
|
+
}
|
|
828
|
+
function clearVisual(port) {
|
|
829
|
+
clearMotion(port);
|
|
830
|
+
restoreRunway(port);
|
|
831
|
+
followMotionStates.delete(port);
|
|
832
|
+
invalidatePaintLimit(port);
|
|
670
833
|
}
|
|
671
|
-
/**
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
834
|
+
/** Keep an already-promoted surface at zero until one stable final paint lands. */
|
|
835
|
+
function holdCompositorAtRest(element) {
|
|
836
|
+
element.style.transform = "translate3d(0, 0px, 0)";
|
|
837
|
+
element.style.willChange = "transform";
|
|
838
|
+
element.style.clipPath = "";
|
|
675
839
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
840
|
+
/** Remove equal offsets, land on the floor, then retire the compositor quietly. */
|
|
841
|
+
function finishAtNaturalFloor(port) {
|
|
842
|
+
const surfaces = shiftSurfacesOf(port);
|
|
843
|
+
const status = turnStatusOf(port);
|
|
844
|
+
const promoted = [...surfaces, ...status === null ? [] : [status]].filter((element) => element.style.transform !== "" || element.style.willChange === "transform");
|
|
845
|
+
const promotedSet = new Set(promoted);
|
|
846
|
+
restoreRunway(port);
|
|
847
|
+
settleAtFloor(port);
|
|
848
|
+
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
849
|
+
port.style.overflowAnchor = "";
|
|
850
|
+
port.style.scrollBehavior = "";
|
|
851
|
+
for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
|
|
852
|
+
else setShift(surface, 0);
|
|
853
|
+
if (status !== null) {
|
|
854
|
+
if (promotedSet.has(status)) holdCompositorAtRest(status);
|
|
855
|
+
else setShift(status, 0);
|
|
681
856
|
}
|
|
682
|
-
|
|
683
|
-
return
|
|
857
|
+
followMotionStates.delete(port);
|
|
858
|
+
if (promoted.length === 0) return;
|
|
859
|
+
requestAnimationFrame(() => {
|
|
860
|
+
requestAnimationFrame(() => {
|
|
861
|
+
if (port.hasAttribute(FOLLOW_OWNED_ATTR)) return;
|
|
862
|
+
for (const element of promoted) if (Math.abs(currentShiftOf(element)) <= .01) setShift(element, 0);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
function settleAtFloor(port) {
|
|
867
|
+
setFollowScrollTop(port, Math.max(0, port.scrollHeight - port.clientHeight));
|
|
684
868
|
}
|
|
869
|
+
/** Only the newest active follower may write one port's shared visual state. */
|
|
870
|
+
const followLeaders = /* @__PURE__ */ new WeakMap();
|
|
871
|
+
let followGeneration = 0;
|
|
685
872
|
/**
|
|
686
873
|
* Own the conversation scrollport's bottom-follow while `active` is true.
|
|
687
874
|
*
|
|
688
875
|
* @param rootRef - An element inside the conversation scrollport.
|
|
689
876
|
* @param active - True while the reply is still revealing.
|
|
690
877
|
* @param speedCpsRef - Live reveal-rate EMA from the smoother.
|
|
878
|
+
* @param revealScaleRef - Optional backpressure control for text reveal.
|
|
879
|
+
* @param predictive - Whether to reserve paint room ahead of growth.
|
|
880
|
+
* @param entrance - Whether the first committed row height should glide in.
|
|
881
|
+
* @param onEntranceSettled - Releases a one-shot entrance owner after catch-up.
|
|
882
|
+
* @param predictiveRef - Optional live visibility gate for predictive runway.
|
|
883
|
+
* @param entranceExtentRef - Optional measured growth delta for a generic row.
|
|
691
884
|
*/
|
|
692
|
-
function useConversationFollow(rootRef, active, speedCpsRef) {
|
|
885
|
+
function useConversationFollow(rootRef, active, speedCpsRef, revealScaleRef, predictive = true, entrance = false, onEntranceSettled, predictiveRef, entranceExtentRef) {
|
|
693
886
|
const activeRef = (0, react.useRef)(active);
|
|
887
|
+
const entranceRef = (0, react.useRef)(entrance);
|
|
888
|
+
const onEntranceSettledRef = (0, react.useRef)(onEntranceSettled);
|
|
889
|
+
entranceRef.current = entrance;
|
|
890
|
+
onEntranceSettledRef.current = onEntranceSettled;
|
|
694
891
|
(0, react.useEffect)(() => {
|
|
695
892
|
activeRef.current = active;
|
|
696
893
|
}, [active]);
|
|
697
|
-
(0, react.
|
|
894
|
+
(0, react.useLayoutEffect)(() => {
|
|
698
895
|
if (!active) return;
|
|
896
|
+
const owner = {};
|
|
897
|
+
const generation = ++followGeneration;
|
|
699
898
|
let rafId = 0;
|
|
700
899
|
let last = performance.now();
|
|
701
900
|
let following = true;
|
|
702
901
|
let primed = false;
|
|
703
902
|
let animatedH = 0;
|
|
903
|
+
let reservePx = 0;
|
|
904
|
+
let velocityPxPerSec = 0;
|
|
704
905
|
let interacting = false;
|
|
906
|
+
let readerGestureIntent = false;
|
|
907
|
+
let readerReleased = false;
|
|
908
|
+
let touchStartY = null;
|
|
705
909
|
let interactTimer = null;
|
|
706
910
|
let port = null;
|
|
707
911
|
let resize = null;
|
|
708
912
|
let holding = null;
|
|
709
|
-
let
|
|
913
|
+
let entrancePending = entranceRef.current;
|
|
914
|
+
const finishEntrance = () => {
|
|
915
|
+
if (!entrancePending) return;
|
|
916
|
+
entrancePending = false;
|
|
917
|
+
onEntranceSettledRef.current?.();
|
|
918
|
+
};
|
|
919
|
+
const updateRevealScale = (next, elapsedMs, urgent = false) => {
|
|
920
|
+
if (revealScaleRef === void 0) return;
|
|
921
|
+
const state = followMotionStates.get(next);
|
|
922
|
+
const target = state === void 0 ? 1 : computeFollowRevealScale(state.lagPx, state.capacityPx, state.constrained);
|
|
923
|
+
const current = Math.min(1, Math.max(FOLLOW_REVEAL_MIN_SCALE, revealScaleRef.current));
|
|
924
|
+
if (target < current || urgent) {
|
|
925
|
+
revealScaleRef.current = Math.min(current, target);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const releaseStep = 1 - Math.exp(-Math.max(0, elapsedMs) / 240);
|
|
929
|
+
revealScaleRef.current = current + (target - current) * releaseStep;
|
|
930
|
+
};
|
|
931
|
+
const releaseRevealScale = () => {
|
|
932
|
+
if (revealScaleRef !== void 0) revealScaleRef.current = 1;
|
|
933
|
+
};
|
|
934
|
+
const isLeader = (next) => followLeaders.get(next)?.owner === owner;
|
|
710
935
|
const hold = (next) => {
|
|
711
|
-
|
|
712
|
-
if (holding
|
|
713
|
-
acquireFollow(next);
|
|
936
|
+
followActivityAt.set(next, performance.now());
|
|
937
|
+
if (holding === next && isLeader(next)) return;
|
|
714
938
|
holding = next;
|
|
939
|
+
const leader = followLeaders.get(next);
|
|
940
|
+
if (leader === void 0 || generation > leader.generation) followLeaders.set(next, {
|
|
941
|
+
generation,
|
|
942
|
+
owner
|
|
943
|
+
});
|
|
715
944
|
};
|
|
716
945
|
const drop = (next) => {
|
|
717
|
-
if (holding === next)
|
|
718
|
-
|
|
719
|
-
|
|
946
|
+
if (holding === next) holding = null;
|
|
947
|
+
if (isLeader(next)) {
|
|
948
|
+
clearMotion(next);
|
|
949
|
+
followMotionStates.delete(next);
|
|
950
|
+
releaseRevealScale();
|
|
720
951
|
}
|
|
721
|
-
clearVisual(next);
|
|
722
952
|
};
|
|
723
|
-
/**
|
|
724
|
-
* Give the reader the visual position the glide was showing before the
|
|
725
|
-
* transforms go away. While transforms carry the lag, the engine sits at
|
|
726
|
-
* the floor and the effective visual top is `engine - lag`; writing that
|
|
727
|
-
* keeps the handover frame-continuous, and the write shows up in
|
|
728
|
-
* ChatView's ledger as reader movement, disarming its snap-follow.
|
|
729
|
-
* Without a shift surface the engine already holds the interpolated top,
|
|
730
|
-
* so there is nothing to compensate.
|
|
731
|
-
*/
|
|
732
953
|
const handBackVisual = (next) => {
|
|
733
|
-
|
|
954
|
+
const shift = currentShiftOf(shiftSurfacesOf(next).at(-1) ?? next);
|
|
955
|
+
const visualTop = Math.max(0, next.scrollTop - Math.max(0, shift));
|
|
734
956
|
const floor = Math.max(0, next.scrollHeight - next.clientHeight);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
next.scrollTop = Math.min(floor, Math.max(0, next.scrollTop - lag));
|
|
957
|
+
next.scrollTop = Math.min(visualTop, Math.max(0, floor - 26));
|
|
958
|
+
followScrollLedgers.set(next, next.scrollTop);
|
|
738
959
|
};
|
|
739
960
|
const markGesture = (event) => {
|
|
740
961
|
interacting = true;
|
|
741
|
-
if (event
|
|
742
|
-
|
|
962
|
+
if (event.type === "wheel") {
|
|
963
|
+
const deltaY = event.deltaY;
|
|
964
|
+
if (Number.isFinite(deltaY) && deltaY < 0) readerGestureIntent = true;
|
|
965
|
+
} else if (event.type === "touchstart") touchStartY = event.touches[0]?.clientY ?? null;
|
|
966
|
+
else if (event.type === "touchmove") {
|
|
967
|
+
const touch = event.touches[0];
|
|
968
|
+
if (touch !== void 0) {
|
|
969
|
+
if (touchStartY === null) touchStartY = touch.clientY;
|
|
970
|
+
if (touch.clientY - touchStartY > 1) readerGestureIntent = true;
|
|
971
|
+
}
|
|
972
|
+
} else if (event.type === "touchend" || event.type === "touchcancel") touchStartY = null;
|
|
743
973
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
744
974
|
interactTimer = setTimeout(() => {
|
|
745
975
|
interacting = false;
|
|
976
|
+
readerGestureIntent = false;
|
|
746
977
|
interactTimer = null;
|
|
747
|
-
awayPx = 0;
|
|
748
978
|
}, 800);
|
|
749
979
|
};
|
|
750
980
|
const restoreBeforePaint = () => {
|
|
751
|
-
if (!following || port === null) return;
|
|
752
|
-
|
|
981
|
+
if (!following || port === null || !isLeader(port)) return;
|
|
982
|
+
invalidatePaintLimit(port);
|
|
983
|
+
animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec);
|
|
984
|
+
updateRevealScale(port, 0, true);
|
|
753
985
|
};
|
|
754
986
|
const bindPort = (next) => {
|
|
755
987
|
if (port === next) return;
|
|
@@ -758,6 +990,7 @@ window.__ModuleLoader__.load({
|
|
|
758
990
|
resize?.disconnect();
|
|
759
991
|
}
|
|
760
992
|
port = next;
|
|
993
|
+
invalidatePaintLimit(port);
|
|
761
994
|
for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
|
|
762
995
|
if (typeof ResizeObserver !== "undefined") {
|
|
763
996
|
resize = new ResizeObserver(restoreBeforePaint);
|
|
@@ -768,48 +1001,96 @@ window.__ModuleLoader__.load({
|
|
|
768
1001
|
};
|
|
769
1002
|
const frame = (now) => {
|
|
770
1003
|
rafId = requestAnimationFrame(frame);
|
|
771
|
-
const
|
|
1004
|
+
const elapsedMs = Math.max(.001, now - last);
|
|
1005
|
+
const dt = Math.min(32, elapsedMs);
|
|
772
1006
|
last = now;
|
|
773
1007
|
const root = rootRef.current;
|
|
774
1008
|
if (root === null) return;
|
|
775
1009
|
const nextPort = root.closest("[data-conversation-scroll]");
|
|
776
1010
|
if (nextPort === null) return;
|
|
777
1011
|
bindPort(nextPort);
|
|
1012
|
+
if (nextPort.clientHeight <= 0) return;
|
|
778
1013
|
const reportedLag = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight) - nextPort.scrollTop;
|
|
779
1014
|
const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
|
|
780
1015
|
if (!primed) {
|
|
781
|
-
|
|
782
|
-
|
|
1016
|
+
const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
|
|
1017
|
+
if (inherited === void 0) {
|
|
1018
|
+
const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
|
|
1019
|
+
animatedH = Math.max(0, nextPort.scrollHeight - entranceExtent);
|
|
1020
|
+
reservePx = 0;
|
|
1021
|
+
velocityPxPerSec = 0;
|
|
1022
|
+
following = Math.max(0, reportedLag - entranceExtent) <= 25;
|
|
1023
|
+
} else {
|
|
1024
|
+
animatedH = Math.min(nextPort.scrollHeight, inherited.extent);
|
|
1025
|
+
reservePx = inherited.reservePx;
|
|
1026
|
+
velocityPxPerSec = inherited.velocityPxPerSec;
|
|
1027
|
+
following = true;
|
|
1028
|
+
}
|
|
783
1029
|
if (following) {
|
|
784
1030
|
hold(nextPort);
|
|
785
|
-
|
|
786
|
-
|
|
1031
|
+
if (isLeader(nextPort)) {
|
|
1032
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec);
|
|
1033
|
+
updateRevealScale(nextPort, elapsedMs);
|
|
1034
|
+
const runwayOffset = runwayOffsetOf(nextPort);
|
|
1035
|
+
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
|
|
1036
|
+
} else finishEntrance();
|
|
1037
|
+
} else finishEntrance();
|
|
787
1038
|
primed = true;
|
|
788
1039
|
return;
|
|
789
1040
|
}
|
|
790
|
-
if (!following && !interacting && reportedLag <= 25) {
|
|
1041
|
+
if (!following && !interacting && reportedLag <= (readerReleased ? 1 : 25)) {
|
|
791
1042
|
following = true;
|
|
1043
|
+
readerReleased = false;
|
|
792
1044
|
animatedH = extent;
|
|
1045
|
+
reservePx = 0;
|
|
1046
|
+
velocityPxPerSec = 0;
|
|
1047
|
+
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
793
1048
|
hold(nextPort);
|
|
794
|
-
} else if (following && interacting &&
|
|
1049
|
+
} else if (following && interacting && (readerGestureIntent || readerScrolledUp(nextPort))) {
|
|
795
1050
|
following = false;
|
|
796
|
-
|
|
1051
|
+
readerGestureIntent = false;
|
|
1052
|
+
readerReleased = true;
|
|
797
1053
|
handBackVisual(nextPort);
|
|
798
1054
|
animatedH = nextPort.scrollHeight;
|
|
1055
|
+
reservePx = 0;
|
|
1056
|
+
velocityPxPerSec = 0;
|
|
799
1057
|
drop(nextPort);
|
|
1058
|
+
finishEntrance();
|
|
1059
|
+
}
|
|
1060
|
+
if (!activeRef.current || !following) {
|
|
1061
|
+
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
1062
|
+
return;
|
|
800
1063
|
}
|
|
801
|
-
if (!activeRef.current || !following) return;
|
|
802
1064
|
hold(nextPort);
|
|
803
|
-
|
|
1065
|
+
if (!isLeader(nextPort)) {
|
|
1066
|
+
finishEntrance();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const runwayOffset = runwayOffsetOf(nextPort);
|
|
1070
|
+
const contentHeight = nextPort.scrollHeight;
|
|
1071
|
+
const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
|
|
1072
|
+
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1073
|
+
const reserveTarget = !predictGrowth ? 0 : computeFollowReserve(speedCpsRef.current, runwayOffset);
|
|
1074
|
+
const reserveStep = 1 - Math.exp(-elapsedMs / 180);
|
|
1075
|
+
reservePx += (reserveTarget - reservePx) * reserveStep;
|
|
804
1076
|
const step = computeFollowStep(dt, {
|
|
805
1077
|
lag,
|
|
806
|
-
speedEma: speedCpsRef.current
|
|
1078
|
+
speedEma: speedCpsRef.current,
|
|
1079
|
+
velocityPxPerSec
|
|
807
1080
|
});
|
|
808
|
-
if (lag <= .1)
|
|
809
|
-
|
|
810
|
-
|
|
1081
|
+
if (lag <= .1) {
|
|
1082
|
+
animatedH = contentHeight - runwayOffset;
|
|
1083
|
+
velocityPxPerSec = 0;
|
|
1084
|
+
} else {
|
|
1085
|
+
const minimumLag = predictGrowth ? 0 : Math.max(0, reservePx);
|
|
1086
|
+
animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
|
|
1087
|
+
velocityPxPerSec = step.velocityPxPerSec;
|
|
1088
|
+
}
|
|
1089
|
+
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec);
|
|
1090
|
+
updateRevealScale(nextPort, elapsedMs);
|
|
1091
|
+
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
|
|
811
1092
|
};
|
|
812
|
-
|
|
1093
|
+
frame(performance.now());
|
|
813
1094
|
return () => {
|
|
814
1095
|
cancelAnimationFrame(rafId);
|
|
815
1096
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
@@ -817,26 +1098,125 @@ window.__ModuleLoader__.load({
|
|
|
817
1098
|
if (port !== null) for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
818
1099
|
const host = rootRef.current?.closest("[data-conversation-scroll]") ?? port;
|
|
819
1100
|
if (host === null) return;
|
|
820
|
-
if (following) handBackVisual(host);
|
|
821
|
-
const remaining = holding === host ? releaseFollow(host) : followOwners.get(host) ?? 0;
|
|
822
1101
|
holding = null;
|
|
823
|
-
if (
|
|
1102
|
+
if (!isLeader(host)) return;
|
|
1103
|
+
const preserveReader = interacting && (readerGestureIntent || readerScrolledUp(host));
|
|
1104
|
+
if (!following || !primed) {
|
|
1105
|
+
clearVisual(host);
|
|
1106
|
+
followLeaders.delete(host);
|
|
1107
|
+
releaseRevealScale();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (preserveReader) {
|
|
1111
|
+
handBackVisual(host);
|
|
1112
|
+
clearVisual(host);
|
|
1113
|
+
followLeaders.delete(host);
|
|
1114
|
+
releaseRevealScale();
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
ensureRunway(host, shiftSurfacesOf(host));
|
|
1118
|
+
const completionRunway = runwayOffsetOf(host);
|
|
1119
|
+
const completionMinimumLag = Math.max(0, reservePx);
|
|
1120
|
+
animatedH = Math.min(animatedH, host.scrollHeight - completionRunway - completionMinimumLag);
|
|
1121
|
+
settleAtFloor(host);
|
|
1122
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec);
|
|
1123
|
+
const runwayOffset = runwayOffsetOf(host);
|
|
1124
|
+
if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && reservePx <= .25) {
|
|
1125
|
+
finishAtNaturalFloor(host);
|
|
1126
|
+
followLeaders.delete(host);
|
|
1127
|
+
releaseRevealScale();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
|
|
1131
|
+
const stopSettleListeners = () => {
|
|
1132
|
+
for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
|
|
1133
|
+
if (interactTimer !== null) {
|
|
1134
|
+
clearTimeout(interactTimer);
|
|
1135
|
+
interactTimer = null;
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
let settleLast = performance.now();
|
|
1139
|
+
const settleFrame = (now) => {
|
|
1140
|
+
if (!isLeader(host)) {
|
|
1141
|
+
stopSettleListeners();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
|
|
1145
|
+
readerGestureIntent = false;
|
|
1146
|
+
handBackVisual(host);
|
|
1147
|
+
clearVisual(host);
|
|
1148
|
+
followLeaders.delete(host);
|
|
1149
|
+
releaseRevealScale();
|
|
1150
|
+
stopSettleListeners();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const dt = Math.min(32, Math.max(0, now - settleLast));
|
|
1154
|
+
settleLast = now;
|
|
1155
|
+
const runwayOffset = runwayOffsetOf(host);
|
|
1156
|
+
const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
|
|
1157
|
+
const reserveStep = 1 - Math.exp(-dt / 180);
|
|
1158
|
+
reservePx += (0 - reservePx) * reserveStep;
|
|
1159
|
+
if (lag <= .25 && reservePx <= .25) {
|
|
1160
|
+
animatedH = host.scrollHeight - runwayOffset;
|
|
1161
|
+
reservePx = 0;
|
|
1162
|
+
velocityPxPerSec = 0;
|
|
1163
|
+
finishAtNaturalFloor(host);
|
|
1164
|
+
followLeaders.delete(host);
|
|
1165
|
+
releaseRevealScale();
|
|
1166
|
+
stopSettleListeners();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const step = computeFollowStep(dt, {
|
|
1170
|
+
lag,
|
|
1171
|
+
speedEma: speedCpsRef.current,
|
|
1172
|
+
velocityPxPerSec
|
|
1173
|
+
});
|
|
1174
|
+
const minimumLag = Math.max(0, reservePx);
|
|
1175
|
+
animatedH = Math.min(host.scrollHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
|
|
1176
|
+
velocityPxPerSec = step.velocityPxPerSec;
|
|
1177
|
+
settleAtFloor(host);
|
|
1178
|
+
animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec);
|
|
1179
|
+
requestAnimationFrame(settleFrame);
|
|
1180
|
+
};
|
|
1181
|
+
requestAnimationFrame(settleFrame);
|
|
824
1182
|
};
|
|
825
1183
|
}, [
|
|
826
1184
|
active,
|
|
827
1185
|
rootRef,
|
|
828
|
-
speedCpsRef
|
|
1186
|
+
speedCpsRef,
|
|
1187
|
+
revealScaleRef,
|
|
1188
|
+
predictive,
|
|
1189
|
+
predictiveRef
|
|
829
1190
|
]);
|
|
830
1191
|
}
|
|
831
1192
|
//#endregion
|
|
832
1193
|
//#region src/client/FollowHost.tsx
|
|
833
1194
|
/**
|
|
834
1195
|
* Document-flow host that owns conversation-port follow while `active`.
|
|
835
|
-
* Shared by assistant blocks and every other
|
|
1196
|
+
* Shared by assistant blocks and every other Agent Chat row. `onGrowth` lets
|
|
1197
|
+
* generic wrapped renderers re-arm one glide when their DOM grows without
|
|
1198
|
+
* requiring a business-kind-specific lifecycle predicate.
|
|
836
1199
|
*/
|
|
837
|
-
function FollowHost({ active, speedCpsRef, children }) {
|
|
838
|
-
const
|
|
839
|
-
|
|
1200
|
+
function FollowHost({ active, entrance = false, onEntranceSettled, onGrowth, entranceExtentRef, speedCpsRef, revealScaleRef, predictive = true, predictiveRef, hostRef, children }) {
|
|
1201
|
+
const localRootRef = (0, react.useRef)(null);
|
|
1202
|
+
const rootRef = hostRef ?? localRootRef;
|
|
1203
|
+
useConversationFollow(rootRef, active || entrance, speedCpsRef, revealScaleRef, predictive, entrance, onEntranceSettled, predictiveRef, entranceExtentRef);
|
|
1204
|
+
(0, react.useEffect)(() => {
|
|
1205
|
+
if (onGrowth === void 0 || typeof ResizeObserver === "undefined") return;
|
|
1206
|
+
const root = rootRef.current;
|
|
1207
|
+
if (root === null) return;
|
|
1208
|
+
let previousHeight = null;
|
|
1209
|
+
const observer = new ResizeObserver((entries) => {
|
|
1210
|
+
const nextHeight = entries[0]?.contentRect.height ?? root.getBoundingClientRect().height;
|
|
1211
|
+
if (!Number.isFinite(nextHeight)) return;
|
|
1212
|
+
if (previousHeight !== null && nextHeight > previousHeight + .5) onGrowth(nextHeight - previousHeight);
|
|
1213
|
+
previousHeight = nextHeight;
|
|
1214
|
+
});
|
|
1215
|
+
observer.observe(root);
|
|
1216
|
+
return () => {
|
|
1217
|
+
observer.disconnect();
|
|
1218
|
+
};
|
|
1219
|
+
}, [onGrowth]);
|
|
840
1220
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
841
1221
|
ref: rootRef,
|
|
842
1222
|
className: TypewriterAssistantNodeView_module_css_default.follow,
|
|
@@ -876,6 +1256,45 @@ window.__ModuleLoader__.load({
|
|
|
876
1256
|
}, []);
|
|
877
1257
|
return reduced;
|
|
878
1258
|
}
|
|
1259
|
+
/** Conservative fallback before the streaming Markdown tail has geometry. */
|
|
1260
|
+
const PREDICTIVE_WRAP_FALLBACK_CHARS = 32;
|
|
1261
|
+
function approximateInlineWidth(text, emPx) {
|
|
1262
|
+
let width = 0;
|
|
1263
|
+
for (const char of text) if (/\s/u.test(char)) width += emPx * .33;
|
|
1264
|
+
else if (/^[\x00-\x7f]$/u.test(char)) width += emPx * .56;
|
|
1265
|
+
else width += emPx;
|
|
1266
|
+
return width;
|
|
1267
|
+
}
|
|
1268
|
+
/** Whether buffered source can reach a new visual line before it drains. */
|
|
1269
|
+
function pendingTextCanGrow(root, pending) {
|
|
1270
|
+
if (pending === "") return false;
|
|
1271
|
+
if (/[\r\n]/u.test(pending)) return true;
|
|
1272
|
+
const pendingChars = [...pending];
|
|
1273
|
+
if (root === null || typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
|
|
1274
|
+
const rootRect = root.getBoundingClientRect();
|
|
1275
|
+
const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
|
|
1276
|
+
if (rootWidth <= 0) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
|
|
1277
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1278
|
+
let tail = null;
|
|
1279
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
|
|
1280
|
+
const parent = tail?.parentElement ?? root;
|
|
1281
|
+
const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
|
|
1282
|
+
const pendingWidth = approximateInlineWidth(pending, fontSize);
|
|
1283
|
+
if (tail === null || typeof document.createRange !== "function") return pendingWidth >= rootWidth;
|
|
1284
|
+
try {
|
|
1285
|
+
const length = tail.textContent?.length ?? 0;
|
|
1286
|
+
if (length <= 0) return pendingWidth >= rootWidth;
|
|
1287
|
+
const range = document.createRange();
|
|
1288
|
+
range.setStart(tail, Math.max(0, length - 1));
|
|
1289
|
+
range.setEnd(tail, length);
|
|
1290
|
+
const tailRect = range.getBoundingClientRect();
|
|
1291
|
+
const contentRight = rootRect.right;
|
|
1292
|
+
if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return pendingWidth >= rootWidth;
|
|
1293
|
+
return pendingWidth >= Math.max(0, contentRight - tailRect.right) + fontSize * .35;
|
|
1294
|
+
} catch {
|
|
1295
|
+
return pendingWidth >= rootWidth;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
879
1298
|
/**
|
|
880
1299
|
* Smooth streaming text arm. While the reply runs, the accumulated source is
|
|
881
1300
|
* revealed through the smoother at a rate that tracks the model's arrival
|
|
@@ -887,18 +1306,39 @@ window.__ModuleLoader__.load({
|
|
|
887
1306
|
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
888
1307
|
* mentions) swaps in exactly once.
|
|
889
1308
|
*/
|
|
890
|
-
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, announce, ownFollow, preset, shouldHoldBack }) {
|
|
1309
|
+
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, announce, ownFollow, followSpeedCpsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack }) {
|
|
891
1310
|
const reduced = usePrefersReducedMotion();
|
|
892
1311
|
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
893
|
-
const
|
|
1312
|
+
const localSpeedCpsRef = (0, react.useRef)(35);
|
|
1313
|
+
const followRootRef = (0, react.useRef)(null);
|
|
1314
|
+
const predictionSourceRef = (0, react.useRef)(null);
|
|
1315
|
+
const predictionStateRef = (0, react.useRef)(false);
|
|
1316
|
+
const speedCpsRef = followSpeedCpsRef ?? localSpeedCpsRef;
|
|
894
1317
|
const displayed = useSmoothStreamContent(text, {
|
|
895
1318
|
enabled: typing && !reduced,
|
|
1319
|
+
inputComplete: !streaming,
|
|
896
1320
|
preset,
|
|
897
1321
|
shouldHoldBack,
|
|
898
|
-
speedCpsRef
|
|
1322
|
+
speedCpsRef,
|
|
1323
|
+
revealScaleRef: followRevealScaleRef
|
|
899
1324
|
});
|
|
900
1325
|
const shown = reduced ? text : displayed;
|
|
901
1326
|
const live = typing && !reduced;
|
|
1327
|
+
(0, react.useLayoutEffect)(() => {
|
|
1328
|
+
if (onPredictiveChange === void 0) return;
|
|
1329
|
+
const pending = text.slice(shown.length);
|
|
1330
|
+
const sourceChanged = predictionSourceRef.current !== text;
|
|
1331
|
+
const next = !live || !streaming || pending === "" ? false : sourceChanged ? pendingTextCanGrow(followRootRef.current, pending) : predictionStateRef.current;
|
|
1332
|
+
predictionSourceRef.current = text;
|
|
1333
|
+
predictionStateRef.current = next;
|
|
1334
|
+
onPredictiveChange(next);
|
|
1335
|
+
}, [
|
|
1336
|
+
live,
|
|
1337
|
+
onPredictiveChange,
|
|
1338
|
+
shown,
|
|
1339
|
+
streaming,
|
|
1340
|
+
text
|
|
1341
|
+
]);
|
|
902
1342
|
(0, react.useEffect)(() => {
|
|
903
1343
|
if (typing && !streaming && shown.length === text.length) setTyping(false);
|
|
904
1344
|
}, [
|
|
@@ -907,24 +1347,23 @@ window.__ModuleLoader__.load({
|
|
|
907
1347
|
text,
|
|
908
1348
|
typing
|
|
909
1349
|
]);
|
|
910
|
-
|
|
1350
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [live && announce && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
911
1351
|
className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
|
|
912
1352
|
"aria-live": "polite",
|
|
913
1353
|
children: text
|
|
914
1354
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
915
|
-
active: ownFollow,
|
|
1355
|
+
active: live && ownFollow,
|
|
916
1356
|
speedCpsRef,
|
|
1357
|
+
revealScaleRef: followRevealScaleRef,
|
|
1358
|
+
predictive: streaming,
|
|
1359
|
+
hostRef: followRootRef,
|
|
917
1360
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
918
|
-
text: shown,
|
|
919
|
-
streaming:
|
|
920
|
-
codeLabels
|
|
1361
|
+
text: live ? shown : text,
|
|
1362
|
+
streaming: live,
|
|
1363
|
+
codeLabels,
|
|
1364
|
+
fileMentions: live ? void 0 : fileMentions
|
|
921
1365
|
})
|
|
922
1366
|
})] });
|
|
923
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
924
|
-
text,
|
|
925
|
-
codeLabels,
|
|
926
|
-
fileMentions
|
|
927
|
-
});
|
|
928
1367
|
}
|
|
929
1368
|
function imageLabels(t) {
|
|
930
1369
|
return {
|
|
@@ -957,30 +1396,32 @@ window.__ModuleLoader__.load({
|
|
|
957
1396
|
* or the assistant node settling — not when the rest of the reply is
|
|
958
1397
|
* still streaming.
|
|
959
1398
|
*/
|
|
960
|
-
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, t }) {
|
|
1399
|
+
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, onExpandedChange, t }) {
|
|
961
1400
|
const reduced = usePrefersReducedMotion();
|
|
962
1401
|
const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
|
|
963
1402
|
const summaryRef = (0, react.useRef)(null);
|
|
964
|
-
const speedCpsRef = (0, react.useRef)(35);
|
|
965
1403
|
const displayed = useSmoothStreamContent(text, {
|
|
966
1404
|
enabled: running && !reduced,
|
|
967
1405
|
preset,
|
|
968
1406
|
shouldHoldBack,
|
|
969
|
-
speedCpsRef
|
|
1407
|
+
speedCpsRef: followSpeedCpsRef,
|
|
1408
|
+
revealScaleRef: followRevealScaleRef
|
|
970
1409
|
});
|
|
971
1410
|
const shown = running && !reduced ? displayed : text;
|
|
972
1411
|
const summary = running ? latestLine(shown) : firstLine(text);
|
|
973
|
-
(0, react.
|
|
1412
|
+
(0, react.useLayoutEffect)(() => {
|
|
974
1413
|
if (thinkAutoExpand) setExpanded(running);
|
|
975
1414
|
}, [running, thinkAutoExpand]);
|
|
1415
|
+
(0, react.useLayoutEffect)(() => {
|
|
1416
|
+
onExpandedChange?.(expanded);
|
|
1417
|
+
}, [expanded, onExpandedChange]);
|
|
976
1418
|
(0, react.useEffect)(() => {
|
|
977
1419
|
const element = summaryRef.current;
|
|
978
1420
|
if (element === null) return;
|
|
979
1421
|
element.scrollLeft = running ? element.scrollWidth - element.clientWidth : 0;
|
|
980
1422
|
}, [running, summary]);
|
|
981
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
982
|
-
|
|
983
|
-
speedCpsRef,
|
|
1423
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1424
|
+
className: TypewriterAssistantNodeView_module_css_default.follow,
|
|
984
1425
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
985
1426
|
className: TypewriterAssistantNodeView_module_css_default.think,
|
|
986
1427
|
"data-variant": "think",
|
|
@@ -1021,16 +1462,33 @@ window.__ModuleLoader__.load({
|
|
|
1021
1462
|
* streaming is revealed by the smoother through the Harness Markdown
|
|
1022
1463
|
* renderer at a rate that tracks arrival. Reasoning blocks keep the
|
|
1023
1464
|
* built-in Think disclosure and only receive a smoothed text feed; the
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
* reveals when the frame rate is degraded. Settled text
|
|
1027
|
-
* full Markdown pipeline.
|
|
1465
|
+
* outer node owns conversation-port follow while streaming; the final text
|
|
1466
|
+
* block keeps ownership while its settled reveal queue drains. The FPS guard
|
|
1467
|
+
* holds offscreen reveals when the frame rate is degraded. Settled text
|
|
1468
|
+
* renders with the full Markdown pipeline.
|
|
1028
1469
|
*/
|
|
1029
1470
|
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 }) {
|
|
1030
1471
|
const data = node.data;
|
|
1031
1472
|
const streaming = data.status === "running";
|
|
1473
|
+
const reduced = usePrefersReducedMotion();
|
|
1032
1474
|
const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
|
|
1033
1475
|
const rootSpeedRef = (0, react.useRef)(35);
|
|
1476
|
+
const rootRevealScaleRef = (0, react.useRef)(1);
|
|
1477
|
+
const reasoningTailIndex = streaming && data.blocks[data.blocks.length - 1]?.kind === "reasoning" ? data.blocks.length - 1 : -1;
|
|
1478
|
+
const reasoningOwnsSpeed = reasoningTailIndex !== -1;
|
|
1479
|
+
const rootPredictiveRef = (0, react.useRef)(false);
|
|
1480
|
+
const previousReasoningTailRef = (0, react.useRef)(-1);
|
|
1481
|
+
if (reasoningTailIndex !== previousReasoningTailRef.current) {
|
|
1482
|
+
rootPredictiveRef.current = reasoningOwnsSpeed ? thinkAutoExpand : false;
|
|
1483
|
+
if (!reasoningOwnsSpeed) rootSpeedRef.current = 35;
|
|
1484
|
+
previousReasoningTailRef.current = reasoningTailIndex;
|
|
1485
|
+
}
|
|
1486
|
+
const updateReasoningExpanded = (0, react.useMemo)(() => (expanded) => {
|
|
1487
|
+
rootPredictiveRef.current = expanded;
|
|
1488
|
+
}, []);
|
|
1489
|
+
const updateTextPrediction = (0, react.useMemo)(() => (predictive) => {
|
|
1490
|
+
rootPredictiveRef.current = predictive;
|
|
1491
|
+
}, []);
|
|
1034
1492
|
const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
|
|
1035
1493
|
const tail = useTurnData("turn-tail");
|
|
1036
1494
|
const owner = (0, react.useMemo)(() => {
|
|
@@ -1075,6 +1533,9 @@ window.__ModuleLoader__.load({
|
|
|
1075
1533
|
streaming,
|
|
1076
1534
|
announce: index === last,
|
|
1077
1535
|
ownFollow: !streaming && index === lastFollow,
|
|
1536
|
+
followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
|
|
1537
|
+
followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
|
|
1538
|
+
onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
|
|
1078
1539
|
preset,
|
|
1079
1540
|
shouldHoldBack
|
|
1080
1541
|
}, index));
|
|
@@ -1086,6 +1547,9 @@ window.__ModuleLoader__.load({
|
|
|
1086
1547
|
preset,
|
|
1087
1548
|
thinkAutoExpand,
|
|
1088
1549
|
shouldHoldBack,
|
|
1550
|
+
followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
|
|
1551
|
+
followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
|
|
1552
|
+
onExpandedChange: index === reasoningTailIndex ? updateReasoningExpanded : void 0,
|
|
1089
1553
|
t
|
|
1090
1554
|
}, index));
|
|
1091
1555
|
break;
|
|
@@ -1107,13 +1571,11 @@ window.__ModuleLoader__.load({
|
|
|
1107
1571
|
break;
|
|
1108
1572
|
}
|
|
1109
1573
|
case "tool-call": break;
|
|
1110
|
-
case "other":
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}, index));
|
|
1116
|
-
break;
|
|
1574
|
+
case "other": rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
|
|
1575
|
+
label: t("message.unknownBlock"),
|
|
1576
|
+
payload: block.block,
|
|
1577
|
+
truncatedLabel: (total) => t("json.truncated", { total })
|
|
1578
|
+
}, index));
|
|
1117
1579
|
}
|
|
1118
1580
|
}
|
|
1119
1581
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -1121,8 +1583,10 @@ window.__ModuleLoader__.load({
|
|
|
1121
1583
|
className: TypewriterAssistantNodeView_module_css_default.root,
|
|
1122
1584
|
"data-streaming": streaming || void 0,
|
|
1123
1585
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1124
|
-
active: streaming,
|
|
1586
|
+
active: streaming && !reduced,
|
|
1125
1587
|
speedCpsRef: rootSpeedRef,
|
|
1588
|
+
revealScaleRef: rootRevealScaleRef,
|
|
1589
|
+
predictiveRef: rootPredictiveRef,
|
|
1126
1590
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1127
1591
|
className: TypewriterAssistantNodeView_module_css_default.body,
|
|
1128
1592
|
children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1134,11 +1598,210 @@ window.__ModuleLoader__.load({
|
|
|
1134
1598
|
});
|
|
1135
1599
|
});
|
|
1136
1600
|
//#endregion
|
|
1601
|
+
//#region src/client/useProgressiveDomText.ts
|
|
1602
|
+
/**
|
|
1603
|
+
* Progressive text reveal for opaque Agent renderers.
|
|
1604
|
+
*
|
|
1605
|
+
* Slot renderers own arbitrary React trees, so the generic integration cannot
|
|
1606
|
+
* clone or classify their business components. This hook leaves that tree and
|
|
1607
|
+
* all of its event handlers in place, and only paces visible Text node data
|
|
1608
|
+
* while the row belongs to the live Agent turn. No clip, mask, overlay, or
|
|
1609
|
+
* duplicate accessibility tree is introduced.
|
|
1610
|
+
*/
|
|
1611
|
+
const SKIP_TEXT_SELECTOR = [
|
|
1612
|
+
"[aria-hidden=\"true\"]",
|
|
1613
|
+
"[aria-live]",
|
|
1614
|
+
"[contenteditable=\"true\"]",
|
|
1615
|
+
"script",
|
|
1616
|
+
"style",
|
|
1617
|
+
"textarea"
|
|
1618
|
+
].join(",");
|
|
1619
|
+
function revealable(node, root) {
|
|
1620
|
+
if (node.data.trim() === "") return false;
|
|
1621
|
+
const parent = node.parentElement;
|
|
1622
|
+
return parent !== null && root.contains(parent) && parent.closest(SKIP_TEXT_SELECTOR) === null;
|
|
1623
|
+
}
|
|
1624
|
+
function commonPrefix(left, right) {
|
|
1625
|
+
const limit = Math.min(left.length, right.length);
|
|
1626
|
+
let index = 0;
|
|
1627
|
+
while (index < limit && left[index] === right[index]) index += 1;
|
|
1628
|
+
return index;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Pace text inside a renderer whose React component is intentionally opaque.
|
|
1632
|
+
* Initial content is revealed only for a genuinely new row; later mutations
|
|
1633
|
+
* stay paced until `enabled` becomes false, at which point the full renderer
|
|
1634
|
+
* content is restored synchronously before paint.
|
|
1635
|
+
*/
|
|
1636
|
+
function useProgressiveDomText(rootRef, enabled, revealInitial, speedCpsRef, onSettled) {
|
|
1637
|
+
(0, react.useLayoutEffect)(() => {
|
|
1638
|
+
if (!enabled) return;
|
|
1639
|
+
const root = rootRef.current;
|
|
1640
|
+
if (root === null || typeof document === "undefined") return;
|
|
1641
|
+
const records = /* @__PURE__ */ new Map();
|
|
1642
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1643
|
+
const internalWrites = /* @__PURE__ */ new WeakMap();
|
|
1644
|
+
let rafId = 0;
|
|
1645
|
+
let lastFrame = null;
|
|
1646
|
+
let debt = 0;
|
|
1647
|
+
let stopped = false;
|
|
1648
|
+
let announcedSettled = false;
|
|
1649
|
+
const announceSettled = () => {
|
|
1650
|
+
lastFrame = null;
|
|
1651
|
+
debt = 0;
|
|
1652
|
+
speedCpsRef.current = 35;
|
|
1653
|
+
if (announcedSettled) return;
|
|
1654
|
+
announcedSettled = true;
|
|
1655
|
+
onSettled?.();
|
|
1656
|
+
};
|
|
1657
|
+
const write = (node, value) => {
|
|
1658
|
+
if (node.data === value) return;
|
|
1659
|
+
internalWrites.set(node, value);
|
|
1660
|
+
node.data = value;
|
|
1661
|
+
};
|
|
1662
|
+
const enqueue = (node, full, preserve) => {
|
|
1663
|
+
if (!revealable(node, root)) return;
|
|
1664
|
+
const chars = [...full];
|
|
1665
|
+
const preserved = preserve === void 0 ? 0 : Math.min(preserve.shown, commonPrefix(preserve.chars, chars));
|
|
1666
|
+
const record = {
|
|
1667
|
+
chars,
|
|
1668
|
+
full,
|
|
1669
|
+
shown: preserved
|
|
1670
|
+
};
|
|
1671
|
+
records.set(node, record);
|
|
1672
|
+
if (preserved < chars.length) {
|
|
1673
|
+
pending.add(node);
|
|
1674
|
+
announcedSettled = false;
|
|
1675
|
+
} else pending.delete(node);
|
|
1676
|
+
write(node, chars.slice(0, preserved).join(""));
|
|
1677
|
+
};
|
|
1678
|
+
const visit = (from, reveal) => {
|
|
1679
|
+
if (from.nodeType === Node.TEXT_NODE) {
|
|
1680
|
+
const text = from;
|
|
1681
|
+
if (reveal) enqueue(text, text.data, records.get(text));
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const walker = document.createTreeWalker(from, NodeFilter.SHOW_TEXT);
|
|
1685
|
+
let current = walker.nextNode();
|
|
1686
|
+
while (current !== null) {
|
|
1687
|
+
const text = current;
|
|
1688
|
+
if (reveal) enqueue(text, text.data, records.get(text));
|
|
1689
|
+
current = walker.nextNode();
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
const scheduleFrame = () => {
|
|
1693
|
+
if (stopped || pending.size === 0 || rafId !== 0) return;
|
|
1694
|
+
rafId = requestAnimationFrame(frame);
|
|
1695
|
+
};
|
|
1696
|
+
const frame = (now) => {
|
|
1697
|
+
rafId = 0;
|
|
1698
|
+
if (stopped) return;
|
|
1699
|
+
if (pending.size === 0) {
|
|
1700
|
+
announceSettled();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
announcedSettled = false;
|
|
1704
|
+
if (lastFrame === null) {
|
|
1705
|
+
lastFrame = now;
|
|
1706
|
+
scheduleFrame();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const elapsed = Math.max(0, now - lastFrame);
|
|
1710
|
+
lastFrame = now;
|
|
1711
|
+
let backlog = 0;
|
|
1712
|
+
for (const node of pending) {
|
|
1713
|
+
const record = records.get(node);
|
|
1714
|
+
if (record !== void 0) backlog += record.chars.length - record.shown;
|
|
1715
|
+
}
|
|
1716
|
+
const step = computeAdaptiveQueueStep(backlog, elapsed, debt);
|
|
1717
|
+
debt = step.debt;
|
|
1718
|
+
speedCpsRef.current = step.speedCps;
|
|
1719
|
+
let remaining = step.revealChars;
|
|
1720
|
+
for (const node of [...pending]) {
|
|
1721
|
+
if (remaining <= 0) break;
|
|
1722
|
+
const record = records.get(node);
|
|
1723
|
+
if (record === void 0 || !node.isConnected) {
|
|
1724
|
+
pending.delete(node);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
const amount = Math.min(remaining, record.chars.length - record.shown);
|
|
1728
|
+
const shown = record.shown + amount;
|
|
1729
|
+
const next = {
|
|
1730
|
+
...record,
|
|
1731
|
+
shown
|
|
1732
|
+
};
|
|
1733
|
+
records.set(node, next);
|
|
1734
|
+
write(node, next.chars.slice(0, shown).join(""));
|
|
1735
|
+
remaining -= amount;
|
|
1736
|
+
if (shown >= next.chars.length) pending.delete(node);
|
|
1737
|
+
}
|
|
1738
|
+
if (pending.size === 0) announceSettled();
|
|
1739
|
+
else scheduleFrame();
|
|
1740
|
+
};
|
|
1741
|
+
if (revealInitial) visit(root, true);
|
|
1742
|
+
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver((mutations) => {
|
|
1743
|
+
for (const mutation of mutations) {
|
|
1744
|
+
if (mutation.type === "characterData") {
|
|
1745
|
+
const node = mutation.target;
|
|
1746
|
+
if (internalWrites.get(node) === node.data) {
|
|
1747
|
+
internalWrites.delete(node);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
enqueue(node, node.data, records.get(node));
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
for (const added of mutation.addedNodes) visit(added, true);
|
|
1754
|
+
}
|
|
1755
|
+
if (pending.size === 0) announceSettled();
|
|
1756
|
+
else scheduleFrame();
|
|
1757
|
+
});
|
|
1758
|
+
observer?.observe(root, {
|
|
1759
|
+
childList: true,
|
|
1760
|
+
characterData: true,
|
|
1761
|
+
subtree: true
|
|
1762
|
+
});
|
|
1763
|
+
if (pending.size === 0) announceSettled();
|
|
1764
|
+
else scheduleFrame();
|
|
1765
|
+
return () => {
|
|
1766
|
+
stopped = true;
|
|
1767
|
+
cancelAnimationFrame(rafId);
|
|
1768
|
+
observer?.disconnect();
|
|
1769
|
+
for (const [node, record] of records) {
|
|
1770
|
+
const controlled = record.chars.slice(0, record.shown).join("");
|
|
1771
|
+
if (node.isConnected && node.data === controlled) write(node, record.full);
|
|
1772
|
+
}
|
|
1773
|
+
pending.clear();
|
|
1774
|
+
speedCpsRef.current = 35;
|
|
1775
|
+
};
|
|
1776
|
+
}, [
|
|
1777
|
+
enabled,
|
|
1778
|
+
onSettled,
|
|
1779
|
+
revealInitial,
|
|
1780
|
+
rootRef,
|
|
1781
|
+
speedCpsRef
|
|
1782
|
+
]);
|
|
1783
|
+
}
|
|
1784
|
+
//#endregion
|
|
1137
1785
|
//#region src/client/TypewriterToolNodeView.tsx
|
|
1786
|
+
function openAgentLocation(node) {
|
|
1787
|
+
if (node === null || typeof node !== "object" || !("location" in node)) return false;
|
|
1788
|
+
const location = node.location;
|
|
1789
|
+
if (location === null || typeof location !== "object" || !("kind" in location)) return false;
|
|
1790
|
+
const kind = location.kind;
|
|
1791
|
+
if (!("turn" in location)) return false;
|
|
1792
|
+
const turn = location.turn;
|
|
1793
|
+
if (turn === null || typeof turn !== "object" || !("status" in turn)) return false;
|
|
1794
|
+
if (kind === "turn") return turn.status === "open";
|
|
1795
|
+
if (kind !== "step" || !("step" in location)) return false;
|
|
1796
|
+
const step = location.step;
|
|
1797
|
+
return step !== null && typeof step === "object" && "status" in step && step.status === "open";
|
|
1798
|
+
}
|
|
1138
1799
|
/**
|
|
1139
|
-
* True while a Chat node
|
|
1140
|
-
* 'running'` payload, a Tool root that has not
|
|
1141
|
-
* model-retry whose current attempt is still
|
|
1800
|
+
* True while a Chat node has an explicitly unfinished lifecycle: an
|
|
1801
|
+
* assistant/workflow `status: 'running'` payload, a Tool root that has not
|
|
1802
|
+
* settled (`kind` absent), or a model-retry whose current attempt is still
|
|
1803
|
+
* `scheduled`. Open-but-otherwise-unknown rows are covered by
|
|
1804
|
+
* `isFollowableChatNode` below.
|
|
1142
1805
|
* @param node - The Chat node's view `node` prop.
|
|
1143
1806
|
* @returns whether this row should own conversation follow.
|
|
1144
1807
|
*/
|
|
@@ -1147,6 +1810,11 @@ window.__ModuleLoader__.load({
|
|
|
1147
1810
|
const data = node.data;
|
|
1148
1811
|
if (data === null || typeof data !== "object") return false;
|
|
1149
1812
|
if ("status" in data && data.status === "running") return true;
|
|
1813
|
+
if ("kind" in data && data.kind === "command" && "outcome" in data && data.outcome === null) return true;
|
|
1814
|
+
if ("command" in data) {
|
|
1815
|
+
const command = data.command;
|
|
1816
|
+
if (command !== null && typeof command === "object" && "outcome" in command && command.outcome === null) return true;
|
|
1817
|
+
}
|
|
1150
1818
|
if ("root" in data) {
|
|
1151
1819
|
const root = data.root;
|
|
1152
1820
|
if (root !== null && typeof root === "object" && !("kind" in root)) return true;
|
|
@@ -1158,18 +1826,128 @@ window.__ModuleLoader__.load({
|
|
|
1158
1826
|
return false;
|
|
1159
1827
|
}
|
|
1160
1828
|
/**
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1829
|
+
* True for any Agent-owned Chat row in the currently open turn/step. This is
|
|
1830
|
+
* the extensibility boundary: a newly registered Context, Command, Tool, or
|
|
1831
|
+
* workflow renderer is followed without adding another kind-specific branch.
|
|
1832
|
+
*/
|
|
1833
|
+
function isFollowableChatNode(node) {
|
|
1834
|
+
return isGrowingChatNode(node) || openAgentLocation(node);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* True when a newly mounted Agent-owned row belongs to the current open
|
|
1838
|
+
* Turn/Step, or is itself an unresolved growing lifecycle.
|
|
1839
|
+
* @param node - The Chat node's view `node` prop.
|
|
1840
|
+
* @returns whether its initial height should enter through conversation follow.
|
|
1841
|
+
*/
|
|
1842
|
+
function shouldAnimateChatNodeEntrance(node) {
|
|
1843
|
+
return isFollowableChatNode(node);
|
|
1844
|
+
}
|
|
1845
|
+
/** Runtime-only fallback for unknown or terminal rows at the active flow tip. */
|
|
1846
|
+
function liveAgentTailMode(root) {
|
|
1847
|
+
const port = root.closest("[data-conversation-scroll]");
|
|
1848
|
+
if (port === null) return null;
|
|
1849
|
+
const row = root.closest("[data-chat-flow-key]");
|
|
1850
|
+
if (row !== null) {
|
|
1851
|
+
let sibling = row.nextElementSibling;
|
|
1852
|
+
while (sibling !== null) {
|
|
1853
|
+
if (sibling instanceof HTMLElement && sibling.hasAttribute("data-chat-flow-key")) return null;
|
|
1854
|
+
sibling = sibling.nextElementSibling;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const flow = root.closest("[data-chat-flow]");
|
|
1858
|
+
if (flow !== null && [...flow.children].some((child) => child instanceof HTMLElement && child.getAttribute("role") === "status" && !child.hasAttribute("data-chat-flow-key"))) return "turn";
|
|
1859
|
+
return hasRecentConversationFollow(port) ? "handoff" : null;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Wrap a prior Agent Chat renderer so its entrance and later growth share
|
|
1863
|
+
* conversation follow. Presentation stays with the wrapped component; kit
|
|
1864
|
+
* seats (`renderSlot`, locale, inject) pass through unchanged.
|
|
1164
1865
|
* @param Inner - The already-registered row component.
|
|
1165
1866
|
* @returns A follow-hosted row.
|
|
1166
1867
|
*/
|
|
1167
1868
|
function wrapFollowNodeView(Inner) {
|
|
1168
1869
|
return function TypewriterFollowNodeView(props) {
|
|
1169
1870
|
const speedCpsRef = (0, react.useRef)(35);
|
|
1871
|
+
const hostRef = (0, react.useRef)(null);
|
|
1872
|
+
const growing = isGrowingChatNode(props.node);
|
|
1873
|
+
const structurallyFollowable = isFollowableChatNode(props.node);
|
|
1874
|
+
const structuralRef = (0, react.useRef)(structurallyFollowable);
|
|
1875
|
+
const [runtimeFollowable, setRuntimeFollowable] = (0, react.useState)(false);
|
|
1876
|
+
const runtimePersistentRef = (0, react.useRef)(false);
|
|
1877
|
+
const runtimeHandledRef = (0, react.useRef)(false);
|
|
1878
|
+
const followable = structurallyFollowable || runtimeFollowable;
|
|
1879
|
+
const revealInitialRef = (0, react.useRef)(true);
|
|
1880
|
+
const [entering, setEntering] = (0, react.useState)(() => shouldAnimateChatNodeEntrance(props.node));
|
|
1881
|
+
const [growthPulse, setGrowthPulse] = (0, react.useState)(false);
|
|
1882
|
+
const followableRef = (0, react.useRef)(false);
|
|
1883
|
+
const growingRef = (0, react.useRef)(growing);
|
|
1884
|
+
const entranceActiveRef = (0, react.useRef)(entering || growthPulse);
|
|
1885
|
+
const growthExtentRef = (0, react.useRef)(null);
|
|
1886
|
+
const mountedRef = (0, react.useRef)(true);
|
|
1887
|
+
const pulseTimerRef = (0, react.useRef)(null);
|
|
1888
|
+
structuralRef.current = structurallyFollowable;
|
|
1889
|
+
followableRef.current = followable;
|
|
1890
|
+
growingRef.current = growing;
|
|
1891
|
+
entranceActiveRef.current = entering || growthPulse;
|
|
1892
|
+
const finishRuntimeReveal = (0, react.useCallback)(() => {
|
|
1893
|
+
if (structuralRef.current || runtimePersistentRef.current) return;
|
|
1894
|
+
runtimeHandledRef.current = true;
|
|
1895
|
+
setRuntimeFollowable(false);
|
|
1896
|
+
}, []);
|
|
1897
|
+
useProgressiveDomText(hostRef, followable, revealInitialRef.current, speedCpsRef, runtimeFollowable ? finishRuntimeReveal : void 0);
|
|
1898
|
+
(0, react.useLayoutEffect)(() => {
|
|
1899
|
+
if (structurallyFollowable) return;
|
|
1900
|
+
const root = hostRef.current;
|
|
1901
|
+
if (root === null) return;
|
|
1902
|
+
const mode = liveAgentTailMode(root);
|
|
1903
|
+
if (runtimeFollowable) {
|
|
1904
|
+
if (runtimePersistentRef.current && mode !== "turn") {
|
|
1905
|
+
runtimePersistentRef.current = false;
|
|
1906
|
+
runtimeHandledRef.current = true;
|
|
1907
|
+
setRuntimeFollowable(false);
|
|
1908
|
+
}
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (runtimeHandledRef.current || mode === null) return;
|
|
1912
|
+
runtimePersistentRef.current = mode === "turn";
|
|
1913
|
+
setRuntimeFollowable(true);
|
|
1914
|
+
setEntering(true);
|
|
1915
|
+
}, [runtimeFollowable, structurallyFollowable]);
|
|
1916
|
+
const finishEntrance = (0, react.useCallback)(() => {
|
|
1917
|
+
growthExtentRef.current = null;
|
|
1918
|
+
if (pulseTimerRef.current !== null) {
|
|
1919
|
+
clearTimeout(pulseTimerRef.current);
|
|
1920
|
+
pulseTimerRef.current = null;
|
|
1921
|
+
}
|
|
1922
|
+
setEntering(false);
|
|
1923
|
+
setGrowthPulse(false);
|
|
1924
|
+
}, []);
|
|
1925
|
+
const onGrowth = (0, react.useCallback)((deltaPx) => {
|
|
1926
|
+
if (!mountedRef.current || !followableRef.current || growingRef.current || entranceActiveRef.current) return;
|
|
1927
|
+
growthExtentRef.current = deltaPx;
|
|
1928
|
+
setGrowthPulse(true);
|
|
1929
|
+
if (pulseTimerRef.current !== null) clearTimeout(pulseTimerRef.current);
|
|
1930
|
+
pulseTimerRef.current = setTimeout(() => {
|
|
1931
|
+
pulseTimerRef.current = null;
|
|
1932
|
+
setGrowthPulse(false);
|
|
1933
|
+
}, 1200);
|
|
1934
|
+
}, []);
|
|
1935
|
+
(0, react.useEffect)(() => {
|
|
1936
|
+
mountedRef.current = true;
|
|
1937
|
+
return () => {
|
|
1938
|
+
mountedRef.current = false;
|
|
1939
|
+
if (pulseTimerRef.current !== null) clearTimeout(pulseTimerRef.current);
|
|
1940
|
+
};
|
|
1941
|
+
}, []);
|
|
1170
1942
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1171
|
-
active:
|
|
1943
|
+
active: growing,
|
|
1944
|
+
entrance: entering || growthPulse,
|
|
1945
|
+
onEntranceSettled: finishEntrance,
|
|
1946
|
+
onGrowth: followable ? onGrowth : void 0,
|
|
1947
|
+
entranceExtentRef: growthExtentRef,
|
|
1172
1948
|
speedCpsRef,
|
|
1949
|
+
predictive: false,
|
|
1950
|
+
hostRef,
|
|
1173
1951
|
children: (0, react.createElement)(Inner, props)
|
|
1174
1952
|
});
|
|
1175
1953
|
};
|
|
@@ -1189,31 +1967,31 @@ window.__ModuleLoader__.load({
|
|
|
1189
1967
|
tag.textContent = css;
|
|
1190
1968
|
}
|
|
1191
1969
|
var SmoothStreamCard_module_css_default = {
|
|
1192
|
-
"
|
|
1193
|
-
"header": "XNGeRG_header",
|
|
1194
|
-
"fieldHead": "XNGeRG_fieldHead",
|
|
1195
|
-
"updateCopy": "XNGeRG_updateCopy",
|
|
1196
|
-
"footer": "XNGeRG_footer",
|
|
1197
|
-
"discard": "XNGeRG_discard",
|
|
1198
|
-
"readOnly": "XNGeRG_readOnly",
|
|
1199
|
-
"failure": "XNGeRG_failure",
|
|
1200
|
-
"chevronOpen": "XNGeRG_chevronOpen",
|
|
1201
|
-
"cardOpen": "XNGeRG_cardOpen",
|
|
1202
|
-
"update": "XNGeRG_update",
|
|
1203
|
-
"save": "XNGeRG_save",
|
|
1204
|
-
"chevron": "XNGeRG_chevron",
|
|
1205
|
-
"label": "XNGeRG_label",
|
|
1206
|
-
"body": "XNGeRG_body",
|
|
1970
|
+
"card": "XNGeRG_card",
|
|
1207
1971
|
"hint": "XNGeRG_hint",
|
|
1972
|
+
"failed": "XNGeRG_failed",
|
|
1973
|
+
"field": "XNGeRG_field",
|
|
1974
|
+
"chevron": "XNGeRG_chevron",
|
|
1208
1975
|
"headText": "XNGeRG_headText",
|
|
1976
|
+
"version": "XNGeRG_version",
|
|
1977
|
+
"body": "XNGeRG_body",
|
|
1978
|
+
"fieldHead": "XNGeRG_fieldHead",
|
|
1979
|
+
"toggle": "XNGeRG_toggle",
|
|
1980
|
+
"readOnly": "XNGeRG_readOnly",
|
|
1981
|
+
"header": "XNGeRG_header",
|
|
1209
1982
|
"pending": "XNGeRG_pending",
|
|
1983
|
+
"label": "XNGeRG_label",
|
|
1984
|
+
"update": "XNGeRG_update",
|
|
1985
|
+
"discard": "XNGeRG_discard",
|
|
1986
|
+
"footer": "XNGeRG_footer",
|
|
1987
|
+
"failure": "XNGeRG_failure",
|
|
1210
1988
|
"name": "XNGeRG_name",
|
|
1211
|
-
"
|
|
1989
|
+
"description": "XNGeRG_description",
|
|
1990
|
+
"cardOpen": "XNGeRG_cardOpen",
|
|
1991
|
+
"chevronOpen": "XNGeRG_chevronOpen",
|
|
1212
1992
|
"updateRow": "XNGeRG_updateRow",
|
|
1213
|
-
"
|
|
1214
|
-
"
|
|
1215
|
-
"field": "XNGeRG_field",
|
|
1216
|
-
"card": "XNGeRG_card"
|
|
1993
|
+
"save": "XNGeRG_save",
|
|
1994
|
+
"updateCopy": "XNGeRG_updateCopy"
|
|
1217
1995
|
};
|
|
1218
1996
|
//#endregion
|
|
1219
1997
|
//#region src/client/SmoothStreamCard.tsx
|
|
@@ -1612,13 +2390,24 @@ window.__ModuleLoader__.load({
|
|
|
1612
2390
|
"balanced",
|
|
1613
2391
|
"silky"
|
|
1614
2392
|
];
|
|
1615
|
-
/**
|
|
1616
|
-
|
|
2393
|
+
/**
|
|
2394
|
+
* The assistant renderer owns its own character queue and conversation
|
|
2395
|
+
* follower, so wrapping it again would create two scroll owners. Human input
|
|
2396
|
+
* stays immediate; every Agent-owned output renderer goes through the same
|
|
2397
|
+
* generic follow boundary. This is deliberately keyed by the owner that
|
|
2398
|
+
* provides the renderer, not by individual tool names, so new Context,
|
|
2399
|
+
* Command, and Tool rows are covered automatically.
|
|
2400
|
+
*/
|
|
2401
|
+
const SKIP_WRAP = /* @__PURE__ */ new Set([
|
|
1617
2402
|
"assistant-step",
|
|
1618
2403
|
"user",
|
|
1619
2404
|
"steering",
|
|
1620
2405
|
"command-input"
|
|
1621
2406
|
]);
|
|
2407
|
+
/** React function/class or an exotic component such as memo/forwardRef/lazy. */
|
|
2408
|
+
function isWrappableComponent(value) {
|
|
2409
|
+
return typeof value === "function" || value !== null && typeof value === "object" && "$$typeof" in value;
|
|
2410
|
+
}
|
|
1622
2411
|
/**
|
|
1623
2412
|
* Read the Host-bridged boot config. The inline script is produced by this
|
|
1624
2413
|
* plugin's Host half from a schema-validated value, so only the structural
|
|
@@ -1638,16 +2427,19 @@ window.__ModuleLoader__.load({
|
|
|
1638
2427
|
return raw;
|
|
1639
2428
|
}
|
|
1640
2429
|
/**
|
|
1641
|
-
* Wrap every keyed Chat row except
|
|
2430
|
+
* Wrap every Agent-owned keyed Chat row except the assistant renderer in
|
|
2431
|
+
* place. A second
|
|
1642
2432
|
* register with the same `children` table throws because the child slot is
|
|
1643
2433
|
* already declared, and only the winning entry receives `renderSlot`;
|
|
1644
2434
|
* swapping `entry.component` keeps the original children, locale, and inject
|
|
1645
2435
|
* seats. `assistant-step` is replaced below so text and Think use the
|
|
1646
|
-
* typewriter reveal.
|
|
2436
|
+
* typewriter reveal. The wrapper owns only the shared layout-growth/follow
|
|
2437
|
+
* lifecycle; the Harness keeps each renderer's controls, disclosures, and
|
|
2438
|
+
* cards intact.
|
|
1647
2439
|
* @param ctx - Browser context carrying the slot registry.
|
|
1648
2440
|
* @returns Restorer that puts the original components back.
|
|
1649
2441
|
*/
|
|
1650
|
-
function
|
|
2442
|
+
function wrapAgentChatRows(ctx) {
|
|
1651
2443
|
const restores = [];
|
|
1652
2444
|
const wrapped = /* @__PURE__ */ new WeakSet();
|
|
1653
2445
|
const wrapAll = () => {
|
|
@@ -1655,7 +2447,7 @@ window.__ModuleLoader__.load({
|
|
|
1655
2447
|
const key = entry.options.key;
|
|
1656
2448
|
if (key === void 0 || SKIP_WRAP.has(key)) continue;
|
|
1657
2449
|
const current = entry.component;
|
|
1658
|
-
if (
|
|
2450
|
+
if (!isWrappableComponent(current) || wrapped.has(current)) continue;
|
|
1659
2451
|
const inner = current;
|
|
1660
2452
|
const next = wrapFollowNodeView(inner);
|
|
1661
2453
|
wrapped.add(next);
|
|
@@ -1718,8 +2510,9 @@ window.__ModuleLoader__.load({
|
|
|
1718
2510
|
/**
|
|
1719
2511
|
* Register the typewriter renderer after the conversation package declares the
|
|
1720
2512
|
* keyed Chat node seat. A lower priority shadows the built-in assistant row;
|
|
1721
|
-
* every other
|
|
1722
|
-
* workflow runs share
|
|
2513
|
+
* every other keyed renderer is wrapped in place so Context, commands, Tool
|
|
2514
|
+
* cards, retries, and workflow runs share one extensible follow boundary. The
|
|
2515
|
+
* Host-bridged configuration
|
|
1723
2516
|
* selects the render direction, smoothing preset, and glide speed; the
|
|
1724
2517
|
* plugin-owned settings RPC supplies the live auto-expand preference when the
|
|
1725
2518
|
* settings surface is composed.
|
|
@@ -1765,7 +2558,7 @@ window.__ModuleLoader__.load({
|
|
|
1765
2558
|
});
|
|
1766
2559
|
};
|
|
1767
2560
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
1768
|
-
const unwrap =
|
|
2561
|
+
const unwrap = wrapAgentChatRows(ctx);
|
|
1769
2562
|
const unshadow = ctx.slots.register({
|
|
1770
2563
|
name: "conversation.chat.node",
|
|
1771
2564
|
key: "assistant-step",
|