dsh-smooth-stream 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +1508 -362
- package/lib/client.js.map +1 -1
- package/lib/index.js +102 -8
- package/package.json +1 -1
- package/src/client/AgentRowEntrance.module.css +1 -4
- package/src/client/AnimatedDisclosure.tsx +3 -3
- package/src/client/DebugPanel.tsx +34 -15
- package/src/client/FrameCoordinator.ts +180 -0
- package/src/client/SmoothStreamCard.tsx +28 -3
- package/src/client/StreamBuffer.ts +145 -0
- package/src/client/TypewriterAssistantNodeView.module.css +82 -12
- package/src/client/TypewriterAssistantNodeView.tsx +216 -102
- package/src/client/debugRuntime.ts +58 -0
- package/src/client/harnessIcons.ts +27 -0
- package/src/client/index.ts +151 -11
- package/src/client/locales.ts +85 -0
- package/src/client/smooth-stream-card-controller.ts +7 -3
- package/src/client/smooth-stream-settings-api.ts +4 -2
- package/src/client/teleprompterGlide.ts +491 -52
- package/src/client/useDecoupledMarkdown.ts +173 -0
- package/src/client/useFpsGuard.ts +9 -6
- package/src/client/useLogarithmicFade.ts +439 -60
- package/src/client/useProgressiveDomText.ts +27 -11
- package/src/client/useSmoothStreamContent.ts +5 -13
- package/src/config.ts +1 -1
- package/src/plugin.ts +83 -8
- package/src/settings-api.ts +2 -0
- package/src/settings-channel.ts +34 -8
- package/src/settings.ts +11 -0
package/lib/client.js
CHANGED
|
@@ -4,13 +4,52 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
7
29
|
let react = require("react");
|
|
8
30
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
31
|
+
_deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
|
|
9
32
|
let _deepseek_ai_dsh_client_ui_attachment = require("@deepseek-ai/dsh-client-ui-attachment");
|
|
10
33
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
34
|
let react_dom = require("react-dom");
|
|
12
|
-
//#region
|
|
13
|
-
const
|
|
35
|
+
//#region src/client/harnessIcons.ts
|
|
36
|
+
const icons = _deepseek_ai_dsh_client_ui_primitives;
|
|
37
|
+
function resolveIcon(currentName, legacyName) {
|
|
38
|
+
const icon = icons[currentName] ?? icons[legacyName];
|
|
39
|
+
if (icon === void 0) throw new Error(`dsh-smooth-stream: Harness icon export is missing (${currentName}, ${legacyName})`);
|
|
40
|
+
return icon;
|
|
41
|
+
}
|
|
42
|
+
const IconChevronDown = resolveIcon("IconChevronDownOutlineRegular", "IconChevronDownOutline14");
|
|
43
|
+
const IconClose = resolveIcon("IconCloseOutlineRegular", "IconCloseOutline16");
|
|
44
|
+
const IconCode = resolveIcon("IconCodeOutlineRegular", "IconCodeOutline16");
|
|
45
|
+
const IconCopy = resolveIcon("IconCopyOutlineRegular", "IconCopyOutline16");
|
|
46
|
+
const IconQuestion = resolveIcon("IconQuestionOutlineRegular", "IconQuestionOutline14");
|
|
47
|
+
const IconRefresh = resolveIcon("IconRefreshOutlineRegular", "IconRefreshOutline14");
|
|
48
|
+
const IconRefreshSmall = resolveIcon("IconRefreshOutlineRegular", "IconRefreshOutline16");
|
|
49
|
+
const IconThink = resolveIcon("IconThinkOutlineRegular", "IconThinkOutline14");
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
|
|
52
|
+
const css$4 = ".ydEAna_root{min-width:0;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size,16px);line-height:calc(24px + var(--dsh-content-font-delta,4px));flex-direction:column;display:flex}.ydEAna_body{flex-direction:column;gap:16px;min-width:0;display:flex}.ydEAna_body>[data-turn-process-inline][hidden]{margin-bottom:-16px}.ydEAna_think{flex-direction:column;display:flex}.ydEAna_thinkRow{position:relative;overflow:hidden}.ydEAna_think[data-state=running] .ydEAna_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 ydEAna_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes ydEAna_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.ydEAna_thinkLeading{flex-shrink:0}.ydEAna_thinkChevron{color:var(--dsw-alias-label-secondary)}.ydEAna_thinkTitle{font-weight:400}.ydEAna_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.ydEAna_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,14px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,4px));text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.ydEAna_thinkSummary[data-follow-end]{text-overflow:clip}.ydEAna_thinkBody{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,14px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,4px));white-space:pre-wrap;word-break:break-word;max-height:var(--dsh-thinking-max-height,300px);overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l4,#80808066) transparent;contain:layout;will-change:scroll-position;overflow-y:auto;transform:translateZ(0)}.ydEAna_thinkBody::-webkit-scrollbar{width:6px}.ydEAna_thinkBody::-webkit-scrollbar-track{background:0 0}.ydEAna_thinkBody::-webkit-scrollbar-thumb{background-color:var(--dsw-alias-border-l4,#80808066);border-radius:3px}.ydEAna_thinkBody::-webkit-scrollbar-thumb:hover{background-color:var(--dsw-alias-border-l3,#80808099)}.ydEAna_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.ydEAna_disclosureRow{height:calc(24px + var(--dsh-content-font-delta,0px));cursor:pointer;align-items:center;min-width:0;display:flex;position:relative;overflow:hidden}.ydEAna_disclosureLeading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.ydEAna_disclosureLeading svg:not([data-state]){width:calc(14px + var(--dsh-content-font-delta,0px));height:calc(14px + var(--dsh-content-font-delta,0px))}.ydEAna_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.ydEAna_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.ydEAna_disclosureRow:hover .ydEAna_disclosureIconIdle{opacity:0}.ydEAna_disclosureRow:hover .ydEAna_disclosureChevronHover{opacity:1}.ydEAna_disclosureTitle{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);flex:none}.ydEAna_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;overflow:hidden}.ydEAna_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}.ydEAna_disclosureContent[data-collapsed]>*{padding-top:0!important;padding-bottom:0!important}.ydEAna_disclosureContent[data-no-transition]{transition:none}.ydEAna_disclosureContent>*{min-height:0}.ydEAna_disclosureContent[data-collapsed]>*{overflow:hidden}@media (prefers-reduced-motion:reduce){.ydEAna_think[data-state=running] .ydEAna_thinkRow:after{animation:none}.ydEAna_disclosureContent,.ydEAna_disclosureContent[data-collapsed]{transition:none}}.ydEAna_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}.ydEAna_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}.ydEAna_follow{contain:layout style;min-width:0}.ydEAna_root :not(pre)>code{line-height:inherit;white-space:normal;overflow-wrap:anywhere;vertical-align:baseline;display:inline}@supports (text-box-trim:trim-both){.ydEAna_root :is(p,h1,h2,h3,h4,h5,h6,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
|
|
14
53
|
const tagId$4 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
|
|
15
54
|
if (typeof document !== "undefined") {
|
|
16
55
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]");
|
|
@@ -23,27 +62,27 @@ window.__ModuleLoader__.load({
|
|
|
23
62
|
tag.textContent = css$4;
|
|
24
63
|
}
|
|
25
64
|
var TypewriterAssistantNodeView_module_css_default = {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
65
|
+
"think": "ydEAna_think",
|
|
66
|
+
"thinkRow": "ydEAna_thinkRow",
|
|
67
|
+
"thinkSummary": "ydEAna_thinkSummary",
|
|
68
|
+
"disclosureLeading": "ydEAna_disclosureLeading",
|
|
69
|
+
"thinkSeparator": "ydEAna_thinkSeparator",
|
|
70
|
+
"root": "ydEAna_root",
|
|
71
|
+
"thinkLeading": "ydEAna_thinkLeading",
|
|
72
|
+
"dsh-smooth-stream-think-sweep": "ydEAna_dsh-smooth-stream-think-sweep",
|
|
73
|
+
"thinkTitle": "ydEAna_thinkTitle",
|
|
74
|
+
"disclosureTitle": "ydEAna_disclosureTitle",
|
|
75
|
+
"thinkChevron": "ydEAna_thinkChevron",
|
|
76
|
+
"disclosureContent": "ydEAna_disclosureContent",
|
|
77
|
+
"disclosureIconIdle": "ydEAna_disclosureIconIdle",
|
|
78
|
+
"thinkBody": "ydEAna_thinkBody",
|
|
79
|
+
"stopped": "ydEAna_stopped",
|
|
80
|
+
"disclosureRoot": "ydEAna_disclosureRoot",
|
|
81
|
+
"body": "ydEAna_body",
|
|
82
|
+
"disclosureChevronHover": "ydEAna_disclosureChevronHover",
|
|
83
|
+
"visuallyHidden": "ydEAna_visuallyHidden",
|
|
84
|
+
"follow": "ydEAna_follow",
|
|
85
|
+
"disclosureRow": "ydEAna_disclosureRow"
|
|
47
86
|
};
|
|
48
87
|
//#endregion
|
|
49
88
|
//#region src/client/AnimatedDisclosure.tsx
|
|
@@ -78,10 +117,10 @@ window.__ModuleLoader__.load({
|
|
|
78
117
|
children: [
|
|
79
118
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
80
119
|
className: cx(TypewriterAssistantNodeView_module_css_default.disclosureLeading, leadingClassName),
|
|
81
|
-
children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
120
|
+
children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: chevronClassName }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
82
121
|
className: TypewriterAssistantNodeView_module_css_default.disclosureIconIdle,
|
|
83
122
|
children: icon
|
|
84
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
123
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: cx(chevronClassName, TypewriterAssistantNodeView_module_css_default.disclosureChevronHover) })] })
|
|
85
124
|
}),
|
|
86
125
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
87
126
|
className: cx(TypewriterAssistantNodeView_module_css_default.disclosureTitle, titleClassName),
|
|
@@ -126,6 +165,7 @@ window.__ModuleLoader__.load({
|
|
|
126
165
|
const DEFAULT_STREAM_SETTINGS = {
|
|
127
166
|
enabled: true,
|
|
128
167
|
controlScroll: true,
|
|
168
|
+
preset: "silky",
|
|
129
169
|
motionPreference: "auto",
|
|
130
170
|
thinkAutoExpand: true,
|
|
131
171
|
logarithmicFade: true,
|
|
@@ -197,6 +237,21 @@ window.__ModuleLoader__.load({
|
|
|
197
237
|
* the renderer and the chat-side panel: renderer loops publish measurements,
|
|
198
238
|
* while panel edits are staged through the settings-card controller.
|
|
199
239
|
*/
|
|
240
|
+
/**
|
|
241
|
+
* Allocation-free read of the newest stream metric. The follower polls this
|
|
242
|
+
* every frame to learn whether the producer has stopped while reveal work
|
|
243
|
+
* remains, and `getSnapshot()` would allocate a fresh state object per frame
|
|
244
|
+
* just to answer one boolean.
|
|
245
|
+
*/
|
|
246
|
+
function readNewestStreamMetric() {
|
|
247
|
+
let newest;
|
|
248
|
+
for (const candidate of streamMetrics.values()) if (newest === void 0 || candidate.updatedAt > newest.updatedAt) newest = candidate;
|
|
249
|
+
if (newest === void 0) return null;
|
|
250
|
+
return {
|
|
251
|
+
producerComplete: newest.producerComplete,
|
|
252
|
+
backlog: newest.backlog
|
|
253
|
+
};
|
|
254
|
+
}
|
|
200
255
|
const EMPTY_METRICS = {
|
|
201
256
|
fps: null,
|
|
202
257
|
frameMs: null,
|
|
@@ -214,6 +269,12 @@ window.__ModuleLoader__.load({
|
|
|
214
269
|
followRevealScale: 1,
|
|
215
270
|
followFollowing: false,
|
|
216
271
|
followConstrained: false,
|
|
272
|
+
followTerminalPhase: "live",
|
|
273
|
+
followRunwayPx: 0,
|
|
274
|
+
followTerminalBudgetPx: 0,
|
|
275
|
+
followBaselineShiftPx: 0,
|
|
276
|
+
followAnchorDeltaPx: null,
|
|
277
|
+
followRemainingRevealChars: 0,
|
|
217
278
|
scrollTop: null,
|
|
218
279
|
scrollHeight: null,
|
|
219
280
|
clientHeight: null,
|
|
@@ -274,6 +335,12 @@ window.__ModuleLoader__.load({
|
|
|
274
335
|
followRevealScale: follow?.revealScale ?? 1,
|
|
275
336
|
followFollowing: follow?.following ?? false,
|
|
276
337
|
followConstrained: follow?.constrained ?? false,
|
|
338
|
+
followTerminalPhase: follow?.terminalPhase ?? "live",
|
|
339
|
+
followRunwayPx: follow?.runwayPx ?? 0,
|
|
340
|
+
followTerminalBudgetPx: follow?.terminalBudgetPx ?? 0,
|
|
341
|
+
followBaselineShiftPx: follow?.baselineShiftPx ?? 0,
|
|
342
|
+
followAnchorDeltaPx: follow?.readingAnchorDeltaPx ?? null,
|
|
343
|
+
followRemainingRevealChars: follow?.remainingRevealChars ?? 0,
|
|
277
344
|
scrollTop: follow?.scrollTop ?? null,
|
|
278
345
|
scrollHeight: follow?.scrollHeight ?? null,
|
|
279
346
|
clientHeight: follow?.clientHeight ?? null,
|
|
@@ -436,6 +503,123 @@ window.__ModuleLoader__.load({
|
|
|
436
503
|
}
|
|
437
504
|
};
|
|
438
505
|
//#endregion
|
|
506
|
+
//#region src/client/FrameCoordinator.ts
|
|
507
|
+
var FrameCoordinator = class FrameCoordinator {
|
|
508
|
+
static coordinators = /* @__PURE__ */ new WeakMap();
|
|
509
|
+
tasks = /* @__PURE__ */ new Map();
|
|
510
|
+
rafId = null;
|
|
511
|
+
lastTs = null;
|
|
512
|
+
/**
|
|
513
|
+
* Owed layout read. Starts false: a fresh coordinator has no unread writes,
|
|
514
|
+
* and starting dirty would keep the clock armed forever after the first
|
|
515
|
+
* frame. Consumers declare reads via {@link markLayoutDirty}.
|
|
516
|
+
*/
|
|
517
|
+
layoutDirty = false;
|
|
518
|
+
/** One id per consumer, so an unregister cannot evict a re-registered task. */
|
|
519
|
+
nextTaskId = 0;
|
|
520
|
+
doc;
|
|
521
|
+
constructor(doc) {
|
|
522
|
+
this.doc = doc;
|
|
523
|
+
}
|
|
524
|
+
static forDocument(doc = document) {
|
|
525
|
+
let coordinator = FrameCoordinator.coordinators.get(doc);
|
|
526
|
+
if (coordinator === void 0) {
|
|
527
|
+
coordinator = new FrameCoordinator(doc);
|
|
528
|
+
FrameCoordinator.coordinators.set(doc, coordinator);
|
|
529
|
+
}
|
|
530
|
+
return coordinator;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Mark that a DOM or geometry change requires a layout read next frame.
|
|
534
|
+
*/
|
|
535
|
+
markLayoutDirty() {
|
|
536
|
+
this.layoutDirty = true;
|
|
537
|
+
this.ensureLoop();
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Declare that geometry written since the last frame must be re-read at the
|
|
541
|
+
* top of the next one, and keep the clock running for it.
|
|
542
|
+
*/
|
|
543
|
+
requestRead() {
|
|
544
|
+
this.markLayoutDirty();
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Register or replace an animation/render task. Returns the task id to hand
|
|
548
|
+
* back to {@link unregisterTask}.
|
|
549
|
+
*/
|
|
550
|
+
registerTask(task) {
|
|
551
|
+
const id = task.id ?? `frame-task-${this.nextTaskId++}`;
|
|
552
|
+
this.tasks.set(id, {
|
|
553
|
+
...task,
|
|
554
|
+
id
|
|
555
|
+
});
|
|
556
|
+
this.ensureLoop();
|
|
557
|
+
return id;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Unregister a task when its stream completes or the element unmounts.
|
|
561
|
+
*/
|
|
562
|
+
unregisterTask(id) {
|
|
563
|
+
if (id === null || id === void 0) return;
|
|
564
|
+
this.tasks.delete(id);
|
|
565
|
+
if (this.tasks.size === 0 && !this.layoutDirty) this.stopLoop();
|
|
566
|
+
}
|
|
567
|
+
ensureLoop() {
|
|
568
|
+
if (this.rafId !== null) return;
|
|
569
|
+
const win = this.doc.defaultView ?? (typeof window !== "undefined" ? window : null);
|
|
570
|
+
if (win === null) return;
|
|
571
|
+
const tick = (now) => {
|
|
572
|
+
this.rafId = null;
|
|
573
|
+
if (this.lastTs === null) {
|
|
574
|
+
this.lastTs = now;
|
|
575
|
+
this.ensureLoop();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const frameIntervalMs = Math.max(0, now - this.lastTs);
|
|
579
|
+
const dtMs = Math.max(1, Math.min(frameIntervalMs, 100));
|
|
580
|
+
this.lastTs = now;
|
|
581
|
+
if (this.layoutDirty) {
|
|
582
|
+
this.layoutDirty = false;
|
|
583
|
+
for (const task of this.tasks.values()) task.onRead?.(now);
|
|
584
|
+
}
|
|
585
|
+
let anyActive = false;
|
|
586
|
+
for (const task of this.tasks.values()) if (task.onSimulate?.(dtMs, now) === true) anyActive = true;
|
|
587
|
+
for (const task of this.tasks.values()) task.onWrite?.(now);
|
|
588
|
+
if ((anyActive || this.layoutDirty) && this.rafId === null) this.ensureLoop();
|
|
589
|
+
else if (!anyActive && !this.layoutDirty) this.lastTs = null;
|
|
590
|
+
};
|
|
591
|
+
this.rafId = win.requestAnimationFrame(tick);
|
|
592
|
+
}
|
|
593
|
+
stopLoop() {
|
|
594
|
+
if (this.rafId !== null) {
|
|
595
|
+
(this.doc.defaultView ?? (typeof window !== "undefined" ? window : null))?.cancelAnimationFrame(this.rafId);
|
|
596
|
+
this.rafId = null;
|
|
597
|
+
}
|
|
598
|
+
this.lastTs = null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Whether this document currently owns a live, still-needed animation
|
|
602
|
+
* frame. A frame that is merely draining an owed read with no registered
|
|
603
|
+
* task left is not considered active work.
|
|
604
|
+
*/
|
|
605
|
+
get active() {
|
|
606
|
+
return this.rafId !== null && (this.tasks.size > 0 || this.layoutDirty);
|
|
607
|
+
}
|
|
608
|
+
/** Number of registered tasks; used by tests to pin the single-clock contract. */
|
|
609
|
+
get taskCount() {
|
|
610
|
+
return this.tasks.size;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Release every task, drop any owed read, and stop the clock. Used by tests
|
|
614
|
+
* for isolation; production consumers unregister their own task.
|
|
615
|
+
*/
|
|
616
|
+
shutdown() {
|
|
617
|
+
this.tasks.clear();
|
|
618
|
+
this.layoutDirty = false;
|
|
619
|
+
this.stopLoop();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
439
623
|
//#region src/client/teleprompterGlide.ts
|
|
440
624
|
/**
|
|
441
625
|
* Conversation-port follow while an assistant reply streams.
|
|
@@ -674,7 +858,7 @@ window.__ModuleLoader__.load({
|
|
|
674
858
|
setDirectShift(element, px);
|
|
675
859
|
}
|
|
676
860
|
function turnStatusOf(port) {
|
|
677
|
-
return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"]");
|
|
861
|
+
return port.querySelector("[data-chat-turn-status], [data-chat-flow] > [role=\"status\"], [data-chat-flow] > [data-chat-running]");
|
|
678
862
|
}
|
|
679
863
|
const FOLLOW_FLOW_FILL_SYMBOL = Symbol.for("dsh-smooth-stream.follow-flow-fill");
|
|
680
864
|
const followFlowFillHost = globalThis;
|
|
@@ -687,6 +871,10 @@ window.__ModuleLoader__.load({
|
|
|
687
871
|
function flowElementOf(port) {
|
|
688
872
|
return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
|
|
689
873
|
}
|
|
874
|
+
/** Keep completion padding on the stable flow wrapper across row replacement. */
|
|
875
|
+
function flowPadElementOf(port) {
|
|
876
|
+
return port.querySelector("[data-chat-flow]") ?? flowElementOf(port);
|
|
877
|
+
}
|
|
690
878
|
function ensureFlowFillsPort(port) {
|
|
691
879
|
const element = flowElementOf(port);
|
|
692
880
|
const owned = followFlowFills.get(port);
|
|
@@ -746,6 +934,8 @@ window.__ModuleLoader__.load({
|
|
|
746
934
|
followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
|
|
747
935
|
const followPaintLimits = /* @__PURE__ */ new WeakMap();
|
|
748
936
|
const followHadChrome = /* @__PURE__ */ new WeakSet();
|
|
937
|
+
/** A status row that existed during this turn makes its removal a host cascade. */
|
|
938
|
+
const followHadStatus = /* @__PURE__ */ new WeakSet();
|
|
749
939
|
/** Last painted shift per port, to spread a wrap's one-line step over frames. */
|
|
750
940
|
const followLastShiftPx = /* @__PURE__ */ new WeakMap();
|
|
751
941
|
/** Last settled floor per port, to size the shift decay bound against extent collapse. */
|
|
@@ -939,7 +1129,7 @@ window.__ModuleLoader__.load({
|
|
|
939
1129
|
return false;
|
|
940
1130
|
}
|
|
941
1131
|
function setFlowPad(port, px) {
|
|
942
|
-
const flow =
|
|
1132
|
+
const flow = flowPadElementOf(port);
|
|
943
1133
|
if (flow === null) return;
|
|
944
1134
|
const existing = followSettlePads.get(port);
|
|
945
1135
|
const original = existing?.original ?? flow.style.paddingBottom;
|
|
@@ -977,12 +1167,83 @@ window.__ModuleLoader__.load({
|
|
|
977
1167
|
if (!traceActive()) return;
|
|
978
1168
|
console.log(`[dsh-follow] ${event}`, JSON.stringify(detail));
|
|
979
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Scroll length observed at the previous measured frame, per port. The
|
|
1172
|
+
* completion handoff uses the delta to seed the growth credit from geometry the
|
|
1173
|
+
* page actually committed, instead of assuming the full runway was earned.
|
|
1174
|
+
*/
|
|
1175
|
+
const followObservedContentHeight = /* @__PURE__ */ new WeakMap();
|
|
1176
|
+
/** Growth this commit added, as last measured by `applyVisual`. */
|
|
1177
|
+
function environmentCommitGrowthPx(port) {
|
|
1178
|
+
const previous = followObservedContentHeight.get(port);
|
|
1179
|
+
if (previous === void 0) return 0;
|
|
1180
|
+
return Math.max(0, port.scrollHeight - previous);
|
|
1181
|
+
}
|
|
980
1182
|
function hostShOf(port) {
|
|
981
1183
|
return port.scrollHeight;
|
|
982
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Reveal characters the smoother still owes, read from the stream ledger the
|
|
1187
|
+
* smoother publishes each frame. Only meaningful together with
|
|
1188
|
+
* `producerComplete`: a non-zero backlog while the producer runs is ordinary
|
|
1189
|
+
* streaming, the same backlog after it stops is the `terminal-drain` phase.
|
|
1190
|
+
*/
|
|
1191
|
+
let followRemainingRevealChars = 0;
|
|
1192
|
+
function refreshTerminalRevealLedger() {
|
|
1193
|
+
const stream = readNewestStreamMetric();
|
|
1194
|
+
followRemainingRevealChars = stream?.backlog ?? 0;
|
|
1195
|
+
return {
|
|
1196
|
+
producerComplete: stream?.producerComplete ?? false,
|
|
1197
|
+
drain: (stream?.producerComplete ?? false) && (stream?.backlog ?? 0) > 0
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Screen-space reading-anchor delta for diagnostics only. Gated on the debug
|
|
1202
|
+
* runtime so the production hot path pays nothing, and it reuses the shared
|
|
1203
|
+
* `measureReadingAnchor` ledger, which already excludes the engine's own glide
|
|
1204
|
+
* and only reports motion the follower did not author.
|
|
1205
|
+
*/
|
|
1206
|
+
function measureAnchorDeltaForTelemetry(port) {
|
|
1207
|
+
if (!debugRuntime.isEnabled()) return null;
|
|
1208
|
+
return measureReadingAnchor(port)?.delta ?? null;
|
|
1209
|
+
}
|
|
983
1210
|
function invalidatePaintLimit(port) {
|
|
984
1211
|
followPaintLimits.delete(port);
|
|
985
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Terminal-follow phase ledger. `terminal-drain` (producer stopped, reveal
|
|
1215
|
+
* queue still owes text) and `host-cascade` (host is swapping status rows,
|
|
1216
|
+
* collapsing Think, mounting the tail) demand opposite corrections: drain may
|
|
1217
|
+
* only continue upward toward the natural floor, while a cascade may need
|
|
1218
|
+
* temporary credit to hold the reading anchor. Keeping them in one readable
|
|
1219
|
+
* field is what lets the settle path stop treating ordinary completion as a
|
|
1220
|
+
* hostile host transaction.
|
|
1221
|
+
*/
|
|
1222
|
+
const followTerminalPhases = /* @__PURE__ */ new WeakMap();
|
|
1223
|
+
/**
|
|
1224
|
+
* Measured terminal budget per port: the owned bottom space this port is
|
|
1225
|
+
* allowed to keep while the producer has stopped and reveal work is still
|
|
1226
|
+
* draining. It is seeded from a real measurement (the owned offset plus the
|
|
1227
|
+
* length the last commit actually added) and may only shrink afterwards — a
|
|
1228
|
+
* budget that grows re-opens speculative blank space below the reply, which is
|
|
1229
|
+
* exactly the "lift" the terminal phase exists to remove.
|
|
1230
|
+
*/
|
|
1231
|
+
const followTerminalBudgets = /* @__PURE__ */ new WeakMap();
|
|
1232
|
+
/**
|
|
1233
|
+
* COMPLETION GROWTH CREDIT. Pixels of floor growth the reader actually
|
|
1234
|
+
* received in the completion window, which a shift rise may be funded from.
|
|
1235
|
+
*
|
|
1236
|
+
* This is a ledger rather than a per-frame clamp because growth and the
|
|
1237
|
+
* matching shift rise do not have to land on the same frame: a completion whose
|
|
1238
|
+
* final append covers several wraps commits its growth over two or three
|
|
1239
|
+
* frames, and a clamp that only ever compared against the PREVIOUS frame's
|
|
1240
|
+
* growth banked nothing across that gap. The uncovered remainder then had no
|
|
1241
|
+
* legal way to rise, so it survived as a real offset and was released later by
|
|
1242
|
+
* the pad retirement — the visible release-then-return this credit exists to
|
|
1243
|
+
* prevent. Credit is still bounded by growth the reader actually got, so a
|
|
1244
|
+
* rise can never manufacture motion that did not come from content.
|
|
1245
|
+
*/
|
|
1246
|
+
const followCompletionGrowthCredit = /* @__PURE__ */ new WeakMap();
|
|
986
1247
|
/** Logical position and velocity survive a React owner handoff and finish. */
|
|
987
1248
|
const followMotionStates = /* @__PURE__ */ new WeakMap();
|
|
988
1249
|
const followReaderHolds = /* @__PURE__ */ new WeakMap();
|
|
@@ -1131,6 +1392,7 @@ window.__ModuleLoader__.load({
|
|
|
1131
1392
|
if (last === void 0) return 0;
|
|
1132
1393
|
const status = turnStatusOf(port);
|
|
1133
1394
|
const composer = port.querySelector("[data-composer-seat]");
|
|
1395
|
+
if (status !== null) followHadStatus.add(port);
|
|
1134
1396
|
if (status !== null || composer !== null) followHadChrome.add(port);
|
|
1135
1397
|
const cached = followPaintLimits.get(port);
|
|
1136
1398
|
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;
|
|
@@ -1205,6 +1467,35 @@ window.__ModuleLoader__.load({
|
|
|
1205
1467
|
function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 72, shiftCeilingPx = Number.POSITIVE_INFINITY, promoteAtRest = false, trajectoryShiftPx, dtMs = 16.7, writeScrollTop = true) {
|
|
1206
1468
|
const surfaces = shiftSurfacesOf(port);
|
|
1207
1469
|
ensureFlowFillsPort(port);
|
|
1470
|
+
if (followTerminalPhases.get(port) === "terminal-drain" && turnStatusOf(port) === null && !followHadStatus.has(port)) {
|
|
1471
|
+
restoreRunway(port);
|
|
1472
|
+
setFlowPad(port, 0);
|
|
1473
|
+
const contentHeight = Math.max(0, port.scrollHeight);
|
|
1474
|
+
const floor = Math.max(0, contentHeight - port.clientHeight);
|
|
1475
|
+
if (port.style.overflowAnchor !== "none") port.style.overflowAnchor = "none";
|
|
1476
|
+
if (port.style.scrollBehavior !== "auto") port.style.scrollBehavior = "auto";
|
|
1477
|
+
if (writeScrollTop) setFollowScrollTop(port, floor);
|
|
1478
|
+
followRunwayOffsetHistory.set(port, 0);
|
|
1479
|
+
followObservedContentHeight.set(port, contentHeight);
|
|
1480
|
+
followLastFloorPx.set(port, floor);
|
|
1481
|
+
followLastShiftPx.set(port, 0);
|
|
1482
|
+
followMotionStates.set(port, {
|
|
1483
|
+
capacityPx: Number.POSITIVE_INFINITY,
|
|
1484
|
+
constrained: false,
|
|
1485
|
+
extent: contentHeight,
|
|
1486
|
+
lagPx: 0,
|
|
1487
|
+
reservePx: 0,
|
|
1488
|
+
velocityPxPerSec: 0,
|
|
1489
|
+
terminalPhase: "terminal-drain",
|
|
1490
|
+
runwayPx: 0,
|
|
1491
|
+
baselineShiftPx: 0,
|
|
1492
|
+
anchorDeltaPx: measureAnchorDeltaForTelemetry(port),
|
|
1493
|
+
remainingRevealChars: followRemainingRevealChars
|
|
1494
|
+
});
|
|
1495
|
+
for (const surface of surfaces) setShift(surface, 0);
|
|
1496
|
+
FrameCoordinator.forDocument().markLayoutDirty();
|
|
1497
|
+
return contentHeight;
|
|
1498
|
+
}
|
|
1208
1499
|
{
|
|
1209
1500
|
const preFloor = Math.max(0, port.scrollHeight - port.clientHeight);
|
|
1210
1501
|
const lastFloorSeen = followFloorHistory.get(port);
|
|
@@ -1216,7 +1507,7 @@ window.__ModuleLoader__.load({
|
|
|
1216
1507
|
}
|
|
1217
1508
|
if (preFloor !== lastFloorSeen) followFloorHistory.set(port, preFloor);
|
|
1218
1509
|
}
|
|
1219
|
-
ensureRunway(port, surfaces, reservePx);
|
|
1510
|
+
ensureRunway(port, surfaces, followTerminalPhases.has(port) ? Math.max(Math.min(72, followTerminalBudgets.get(port) ?? 72), reservePx) : 72);
|
|
1220
1511
|
const contentHeight2 = Math.max(0, port.scrollHeight);
|
|
1221
1512
|
const runwayOffset2 = runwayOffsetOf(port);
|
|
1222
1513
|
const prevOffset2 = followRunwayOffsetHistory.get(port);
|
|
@@ -1224,6 +1515,7 @@ window.__ModuleLoader__.load({
|
|
|
1224
1515
|
followRunwayOffsetHistory.set(port, runwayOffset2);
|
|
1225
1516
|
const contentHeight = contentHeight2;
|
|
1226
1517
|
const runwayOffset = runwayOffset2;
|
|
1518
|
+
followObservedContentHeight.set(port, contentHeight2);
|
|
1227
1519
|
const targetHeight = Math.max(0, contentHeight - runwayOffset);
|
|
1228
1520
|
const floor = Math.max(0, contentHeight - port.clientHeight);
|
|
1229
1521
|
const extent = Math.min(targetHeight, Math.max(0, animatedH));
|
|
@@ -1262,7 +1554,15 @@ window.__ModuleLoader__.load({
|
|
|
1262
1554
|
if (previousShift !== void 0 && shift < previousShift - maxDecayPx) shift = previousShift - maxDecayPx;
|
|
1263
1555
|
if (followCompletionSettle.has(port) && previousShift !== void 0 && previousFloor !== void 0) {
|
|
1264
1556
|
const confirmedGrowthPx = Math.max(0, floor - previousFloor);
|
|
1265
|
-
|
|
1557
|
+
let credit = (followCompletionGrowthCredit.get(port) ?? 0) + confirmedGrowthPx;
|
|
1558
|
+
if (shift > previousShift) {
|
|
1559
|
+
const rise = shift - previousShift;
|
|
1560
|
+
const spend = Math.min(Math.min(rise, 24), credit);
|
|
1561
|
+
const unfunded = rise - spend;
|
|
1562
|
+
if (unfunded > 0) shift -= unfunded;
|
|
1563
|
+
credit -= spend;
|
|
1564
|
+
}
|
|
1565
|
+
followCompletionGrowthCredit.set(port, credit);
|
|
1266
1566
|
}
|
|
1267
1567
|
followLastShiftPx.set(port, shift);
|
|
1268
1568
|
const requestedShift = trajectoryShiftPx ?? baselineShift + requestedLag;
|
|
@@ -1277,11 +1577,17 @@ window.__ModuleLoader__.load({
|
|
|
1277
1577
|
extent: effectiveExtent,
|
|
1278
1578
|
lagPx: effectiveLag,
|
|
1279
1579
|
reservePx: visibleReserve,
|
|
1280
|
-
velocityPxPerSec
|
|
1580
|
+
velocityPxPerSec,
|
|
1581
|
+
terminalPhase: followTerminalPhases.get(port) ?? "live",
|
|
1582
|
+
runwayPx: runwayOffset,
|
|
1583
|
+
baselineShiftPx: baselineShift,
|
|
1584
|
+
anchorDeltaPx: followTerminalPhases.has(port) ? measureAnchorDeltaForTelemetry(port) : null,
|
|
1585
|
+
remainingRevealChars: followRemainingRevealChars
|
|
1281
1586
|
});
|
|
1282
1587
|
for (const surface of surfaces) setShift(surface, shift);
|
|
1283
1588
|
const status = turnStatusOf(port);
|
|
1284
1589
|
if (status !== null) setShift(status, 0);
|
|
1590
|
+
FrameCoordinator.forDocument().markLayoutDirty();
|
|
1285
1591
|
return effectiveExtent;
|
|
1286
1592
|
}
|
|
1287
1593
|
function clearMotion(port) {
|
|
@@ -1307,8 +1613,10 @@ window.__ModuleLoader__.load({
|
|
|
1307
1613
|
element.style.clipPath = "";
|
|
1308
1614
|
}
|
|
1309
1615
|
/** Remove equal offsets, land on the floor, then retire the compositor quietly. */
|
|
1310
|
-
function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true) {
|
|
1616
|
+
function finishAtNaturalFloor(port, retainCompositor = true, writeScrollTop = true, deferCompositor = false) {
|
|
1311
1617
|
followCompletionSettle.delete(port);
|
|
1618
|
+
followTerminalPhases.set(port, "natural");
|
|
1619
|
+
followCompletionGrowthCredit.delete(port);
|
|
1312
1620
|
followTraceUntilMs = Math.max(followTraceUntilMs, performance.now() + 1e4);
|
|
1313
1621
|
followTrace("finish-enter", {
|
|
1314
1622
|
sh: hostShOf(port),
|
|
@@ -1331,6 +1639,10 @@ window.__ModuleLoader__.load({
|
|
|
1331
1639
|
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
1332
1640
|
port.style.overflowAnchor = "";
|
|
1333
1641
|
port.style.scrollBehavior = "";
|
|
1642
|
+
if (deferCompositor) {
|
|
1643
|
+
followMotionStates.delete(port);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1334
1646
|
for (const surface of surfaces) if (promotedSet.has(surface)) holdCompositorAtRest(surface);
|
|
1335
1647
|
else setShift(surface, 0);
|
|
1336
1648
|
if (status !== null) {
|
|
@@ -1384,13 +1696,20 @@ window.__ModuleLoader__.load({
|
|
|
1384
1696
|
const startedAsEntrance = entrance;
|
|
1385
1697
|
const owner = {};
|
|
1386
1698
|
const generation = ++followGeneration;
|
|
1387
|
-
let rafId = 0;
|
|
1388
1699
|
let last = performance.now();
|
|
1389
1700
|
let following = true;
|
|
1390
1701
|
let primed = false;
|
|
1391
1702
|
let animatedH = 0;
|
|
1392
1703
|
let reservePx = 0;
|
|
1393
1704
|
let velocityPxPerSec = 0;
|
|
1705
|
+
/**
|
|
1706
|
+
* Scroll length the most recent observed commit actually added. This is the
|
|
1707
|
+
* honest measure of how much room the still-draining text needs, and it is
|
|
1708
|
+
* what seeds the terminal budget in place of the fixed 72px streaming
|
|
1709
|
+
* runway.
|
|
1710
|
+
*/
|
|
1711
|
+
let lastCommitGrowthPx = 0;
|
|
1712
|
+
let lastObservedContentHeight = 0;
|
|
1394
1713
|
let interacting = false;
|
|
1395
1714
|
let readerGestureIntent = false;
|
|
1396
1715
|
let readerReleased = false;
|
|
@@ -1414,7 +1733,28 @@ window.__ModuleLoader__.load({
|
|
|
1414
1733
|
const revealPhase = new FollowRevealPhaseTracker();
|
|
1415
1734
|
let holding = null;
|
|
1416
1735
|
let entrancePending = entranceRef.current;
|
|
1736
|
+
/**
|
|
1737
|
+
* Tall-row entrance clamp. A surface shift can only mask lag up to the
|
|
1738
|
+
* real gap to status/composer chrome; a row that commits more than that
|
|
1739
|
+
* (an Edit call with its diff, a wide tool card) repaints the remainder
|
|
1740
|
+
* as an instant upward step — and the 0.2.x host's order-length tail
|
|
1741
|
+
* snap pins scrollTop to the new floor in the same task, so no
|
|
1742
|
+
* scroll-domain lag can be carried either. Reveal the excess through the
|
|
1743
|
+
* wrapper's REAL height instead: clamp it to 0 at prime and raise it at
|
|
1744
|
+
* the spring cadence, so the floor grows gradually and every bottom
|
|
1745
|
+
* writer (this engine and the host snap alike) glides the row in.
|
|
1746
|
+
*/
|
|
1747
|
+
let entranceClamp = null;
|
|
1748
|
+
const releaseEntranceClamp = () => {
|
|
1749
|
+
if (entranceClamp === null) return;
|
|
1750
|
+
const clamp = entranceClamp;
|
|
1751
|
+
entranceClamp = null;
|
|
1752
|
+
clamp.element.style.maxHeight = clamp.maxHeight;
|
|
1753
|
+
clamp.element.style.overflow = clamp.overflow;
|
|
1754
|
+
};
|
|
1755
|
+
const entranceClampLagPx = () => entranceClamp === null ? 0 : Math.max(0, entranceClamp.extent - entranceClamp.revealedPx);
|
|
1417
1756
|
const finishEntrance = () => {
|
|
1757
|
+
releaseEntranceClamp();
|
|
1418
1758
|
if (!entrancePending) return;
|
|
1419
1759
|
entrancePending = false;
|
|
1420
1760
|
onEntranceSettledRef.current?.();
|
|
@@ -1437,6 +1777,8 @@ window.__ModuleLoader__.load({
|
|
|
1437
1777
|
};
|
|
1438
1778
|
const reportFollow = (next, isActive) => {
|
|
1439
1779
|
const state = followMotionStates.get(next);
|
|
1780
|
+
const phase = followTerminalPhases.get(next) ?? (isActive ? "live" : "natural");
|
|
1781
|
+
const runwayPx = state?.runwayPx ?? runwayOffsetOf(next);
|
|
1440
1782
|
debugRuntime.reportFollow(next, {
|
|
1441
1783
|
lagPx: state ? state.lagPx : -1,
|
|
1442
1784
|
velocityPxPerSec: state?.velocityPxPerSec ?? 0,
|
|
@@ -1448,7 +1790,13 @@ window.__ModuleLoader__.load({
|
|
|
1448
1790
|
scrollTop: next.scrollTop,
|
|
1449
1791
|
scrollHeight: next.scrollHeight,
|
|
1450
1792
|
clientHeight: next.clientHeight,
|
|
1451
|
-
active: isActive
|
|
1793
|
+
active: isActive,
|
|
1794
|
+
terminalPhase: phase,
|
|
1795
|
+
runwayPx,
|
|
1796
|
+
terminalBudgetPx: followTerminalBudgets.get(next) ?? runwayPx,
|
|
1797
|
+
baselineShiftPx: state?.baselineShiftPx ?? Math.max(0, runwayPx - (state?.reservePx ?? 0)),
|
|
1798
|
+
readingAnchorDeltaPx: state?.anchorDeltaPx ?? null,
|
|
1799
|
+
remainingRevealChars: followRemainingRevealChars
|
|
1452
1800
|
});
|
|
1453
1801
|
};
|
|
1454
1802
|
const isLeader = (next) => followLeaders.get(next)?.owner === owner;
|
|
@@ -1681,23 +2029,29 @@ window.__ModuleLoader__.load({
|
|
|
1681
2029
|
observedTail = tail;
|
|
1682
2030
|
if (tail !== null) resize.observe(tail);
|
|
1683
2031
|
};
|
|
2032
|
+
const coordinator = FrameCoordinator.forDocument();
|
|
2033
|
+
const frameTaskRef = { id: null };
|
|
2034
|
+
const stopFollowTask = () => {
|
|
2035
|
+
if (frameTaskRef.id === null) return;
|
|
2036
|
+
coordinator.unregisterTask(frameTaskRef.id);
|
|
2037
|
+
frameTaskRef.id = null;
|
|
2038
|
+
};
|
|
1684
2039
|
const frame = (now) => {
|
|
1685
|
-
rafId = requestAnimationFrame(frame);
|
|
1686
2040
|
const elapsedMs = Math.max(.001, now - last);
|
|
1687
2041
|
const dt = Math.min(32, elapsedMs);
|
|
1688
2042
|
const tuning = debugRuntime.activeTuning();
|
|
1689
2043
|
last = now;
|
|
1690
2044
|
const root = rootRef.current;
|
|
1691
|
-
if (root === null) return;
|
|
2045
|
+
if (root === null) return activeRef.current;
|
|
1692
2046
|
const nextPort = root.closest("[data-conversation-scroll]");
|
|
1693
|
-
if (nextPort === null) return;
|
|
2047
|
+
if (nextPort === null) return activeRef.current;
|
|
1694
2048
|
bindPort(nextPort);
|
|
1695
2049
|
observeTailSurface();
|
|
1696
2050
|
resetHostScrollOwnershipForNewTurn(nextPort);
|
|
1697
2051
|
hostOwnsScroll = followHostScrollPorts.has(nextPort);
|
|
1698
2052
|
if (activeRef.current) followActivePorts.add(nextPort);
|
|
1699
2053
|
else followActivePorts.delete(nextPort);
|
|
1700
|
-
if (nextPort.clientHeight <= 0) return;
|
|
2054
|
+
if (nextPort.clientHeight <= 0) return true;
|
|
1701
2055
|
const floor = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight);
|
|
1702
2056
|
const reportedLag = floor - nextPort.scrollTop;
|
|
1703
2057
|
const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
|
|
@@ -1705,14 +2059,37 @@ window.__ModuleLoader__.load({
|
|
|
1705
2059
|
if (completionSettleGuardsPort(nextPort)) {
|
|
1706
2060
|
primed = true;
|
|
1707
2061
|
following = false;
|
|
1708
|
-
return;
|
|
2062
|
+
return activeRef.current;
|
|
2063
|
+
}
|
|
2064
|
+
if (activeRef.current) {
|
|
2065
|
+
followTerminalPhases.delete(nextPort);
|
|
2066
|
+
followTerminalBudgets.delete(nextPort);
|
|
2067
|
+
followCompletionGrowthCredit.delete(nextPort);
|
|
2068
|
+
followHadStatus.delete(nextPort);
|
|
1709
2069
|
}
|
|
1710
2070
|
const inherited = nextPort.hasAttribute(FOLLOW_OWNED_ATTR) ? followMotionStates.get(nextPort) : void 0;
|
|
1711
2071
|
if (inherited === void 0) {
|
|
1712
2072
|
const entranceExtent = entrancePending ? entranceExtentRef?.current ?? entranceExtentOf(root) : 0;
|
|
1713
2073
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1714
2074
|
animatedH = entrancePending ? Math.max(0, nextPort.scrollHeight - entranceExtent) : nextPort.scrollHeight;
|
|
2075
|
+
if (entrancePending && entranceExtent > safeShiftLimit(nextPort, shiftSurfacesOf(nextPort)) + .25) {
|
|
2076
|
+
const fullPx = root.offsetHeight;
|
|
2077
|
+
if (fullPx > 0) {
|
|
2078
|
+
const basePx = Math.max(0, fullPx - entranceExtent);
|
|
2079
|
+
entranceClamp = {
|
|
2080
|
+
element: root,
|
|
2081
|
+
extent: entranceExtent,
|
|
2082
|
+
basePx,
|
|
2083
|
+
revealedPx: 0,
|
|
2084
|
+
maxHeight: root.style.maxHeight,
|
|
2085
|
+
overflow: root.style.overflow
|
|
2086
|
+
};
|
|
2087
|
+
root.style.overflow = "hidden";
|
|
2088
|
+
root.style.maxHeight = `${basePx}px`;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1715
2091
|
const hasStatus = turnStatusOf(nextPort) !== null;
|
|
2092
|
+
if (hasStatus) followHadStatus.add(nextPort);
|
|
1716
2093
|
reservePx = Math.max(ownedBottomSpaceOf(nextPort), predictGrowth && (hasStatus || speedCpsRef.current > 90) ? computeFollowReserve(speedCpsRef.current, tuning.runwayPx) : 0);
|
|
1717
2094
|
if (!completionSettleGuardsPort(nextPort)) setFlowPad(nextPort, 0);
|
|
1718
2095
|
statusWasPresent = hasStatus;
|
|
@@ -1747,11 +2124,11 @@ window.__ModuleLoader__.load({
|
|
|
1747
2124
|
updateRevealScale(nextPort, elapsedMs);
|
|
1748
2125
|
reportFollow(nextPort, activeRef.current);
|
|
1749
2126
|
const runwayOffset = runwayOffsetOf(nextPort);
|
|
1750
|
-
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
|
|
2127
|
+
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) + entranceClampLagPx() <= .25) finishEntrance();
|
|
1751
2128
|
} else finishEntrance();
|
|
1752
2129
|
} else finishEntrance();
|
|
1753
2130
|
primed = true;
|
|
1754
|
-
return;
|
|
2131
|
+
return activeRef.current;
|
|
1755
2132
|
}
|
|
1756
2133
|
if (!following && (!interacting || readerReleased && !readerGestureIntent && reportedLag <= 1) && reportedLag <= (readerReleased ? 1 : 25)) {
|
|
1757
2134
|
following = true;
|
|
@@ -1777,22 +2154,23 @@ window.__ModuleLoader__.load({
|
|
|
1777
2154
|
if (!activeRef.current || !following) {
|
|
1778
2155
|
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
1779
2156
|
reportFollow(nextPort, activeRef.current);
|
|
1780
|
-
return;
|
|
2157
|
+
return activeRef.current;
|
|
1781
2158
|
}
|
|
1782
2159
|
detectHostScroll(nextPort, floor);
|
|
1783
2160
|
if (hostOwnsScroll) {
|
|
1784
2161
|
followScrollLedgers.set(nextPort, nextPort.scrollTop);
|
|
1785
2162
|
reportFollow(nextPort, false);
|
|
1786
|
-
return;
|
|
2163
|
+
return false;
|
|
1787
2164
|
}
|
|
1788
2165
|
hold(nextPort);
|
|
1789
2166
|
if (!isLeader(nextPort)) {
|
|
1790
2167
|
finishEntrance();
|
|
1791
|
-
return;
|
|
2168
|
+
return true;
|
|
1792
2169
|
}
|
|
1793
2170
|
const predictGrowth = predictiveRef?.current ?? predictive;
|
|
1794
2171
|
const statusElement = turnStatusOf(nextPort);
|
|
1795
2172
|
const hasStatus = statusElement !== null;
|
|
2173
|
+
if (hasStatus) followHadStatus.add(nextPort);
|
|
1796
2174
|
if (statusElement !== null) lastStatusHeightPx = statusElement.offsetHeight;
|
|
1797
2175
|
const statusJustRemoved = predictGrowth && statusWasPresent === true && !hasStatus;
|
|
1798
2176
|
if (statusJustRemoved) reservePx = Math.max(reservePx, tuning.runwayPx) + lastStatusHeightPx;
|
|
@@ -1802,10 +2180,25 @@ window.__ModuleLoader__.load({
|
|
|
1802
2180
|
const effectiveReserveTarget = Math.max(reservePx, pressureReserveTarget);
|
|
1803
2181
|
const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
|
|
1804
2182
|
reservePx += (effectiveReserveTarget - reservePx) * reserveStep;
|
|
1805
|
-
if (
|
|
2183
|
+
if (refreshTerminalRevealLedger().drain) {
|
|
2184
|
+
if (!followTerminalPhases.has(nextPort)) {
|
|
2185
|
+
const seeded = Math.min(72, Math.max(runwayOffsetOf(nextPort), 0) + Math.max(0, lastCommitGrowthPx));
|
|
2186
|
+
followTerminalPhases.set(nextPort, "terminal-drain");
|
|
2187
|
+
followTerminalBudgets.set(nextPort, seeded);
|
|
2188
|
+
followTrace("terminal-drain", {
|
|
2189
|
+
budget: Math.round(seeded),
|
|
2190
|
+
reserve: Math.round(reservePx),
|
|
2191
|
+
backlog: followRemainingRevealChars
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const terminalBudget = followTerminalPhases.has(nextPort) ? Math.max(Math.min(followTerminalBudgets.get(nextPort) ?? 72, 72), reservePx) : 72;
|
|
2196
|
+
if (predictGrowth || runwayOffsetOf(nextPort) > .5) ensureRunway(nextPort, shiftSurfacesOf(nextPort), terminalBudget);
|
|
1806
2197
|
const runwayOffset = runwayOffsetOf(nextPort);
|
|
1807
2198
|
const contentHeight = nextPort.scrollHeight;
|
|
1808
2199
|
const floorNow = Math.max(0, contentHeight - nextPort.clientHeight);
|
|
2200
|
+
lastCommitGrowthPx = lastObservedContentHeight > 0 ? Math.max(0, contentHeight - lastObservedContentHeight) : 0;
|
|
2201
|
+
lastObservedContentHeight = contentHeight;
|
|
1809
2202
|
const trajectoryActive = predictive && root.querySelector("[data-variant=\"think\"]") === null && runwayOffset > 0;
|
|
1810
2203
|
let trajectoryShift;
|
|
1811
2204
|
if (trajectoryActive) {
|
|
@@ -1863,18 +2256,38 @@ window.__ModuleLoader__.load({
|
|
|
1863
2256
|
trajectoryPositionPx = null;
|
|
1864
2257
|
trajectoryWasActive = false;
|
|
1865
2258
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2259
|
+
if (entranceClamp !== null) {
|
|
2260
|
+
const clamp = entranceClamp;
|
|
2261
|
+
const remainingPx = Math.max(0, clamp.extent - clamp.revealedPx);
|
|
2262
|
+
const clampStep = computeFollowStep(dt, {
|
|
2263
|
+
lag: remainingPx,
|
|
2264
|
+
speedEma: speedCpsRef.current,
|
|
2265
|
+
velocityPxPerSec
|
|
2266
|
+
}, tuning);
|
|
2267
|
+
if (remainingPx <= .1) {
|
|
2268
|
+
clamp.revealedPx = clamp.extent;
|
|
2269
|
+
clamp.element.style.maxHeight = `${clamp.basePx + clamp.extent}px`;
|
|
2270
|
+
velocityPxPerSec = 0;
|
|
2271
|
+
} else {
|
|
2272
|
+
clamp.revealedPx = Math.min(clamp.extent, clamp.revealedPx + clampStep.advancePx);
|
|
2273
|
+
clamp.element.style.maxHeight = `${clamp.basePx + clamp.revealedPx}px`;
|
|
2274
|
+
velocityPxPerSec = clampStep.velocityPxPerSec;
|
|
2275
|
+
}
|
|
1873
2276
|
animatedH = contentHeight - runwayOffset;
|
|
1874
|
-
velocityPxPerSec = 0;
|
|
1875
2277
|
} else {
|
|
1876
|
-
|
|
1877
|
-
|
|
2278
|
+
const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
|
|
2279
|
+
const step = computeFollowStep(dt, {
|
|
2280
|
+
lag,
|
|
2281
|
+
speedEma: speedCpsRef.current,
|
|
2282
|
+
velocityPxPerSec
|
|
2283
|
+
}, tuning);
|
|
2284
|
+
if (lag <= .1) {
|
|
2285
|
+
animatedH = contentHeight - runwayOffset;
|
|
2286
|
+
velocityPxPerSec = 0;
|
|
2287
|
+
} else {
|
|
2288
|
+
animatedH = Math.min(contentHeight - runwayOffset, animatedH + step.advancePx);
|
|
2289
|
+
velocityPxPerSec = step.velocityPxPerSec;
|
|
2290
|
+
}
|
|
1878
2291
|
}
|
|
1879
2292
|
}
|
|
1880
2293
|
animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx, Number.POSITIVE_INFINITY, !predictGrowth, trajectoryShift, elapsedMs, !hostOwnsScroll);
|
|
@@ -1885,12 +2298,15 @@ window.__ModuleLoader__.load({
|
|
|
1885
2298
|
if (!activeRef.current) enforceReadingAnchor(nextPort);
|
|
1886
2299
|
else measureReadingAnchor(nextPort);
|
|
1887
2300
|
}
|
|
1888
|
-
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
|
|
2301
|
+
if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) + entranceClampLagPx() <= .25) finishEntrance();
|
|
2302
|
+
return true;
|
|
1889
2303
|
};
|
|
2304
|
+
frameTaskRef.id = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
|
|
1890
2305
|
frame(performance.now());
|
|
1891
2306
|
return () => {
|
|
2307
|
+
releaseEntranceClamp();
|
|
1892
2308
|
if (!controlScrollRef.current) {
|
|
1893
|
-
|
|
2309
|
+
stopFollowTask();
|
|
1894
2310
|
if (port !== null) followActivePorts.delete(port);
|
|
1895
2311
|
unsubscribeCommit?.();
|
|
1896
2312
|
resize?.disconnect();
|
|
@@ -1906,7 +2322,7 @@ window.__ModuleLoader__.load({
|
|
|
1906
2322
|
releaseRevealScale();
|
|
1907
2323
|
return;
|
|
1908
2324
|
}
|
|
1909
|
-
|
|
2325
|
+
stopFollowTask();
|
|
1910
2326
|
if (port !== null) followActivePorts.delete(port);
|
|
1911
2327
|
unsubscribeCommit?.();
|
|
1912
2328
|
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
@@ -1944,6 +2360,18 @@ window.__ModuleLoader__.load({
|
|
|
1944
2360
|
st: Math.round(host.scrollTop),
|
|
1945
2361
|
pad: Math.round(flowPadOf(host))
|
|
1946
2362
|
});
|
|
2363
|
+
const stableTail = turnStatusOf(host) === null && followTerminalPhases.get(host) !== "host-cascade" && !followHadStatus.has(host);
|
|
2364
|
+
const ownedRunway = runwayOffsetOf(host);
|
|
2365
|
+
if (stableTail && (ownedRunway > .25 || flowPadOf(host) > .25)) {
|
|
2366
|
+
restoreRunway(host);
|
|
2367
|
+
setFlowPad(host, 0);
|
|
2368
|
+
finishAtNaturalFloor(host, !startedAsEntrance, true);
|
|
2369
|
+
followLeaders.delete(host);
|
|
2370
|
+
followCompletionSettle.delete(host);
|
|
2371
|
+
releaseRevealScale();
|
|
2372
|
+
debugRuntime.reportFollow(host, null);
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
1947
2375
|
restoreRunway(host);
|
|
1948
2376
|
setFlowPad(host, 0);
|
|
1949
2377
|
finishAtNaturalFloor(host, !startedAsEntrance, true);
|
|
@@ -1965,6 +2393,9 @@ window.__ModuleLoader__.load({
|
|
|
1965
2393
|
return;
|
|
1966
2394
|
}
|
|
1967
2395
|
followTraceUntilMs = performance.now() + 15e3;
|
|
2396
|
+
followTerminalPhases.set(host, statusWasPresent === true || turnStatusOf(host) !== null || followHadStatus.has(host) ? "host-cascade" : "terminal-drain");
|
|
2397
|
+
if (!followTerminalBudgets.has(host)) followTerminalBudgets.set(host, Math.min(72, Math.max(0, reservePx)));
|
|
2398
|
+
followCompletionGrowthCredit.set(host, Math.max(0, environmentCommitGrowthPx(host)));
|
|
1968
2399
|
if (pruneDeadRunway(host)) {
|
|
1969
2400
|
followTrace("cleanup-dead-margin", {
|
|
1970
2401
|
sh: host.scrollHeight,
|
|
@@ -1991,7 +2422,14 @@ window.__ModuleLoader__.load({
|
|
|
1991
2422
|
return;
|
|
1992
2423
|
}
|
|
1993
2424
|
for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
|
|
2425
|
+
const settleTaskRef = { id: null };
|
|
2426
|
+
const stopSettleTask = () => {
|
|
2427
|
+
if (settleTaskRef.id === null) return;
|
|
2428
|
+
coordinator.unregisterTask(settleTaskRef.id);
|
|
2429
|
+
settleTaskRef.id = null;
|
|
2430
|
+
};
|
|
1994
2431
|
const stopSettleListeners = () => {
|
|
2432
|
+
stopSettleTask();
|
|
1995
2433
|
for (const name of GESTURE_EVENTS) host.removeEventListener(name, markGesture);
|
|
1996
2434
|
resize?.disconnect();
|
|
1997
2435
|
mutations?.disconnect();
|
|
@@ -2025,7 +2463,7 @@ window.__ModuleLoader__.load({
|
|
|
2025
2463
|
const settleFrame = (now) => {
|
|
2026
2464
|
if (!isLeader(host)) {
|
|
2027
2465
|
stopSettleListeners();
|
|
2028
|
-
return;
|
|
2466
|
+
return false;
|
|
2029
2467
|
}
|
|
2030
2468
|
if (interacting && (readerGestureIntent || readerScrolledUp(host))) {
|
|
2031
2469
|
readerGestureIntent = false;
|
|
@@ -2036,7 +2474,7 @@ window.__ModuleLoader__.load({
|
|
|
2036
2474
|
releaseRevealScale();
|
|
2037
2475
|
debugRuntime.reportFollow(host, null);
|
|
2038
2476
|
stopSettleListeners();
|
|
2039
|
-
return;
|
|
2477
|
+
return false;
|
|
2040
2478
|
}
|
|
2041
2479
|
const dt = Math.min(32, Math.max(0, now - settleLast));
|
|
2042
2480
|
const tuning = debugRuntime.activeTuning();
|
|
@@ -2051,17 +2489,22 @@ window.__ModuleLoader__.load({
|
|
|
2051
2489
|
releaseRevealScale();
|
|
2052
2490
|
debugRuntime.reportFollow(host, null);
|
|
2053
2491
|
stopSettleListeners();
|
|
2054
|
-
return;
|
|
2492
|
+
return false;
|
|
2055
2493
|
}
|
|
2056
2494
|
const guardDelta = !settleRetiring && !followActivePorts.has(host) ? enforceReadingAnchor(host, true) : null;
|
|
2057
2495
|
settleQuietMs = !settleRetiring && (guardDelta === null || Math.abs(guardDelta) <= .5) ? settleQuietMs + dt : 0;
|
|
2496
|
+
if (followTerminalPhases.get(host) !== "host-cascade") followTerminalPhases.set(host, turnStatusOf(host) === null ? "terminal-drain" : "host-cascade");
|
|
2058
2497
|
const settleStatus = turnStatusOf(host);
|
|
2059
2498
|
const ownedRunwayPx = runwayOffsetOf(host);
|
|
2060
2499
|
if (ownedRunwayPx > .25) {
|
|
2500
|
+
const terminalDrain = followTerminalPhases.get(host) === "terminal-drain";
|
|
2501
|
+
const earnedBudgetPx = Math.min(72, Math.max(followTerminalBudgets.get(host) ?? 72, reservePx));
|
|
2061
2502
|
const requestedTransferPx = settleStatus === null ? ownedRunwayPx : Math.min(reservePx, (tuning.runwayPx || 72) / FOLLOW_RUNWAY_RETIRE_MS * dt);
|
|
2062
|
-
const
|
|
2503
|
+
const cappedTransferPx = terminalDrain ? Math.min(requestedTransferPx, Math.max(0, ownedRunwayPx - earnedBudgetPx)) : requestedTransferPx;
|
|
2504
|
+
const transferredPx = transferRunwayToFlowPad(host, cappedTransferPx);
|
|
2063
2505
|
if (transferredPx > 0) setFlowPad(host, Math.max(0, flowPadOf(host) - transferredPx));
|
|
2064
2506
|
reservePx = Math.max(0, reservePx - transferredPx);
|
|
2507
|
+
followTerminalBudgets.set(host, Math.max(reservePx, Math.min(earnedBudgetPx, runwayOffsetOf(host))));
|
|
2065
2508
|
animatedH += transferredPx;
|
|
2066
2509
|
}
|
|
2067
2510
|
const runwayOffset = runwayOffsetOf(host);
|
|
@@ -2095,7 +2538,7 @@ window.__ModuleLoader__.load({
|
|
|
2095
2538
|
releaseRevealScale();
|
|
2096
2539
|
debugRuntime.reportFollow(host, null);
|
|
2097
2540
|
stopSettleListeners();
|
|
2098
|
-
return;
|
|
2541
|
+
return false;
|
|
2099
2542
|
}
|
|
2100
2543
|
const step = computeFollowStep(dt, {
|
|
2101
2544
|
lag,
|
|
@@ -2121,9 +2564,9 @@ window.__ModuleLoader__.load({
|
|
|
2121
2564
|
}
|
|
2122
2565
|
}
|
|
2123
2566
|
reportFollow(host, false);
|
|
2124
|
-
|
|
2567
|
+
return true;
|
|
2125
2568
|
};
|
|
2126
|
-
|
|
2569
|
+
settleTaskRef.id = coordinator.registerTask({ onSimulate: (_dtMs, now) => settleFrame(now) });
|
|
2127
2570
|
};
|
|
2128
2571
|
}, [
|
|
2129
2572
|
active,
|
|
@@ -2434,7 +2877,8 @@ window.__ModuleLoader__.load({
|
|
|
2434
2877
|
speedCps: revealSpeedCps,
|
|
2435
2878
|
targetChars: targetCount,
|
|
2436
2879
|
displayedChars: displayedCount,
|
|
2437
|
-
active: !producerComplete
|
|
2880
|
+
active: !producerComplete || backlog > 0,
|
|
2881
|
+
producerComplete
|
|
2438
2882
|
});
|
|
2439
2883
|
if (holdBackRef.current?.() === true) {
|
|
2440
2884
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -2550,14 +2994,12 @@ window.__ModuleLoader__.load({
|
|
|
2550
2994
|
const elementRef = (0, react.useRef)(null);
|
|
2551
2995
|
(0, react.useEffect)(() => {
|
|
2552
2996
|
if (!active) return;
|
|
2553
|
-
let rafId = 0;
|
|
2554
2997
|
let lastDebugReport = 0;
|
|
2555
2998
|
const frame = (now) => {
|
|
2556
|
-
rafId = requestAnimationFrame(frame);
|
|
2557
2999
|
const fps = fpsRef.current;
|
|
2558
3000
|
if (fps.lastMs === 0) {
|
|
2559
3001
|
fps.lastMs = now;
|
|
2560
|
-
return;
|
|
3002
|
+
return true;
|
|
2561
3003
|
}
|
|
2562
3004
|
const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs));
|
|
2563
3005
|
fps.lastMs = now;
|
|
@@ -2574,10 +3016,12 @@ window.__ModuleLoader__.load({
|
|
|
2574
3016
|
debugRuntime.reportFps(currentFps, fps.emaMs, fps.degraded);
|
|
2575
3017
|
lastDebugReport = now;
|
|
2576
3018
|
}
|
|
3019
|
+
return true;
|
|
2577
3020
|
};
|
|
2578
|
-
|
|
3021
|
+
const coordinator = FrameCoordinator.forDocument();
|
|
3022
|
+
const taskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
|
|
2579
3023
|
return () => {
|
|
2580
|
-
|
|
3024
|
+
coordinator.unregisterTask(taskId);
|
|
2581
3025
|
fpsRef.current = {
|
|
2582
3026
|
emaMs: 0,
|
|
2583
3027
|
lastMs: 0,
|
|
@@ -2607,8 +3051,8 @@ window.__ModuleLoader__.load({
|
|
|
2607
3051
|
};
|
|
2608
3052
|
}
|
|
2609
3053
|
//#endregion
|
|
2610
|
-
//#region \0dsh-css:/Users/dzlin/
|
|
2611
|
-
const css$3 = ".
|
|
3054
|
+
//#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/LogarithmicFade.module.css.mjs
|
|
3055
|
+
const css$3 = ".Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-0),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-0){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 0.0%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-1),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-1){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 3.22581%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-2),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-2){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 6.45161%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-3),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-3){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 9.67742%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-4),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-4){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 12.9032%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-5),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-5){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 16.129%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-6),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-6){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 19.3548%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-7),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-7){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 22.5807%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-8),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-8){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 25.8065%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-9),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-9){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 29.0323%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-10),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-10){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 32.2581%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-11),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-11){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 35.4839%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-12),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-12){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 38.7097%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-13),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-13){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 41.9355%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-14),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-14){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 45.1613%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-15),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-15){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 48.3871%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-16),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-16){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 51.6129%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-17),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-17){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 54.8387%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-18),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-18){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 58.0645%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-19),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-19){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 61.2903%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-20),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-20){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 64.5161%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-21),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-21){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 67.7419%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-22),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-22){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 70.9677%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-23),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-23){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 74.1936%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-24),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-24){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 77.4194%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-25),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-25){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 80.6452%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-26),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-26){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 83.871%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-27),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-27){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 87.0968%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-28),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-28){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 90.3226%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-29),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-29){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 93.5484%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-30),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-30){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 96.7742%, transparent)}.Sv_6BW_scope::highlight(Sv_6BW_dsh-smooth-stream-log-fade-31),.Sv_6BW_scope ::highlight(Sv_6BW_dsh-smooth-stream-log-fade-31){color:color-mix(in srgb, var(--dsh-smooth-stream-fade-color,currentColor) 100.0%, transparent)}";
|
|
2612
3056
|
const tagId$3 = "dsh-smooth-stream/LogarithmicFade.module.css";
|
|
2613
3057
|
if (typeof document !== "undefined") {
|
|
2614
3058
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]");
|
|
@@ -2621,45 +3065,59 @@ window.__ModuleLoader__.load({
|
|
|
2621
3065
|
tag.textContent = css$3;
|
|
2622
3066
|
}
|
|
2623
3067
|
var LogarithmicFade_module_css_default = {
|
|
2624
|
-
"dsh-smooth-stream-log-fade-
|
|
2625
|
-
"dsh-smooth-stream-log-fade-
|
|
2626
|
-
"dsh-smooth-stream-log-fade-
|
|
2627
|
-
"dsh-smooth-stream-log-fade-
|
|
2628
|
-
"dsh-smooth-stream-log-fade-
|
|
2629
|
-
"dsh-smooth-stream-log-fade-
|
|
2630
|
-
"dsh-smooth-stream-log-fade-
|
|
2631
|
-
"dsh-smooth-stream-log-fade-
|
|
2632
|
-
"dsh-smooth-stream-log-fade-
|
|
2633
|
-
"dsh-smooth-stream-log-fade-
|
|
2634
|
-
"
|
|
2635
|
-
"dsh-smooth-stream-log-fade-
|
|
2636
|
-
"dsh-smooth-stream-log-fade-
|
|
2637
|
-
"dsh-smooth-stream-log-fade-
|
|
2638
|
-
"dsh-smooth-stream-log-fade-
|
|
2639
|
-
"dsh-smooth-stream-log-fade-
|
|
2640
|
-
"dsh-smooth-stream-log-fade-
|
|
2641
|
-
"dsh-smooth-stream-log-fade-
|
|
2642
|
-
"dsh-smooth-stream-log-fade-
|
|
2643
|
-
"dsh-smooth-stream-log-fade-
|
|
2644
|
-
"dsh-smooth-stream-log-fade-
|
|
2645
|
-
"dsh-smooth-stream-log-fade-
|
|
2646
|
-
"dsh-smooth-stream-log-fade-
|
|
2647
|
-
"dsh-smooth-stream-log-fade-
|
|
2648
|
-
"dsh-smooth-stream-log-fade-
|
|
2649
|
-
"dsh-smooth-stream-log-fade-
|
|
2650
|
-
"dsh-smooth-stream-log-fade-
|
|
2651
|
-
"dsh-smooth-stream-log-fade-
|
|
2652
|
-
"
|
|
2653
|
-
"dsh-smooth-stream-log-fade-
|
|
2654
|
-
"dsh-smooth-stream-log-fade-
|
|
2655
|
-
"dsh-smooth-stream-log-fade-
|
|
2656
|
-
"dsh-smooth-stream-log-fade-
|
|
3068
|
+
"dsh-smooth-stream-log-fade-30": "Sv_6BW_dsh-smooth-stream-log-fade-30",
|
|
3069
|
+
"dsh-smooth-stream-log-fade-26": "Sv_6BW_dsh-smooth-stream-log-fade-26",
|
|
3070
|
+
"dsh-smooth-stream-log-fade-3": "Sv_6BW_dsh-smooth-stream-log-fade-3",
|
|
3071
|
+
"dsh-smooth-stream-log-fade-14": "Sv_6BW_dsh-smooth-stream-log-fade-14",
|
|
3072
|
+
"dsh-smooth-stream-log-fade-28": "Sv_6BW_dsh-smooth-stream-log-fade-28",
|
|
3073
|
+
"dsh-smooth-stream-log-fade-6": "Sv_6BW_dsh-smooth-stream-log-fade-6",
|
|
3074
|
+
"dsh-smooth-stream-log-fade-7": "Sv_6BW_dsh-smooth-stream-log-fade-7",
|
|
3075
|
+
"dsh-smooth-stream-log-fade-24": "Sv_6BW_dsh-smooth-stream-log-fade-24",
|
|
3076
|
+
"dsh-smooth-stream-log-fade-12": "Sv_6BW_dsh-smooth-stream-log-fade-12",
|
|
3077
|
+
"dsh-smooth-stream-log-fade-16": "Sv_6BW_dsh-smooth-stream-log-fade-16",
|
|
3078
|
+
"scope": "Sv_6BW_scope",
|
|
3079
|
+
"dsh-smooth-stream-log-fade-0": "Sv_6BW_dsh-smooth-stream-log-fade-0",
|
|
3080
|
+
"dsh-smooth-stream-log-fade-8": "Sv_6BW_dsh-smooth-stream-log-fade-8",
|
|
3081
|
+
"dsh-smooth-stream-log-fade-10": "Sv_6BW_dsh-smooth-stream-log-fade-10",
|
|
3082
|
+
"dsh-smooth-stream-log-fade-5": "Sv_6BW_dsh-smooth-stream-log-fade-5",
|
|
3083
|
+
"dsh-smooth-stream-log-fade-11": "Sv_6BW_dsh-smooth-stream-log-fade-11",
|
|
3084
|
+
"dsh-smooth-stream-log-fade-1": "Sv_6BW_dsh-smooth-stream-log-fade-1",
|
|
3085
|
+
"dsh-smooth-stream-log-fade-9": "Sv_6BW_dsh-smooth-stream-log-fade-9",
|
|
3086
|
+
"dsh-smooth-stream-log-fade-21": "Sv_6BW_dsh-smooth-stream-log-fade-21",
|
|
3087
|
+
"dsh-smooth-stream-log-fade-31": "Sv_6BW_dsh-smooth-stream-log-fade-31",
|
|
3088
|
+
"dsh-smooth-stream-log-fade-20": "Sv_6BW_dsh-smooth-stream-log-fade-20",
|
|
3089
|
+
"dsh-smooth-stream-log-fade-15": "Sv_6BW_dsh-smooth-stream-log-fade-15",
|
|
3090
|
+
"dsh-smooth-stream-log-fade-25": "Sv_6BW_dsh-smooth-stream-log-fade-25",
|
|
3091
|
+
"dsh-smooth-stream-log-fade-18": "Sv_6BW_dsh-smooth-stream-log-fade-18",
|
|
3092
|
+
"dsh-smooth-stream-log-fade-4": "Sv_6BW_dsh-smooth-stream-log-fade-4",
|
|
3093
|
+
"dsh-smooth-stream-log-fade-23": "Sv_6BW_dsh-smooth-stream-log-fade-23",
|
|
3094
|
+
"dsh-smooth-stream-log-fade-27": "Sv_6BW_dsh-smooth-stream-log-fade-27",
|
|
3095
|
+
"dsh-smooth-stream-log-fade-13": "Sv_6BW_dsh-smooth-stream-log-fade-13",
|
|
3096
|
+
"dsh-smooth-stream-log-fade-17": "Sv_6BW_dsh-smooth-stream-log-fade-17",
|
|
3097
|
+
"dsh-smooth-stream-log-fade-22": "Sv_6BW_dsh-smooth-stream-log-fade-22",
|
|
3098
|
+
"dsh-smooth-stream-log-fade-2": "Sv_6BW_dsh-smooth-stream-log-fade-2",
|
|
3099
|
+
"dsh-smooth-stream-log-fade-29": "Sv_6BW_dsh-smooth-stream-log-fade-29",
|
|
3100
|
+
"dsh-smooth-stream-log-fade-19": "Sv_6BW_dsh-smooth-stream-log-fade-19"
|
|
2657
3101
|
};
|
|
2658
3102
|
const FADE_STEPS = 32;
|
|
2659
3103
|
const PREFIX = "dsh-smooth-stream-log-fade-";
|
|
2660
3104
|
const COLOR_PROPERTY = "--dsh-smooth-stream-fade-color";
|
|
2661
3105
|
const highlightName = (index) => LogarithmicFade_module_css_default[`${PREFIX}${index}`] ?? `${PREFIX}${index}`;
|
|
2662
3106
|
const EXCLUDED = "pre,code,math,.katex,.katex-display,mjx-container,svg,script,style,textarea,input,button,select,[role=\"button\"],[contenteditable],[hidden],[aria-hidden=\"true\"],[aria-live]";
|
|
3107
|
+
const EXCLUDED_ATTRIBUTES = [
|
|
3108
|
+
"hidden",
|
|
3109
|
+
"aria-hidden",
|
|
3110
|
+
"role",
|
|
3111
|
+
"contenteditable",
|
|
3112
|
+
"class"
|
|
3113
|
+
];
|
|
3114
|
+
const APPEARANCE_ATTRIBUTES = [
|
|
3115
|
+
"class",
|
|
3116
|
+
"style",
|
|
3117
|
+
"data-theme",
|
|
3118
|
+
"data-color-scheme",
|
|
3119
|
+
"data-appearance"
|
|
3120
|
+
];
|
|
2663
3121
|
function logarithmicOpacity(progress) {
|
|
2664
3122
|
return 0 + 1 * (1 - Math.log1p(5 * (1 - (Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : 1))) / Math.log(6));
|
|
2665
3123
|
}
|
|
@@ -2668,13 +3126,75 @@ window.__ModuleLoader__.load({
|
|
|
2668
3126
|
}
|
|
2669
3127
|
const schedulers = /* @__PURE__ */ new WeakMap();
|
|
2670
3128
|
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3129
|
+
/**
|
|
3130
|
+
* Hot-path counters for the streaming benchmark. Increments only — this is the
|
|
3131
|
+
* evidence that the per-frame pass count collapsed, not a control input.
|
|
3132
|
+
*/
|
|
3133
|
+
const fadeHotPathStats = {
|
|
3134
|
+
/** Observer batches that reached reconcile(). */
|
|
3135
|
+
reconcileCalls: 0,
|
|
3136
|
+
/** Reconciles that did real work (the rest are dirty-check no-ops). */
|
|
3137
|
+
reconcilePasses: 0,
|
|
3138
|
+
/** Full text-node table rebuilds (structural DOM change). */
|
|
3139
|
+
nodeScans: 0,
|
|
3140
|
+
/** Forced style resolutions for newly faded elements. */
|
|
3141
|
+
styleReads: 0
|
|
3142
|
+
};
|
|
2671
3143
|
function schedule(scheduler) {
|
|
2672
|
-
if (scheduler.
|
|
2673
|
-
scheduler.
|
|
2674
|
-
scheduler.frame = 0;
|
|
3144
|
+
if (scheduler.taskId !== null || scheduler.pending.size === 0) return;
|
|
3145
|
+
scheduler.taskId = scheduler.coordinator.registerTask({ onSimulate: (_dtMs, now) => {
|
|
2675
3146
|
for (const client of scheduler.pending) if (!client.paint(now)) scheduler.pending.delete(client);
|
|
2676
|
-
|
|
3147
|
+
if (scheduler.pending.size === 0) {
|
|
3148
|
+
scheduler.taskId = null;
|
|
3149
|
+
return false;
|
|
3150
|
+
}
|
|
3151
|
+
return true;
|
|
3152
|
+
} });
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Whether an attribute record actually moved the attribute's value. jsdom (and
|
|
3156
|
+
* some engines) report a write even when the serialized value is unchanged, and
|
|
3157
|
+
* this controller writes the scope class on every enable/disable transition —
|
|
3158
|
+
* counting a no-op record as a DOM change would re-enter the observer forever.
|
|
3159
|
+
* Requires `attributeOldValue` on the observing call.
|
|
3160
|
+
* @param record - attribute mutation record from either observer.
|
|
3161
|
+
* @returns true only when the value differs from the recorded old value.
|
|
3162
|
+
*/
|
|
3163
|
+
function attributeChanged(record) {
|
|
3164
|
+
const attribute = record.attributeName;
|
|
3165
|
+
if (attribute === null) return true;
|
|
3166
|
+
return record.oldValue !== record.target.getAttribute(attribute);
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* One appearance watcher per document. A theme switch arrives either as an OS
|
|
3170
|
+
* media-query flip (the app's default `preference: system`) or as an attribute
|
|
3171
|
+
* write on <html>/<body>, so both are observed; the fade only ever writes
|
|
3172
|
+
* custom properties on its own root, never on those elements, so this watcher
|
|
3173
|
+
* cannot observe the controller's own output.
|
|
3174
|
+
* @param scheduler - document-scoped registry notified when the look changes.
|
|
3175
|
+
* @returns teardown for the last client's dispose.
|
|
3176
|
+
*/
|
|
3177
|
+
function watchAppearance(scheduler) {
|
|
3178
|
+
const win = scheduler.window;
|
|
3179
|
+
const doc = win.document;
|
|
3180
|
+
const invalidate = () => {
|
|
3181
|
+
for (const client of scheduler.clients) client.invalidateColors();
|
|
3182
|
+
};
|
|
3183
|
+
const observer = new win.MutationObserver((records) => {
|
|
3184
|
+
if (records.some(attributeChanged)) invalidate();
|
|
2677
3185
|
});
|
|
3186
|
+
const watched = doc.body === null ? [doc.documentElement] : [doc.documentElement, doc.body];
|
|
3187
|
+
for (const element of watched) observer.observe(element, {
|
|
3188
|
+
attributes: true,
|
|
3189
|
+
attributeFilter: APPEARANCE_ATTRIBUTES,
|
|
3190
|
+
attributeOldValue: true
|
|
3191
|
+
});
|
|
3192
|
+
const query = typeof win.matchMedia === "function" ? win.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
3193
|
+
if (query !== null) query.addEventListener("change", invalidate);
|
|
3194
|
+
return () => {
|
|
3195
|
+
observer.disconnect();
|
|
3196
|
+
query?.removeEventListener("change", invalidate);
|
|
3197
|
+
};
|
|
2678
3198
|
}
|
|
2679
3199
|
function schedulerFor(root) {
|
|
2680
3200
|
const doc = root.ownerDocument;
|
|
@@ -2690,15 +3210,26 @@ window.__ModuleLoader__.load({
|
|
|
2690
3210
|
highlights,
|
|
2691
3211
|
clients: /* @__PURE__ */ new Set(),
|
|
2692
3212
|
pending: /* @__PURE__ */ new Set(),
|
|
2693
|
-
|
|
3213
|
+
taskId: null,
|
|
2694
3214
|
registry: realm.CSS.highlights,
|
|
2695
|
-
window: win
|
|
3215
|
+
window: win,
|
|
3216
|
+
coordinator: FrameCoordinator.forDocument(doc),
|
|
3217
|
+
unwatchAppearance: () => {}
|
|
2696
3218
|
};
|
|
3219
|
+
scheduler.unwatchAppearance = watchAppearance(scheduler);
|
|
2697
3220
|
schedulers.set(doc, scheduler);
|
|
2698
3221
|
}
|
|
2699
3222
|
return scheduler;
|
|
2700
3223
|
}
|
|
2701
|
-
/**
|
|
3224
|
+
/**
|
|
3225
|
+
* Owns ranges only: React retains ownership of every element and Text node.
|
|
3226
|
+
*
|
|
3227
|
+
* The controller used to re-derive its whole view of the subtree (textContent,
|
|
3228
|
+
* prefix walk, full TreeWalker, an O(tail x #nodes) filter) on *every* commit
|
|
3229
|
+
* AND on every observer batch for that same commit. It now keeps an
|
|
3230
|
+
* incremental text-node table, reconciles at most once per observed DOM
|
|
3231
|
+
* change, and preserves the per-element colour memo across passes.
|
|
3232
|
+
*/
|
|
2702
3233
|
var LogarithmicFadeController = class LogarithmicFadeController {
|
|
2703
3234
|
root;
|
|
2704
3235
|
scheduler;
|
|
@@ -2711,18 +3242,46 @@ window.__ModuleLoader__.load({
|
|
|
2711
3242
|
pausedAt = null;
|
|
2712
3243
|
disposed = false;
|
|
2713
3244
|
observer;
|
|
3245
|
+
/** Text nodes of the root with their source offsets; survives data-only edits. */
|
|
3246
|
+
nodes = [];
|
|
3247
|
+
/** Ineligible nodes in nodes[0..index): a span is fadeable when its bounds match. */
|
|
3248
|
+
ineligiblePrefix = [0];
|
|
3249
|
+
/**
|
|
3250
|
+
* `closest(EXCLUDED)` derived once per element instead of once per Text node:
|
|
3251
|
+
* exclusion is inherited from the ancestor chain, so a memo stays usable only
|
|
3252
|
+
* while the element keeps its own parent. Moving a subtree into `code` — or
|
|
3253
|
+
* into anything else that matches — invalidates exactly the moved chain the
|
|
3254
|
+
* next time it is read.
|
|
3255
|
+
*/
|
|
3256
|
+
excludedElements = /* @__PURE__ */ new WeakMap();
|
|
3257
|
+
/** Own tag/attribute match per element; dropped when an exclusion attribute moves. */
|
|
3258
|
+
ownExcluded = /* @__PURE__ */ new WeakMap();
|
|
3259
|
+
/** Bumped by every observed mutation; reconcile() no-ops while it does not move. */
|
|
3260
|
+
domRevision = 0;
|
|
3261
|
+
/** Bumped only by structural (childList) mutations; the node table is rebuilt then. */
|
|
3262
|
+
structureRevision = 0;
|
|
3263
|
+
scannedStructureRevision = -1;
|
|
3264
|
+
reconciledRevision = -1;
|
|
3265
|
+
reconciledEnabled = false;
|
|
3266
|
+
reconciledActive = false;
|
|
3267
|
+
reconciledPaused = false;
|
|
3268
|
+
reconciledTailSize = 0;
|
|
3269
|
+
colorGeneration = 0;
|
|
2714
3270
|
constructor(root, scheduler) {
|
|
2715
3271
|
this.root = root;
|
|
2716
3272
|
this.scheduler = scheduler;
|
|
2717
3273
|
scheduler.clients.add(this);
|
|
2718
3274
|
const win = root.ownerDocument.defaultView;
|
|
2719
|
-
this.observer = new win.MutationObserver(() => {
|
|
2720
|
-
this.
|
|
3275
|
+
this.observer = new win.MutationObserver((records) => {
|
|
3276
|
+
this.onDomMutation(records);
|
|
2721
3277
|
});
|
|
2722
3278
|
this.observer.observe(root, {
|
|
2723
3279
|
subtree: true,
|
|
2724
3280
|
childList: true,
|
|
2725
|
-
characterData: true
|
|
3281
|
+
characterData: true,
|
|
3282
|
+
attributes: true,
|
|
3283
|
+
attributeFilter: EXCLUDED_ATTRIBUTES,
|
|
3284
|
+
attributeOldValue: true
|
|
2726
3285
|
});
|
|
2727
3286
|
}
|
|
2728
3287
|
static create(root) {
|
|
@@ -2742,80 +3301,240 @@ window.__ModuleLoader__.load({
|
|
|
2742
3301
|
this.speedCps = speedCps;
|
|
2743
3302
|
this.reconcile();
|
|
2744
3303
|
}
|
|
3304
|
+
/** Invalidate and reconcile from one observer batch (one batch per commit). */
|
|
3305
|
+
onDomMutation(records) {
|
|
3306
|
+
let changed = false;
|
|
3307
|
+
let exclusionAttributesMoved = false;
|
|
3308
|
+
for (const record of records) {
|
|
3309
|
+
if (record.type === "childList") {
|
|
3310
|
+
this.structureRevision += 1;
|
|
3311
|
+
changed = true;
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
if (record.type === "characterData") {
|
|
3315
|
+
changed = true;
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
if (!attributeChanged(record)) continue;
|
|
3319
|
+
exclusionAttributesMoved = true;
|
|
3320
|
+
changed = true;
|
|
3321
|
+
}
|
|
3322
|
+
if (!changed) return;
|
|
3323
|
+
if (exclusionAttributesMoved) {
|
|
3324
|
+
this.excludedElements = /* @__PURE__ */ new WeakMap();
|
|
3325
|
+
this.ownExcluded = /* @__PURE__ */ new WeakMap();
|
|
3326
|
+
this.structureRevision += 1;
|
|
3327
|
+
}
|
|
3328
|
+
this.domRevision += 1;
|
|
3329
|
+
this.reconcile();
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* Whether this element matches a SKIP rule itself or inherits one from an
|
|
3333
|
+
* ancestor — the memoised form of `element.closest(EXCLUDED) !== null`.
|
|
3334
|
+
* Memoised per element and revalidated through the parent chain, so a node
|
|
3335
|
+
* that moves into `code` — or whose ancestor does — is re-decided without
|
|
3336
|
+
* paying the selector walk once per Text node per pass.
|
|
3337
|
+
* @param element - element owning the Text node being classified.
|
|
3338
|
+
* @returns true when text under this element must stay out of the fade.
|
|
3339
|
+
*/
|
|
3340
|
+
elementExcluded(element) {
|
|
3341
|
+
const cached = this.excludedElements.get(element);
|
|
3342
|
+
if (cached !== void 0 && cached.parent === element.parentElement) return cached.excluded;
|
|
3343
|
+
const parent = element.parentElement;
|
|
3344
|
+
const inherited = parent === null ? false : this.elementExcluded(parent);
|
|
3345
|
+
let own = this.ownExcluded.get(element);
|
|
3346
|
+
if (own === void 0) {
|
|
3347
|
+
own = element.matches(EXCLUDED);
|
|
3348
|
+
this.ownExcluded.set(element, own);
|
|
3349
|
+
}
|
|
3350
|
+
const excluded = inherited || own;
|
|
3351
|
+
this.excludedElements.set(element, {
|
|
3352
|
+
parent,
|
|
3353
|
+
excluded
|
|
3354
|
+
});
|
|
3355
|
+
return excluded;
|
|
3356
|
+
}
|
|
3357
|
+
/** Rebuild the text-node table after a structural change. */
|
|
3358
|
+
scanNodes() {
|
|
3359
|
+
const nodes = [];
|
|
3360
|
+
const walker = this.root.ownerDocument.createTreeWalker(this.root, NodeFilter.SHOW_TEXT);
|
|
3361
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
3362
|
+
const text = node;
|
|
3363
|
+
const parent = text.parentElement;
|
|
3364
|
+
const eligible = parent !== null && !this.elementExcluded(parent);
|
|
3365
|
+
nodes.push({
|
|
3366
|
+
node: text,
|
|
3367
|
+
start: 0,
|
|
3368
|
+
end: 0,
|
|
3369
|
+
eligible
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
this.nodes = nodes;
|
|
3373
|
+
this.scannedStructureRevision = this.structureRevision;
|
|
3374
|
+
fadeHotPathStats.nodeScans += 1;
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Re-derive the source text, the per-node spans and the eligibility prefix
|
|
3378
|
+
* sums from the cached table. No DOM read, no selector match, no allocation
|
|
3379
|
+
* beyond the string itself.
|
|
3380
|
+
*/
|
|
3381
|
+
readText() {
|
|
3382
|
+
if (this.scannedStructureRevision !== this.structureRevision) this.scanNodes();
|
|
3383
|
+
for (const entry of this.nodes) {
|
|
3384
|
+
if (entry.node.parentNode !== null) continue;
|
|
3385
|
+
this.scanNodes();
|
|
3386
|
+
break;
|
|
3387
|
+
}
|
|
3388
|
+
const nodes = this.nodes;
|
|
3389
|
+
const prefix = this.ineligiblePrefix;
|
|
3390
|
+
if (prefix.length < nodes.length + 1) prefix.length = nodes.length + 1;
|
|
3391
|
+
prefix[0] = 0;
|
|
3392
|
+
let text = "";
|
|
3393
|
+
let offset = 0;
|
|
3394
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
3395
|
+
const entry = nodes[index];
|
|
3396
|
+
const data = entry.node.data;
|
|
3397
|
+
entry.start = offset;
|
|
3398
|
+
offset += data.length;
|
|
3399
|
+
entry.end = offset;
|
|
3400
|
+
text += data;
|
|
3401
|
+
prefix[index + 1] = prefix[index] + (entry.eligible ? 0 : 1);
|
|
3402
|
+
}
|
|
3403
|
+
prefix.length = nodes.length + 1;
|
|
3404
|
+
this.ineligiblePrefix = prefix;
|
|
3405
|
+
return text;
|
|
3406
|
+
}
|
|
3407
|
+
/** First cached node whose span ends after `offset` (binary search). */
|
|
3408
|
+
firstNodeEndingAfter(offset) {
|
|
3409
|
+
const nodes = this.nodes;
|
|
3410
|
+
let low = 0;
|
|
3411
|
+
let high = nodes.length;
|
|
3412
|
+
while (low < high) {
|
|
3413
|
+
const mid = low + high >> 1;
|
|
3414
|
+
if (nodes[mid].end > offset) high = mid;
|
|
3415
|
+
else low = mid + 1;
|
|
3416
|
+
}
|
|
3417
|
+
return low;
|
|
3418
|
+
}
|
|
3419
|
+
/** Last cached node whose span starts before `offset` (binary search). */
|
|
3420
|
+
lastNodeStartingBefore(offset) {
|
|
3421
|
+
const nodes = this.nodes;
|
|
3422
|
+
let low = -1;
|
|
3423
|
+
let high = nodes.length - 1;
|
|
3424
|
+
while (low < high) {
|
|
3425
|
+
const mid = low + high + 1 >> 1;
|
|
3426
|
+
if (nodes[mid].start < offset) low = mid;
|
|
3427
|
+
else high = mid - 1;
|
|
3428
|
+
}
|
|
3429
|
+
return low;
|
|
3430
|
+
}
|
|
2745
3431
|
clearRanges() {
|
|
2746
3432
|
for (const character of this.characters) this.scheduler.highlights[character.bucket]?.delete(character.range);
|
|
2747
3433
|
this.characters = [];
|
|
2748
|
-
|
|
3434
|
+
}
|
|
3435
|
+
restoreColor(element, preserved) {
|
|
3436
|
+
if (preserved.value === "") element.style.removeProperty(COLOR_PROPERTY);
|
|
3437
|
+
else element.style.setProperty(COLOR_PROPERTY, preserved.value, preserved.priority);
|
|
3438
|
+
}
|
|
3439
|
+
rootColorSet = false;
|
|
3440
|
+
ensureRootColor() {
|
|
3441
|
+
if (this.rootColorSet) return;
|
|
3442
|
+
const color = this.scheduler.window.getComputedStyle(this.root).color || "currentColor";
|
|
3443
|
+
this.root.style.setProperty(COLOR_PROPERTY, color);
|
|
3444
|
+
this.rootColorSet = true;
|
|
2749
3445
|
}
|
|
2750
3446
|
restoreColors() {
|
|
2751
|
-
|
|
2752
|
-
|
|
3447
|
+
if (this.rootColorSet) {
|
|
3448
|
+
this.root.style.removeProperty(COLOR_PROPERTY);
|
|
3449
|
+
this.rootColorSet = false;
|
|
3450
|
+
}
|
|
3451
|
+
for (const [element, preserved] of this.colors) this.restoreColor(element, preserved);
|
|
2753
3452
|
this.colors.clear();
|
|
2754
3453
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
3454
|
+
/**
|
|
3455
|
+
* The document's appearance moved (theme switch, restyle): the captured ink
|
|
3456
|
+
* colour is stale, so drop it and let the next pass read the new one. Costs
|
|
3457
|
+
* nothing while no colour is captured, which is the common case — the read
|
|
3458
|
+
* still happens only when text is actually fading.
|
|
3459
|
+
*/
|
|
3460
|
+
invalidateColors() {
|
|
3461
|
+
if (this.disposed || !this.rootColorSet && this.colors.size === 0) return;
|
|
3462
|
+
this.restoreColors();
|
|
3463
|
+
this.domRevision += 1;
|
|
3464
|
+
this.reconcile();
|
|
2763
3465
|
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Refresh the fade ranges. Two triggers used to run this 2-3x per frame with
|
|
3468
|
+
* no dirty check: the commit-driven `update()` and the observer batch for the
|
|
3469
|
+
* very same DOM write. Both converge here, and the pass is skipped unless the
|
|
3470
|
+
* DOM revision, the gate, the fade window or the pause state actually moved.
|
|
3471
|
+
*/
|
|
2764
3472
|
reconcile() {
|
|
2765
3473
|
if (this.disposed) return;
|
|
2766
|
-
|
|
3474
|
+
fadeHotPathStats.reconcileCalls += 1;
|
|
3475
|
+
const tailSize = fadeTailSize(this.speedCps);
|
|
3476
|
+
const paused = this.pausedAt !== null;
|
|
3477
|
+
const domChanged = this.domRevision !== this.reconciledRevision;
|
|
3478
|
+
const paramsChanged = this.enabled !== this.reconciledEnabled || this.active !== this.reconciledActive || paused !== this.reconciledPaused || tailSize !== this.reconciledTailSize && this.characters.length > 0;
|
|
3479
|
+
if (!domChanged && !paramsChanged) return;
|
|
3480
|
+
fadeHotPathStats.reconcilePasses += 1;
|
|
3481
|
+
this.reconciledRevision = this.domRevision;
|
|
3482
|
+
this.reconciledEnabled = this.enabled;
|
|
3483
|
+
this.reconciledActive = this.active;
|
|
3484
|
+
this.reconciledPaused = paused;
|
|
3485
|
+
this.reconciledTailSize = tailSize;
|
|
3486
|
+
const text = this.readText();
|
|
2767
3487
|
const previous = this.previous;
|
|
2768
3488
|
this.previous = text;
|
|
2769
3489
|
const old = this.characters;
|
|
2770
3490
|
this.clearRanges();
|
|
2771
3491
|
if (!this.enabled) {
|
|
2772
3492
|
this.root.classList.remove(LogarithmicFade_module_css_default.scope);
|
|
3493
|
+
this.restoreColors();
|
|
2773
3494
|
this.scheduler.pending.delete(this);
|
|
2774
3495
|
this.stopIfIdle();
|
|
2775
3496
|
return;
|
|
2776
3497
|
}
|
|
2777
3498
|
this.root.classList.add(LogarithmicFade_module_css_default.scope);
|
|
3499
|
+
this.ensureRootColor();
|
|
2778
3500
|
const now = this.pausedAt ?? this.scheduler.window.performance.now();
|
|
2779
|
-
let prefix = 0;
|
|
2780
|
-
while (prefix < previous.length && prefix < text.length && previous[prefix] === text[prefix]) prefix += 1;
|
|
2781
3501
|
const appended = text.startsWith(previous);
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const end = offset + (node.textContent?.length ?? 0);
|
|
2787
|
-
nodes.push({
|
|
2788
|
-
node,
|
|
2789
|
-
start: offset,
|
|
2790
|
-
end,
|
|
2791
|
-
eligible: node.parentElement?.closest(EXCLUDED) === null
|
|
2792
|
-
});
|
|
2793
|
-
offset = end;
|
|
2794
|
-
}
|
|
2795
|
-
const segments = segmenter.segment(text);
|
|
2796
|
-
const tailSize = fadeTailSize(this.speedCps);
|
|
3502
|
+
let prefix = appended ? previous.length : 0;
|
|
3503
|
+
if (!appended) while (prefix < previous.length && prefix < text.length && previous[prefix] === text[prefix]) prefix += 1;
|
|
3504
|
+
const nodes = this.nodes;
|
|
3505
|
+
const ineligible = this.ineligiblePrefix;
|
|
2797
3506
|
const oldestLiveStart = old.reduce((start, character) => now - character.born < 240 && character.end <= prefix ? Math.min(start, character.start) : start, Infinity);
|
|
3507
|
+
const retainedBySpan = /* @__PURE__ */ new Map();
|
|
3508
|
+
for (const character of old) retainedBySpan.set(`${String(character.start)}:${String(character.end)}`, character);
|
|
3509
|
+
this.colorGeneration += 1;
|
|
3510
|
+
const windowStart = Math.max(0, Math.min(text.length - 320, oldestLiveStart < Infinity ? oldestLiveStart : text.length));
|
|
3511
|
+
const tailText = windowStart > 0 ? text.slice(windowStart) : text;
|
|
3512
|
+
const segments = segmenter.segment(tailText);
|
|
2798
3513
|
let end = text.length;
|
|
2799
3514
|
for (let count = 0; count < 160 && end > 0 && (count < tailSize || end > oldestLiveStart); count += 1) {
|
|
2800
|
-
const
|
|
3515
|
+
const localEnd = end - windowStart;
|
|
3516
|
+
if (localEnd <= 0) break;
|
|
3517
|
+
const segment = segments.containing(localEnd - 1);
|
|
2801
3518
|
if (segment === void 0) break;
|
|
2802
|
-
const start = segment.index;
|
|
2803
|
-
const
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
3519
|
+
const start = segment.index + windowStart;
|
|
3520
|
+
const born = (end <= prefix ? retainedBySpan.get(`${String(start)}:${String(end)}`) : void 0)?.born ?? (this.active && appended && start >= previous.length ? now : null);
|
|
3521
|
+
if (born !== null && now - born < 240 && segment.segment.trim() !== "") {
|
|
3522
|
+
const first = this.firstNodeEndingAfter(start);
|
|
3523
|
+
const last = this.lastNodeStartingBefore(end);
|
|
3524
|
+
if (last >= first && first < nodes.length && nodes[last].end > start && ineligible[last + 1] === ineligible[first]) {
|
|
3525
|
+
const firstPart = nodes[first];
|
|
3526
|
+
const lastPart = nodes[last];
|
|
3527
|
+
const range = this.root.ownerDocument.createRange();
|
|
3528
|
+
range.setStart(firstPart.node, start - firstPart.start);
|
|
3529
|
+
range.setEnd(lastPart.node, end - lastPart.start);
|
|
3530
|
+
this.characters.push({
|
|
3531
|
+
start,
|
|
3532
|
+
end,
|
|
3533
|
+
born,
|
|
3534
|
+
range,
|
|
3535
|
+
bucket: -1
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
2819
3538
|
}
|
|
2820
3539
|
end = start;
|
|
2821
3540
|
}
|
|
@@ -2852,14 +3571,18 @@ window.__ModuleLoader__.load({
|
|
|
2852
3571
|
}
|
|
2853
3572
|
stopIfIdle() {
|
|
2854
3573
|
if (this.scheduler.pending.size !== 0) return;
|
|
2855
|
-
|
|
2856
|
-
this.scheduler.
|
|
3574
|
+
if (this.scheduler.taskId === null) return;
|
|
3575
|
+
this.scheduler.coordinator.unregisterTask(this.scheduler.taskId);
|
|
3576
|
+
this.scheduler.taskId = null;
|
|
2857
3577
|
}
|
|
2858
3578
|
dispose() {
|
|
2859
3579
|
if (this.disposed) return;
|
|
2860
3580
|
this.disposed = true;
|
|
2861
3581
|
this.observer.disconnect();
|
|
2862
3582
|
this.clearRanges();
|
|
3583
|
+
this.restoreColors();
|
|
3584
|
+
this.nodes = [];
|
|
3585
|
+
this.scannedStructureRevision = -1;
|
|
2863
3586
|
this.root.classList.remove(LogarithmicFade_module_css_default.scope);
|
|
2864
3587
|
this.scheduler.pending.delete(this);
|
|
2865
3588
|
this.scheduler.clients.delete(this);
|
|
@@ -2869,6 +3592,7 @@ window.__ModuleLoader__.load({
|
|
|
2869
3592
|
const name = highlightName(index);
|
|
2870
3593
|
if (this.scheduler.registry.get(name) === highlight) this.scheduler.registry.delete(name);
|
|
2871
3594
|
}
|
|
3595
|
+
this.scheduler.unwatchAppearance();
|
|
2872
3596
|
schedulers.delete(this.root.ownerDocument);
|
|
2873
3597
|
}
|
|
2874
3598
|
}
|
|
@@ -2885,7 +3609,15 @@ window.__ModuleLoader__.load({
|
|
|
2885
3609
|
}, [rootRef]);
|
|
2886
3610
|
(0, react.useLayoutEffect)(() => {
|
|
2887
3611
|
const root = rootRef.current;
|
|
2888
|
-
if (
|
|
3612
|
+
if (!enabled) {
|
|
3613
|
+
if (controller.current !== null) {
|
|
3614
|
+
controller.current.dispose();
|
|
3615
|
+
controller.current = null;
|
|
3616
|
+
}
|
|
3617
|
+
committed.current = true;
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
if (controller.current === null && root !== null && active) {
|
|
2889
3621
|
controller.current = LogarithmicFadeController.create(root);
|
|
2890
3622
|
if (committed.current) controller.current?.update(false, false);
|
|
2891
3623
|
}
|
|
@@ -2894,6 +3626,141 @@ window.__ModuleLoader__.load({
|
|
|
2894
3626
|
});
|
|
2895
3627
|
}
|
|
2896
3628
|
//#endregion
|
|
3629
|
+
//#region src/client/useDecoupledMarkdown.ts
|
|
3630
|
+
/**
|
|
3631
|
+
* Detect whether the given markdown text ends inside an unclosed code block (fence).
|
|
3632
|
+
* Supports both backtick (```) and tilde (~~~) fences per CommonMark rules.
|
|
3633
|
+
*
|
|
3634
|
+
* Rules:
|
|
3635
|
+
* 1. An opening fence begins on a line with 0-3 leading spaces, followed by 3+ backticks or tildes.
|
|
3636
|
+
* 2. An opening backtick fence info-string cannot contain backticks.
|
|
3637
|
+
* 3. A closing fence must have at least as many characters as the opening fence, matching fence char,
|
|
3638
|
+
* and cannot have non-whitespace content after the fence characters.
|
|
3639
|
+
* 4. While inside a code block, all lines belong to the block until a matching closing fence is found.
|
|
3640
|
+
*/
|
|
3641
|
+
function hasUnclosedCodeFence(text) {
|
|
3642
|
+
if (!text.includes("```") && !text.includes("~~~")) return false;
|
|
3643
|
+
let inFence = false;
|
|
3644
|
+
let fenceChar = "";
|
|
3645
|
+
let fenceLen = 0;
|
|
3646
|
+
let start = 0;
|
|
3647
|
+
const len = text.length;
|
|
3648
|
+
while (start < len) {
|
|
3649
|
+
let end = text.indexOf("\n", start);
|
|
3650
|
+
if (end === -1) end = len;
|
|
3651
|
+
let line = text.slice(start, end);
|
|
3652
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
3653
|
+
let indent = 0;
|
|
3654
|
+
while (indent < line.length && line[indent] === " " && indent < 4) indent++;
|
|
3655
|
+
if (indent < 4) {
|
|
3656
|
+
const rest = line.slice(indent);
|
|
3657
|
+
const firstChar = rest[0];
|
|
3658
|
+
if (firstChar === "`" || firstChar === "~") {
|
|
3659
|
+
let count = 0;
|
|
3660
|
+
while (count < rest.length && rest[count] === firstChar) count++;
|
|
3661
|
+
if (count >= 3) {
|
|
3662
|
+
const afterFence = rest.slice(count);
|
|
3663
|
+
if (!inFence) {
|
|
3664
|
+
if (firstChar !== "`" || !afterFence.includes("`")) {
|
|
3665
|
+
inFence = true;
|
|
3666
|
+
fenceChar = firstChar;
|
|
3667
|
+
fenceLen = count;
|
|
3668
|
+
}
|
|
3669
|
+
} else if (firstChar === fenceChar && count >= fenceLen) {
|
|
3670
|
+
if (afterFence.trim() === "") {
|
|
3671
|
+
inFence = false;
|
|
3672
|
+
fenceChar = "";
|
|
3673
|
+
fenceLen = 0;
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
start = end + 1;
|
|
3680
|
+
}
|
|
3681
|
+
return inFence;
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Decouple the smooth 60 FPS animation clock from the heavyweight Markdown AST compilation clock.
|
|
3685
|
+
*
|
|
3686
|
+
* Outside unclosed code blocks (or when settled), Markdown renders 1:1 with `shown`.
|
|
3687
|
+
* Inside unclosed code blocks (where host incremental parsing degenerates to O(n) full-text compilation),
|
|
3688
|
+
* Markdown re-renders are throttled to ~33ms (30Hz).
|
|
3689
|
+
* Upon code block closure or stream end, updates are synchronized immediately without latency.
|
|
3690
|
+
*/
|
|
3691
|
+
function useDecoupledMarkdown(shown, live, options) {
|
|
3692
|
+
const throttleMs = options?.throttleMs ?? 33;
|
|
3693
|
+
const [markdownShown, setMarkdownShown] = (0, react.useState)(shown);
|
|
3694
|
+
const lastCommitTimeRef = (0, react.useRef)(0);
|
|
3695
|
+
const timerRef = (0, react.useRef)(null);
|
|
3696
|
+
const shownRef = (0, react.useRef)(shown);
|
|
3697
|
+
shownRef.current = shown;
|
|
3698
|
+
const wasUnclosedRef = (0, react.useRef)(false);
|
|
3699
|
+
(0, react.useEffect)(() => {
|
|
3700
|
+
if (!live) {
|
|
3701
|
+
if (timerRef.current !== null) {
|
|
3702
|
+
clearTimeout(timerRef.current);
|
|
3703
|
+
timerRef.current = null;
|
|
3704
|
+
}
|
|
3705
|
+
setMarkdownShown(shown);
|
|
3706
|
+
lastCommitTimeRef.current = performance.now();
|
|
3707
|
+
wasUnclosedRef.current = false;
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
const unclosed = hasUnclosedCodeFence(shown);
|
|
3711
|
+
const now = performance.now();
|
|
3712
|
+
if (wasUnclosedRef.current && !unclosed) {
|
|
3713
|
+
if (timerRef.current !== null) {
|
|
3714
|
+
clearTimeout(timerRef.current);
|
|
3715
|
+
timerRef.current = null;
|
|
3716
|
+
}
|
|
3717
|
+
wasUnclosedRef.current = false;
|
|
3718
|
+
setMarkdownShown(shown);
|
|
3719
|
+
lastCommitTimeRef.current = now;
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
wasUnclosedRef.current = unclosed;
|
|
3723
|
+
if (!unclosed) {
|
|
3724
|
+
if (timerRef.current !== null) {
|
|
3725
|
+
clearTimeout(timerRef.current);
|
|
3726
|
+
timerRef.current = null;
|
|
3727
|
+
}
|
|
3728
|
+
setMarkdownShown(shown);
|
|
3729
|
+
lastCommitTimeRef.current = now;
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
const elapsed = now - lastCommitTimeRef.current;
|
|
3733
|
+
if (elapsed >= throttleMs) {
|
|
3734
|
+
if (timerRef.current !== null) {
|
|
3735
|
+
clearTimeout(timerRef.current);
|
|
3736
|
+
timerRef.current = null;
|
|
3737
|
+
}
|
|
3738
|
+
setMarkdownShown(shown);
|
|
3739
|
+
lastCommitTimeRef.current = now;
|
|
3740
|
+
} else if (timerRef.current === null) {
|
|
3741
|
+
const delay = Math.max(16, throttleMs - elapsed);
|
|
3742
|
+
timerRef.current = setTimeout(() => {
|
|
3743
|
+
timerRef.current = null;
|
|
3744
|
+
setMarkdownShown(shownRef.current);
|
|
3745
|
+
lastCommitTimeRef.current = performance.now();
|
|
3746
|
+
}, delay);
|
|
3747
|
+
}
|
|
3748
|
+
}, [
|
|
3749
|
+
shown,
|
|
3750
|
+
live,
|
|
3751
|
+
throttleMs
|
|
3752
|
+
]);
|
|
3753
|
+
(0, react.useEffect)(() => {
|
|
3754
|
+
return () => {
|
|
3755
|
+
if (timerRef.current !== null) {
|
|
3756
|
+
clearTimeout(timerRef.current);
|
|
3757
|
+
timerRef.current = null;
|
|
3758
|
+
}
|
|
3759
|
+
};
|
|
3760
|
+
}, []);
|
|
3761
|
+
return live ? markdownShown : shown;
|
|
3762
|
+
}
|
|
3763
|
+
//#endregion
|
|
2897
3764
|
//#region src/client/FollowHost.tsx
|
|
2898
3765
|
/**
|
|
2899
3766
|
* Document-flow host that owns conversation-port follow while `active`.
|
|
@@ -2947,7 +3814,7 @@ window.__ModuleLoader__.load({
|
|
|
2947
3814
|
/** Defaults shared by the Host schema and the client-side fallback. */
|
|
2948
3815
|
const DEFAULT_STREAM_CONFIG = {
|
|
2949
3816
|
mode: "typewriter",
|
|
2950
|
-
preset: "
|
|
3817
|
+
preset: "silky",
|
|
2951
3818
|
revealCharsPerSec: 80,
|
|
2952
3819
|
scrollSpeedPxPerSec: 48,
|
|
2953
3820
|
maxScrollSpeedPxPerSec: 1e3
|
|
@@ -2996,79 +3863,24 @@ window.__ModuleLoader__.load({
|
|
|
2996
3863
|
else width += emPx;
|
|
2997
3864
|
return width;
|
|
2998
3865
|
}
|
|
2999
|
-
function measurePendingTextGeometry(root, visibleText) {
|
|
3000
|
-
if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
|
|
3001
|
-
root,
|
|
3002
|
-
visibleText,
|
|
3003
|
-
fontSize: 14,
|
|
3004
|
-
wrapThresholdWidth: null
|
|
3005
|
-
};
|
|
3006
|
-
const rootRect = root.getBoundingClientRect();
|
|
3007
|
-
const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
|
|
3008
|
-
if (rootWidth <= 0) return {
|
|
3009
|
-
root,
|
|
3010
|
-
visibleText,
|
|
3011
|
-
fontSize: 14,
|
|
3012
|
-
wrapThresholdWidth: null
|
|
3013
|
-
};
|
|
3014
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
3015
|
-
let tail = null;
|
|
3016
|
-
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
|
|
3017
|
-
const parent = tail?.parentElement ?? root;
|
|
3018
|
-
const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
|
|
3019
|
-
if (tail === null || typeof document.createRange !== "function") return {
|
|
3020
|
-
root,
|
|
3021
|
-
visibleText,
|
|
3022
|
-
fontSize,
|
|
3023
|
-
wrapThresholdWidth: rootWidth
|
|
3024
|
-
};
|
|
3025
|
-
try {
|
|
3026
|
-
const length = tail.textContent?.length ?? 0;
|
|
3027
|
-
if (length <= 0) return {
|
|
3028
|
-
root,
|
|
3029
|
-
visibleText,
|
|
3030
|
-
fontSize,
|
|
3031
|
-
wrapThresholdWidth: rootWidth
|
|
3032
|
-
};
|
|
3033
|
-
const range = document.createRange();
|
|
3034
|
-
range.setStart(tail, Math.max(0, length - 1));
|
|
3035
|
-
range.setEnd(tail, length);
|
|
3036
|
-
const tailRect = range.getBoundingClientRect();
|
|
3037
|
-
const contentRight = rootRect.right;
|
|
3038
|
-
if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
|
|
3039
|
-
root,
|
|
3040
|
-
visibleText,
|
|
3041
|
-
fontSize,
|
|
3042
|
-
wrapThresholdWidth: rootWidth
|
|
3043
|
-
};
|
|
3044
|
-
return {
|
|
3045
|
-
root,
|
|
3046
|
-
visibleText,
|
|
3047
|
-
fontSize,
|
|
3048
|
-
wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
|
|
3049
|
-
};
|
|
3050
|
-
} catch {
|
|
3051
|
-
return {
|
|
3052
|
-
root,
|
|
3053
|
-
visibleText,
|
|
3054
|
-
fontSize,
|
|
3055
|
-
wrapThresholdWidth: rootWidth
|
|
3056
|
-
};
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
3866
|
/** Whether buffered source can reach a new visual line before it drains. */
|
|
3060
3867
|
function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
|
|
3061
3868
|
if (pending === "") return false;
|
|
3062
3869
|
if (/[\r\n]/u.test(pending)) return true;
|
|
3063
|
-
|
|
3064
|
-
if (root === null) return
|
|
3870
|
+
if ([...pending].length >= PREDICTIVE_WRAP_FALLBACK_CHARS) return true;
|
|
3871
|
+
if (root === null) return false;
|
|
3065
3872
|
let geometry = geometryRef.current;
|
|
3066
|
-
if (geometry?.root !== root
|
|
3067
|
-
|
|
3873
|
+
if (geometry?.root !== root) {
|
|
3874
|
+
const rootWidth = root.clientWidth || 600;
|
|
3875
|
+
geometry = {
|
|
3876
|
+
root,
|
|
3877
|
+
visibleText,
|
|
3878
|
+
fontSize: 14,
|
|
3879
|
+
wrapThresholdWidth: Math.max(120, rootWidth * .4)
|
|
3880
|
+
};
|
|
3068
3881
|
geometryRef.current = geometry;
|
|
3069
3882
|
}
|
|
3070
|
-
|
|
3071
|
-
return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
|
|
3883
|
+
return approximateInlineWidth(pending, geometry.fontSize) >= (geometry.wrapThresholdWidth ?? geometry.fontSize * PREDICTIVE_WRAP_FALLBACK_CHARS);
|
|
3072
3884
|
}
|
|
3073
3885
|
function announcementChunkEnd(source, start) {
|
|
3074
3886
|
const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
|
|
@@ -3185,12 +3997,12 @@ window.__ModuleLoader__.load({
|
|
|
3185
3997
|
* and rendered by the Harness `MarkdownText`
|
|
3186
3998
|
* streaming arm (incremental parse, frozen non-tail blocks), so there is no
|
|
3187
3999
|
* raw-text tail and no text-to-markdown swap: the tree stays markdown
|
|
3188
|
-
* throughout. The
|
|
3189
|
-
*
|
|
3190
|
-
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
4000
|
+
* throughout. The outer assistant owner follows all text growth, including
|
|
4001
|
+
* the final drain, so wraps glide without a completion handoff. Once the
|
|
4002
|
+
* queue drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
3191
4003
|
* mentions) swaps in exactly once.
|
|
3192
4004
|
*/
|
|
3193
|
-
function AnimatedMarkdownText({ text, labels, fileMentions, streaming, logarithmicFade, motionReduced,
|
|
4005
|
+
function AnimatedMarkdownText({ text, labels, fileMentions, streaming, logarithmicFade, motionReduced, followSpeedCpsRef, followRevealedCharsRef, followRevealScaleRef, onPredictiveChange, onRevealActivityChange, preset, shouldHoldBack }) {
|
|
3194
4006
|
const reduced = motionReduced;
|
|
3195
4007
|
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
3196
4008
|
const localSpeedCpsRef = (0, react.useRef)(35);
|
|
@@ -3213,6 +4025,7 @@ window.__ModuleLoader__.load({
|
|
|
3213
4025
|
});
|
|
3214
4026
|
const shown = reduced ? text : displayed;
|
|
3215
4027
|
const live = typing && !reduced;
|
|
4028
|
+
const markdownShown = useDecoupledMarkdown(shown, live);
|
|
3216
4029
|
useLogarithmicFade(followRootRef, logarithmicFade && !reduced, live, speedCpsRef);
|
|
3217
4030
|
(0, react.useEffect)(() => {
|
|
3218
4031
|
const root = followRootRef.current;
|
|
@@ -3240,6 +4053,9 @@ window.__ModuleLoader__.load({
|
|
|
3240
4053
|
streaming,
|
|
3241
4054
|
text
|
|
3242
4055
|
]);
|
|
4056
|
+
(0, react.useLayoutEffect)(() => {
|
|
4057
|
+
onRevealActivityChange?.(live);
|
|
4058
|
+
}, [live, onRevealActivityChange]);
|
|
3243
4059
|
(0, react.useEffect)(() => {
|
|
3244
4060
|
if (typing && !streaming && shown.length === text.length) setTyping(false);
|
|
3245
4061
|
}, [
|
|
@@ -3251,16 +4067,13 @@ window.__ModuleLoader__.load({
|
|
|
3251
4067
|
(0, react.useEffect)(() => {
|
|
3252
4068
|
if (streaming) setTyping(true);
|
|
3253
4069
|
}, [streaming]);
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
predictive: streaming,
|
|
3260
|
-
controlScroll,
|
|
3261
|
-
hostRef: followRootRef,
|
|
4070
|
+
if (!streaming && !live && text.trim() === "") return null;
|
|
4071
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4072
|
+
ref: followRootRef,
|
|
4073
|
+
className: TypewriterAssistantNodeView_module_css_default.follow,
|
|
4074
|
+
"data-follow-text": "",
|
|
3262
4075
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
3263
|
-
text: live ?
|
|
4076
|
+
text: live ? markdownShown : text,
|
|
3264
4077
|
streaming: live,
|
|
3265
4078
|
labels,
|
|
3266
4079
|
fileMentions: live ? void 0 : fileMentions
|
|
@@ -3281,7 +4094,7 @@ window.__ModuleLoader__.load({
|
|
|
3281
4094
|
};
|
|
3282
4095
|
}
|
|
3283
4096
|
/**
|
|
3284
|
-
* Apply searchable hidden state without unmounting a stable subtree
|
|
4097
|
+
* Apply searchable hidden state without unmounting a stable subtree 鈥?the
|
|
3285
4098
|
* Host's completion fold hides the answer-inline reasoning this way, so the
|
|
3286
4099
|
* takeover renderer must reproduce it to stay inside the contract: the row
|
|
3287
4100
|
* disappears into the Host's process summary and comes back on find-in-page.
|
|
@@ -3338,18 +4151,23 @@ window.__ModuleLoader__.load({
|
|
|
3338
4151
|
* click stay on the disclosure chrome, which the plugin's AnimatedDisclosure
|
|
3339
4152
|
* renders with a height-animated body (the harness primitive would mount and
|
|
3340
4153
|
* unmount it, which cannot glide). The row opens only while this block is
|
|
3341
|
-
* the streaming tail and closes as soon as thinking ends
|
|
3342
|
-
* or the assistant node settling
|
|
4154
|
+
* the streaming tail and closes as soon as thinking ends 鈥?a later block,
|
|
4155
|
+
* or the assistant node settling 鈥?not when the rest of the reply is
|
|
3343
4156
|
* still streaming.
|
|
3344
4157
|
*/
|
|
3345
|
-
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, logarithmicFade, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
|
|
4158
|
+
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, motionReduced, logarithmicFade, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, snapAutoCollapse, t }) {
|
|
3346
4159
|
const reduced = motionReduced;
|
|
3347
4160
|
const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
|
|
3348
4161
|
const [autoClosed, setAutoClosed] = (0, react.useState)(false);
|
|
3349
4162
|
const summaryRef = (0, react.useRef)(null);
|
|
3350
4163
|
const fadeRootRef = (0, react.useRef)(null);
|
|
4164
|
+
const thinkBodyRef = fadeRootRef;
|
|
3351
4165
|
const localFadeSpeedRef = (0, react.useRef)(35);
|
|
3352
4166
|
const fadeSpeedRef = followSpeedCpsRef ?? localFadeSpeedRef;
|
|
4167
|
+
const userScrolledRef = (0, react.useRef)(false);
|
|
4168
|
+
const rafIdRef = (0, react.useRef)(0);
|
|
4169
|
+
const followActiveRef = (0, react.useRef)(false);
|
|
4170
|
+
followActiveRef.current = running && expanded;
|
|
3353
4171
|
const commitAnchorRef = (0, react.useRef)(null);
|
|
3354
4172
|
const displayed = useSmoothStreamContent(text, {
|
|
3355
4173
|
enabled: running && !reduced,
|
|
@@ -3369,8 +4187,64 @@ window.__ModuleLoader__.load({
|
|
|
3369
4187
|
setExpanded(running);
|
|
3370
4188
|
setAutoClosed(!running);
|
|
3371
4189
|
}
|
|
4190
|
+
if (running) userScrolledRef.current = false;
|
|
3372
4191
|
notifyFollowCommit(commitAnchorRef.current);
|
|
3373
4192
|
}, [running, thinkAutoExpand]);
|
|
4193
|
+
(0, react.useEffect)(() => {
|
|
4194
|
+
if (!running || !expanded || userScrolledRef.current) return;
|
|
4195
|
+
const el = thinkBodyRef.current;
|
|
4196
|
+
if (el === null) return;
|
|
4197
|
+
if (rafIdRef.current === 0) rafIdRef.current = requestAnimationFrame(() => {
|
|
4198
|
+
rafIdRef.current = 0;
|
|
4199
|
+
if (!followActiveRef.current || userScrolledRef.current || el === null) return;
|
|
4200
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight > 2) el.scrollTop = Number.MAX_SAFE_INTEGER;
|
|
4201
|
+
});
|
|
4202
|
+
}, [
|
|
4203
|
+
running,
|
|
4204
|
+
expanded,
|
|
4205
|
+
shown
|
|
4206
|
+
]);
|
|
4207
|
+
(0, react.useEffect)(() => {
|
|
4208
|
+
return () => {
|
|
4209
|
+
if (rafIdRef.current !== 0) {
|
|
4210
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
4211
|
+
rafIdRef.current = 0;
|
|
4212
|
+
}
|
|
4213
|
+
};
|
|
4214
|
+
}, []);
|
|
4215
|
+
(0, react.useEffect)(() => {
|
|
4216
|
+
const el = thinkBodyRef.current;
|
|
4217
|
+
if (el === null) return;
|
|
4218
|
+
let isPointerDown = false;
|
|
4219
|
+
const onWheel = (e) => {
|
|
4220
|
+
if (e.deltaY < 0) userScrolledRef.current = true;
|
|
4221
|
+
else if (e.deltaY > 0) {
|
|
4222
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight <= 30) userScrolledRef.current = false;
|
|
4223
|
+
}
|
|
4224
|
+
};
|
|
4225
|
+
const onPointerDown = () => {
|
|
4226
|
+
isPointerDown = true;
|
|
4227
|
+
};
|
|
4228
|
+
const onPointerUp = () => {
|
|
4229
|
+
isPointerDown = false;
|
|
4230
|
+
};
|
|
4231
|
+
const onScroll = () => {
|
|
4232
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight <= 30) userScrolledRef.current = false;
|
|
4233
|
+
else if (isPointerDown) userScrolledRef.current = true;
|
|
4234
|
+
};
|
|
4235
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
4236
|
+
el.addEventListener("pointerdown", onPointerDown, { passive: true });
|
|
4237
|
+
window.addEventListener("pointerup", onPointerUp, { passive: true });
|
|
4238
|
+
window.addEventListener("pointercancel", onPointerUp, { passive: true });
|
|
4239
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
4240
|
+
return () => {
|
|
4241
|
+
el.removeEventListener("wheel", onWheel);
|
|
4242
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
4243
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
4244
|
+
window.removeEventListener("pointercancel", onPointerUp);
|
|
4245
|
+
el.removeEventListener("scroll", onScroll);
|
|
4246
|
+
};
|
|
4247
|
+
}, []);
|
|
3374
4248
|
(0, react.useEffect)(() => {
|
|
3375
4249
|
const element = summaryRef.current;
|
|
3376
4250
|
if (element === null) return;
|
|
@@ -3391,14 +4265,14 @@ window.__ModuleLoader__.load({
|
|
|
3391
4265
|
leadingClassName: TypewriterAssistantNodeView_module_css_default.thinkLeading,
|
|
3392
4266
|
titleClassName: TypewriterAssistantNodeView_module_css_default.thinkTitle,
|
|
3393
4267
|
chevronClassName: TypewriterAssistantNodeView_module_css_default.thinkChevron,
|
|
3394
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3395
|
-
title: "
|
|
4268
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconThink, { size: 14 }),
|
|
4269
|
+
title: t("message.think"),
|
|
3396
4270
|
open: expanded,
|
|
3397
4271
|
onToggle: () => {
|
|
3398
4272
|
setAutoClosed(false);
|
|
3399
4273
|
setExpanded((value) => !value);
|
|
3400
4274
|
},
|
|
3401
|
-
bodyTransition: !autoClosed,
|
|
4275
|
+
bodyTransition: !autoClosed || !snapAutoCollapse,
|
|
3402
4276
|
collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3403
4277
|
className: TypewriterAssistantNodeView_module_css_default.thinkSeparator,
|
|
3404
4278
|
"aria-hidden": true
|
|
@@ -3422,14 +4296,17 @@ window.__ModuleLoader__.load({
|
|
|
3422
4296
|
* streaming is revealed by the smoother through the Harness Markdown
|
|
3423
4297
|
* renderer at a rate that tracks arrival. Reasoning blocks keep the
|
|
3424
4298
|
* built-in Think disclosure and only receive a smoothed text feed; the
|
|
3425
|
-
* outer node owns conversation-port follow
|
|
3426
|
-
*
|
|
3427
|
-
*
|
|
3428
|
-
* renders with the full
|
|
4299
|
+
* outer node owns conversation-port follow through both streaming and the
|
|
4300
|
+
* final text drain. Keeping one owner avoids a lifecycle handoff that would
|
|
4301
|
+
* reopen and later retire a second runway. The FPS guard holds offscreen
|
|
4302
|
+
* reveals when the frame rate is degraded. Settled text renders with the full
|
|
4303
|
+
* Markdown pipeline.
|
|
3429
4304
|
*/
|
|
3430
|
-
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, logarithmicFade = DEFAULT_STREAM_SETTINGS.logarithmicFade, controlScroll = true, motionPreference = DEFAULT_STREAM_SETTINGS.motionPreference, node, useTurnData, openFile, loadImage, fileMentions, turnProcess, t }) {
|
|
4305
|
+
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, logarithmicFade = DEFAULT_STREAM_SETTINGS.logarithmicFade, controlScroll = true, motionPreference = DEFAULT_STREAM_SETTINGS.motionPreference, node, groupPart, useTurnData, openFile, loadImage, fileMentions, turnProcess, t }) {
|
|
3431
4306
|
const data = node.data;
|
|
3432
4307
|
const streaming = data.status === "running";
|
|
4308
|
+
const blocks = groupPart === "reasoning" ? data.blocks.filter((block) => block.kind === "reasoning") : groupPart === "response" ? data.blocks.filter((block) => block.kind !== "reasoning") : data.blocks;
|
|
4309
|
+
const rowOwnsPort = groupPart !== "reasoning";
|
|
3433
4310
|
const reduced = useMotionReduced(motionPreference);
|
|
3434
4311
|
const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
|
|
3435
4312
|
const revealProcess = (0, react.useCallback)(() => {
|
|
@@ -3439,7 +4316,16 @@ window.__ModuleLoader__.load({
|
|
|
3439
4316
|
const rootSpeedRef = (0, react.useRef)(35);
|
|
3440
4317
|
const rootRevealedCharsRef = (0, react.useRef)(0);
|
|
3441
4318
|
const rootRevealScaleRef = (0, react.useRef)(1);
|
|
3442
|
-
const
|
|
4319
|
+
const previousStreamingRef = (0, react.useRef)(streaming);
|
|
4320
|
+
const [textRevealActive, setTextRevealActive] = (0, react.useState)(false);
|
|
4321
|
+
const completionCandidate = !streaming && previousStreamingRef.current && blocks.some((block) => block.kind === "text" && block.text.trim() !== "");
|
|
4322
|
+
(0, react.useLayoutEffect)(() => {
|
|
4323
|
+
previousStreamingRef.current = streaming;
|
|
4324
|
+
}, [streaming]);
|
|
4325
|
+
const updateTextRevealActivity = (0, react.useCallback)((active) => {
|
|
4326
|
+
setTextRevealActive((previous) => previous === active ? previous : active);
|
|
4327
|
+
}, []);
|
|
4328
|
+
const reasoningTailIndex = streaming && blocks[blocks.length - 1]?.kind === "reasoning" ? blocks.length - 1 : -1;
|
|
3443
4329
|
const reasoningOwnsSpeed = reasoningTailIndex !== -1;
|
|
3444
4330
|
const rootPredictiveRef = (0, react.useRef)(false);
|
|
3445
4331
|
const previousReasoningTailRef = (0, react.useRef)(-1);
|
|
@@ -3478,20 +4364,24 @@ window.__ModuleLoader__.load({
|
|
|
3478
4364
|
const imageLoader = loadImage ?? (async () => {
|
|
3479
4365
|
throw new Error(t("image.serviceUnavailable"));
|
|
3480
4366
|
});
|
|
3481
|
-
if (!(streaming || data.status === "interrupted" ||
|
|
3482
|
-
const announcementText =
|
|
4367
|
+
if (!(streaming || data.status === "interrupted" || blocks.some((block) => block.kind !== "tool-call"))) return null;
|
|
4368
|
+
const announcementText = blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
|
|
4369
|
+
const stepTailIsReasoning = data.blocks[data.blocks.length - 1]?.kind === "reasoning";
|
|
3483
4370
|
const rendered = [];
|
|
3484
|
-
const last =
|
|
4371
|
+
const last = blocks.length - 1;
|
|
3485
4372
|
let lastFollow = -1;
|
|
3486
|
-
|
|
3487
|
-
|
|
4373
|
+
let lastText = -1;
|
|
4374
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
4375
|
+
const kind = blocks[index]?.kind;
|
|
3488
4376
|
if (kind === "text" || kind === "reasoning") lastFollow = index;
|
|
4377
|
+
if (kind === "text") lastText = index;
|
|
3489
4378
|
}
|
|
3490
|
-
for (let index = 0; index <
|
|
3491
|
-
const block =
|
|
4379
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
4380
|
+
const block = blocks[index];
|
|
3492
4381
|
if (block === void 0) continue;
|
|
3493
4382
|
switch (block.kind) {
|
|
3494
4383
|
case "text":
|
|
4384
|
+
if (!streaming && block.text.trim() === "") break;
|
|
3495
4385
|
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
|
|
3496
4386
|
text: block.text,
|
|
3497
4387
|
labels: markdownLabels,
|
|
@@ -3499,14 +4389,13 @@ window.__ModuleLoader__.load({
|
|
|
3499
4389
|
streaming,
|
|
3500
4390
|
logarithmicFade: logarithmicFade && data.status !== "interrupted",
|
|
3501
4391
|
motionReduced: reduced,
|
|
3502
|
-
ownFollow: !streaming && index === lastFollow,
|
|
3503
4392
|
followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
|
|
3504
4393
|
followRevealedCharsRef: index === lastFollow ? rootRevealedCharsRef : void 0,
|
|
3505
4394
|
followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
|
|
3506
4395
|
onPredictiveChange: index === lastFollow ? updateTextPrediction : void 0,
|
|
4396
|
+
onRevealActivityChange: index === lastText ? updateTextRevealActivity : void 0,
|
|
3507
4397
|
preset,
|
|
3508
|
-
shouldHoldBack
|
|
3509
|
-
controlScroll
|
|
4398
|
+
shouldHoldBack
|
|
3510
4399
|
}, index));
|
|
3511
4400
|
break;
|
|
3512
4401
|
case "reasoning":
|
|
@@ -3515,7 +4404,7 @@ window.__ModuleLoader__.load({
|
|
|
3515
4404
|
reveal: revealProcess,
|
|
3516
4405
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
|
|
3517
4406
|
text: block.text,
|
|
3518
|
-
running: streaming && index === last,
|
|
4407
|
+
running: streaming && index === last && (groupPart !== "reasoning" || stepTailIsReasoning),
|
|
3519
4408
|
preset,
|
|
3520
4409
|
thinkAutoExpand,
|
|
3521
4410
|
logarithmicFade: logarithmicFade && data.status !== "interrupted",
|
|
@@ -3523,6 +4412,7 @@ window.__ModuleLoader__.load({
|
|
|
3523
4412
|
shouldHoldBack,
|
|
3524
4413
|
followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
|
|
3525
4414
|
followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
|
|
4415
|
+
snapAutoCollapse: groupPart === void 0,
|
|
3526
4416
|
t
|
|
3527
4417
|
})
|
|
3528
4418
|
}, index));
|
|
@@ -3530,8 +4420,8 @@ window.__ModuleLoader__.load({
|
|
|
3530
4420
|
case "image": {
|
|
3531
4421
|
const start = index;
|
|
3532
4422
|
const group = [block];
|
|
3533
|
-
while (index + 1 <
|
|
3534
|
-
const next =
|
|
4423
|
+
while (index + 1 < blocks.length) {
|
|
4424
|
+
const next = blocks[index + 1];
|
|
3535
4425
|
if (next === void 0 || next.kind !== "image") break;
|
|
3536
4426
|
group.push(next);
|
|
3537
4427
|
index += 1;
|
|
@@ -3560,12 +4450,12 @@ window.__ModuleLoader__.load({
|
|
|
3560
4450
|
text: announcementText,
|
|
3561
4451
|
active: streaming && !reduced
|
|
3562
4452
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
3563
|
-
active:
|
|
4453
|
+
active: !reduced && (streaming || completionCandidate || textRevealActive),
|
|
3564
4454
|
speedCpsRef: rootSpeedRef,
|
|
3565
4455
|
revealedCharsRef: rootRevealedCharsRef,
|
|
3566
4456
|
revealScaleRef: rootRevealScaleRef,
|
|
3567
4457
|
predictiveRef: rootPredictiveRef,
|
|
3568
|
-
controlScroll,
|
|
4458
|
+
controlScroll: controlScroll && rowOwnsPort,
|
|
3569
4459
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3570
4460
|
className: TypewriterAssistantNodeView_module_css_default.body,
|
|
3571
4461
|
children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -3668,12 +4558,18 @@ window.__ModuleLoader__.load({
|
|
|
3668
4558
|
}
|
|
3669
4559
|
const pending = /* @__PURE__ */ new Set();
|
|
3670
4560
|
const internalWrites = /* @__PURE__ */ new WeakMap();
|
|
3671
|
-
let
|
|
4561
|
+
let frameTaskId = null;
|
|
3672
4562
|
let lastFrame = null;
|
|
3673
4563
|
let debt = 0;
|
|
3674
4564
|
let stopped = false;
|
|
3675
4565
|
let announcedSettled = false;
|
|
3676
4566
|
const streamId = `dom-${Math.random().toString(36).slice(2)}`;
|
|
4567
|
+
const coordinator = FrameCoordinator.forDocument();
|
|
4568
|
+
const stopFrameTask = () => {
|
|
4569
|
+
if (frameTaskId === null) return;
|
|
4570
|
+
coordinator.unregisterTask(frameTaskId);
|
|
4571
|
+
frameTaskId = null;
|
|
4572
|
+
};
|
|
3677
4573
|
const announceSettled = () => {
|
|
3678
4574
|
lastFrame = null;
|
|
3679
4575
|
debt = 0;
|
|
@@ -3722,21 +4618,20 @@ window.__ModuleLoader__.load({
|
|
|
3722
4618
|
});
|
|
3723
4619
|
};
|
|
3724
4620
|
const scheduleFrame = () => {
|
|
3725
|
-
if (stopped || pending.size === 0 ||
|
|
3726
|
-
|
|
4621
|
+
if (stopped || pending.size === 0 || frameTaskId !== null) return;
|
|
4622
|
+
frameTaskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) });
|
|
3727
4623
|
};
|
|
3728
4624
|
const frame = (now) => {
|
|
3729
|
-
|
|
3730
|
-
if (stopped) return;
|
|
4625
|
+
if (stopped) return false;
|
|
3731
4626
|
if (pending.size === 0) {
|
|
3732
4627
|
announceSettled();
|
|
3733
|
-
|
|
4628
|
+
stopFrameTask();
|
|
4629
|
+
return false;
|
|
3734
4630
|
}
|
|
3735
4631
|
announcedSettled = false;
|
|
3736
4632
|
if (lastFrame === null) {
|
|
3737
4633
|
lastFrame = now;
|
|
3738
|
-
|
|
3739
|
-
return;
|
|
4634
|
+
return true;
|
|
3740
4635
|
}
|
|
3741
4636
|
const elapsed = Math.max(0, now - lastFrame);
|
|
3742
4637
|
lastFrame = now;
|
|
@@ -3786,10 +4681,11 @@ window.__ModuleLoader__.load({
|
|
|
3786
4681
|
speedCps: step.speedCps,
|
|
3787
4682
|
targetChars,
|
|
3788
4683
|
displayedChars,
|
|
3789
|
-
active: pending.size > 0
|
|
4684
|
+
active: pending.size > 0,
|
|
4685
|
+
producerComplete: false
|
|
3790
4686
|
});
|
|
3791
4687
|
if (pending.size === 0) announceSettled();
|
|
3792
|
-
|
|
4688
|
+
return pending.size > 0;
|
|
3793
4689
|
};
|
|
3794
4690
|
visit(root, revealInitial);
|
|
3795
4691
|
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver((mutations) => {
|
|
@@ -3818,7 +4714,7 @@ window.__ModuleLoader__.load({
|
|
|
3818
4714
|
else scheduleFrame();
|
|
3819
4715
|
return () => {
|
|
3820
4716
|
stopped = true;
|
|
3821
|
-
|
|
4717
|
+
stopFrameTask();
|
|
3822
4718
|
observer?.disconnect();
|
|
3823
4719
|
for (const [node, record] of records) {
|
|
3824
4720
|
const controlled = record.chars.slice(0, record.shown).join("");
|
|
@@ -3837,8 +4733,8 @@ window.__ModuleLoader__.load({
|
|
|
3837
4733
|
]);
|
|
3838
4734
|
}
|
|
3839
4735
|
//#endregion
|
|
3840
|
-
//#region \0dsh-css:/Users/dzlin/
|
|
3841
|
-
const css$2 = ".
|
|
4736
|
+
//#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/AgentRowEntrance.module.css.mjs
|
|
4737
|
+
const css$2 = ".Mhu9Iq_surface{min-width:0}.Mhu9Iq_surface[data-entrance=active]{will-change:opacity;animation:.22s cubic-bezier(.2,.8,.2,1) both Mhu9Iq_dsh-smooth-stream-agent-row-in}@keyframes Mhu9Iq_dsh-smooth-stream-agent-row-in{0%{opacity:0;clip-path:inset(0 100% 0 0)}55%{opacity:1;clip-path:inset(0 18% 0 0)}to{opacity:1;clip-path:inset(0)}}@media (prefers-reduced-motion:reduce){.Mhu9Iq_surface[data-entrance=active]{opacity:1;clip-path:none;will-change:auto;animation:none}}";
|
|
3842
4738
|
const tagId$2 = "dsh-smooth-stream/AgentRowEntrance.module.css";
|
|
3843
4739
|
if (typeof document !== "undefined") {
|
|
3844
4740
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
|
|
@@ -3851,8 +4747,8 @@ window.__ModuleLoader__.load({
|
|
|
3851
4747
|
tag.textContent = css$2;
|
|
3852
4748
|
}
|
|
3853
4749
|
var AgentRowEntrance_module_css_default = {
|
|
3854
|
-
"
|
|
3855
|
-
"
|
|
4750
|
+
"surface": "Mhu9Iq_surface",
|
|
4751
|
+
"dsh-smooth-stream-agent-row-in": "Mhu9Iq_dsh-smooth-stream-agent-row-in"
|
|
3856
4752
|
};
|
|
3857
4753
|
//#endregion
|
|
3858
4754
|
//#region src/client/TypewriterToolNodeView.tsx
|
|
@@ -4030,8 +4926,8 @@ window.__ModuleLoader__.load({
|
|
|
4030
4926
|
};
|
|
4031
4927
|
}
|
|
4032
4928
|
//#endregion
|
|
4033
|
-
//#region \0dsh-css:/Users/dzlin/
|
|
4034
|
-
const css$1 = ".
|
|
4929
|
+
//#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
|
|
4930
|
+
const css$1 = ".v5eDwW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.v5eDwW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.v5eDwW_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.v5eDwW_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.v5eDwW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.v5eDwW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.v5eDwW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.v5eDwW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.v5eDwW_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.v5eDwW_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.v5eDwW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.v5eDwW_chevronOpen{transform:rotate(180deg)}.v5eDwW_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.v5eDwW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.v5eDwW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.v5eDwW_field+.v5eDwW_field{border-top:1px solid var(--dsw-alias-border-l2)}.v5eDwW_fieldDisabled{opacity:.5}.v5eDwW_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.v5eDwW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.v5eDwW_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.v5eDwW_toggle:disabled{cursor:default}.v5eDwW_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.v5eDwW_choiceRow{flex-wrap:wrap;gap:6px 14px;margin:2px 0 0;padding:0;display:flex}.v5eDwW_choice{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:5px;font-size:12px;line-height:1.5;display:flex}.v5eDwW_choiceInput{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.v5eDwW_choice:has(.v5eDwW_choiceInput:disabled){cursor:default;opacity:.55}.v5eDwW_choiceInput:disabled{cursor:default}.v5eDwW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.v5eDwW_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.v5eDwW_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.v5eDwW_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.v5eDwW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.v5eDwW_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.v5eDwW_save,.v5eDwW_discard,.v5eDwW_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.v5eDwW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.v5eDwW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.v5eDwW_discard:hover:not(:disabled),.v5eDwW_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.v5eDwW_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.v5eDwW_discard:disabled,.v5eDwW_save:disabled,.v5eDwW_update:disabled{opacity:.4;cursor:default}.v5eDwW_discard:focus-visible,.v5eDwW_save:focus-visible,.v5eDwW_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
|
|
4035
4931
|
const tagId$1 = "dsh-smooth-stream/SmoothStreamCard.module.css";
|
|
4036
4932
|
if (typeof document !== "undefined") {
|
|
4037
4933
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
|
|
@@ -4044,35 +4940,35 @@ window.__ModuleLoader__.load({
|
|
|
4044
4940
|
tag.textContent = css$1;
|
|
4045
4941
|
}
|
|
4046
4942
|
var SmoothStreamCard_module_css_default = {
|
|
4047
|
-
"
|
|
4048
|
-
"
|
|
4049
|
-
"
|
|
4050
|
-
"
|
|
4051
|
-
"
|
|
4052
|
-
"
|
|
4053
|
-
"
|
|
4054
|
-
"
|
|
4055
|
-
"
|
|
4056
|
-
"
|
|
4057
|
-
"
|
|
4058
|
-
"
|
|
4059
|
-
"
|
|
4060
|
-
"
|
|
4061
|
-
"
|
|
4062
|
-
"
|
|
4063
|
-
"
|
|
4064
|
-
"
|
|
4065
|
-
"
|
|
4066
|
-
"
|
|
4067
|
-
"
|
|
4068
|
-
"
|
|
4069
|
-
"
|
|
4070
|
-
"
|
|
4071
|
-
"
|
|
4072
|
-
"
|
|
4073
|
-
"label": "
|
|
4074
|
-
"
|
|
4075
|
-
"
|
|
4943
|
+
"footer": "v5eDwW_footer",
|
|
4944
|
+
"name": "v5eDwW_name",
|
|
4945
|
+
"body": "v5eDwW_body",
|
|
4946
|
+
"fieldHead": "v5eDwW_fieldHead",
|
|
4947
|
+
"fieldDisabled": "v5eDwW_fieldDisabled",
|
|
4948
|
+
"choice": "v5eDwW_choice",
|
|
4949
|
+
"chevronOpen": "v5eDwW_chevronOpen",
|
|
4950
|
+
"update": "v5eDwW_update",
|
|
4951
|
+
"description": "v5eDwW_description",
|
|
4952
|
+
"card": "v5eDwW_card",
|
|
4953
|
+
"pending": "v5eDwW_pending",
|
|
4954
|
+
"updateRow": "v5eDwW_updateRow",
|
|
4955
|
+
"readOnly": "v5eDwW_readOnly",
|
|
4956
|
+
"chevron": "v5eDwW_chevron",
|
|
4957
|
+
"cardOpen": "v5eDwW_cardOpen",
|
|
4958
|
+
"headText": "v5eDwW_headText",
|
|
4959
|
+
"choiceRow": "v5eDwW_choiceRow",
|
|
4960
|
+
"failure": "v5eDwW_failure",
|
|
4961
|
+
"failed": "v5eDwW_failed",
|
|
4962
|
+
"header": "v5eDwW_header",
|
|
4963
|
+
"save": "v5eDwW_save",
|
|
4964
|
+
"toggle": "v5eDwW_toggle",
|
|
4965
|
+
"field": "v5eDwW_field",
|
|
4966
|
+
"choiceInput": "v5eDwW_choiceInput",
|
|
4967
|
+
"version": "v5eDwW_version",
|
|
4968
|
+
"updateCopy": "v5eDwW_updateCopy",
|
|
4969
|
+
"label": "v5eDwW_label",
|
|
4970
|
+
"discard": "v5eDwW_discard",
|
|
4971
|
+
"hint": "v5eDwW_hint"
|
|
4076
4972
|
};
|
|
4077
4973
|
//#endregion
|
|
4078
4974
|
//#region src/client/SmoothStreamCard.tsx
|
|
@@ -4117,7 +5013,7 @@ window.__ModuleLoader__.load({
|
|
|
4117
5013
|
className: SmoothStreamCard_module_css_default.pending,
|
|
4118
5014
|
children: t("unsaved")
|
|
4119
5015
|
}) : null,
|
|
4120
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: open ? `${SmoothStreamCard_module_css_default.chevron} ${SmoothStreamCard_module_css_default.chevronOpen}` : SmoothStreamCard_module_css_default.chevron })
|
|
4121
5017
|
]
|
|
4122
5018
|
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4123
5019
|
className: SmoothStreamCard_module_css_default.body,
|
|
@@ -4260,6 +5156,57 @@ window.__ModuleLoader__.load({
|
|
|
4260
5156
|
})
|
|
4261
5157
|
]
|
|
4262
5158
|
}),
|
|
5159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5160
|
+
className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
|
|
5161
|
+
children: [
|
|
5162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5163
|
+
className: SmoothStreamCard_module_css_default.fieldHead,
|
|
5164
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5165
|
+
className: SmoothStreamCard_module_css_default.label,
|
|
5166
|
+
children: t("preset")
|
|
5167
|
+
})
|
|
5168
|
+
}),
|
|
5169
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5170
|
+
className: SmoothStreamCard_module_css_default.hint,
|
|
5171
|
+
children: t("presetHint")
|
|
5172
|
+
}),
|
|
5173
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5174
|
+
className: SmoothStreamCard_module_css_default.choiceRow,
|
|
5175
|
+
role: "radiogroup",
|
|
5176
|
+
"aria-label": t("preset"),
|
|
5177
|
+
children: [
|
|
5178
|
+
[
|
|
5179
|
+
"realtime",
|
|
5180
|
+
"presetRealtime",
|
|
5181
|
+
"presetRealtimeHint"
|
|
5182
|
+
],
|
|
5183
|
+
[
|
|
5184
|
+
"balanced",
|
|
5185
|
+
"presetBalanced",
|
|
5186
|
+
"presetBalancedHint"
|
|
5187
|
+
],
|
|
5188
|
+
[
|
|
5189
|
+
"silky",
|
|
5190
|
+
"presetSilky",
|
|
5191
|
+
"presetSilkyHint"
|
|
5192
|
+
]
|
|
5193
|
+
].map(([value, label, hint]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5194
|
+
className: SmoothStreamCard_module_css_default.choice,
|
|
5195
|
+
title: t(hint),
|
|
5196
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5197
|
+
type: "radio",
|
|
5198
|
+
className: SmoothStreamCard_module_css_default.choiceInput,
|
|
5199
|
+
name: "smooth-stream-preset",
|
|
5200
|
+
checked: state.preset === value,
|
|
5201
|
+
disabled: !state.writable || state.saving || !state.enabled,
|
|
5202
|
+
onChange: () => {
|
|
5203
|
+
props.edit({ preset: value });
|
|
5204
|
+
}
|
|
5205
|
+
}), t(label)]
|
|
5206
|
+
}, value))
|
|
5207
|
+
})
|
|
5208
|
+
]
|
|
5209
|
+
}),
|
|
4263
5210
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4264
5211
|
className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
|
|
4265
5212
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -4321,7 +5268,7 @@ window.__ModuleLoader__.load({
|
|
|
4321
5268
|
onClick: props.upgrade,
|
|
4322
5269
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4323
5270
|
"aria-hidden": "true",
|
|
4324
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5271
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, {})
|
|
4325
5272
|
}), t(state.upgrading ? "updating" : "update")]
|
|
4326
5273
|
})]
|
|
4327
5274
|
}),
|
|
@@ -4402,10 +5349,11 @@ window.__ModuleLoader__.load({
|
|
|
4402
5349
|
hooks: { smoothStreamCard: this.store },
|
|
4403
5350
|
edit: (patch) => {
|
|
4404
5351
|
if (this.saving) return;
|
|
4405
|
-
if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.motionPreference !== void 0 || patch.thinkAutoExpand !== void 0 || patch.logarithmicFade !== void 0) this.stagedBase = {
|
|
5352
|
+
if (patch.enabled !== void 0 || patch.controlScroll !== void 0 || patch.preset !== void 0 || patch.motionPreference !== void 0 || patch.thinkAutoExpand !== void 0 || patch.logarithmicFade !== void 0) this.stagedBase = {
|
|
4406
5353
|
...this.baseValues(),
|
|
4407
5354
|
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
4408
5355
|
...patch.controlScroll === void 0 ? {} : { controlScroll: patch.controlScroll },
|
|
5356
|
+
...patch.preset === void 0 ? {} : { preset: patch.preset },
|
|
4409
5357
|
...patch.motionPreference === void 0 ? {} : { motionPreference: patch.motionPreference },
|
|
4410
5358
|
...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand },
|
|
4411
5359
|
...patch.logarithmicFade === void 0 ? {} : { logarithmicFade: patch.logarithmicFade }
|
|
@@ -4461,6 +5409,7 @@ window.__ModuleLoader__.load({
|
|
|
4461
5409
|
return this.stagedBase ?? {
|
|
4462
5410
|
enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
|
|
4463
5411
|
controlScroll: this.loaded?.controlScroll ?? DEFAULT_STREAM_SETTINGS.controlScroll,
|
|
5412
|
+
preset: this.loaded?.preset ?? DEFAULT_STREAM_SETTINGS.preset,
|
|
4464
5413
|
motionPreference: this.loaded?.motionPreference ?? DEFAULT_STREAM_SETTINGS.motionPreference,
|
|
4465
5414
|
thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
4466
5415
|
logarithmicFade: this.loaded?.logarithmicFade ?? DEFAULT_STREAM_SETTINGS.logarithmicFade
|
|
@@ -4583,13 +5532,18 @@ window.__ModuleLoader__.load({
|
|
|
4583
5532
|
"npm",
|
|
4584
5533
|
"development",
|
|
4585
5534
|
"unmanaged"
|
|
4586
|
-
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.controlScroll !== "boolean" || ![
|
|
5535
|
+
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.controlScroll !== "boolean" || data.preset !== void 0 && ![
|
|
5536
|
+
"realtime",
|
|
5537
|
+
"balanced",
|
|
5538
|
+
"silky"
|
|
5539
|
+
].includes(data.preset) || ![
|
|
4587
5540
|
"auto",
|
|
4588
5541
|
"force-smooth",
|
|
4589
5542
|
"force-reduced"
|
|
4590
5543
|
].includes(data.motionPreference) || typeof data.thinkAutoExpand !== "boolean" || data.logarithmicFade !== void 0 && typeof data.logarithmicFade !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
|
|
4591
5544
|
return {
|
|
4592
5545
|
...data,
|
|
5546
|
+
preset: data.preset ?? "balanced",
|
|
4593
5547
|
logarithmicFade: data.logarithmicFade ?? true
|
|
4594
5548
|
};
|
|
4595
5549
|
}
|
|
@@ -4620,6 +5574,7 @@ window.__ModuleLoader__.load({
|
|
|
4620
5574
|
return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, {
|
|
4621
5575
|
enabled: settings.enabled,
|
|
4622
5576
|
controlScroll: settings.controlScroll,
|
|
5577
|
+
preset: settings.preset,
|
|
4623
5578
|
motionPreference: settings.motionPreference,
|
|
4624
5579
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
4625
5580
|
logarithmicFade: settings.logarithmicFade,
|
|
@@ -4644,8 +5599,8 @@ window.__ModuleLoader__.load({
|
|
|
4644
5599
|
};
|
|
4645
5600
|
}
|
|
4646
5601
|
//#endregion
|
|
4647
|
-
//#region \0dsh-css:/Users/dzlin/
|
|
4648
|
-
const css = ".
|
|
5602
|
+
//#region \0dsh-css:/Users/dzlin/.codex/worktrees/pr-39-review/dsh-smooth-stream/src/client/DebugPanel.module.css.mjs
|
|
5603
|
+
const css = "._6TTfZq_trigger,._6TTfZq_iconButton{appearance:none;width:32px;height:32px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}._6TTfZq_trigger:hover,._6TTfZq_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._6TTfZq_iconButton:disabled{opacity:.45;cursor:not-allowed}._6TTfZq_triggerActive{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-business-primary)}._6TTfZq_trigger:focus-visible,._6TTfZq_iconButton:focus-visible,._6TTfZq_secondaryButton:focus-visible,._6TTfZq_primaryButton:focus-visible,._6TTfZq_number:focus-visible,._6TTfZq_range:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}._6TTfZq_panel{z-index:30;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);width:min(328px,100vw - 32px);min-height:280px;box-shadow:0 12px 32px color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent);color:var(--dsw-alias-label-primary);border-radius:8px;flex-direction:column;display:flex;position:fixed;top:88px;bottom:16px;right:16px;overflow:hidden}._6TTfZq_panelHeader{border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:8px;min-height:48px;padding:0 8px 0 14px;display:flex}._6TTfZq_statusDot{background:var(--dsw-alias-label-caption);border-radius:50%;flex:none;width:7px;height:7px}._6TTfZq_statusLive{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}._6TTfZq_title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px;overflow:hidden}._6TTfZq_state,._6TTfZq_unsaved{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:10px;line-height:16px}._6TTfZq_unsaved{color:var(--dsw-alias-state-warn-primary)}._6TTfZq_scrollArea{overscroll-behavior:contain;min-height:0;overflow-y:auto}._6TTfZq_section{padding:14px}._6TTfZq_guide{border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin:0;padding:12px 14px;font-size:11px;line-height:1.55}._6TTfZq_section+._6TTfZq_section{border-top:1px solid var(--dsw-alias-border-l2)}._6TTfZq_section h2{color:var(--dsw-alias-label-secondary);margin:0 0 10px;font-size:12px;font-weight:600;line-height:18px}._6TTfZq_metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:0 16px;margin:0;display:grid}._6TTfZq_metric{border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent);justify-content:space-between;align-items:baseline;gap:8px;min-width:0;padding:5px 0;display:flex}._6TTfZq_metric dt{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;overflow:hidden}._6TTfZq_metric dd{min-width:0;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:16px;overflow:hidden}._6TTfZq_metric dd[data-tone=good]{color:var(--dsw-alias-state-success-primary)}._6TTfZq_metric dd[data-tone=warn]{color:var(--dsw-alias-state-warn-primary)}._6TTfZq_control{flex-direction:column;gap:5px;padding:6px 0;display:flex}._6TTfZq_controlHead{color:var(--dsw-alias-label-secondary);justify-content:space-between;align-items:center;gap:10px;font-size:11px;line-height:18px;display:flex}._6TTfZq_controlLabel{align-items:center;gap:4px;min-width:0;display:inline-flex}._6TTfZq_infoButton{appearance:none;width:20px;height:20px;color:var(--dsw-alias-label-tertiary);cursor:help;background:0 0;border:0;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex}._6TTfZq_infoButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._6TTfZq_infoButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}._6TTfZq_numberWrap{flex:none;align-items:center;gap:4px;display:inline-flex}._6TTfZq_number{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:68px;height:26px;color:var(--dsw-alias-label-primary);font:inherit;font-variant-numeric:tabular-nums;text-align:right;border-radius:5px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}._6TTfZq_unit{width:30px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px}._6TTfZq_range{width:100%;height:18px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer;margin:0}._6TTfZq_range:disabled,._6TTfZq_number:disabled{opacity:.5;cursor:default}._6TTfZq_footer{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);flex:none;align-items:center;gap:6px;min-height:50px;padding:8px 10px;display:flex}._6TTfZq_footerSpacer{flex:1}._6TTfZq_secondaryButton,._6TTfZq_primaryButton{appearance:none;border:1px solid var(--dsw-alias-border-l2);min-height:30px;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:6px;justify-content:center;align-items:center;gap:5px;padding:4px 9px;font-size:11px;line-height:18px;display:inline-flex}._6TTfZq_primaryButton{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:#0000}._6TTfZq_secondaryButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}._6TTfZq_secondaryButton:disabled,._6TTfZq_primaryButton:disabled{opacity:.4;cursor:default}._6TTfZq_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}@media (width<=900px){._6TTfZq_trigger,._6TTfZq_iconButton{width:44px;height:44px}._6TTfZq_panel{top:auto;right:12px;bottom:max(12px, env(safe-area-inset-bottom));width:auto;max-height:min(70dvh,640px);left:12px}._6TTfZq_secondaryButton,._6TTfZq_primaryButton{min-height:44px}}@media (prefers-reduced-motion:reduce){._6TTfZq_statusLive{box-shadow:none}}";
|
|
4649
5604
|
const tagId = "dsh-smooth-stream/DebugPanel.module.css";
|
|
4650
5605
|
if (typeof document !== "undefined") {
|
|
4651
5606
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
@@ -4658,37 +5613,49 @@ window.__ModuleLoader__.load({
|
|
|
4658
5613
|
tag.textContent = css;
|
|
4659
5614
|
}
|
|
4660
5615
|
var DebugPanel_module_css_default = {
|
|
4661
|
-
"
|
|
4662
|
-
"
|
|
4663
|
-
"
|
|
4664
|
-
"
|
|
4665
|
-
"
|
|
4666
|
-
"
|
|
4667
|
-
"
|
|
4668
|
-
"
|
|
4669
|
-
"
|
|
4670
|
-
"
|
|
4671
|
-
"
|
|
4672
|
-
"
|
|
4673
|
-
"
|
|
4674
|
-
"
|
|
4675
|
-
"
|
|
4676
|
-
"
|
|
4677
|
-
"
|
|
4678
|
-
"
|
|
4679
|
-
"
|
|
4680
|
-
"
|
|
4681
|
-
"
|
|
4682
|
-
"
|
|
4683
|
-
"
|
|
4684
|
-
"
|
|
4685
|
-
"
|
|
4686
|
-
"
|
|
4687
|
-
"
|
|
4688
|
-
"
|
|
5616
|
+
"footerSpacer": "_6TTfZq_footerSpacer",
|
|
5617
|
+
"primaryButton": "_6TTfZq_primaryButton",
|
|
5618
|
+
"statusDot": "_6TTfZq_statusDot",
|
|
5619
|
+
"section": "_6TTfZq_section",
|
|
5620
|
+
"numberWrap": "_6TTfZq_numberWrap",
|
|
5621
|
+
"unsaved": "_6TTfZq_unsaved",
|
|
5622
|
+
"trigger": "_6TTfZq_trigger",
|
|
5623
|
+
"metric": "_6TTfZq_metric",
|
|
5624
|
+
"panel": "_6TTfZq_panel",
|
|
5625
|
+
"range": "_6TTfZq_range",
|
|
5626
|
+
"iconButton": "_6TTfZq_iconButton",
|
|
5627
|
+
"panelHeader": "_6TTfZq_panelHeader",
|
|
5628
|
+
"footer": "_6TTfZq_footer",
|
|
5629
|
+
"controlLabel": "_6TTfZq_controlLabel",
|
|
5630
|
+
"triggerActive": "_6TTfZq_triggerActive",
|
|
5631
|
+
"number": "_6TTfZq_number",
|
|
5632
|
+
"statusLive": "_6TTfZq_statusLive",
|
|
5633
|
+
"controlHead": "_6TTfZq_controlHead",
|
|
5634
|
+
"guide": "_6TTfZq_guide",
|
|
5635
|
+
"unit": "_6TTfZq_unit",
|
|
5636
|
+
"control": "_6TTfZq_control",
|
|
5637
|
+
"state": "_6TTfZq_state",
|
|
5638
|
+
"scrollArea": "_6TTfZq_scrollArea",
|
|
5639
|
+
"visuallyHidden": "_6TTfZq_visuallyHidden",
|
|
5640
|
+
"title": "_6TTfZq_title",
|
|
5641
|
+
"metrics": "_6TTfZq_metrics",
|
|
5642
|
+
"secondaryButton": "_6TTfZq_secondaryButton",
|
|
5643
|
+
"infoButton": "_6TTfZq_infoButton"
|
|
4689
5644
|
};
|
|
4690
5645
|
//#endregion
|
|
4691
5646
|
//#region src/client/DebugPanel.tsx
|
|
5647
|
+
/**
|
|
5648
|
+
* Locale key per terminal-follow phase. `terminal-drain` and `host-cascade`
|
|
5649
|
+
* are the two that must never be confused: the first is the engine finishing
|
|
5650
|
+
* ordinary text, the second is a hostile host commit that needs temporary
|
|
5651
|
+
* credit.
|
|
5652
|
+
*/
|
|
5653
|
+
const PHASE_LABEL = {
|
|
5654
|
+
"live": "debugPhaseLive",
|
|
5655
|
+
"terminal-drain": "debugPhaseDrain",
|
|
5656
|
+
"host-cascade": "debugPhaseCascade",
|
|
5657
|
+
"natural": "debugPhaseNatural"
|
|
5658
|
+
};
|
|
4692
5659
|
const REVEAL_CONTROLS = [
|
|
4693
5660
|
{
|
|
4694
5661
|
key: "revealScale",
|
|
@@ -4815,7 +5782,7 @@ window.__ModuleLoader__.load({
|
|
|
4815
5782
|
type: "button",
|
|
4816
5783
|
"aria-label": label,
|
|
4817
5784
|
title: t(control.tip),
|
|
4818
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5785
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconQuestion, {})
|
|
4819
5786
|
})
|
|
4820
5787
|
})]
|
|
4821
5788
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -4911,7 +5878,7 @@ window.__ModuleLoader__.load({
|
|
|
4911
5878
|
onClick: () => {
|
|
4912
5879
|
copyDiagnostics();
|
|
4913
5880
|
},
|
|
4914
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5881
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCopy, {})
|
|
4915
5882
|
}),
|
|
4916
5883
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4917
5884
|
className: DebugPanel_module_css_default.iconButton,
|
|
@@ -4924,7 +5891,7 @@ window.__ModuleLoader__.load({
|
|
|
4924
5891
|
props.edit({ debugEnabled: false });
|
|
4925
5892
|
props.save();
|
|
4926
5893
|
},
|
|
4927
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5894
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconClose, {})
|
|
4928
5895
|
}),
|
|
4929
5896
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4930
5897
|
className: DebugPanel_module_css_default.visuallyHidden,
|
|
@@ -4992,6 +5959,28 @@ window.__ModuleLoader__.load({
|
|
|
4992
5959
|
label: t("debugCapacity"),
|
|
4993
5960
|
value: `${fixed(metrics.followCapacityPx)} px`
|
|
4994
5961
|
}),
|
|
5962
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
5963
|
+
label: t("debugTerminalPhase"),
|
|
5964
|
+
value: t(PHASE_LABEL[metrics.followTerminalPhase]),
|
|
5965
|
+
tone: metrics.followTerminalPhase === "host-cascade" ? "warn" : void 0
|
|
5966
|
+
}),
|
|
5967
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
5968
|
+
label: t("debugOwnedRunway"),
|
|
5969
|
+
value: `${fixed(metrics.followRunwayPx)} px`
|
|
5970
|
+
}),
|
|
5971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
5972
|
+
label: t("debugTerminalBudget"),
|
|
5973
|
+
value: `${fixed(metrics.followTerminalBudgetPx)} px`
|
|
5974
|
+
}),
|
|
5975
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
5976
|
+
label: t("debugBaselineShift"),
|
|
5977
|
+
value: `${fixed(metrics.followBaselineShiftPx)} px`
|
|
5978
|
+
}),
|
|
5979
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
5980
|
+
label: t("debugAnchorDelta"),
|
|
5981
|
+
value: metrics.followAnchorDeltaPx === null ? "—" : `${fixed(metrics.followAnchorDeltaPx)} px`,
|
|
5982
|
+
tone: (metrics.followAnchorDeltaPx ?? 0) > .35 ? "warn" : void 0
|
|
5983
|
+
}),
|
|
4995
5984
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
4996
5985
|
label: t("debugAppliedScale"),
|
|
4997
5986
|
value: `${fixed(metrics.followRevealScale, 2)}x`
|
|
@@ -5037,7 +6026,7 @@ window.__ModuleLoader__.load({
|
|
|
5037
6026
|
type: "button",
|
|
5038
6027
|
disabled: !state.writable,
|
|
5039
6028
|
onClick: props.reset,
|
|
5040
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
6029
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefreshSmall, {}), t("debugReset")]
|
|
5041
6030
|
}),
|
|
5042
6031
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: DebugPanel_module_css_default.footerSpacer }),
|
|
5043
6032
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -5067,7 +6056,7 @@ window.__ModuleLoader__.load({
|
|
|
5067
6056
|
onClick: () => {
|
|
5068
6057
|
setOpen((current) => !current);
|
|
5069
6058
|
},
|
|
5070
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
6059
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCode, {})
|
|
5071
6060
|
}), typeof document === "undefined" || panel === null ? null : (0, react_dom.createPortal)(panel, document.body)] });
|
|
5072
6061
|
}
|
|
5073
6062
|
//#endregion
|
|
@@ -5075,6 +6064,37 @@ window.__ModuleLoader__.load({
|
|
|
5075
6064
|
/** Locale bundles for the smooth-stream plugin configuration card. */
|
|
5076
6065
|
/** Dictionary namespace owned by this plugin's settings card. */
|
|
5077
6066
|
const NS = "settings.smoothStream";
|
|
6067
|
+
/**
|
|
6068
|
+
* Namespace owning the conversation keys THIS PLUGIN must be able to render
|
|
6069
|
+
* but the Harness does not always provide.
|
|
6070
|
+
*
|
|
6071
|
+
* The plugin consumes keys from two Harness namespaces, and which one owns a
|
|
6072
|
+
* given key is not stable across Harness versions:
|
|
6073
|
+
*
|
|
6074
|
+
* - `conversation` (`dsh-client-ui-conversation`) owns the `image.*` family in
|
|
6075
|
+
* every version, and also owned `message.stopped`, `message.unknownBlock`
|
|
6076
|
+
* and `json.truncated` before those moved to `chat`.
|
|
6077
|
+
* - `chat` (`dsh-client-ui-chat`, present from 0.1.5 on) owns `message.think`
|
|
6078
|
+
* and now owns the three keys above. It does not exist at all in rc.6, the
|
|
6079
|
+
* version this package still pins in devDependencies.
|
|
6080
|
+
*
|
|
6081
|
+
* Neither namespace alone can serve every key this plugin renders, so
|
|
6082
|
+
* `index.ts` layers them. This plugin-owned namespace is the final layer; it
|
|
6083
|
+
* exists because a plugin may NOT register into `chat` or `conversation` —
|
|
6084
|
+
* `LocaleRuntime.register` throws when the (namespace, locale) pair already
|
|
6085
|
+
* has an owner, and the Harness owns both.
|
|
6086
|
+
*/
|
|
6087
|
+
const CHAT_NS = "smoothStream.chat";
|
|
6088
|
+
/** English fallback for {@link SmoothStreamChatKey}. */
|
|
6089
|
+
const chatEn = {
|
|
6090
|
+
"message.think": "Think",
|
|
6091
|
+
"image.serviceUnavailable": "Image service unavailable"
|
|
6092
|
+
};
|
|
6093
|
+
/** Simplified Chinese fallback for {@link SmoothStreamChatKey}. */
|
|
6094
|
+
const chatZh = {
|
|
6095
|
+
"message.think": "思考",
|
|
6096
|
+
"image.serviceUnavailable": "图片服务不可用"
|
|
6097
|
+
};
|
|
5078
6098
|
/** English copy. */
|
|
5079
6099
|
const en = {
|
|
5080
6100
|
title: "Smooth stream",
|
|
@@ -5083,6 +6103,14 @@ window.__ModuleLoader__.load({
|
|
|
5083
6103
|
enabledHint: "Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.",
|
|
5084
6104
|
controlScroll: "Take over scrolling",
|
|
5085
6105
|
controlScrollHint: "On by default; smooth-stream writes the conversation scroll position. Turn off to leave bottom-follow to Harness.",
|
|
6106
|
+
preset: "Pacing preset",
|
|
6107
|
+
presetHint: "Smoothing cadence profile for text reveal and glide.",
|
|
6108
|
+
presetRealtime: "Realtime",
|
|
6109
|
+
presetRealtimeHint: "Aggressive cadence, low latency, tracks output closely.",
|
|
6110
|
+
presetBalanced: "Balanced",
|
|
6111
|
+
presetBalancedHint: "Default balance between pace and visual rhythm.",
|
|
6112
|
+
presetSilky: "Silky",
|
|
6113
|
+
presetSilkyHint: "Higher backlog buffer, gentle EMA damping, maximum fluidity.",
|
|
5086
6114
|
motionPreference: "Motion",
|
|
5087
6115
|
motionPreferenceHint: "How streaming responds to the system reduce-motion setting.",
|
|
5088
6116
|
motionAuto: "Follow system",
|
|
@@ -5126,6 +6154,15 @@ window.__ModuleLoader__.load({
|
|
|
5126
6154
|
debugReserve: "Reserve",
|
|
5127
6155
|
debugCapacity: "Capacity",
|
|
5128
6156
|
debugAppliedScale: "Applied scale",
|
|
6157
|
+
debugTerminalPhase: "Terminal phase",
|
|
6158
|
+
debugOwnedRunway: "Owned runway",
|
|
6159
|
+
debugTerminalBudget: "Terminal budget",
|
|
6160
|
+
debugBaselineShift: "Baseline shift",
|
|
6161
|
+
debugAnchorDelta: "Anchor Δ",
|
|
6162
|
+
debugPhaseLive: "live",
|
|
6163
|
+
debugPhaseDrain: "terminal-drain",
|
|
6164
|
+
debugPhaseCascade: "host-cascade",
|
|
6165
|
+
debugPhaseNatural: "natural",
|
|
5129
6166
|
debugRevealMultiplier: "Reveal multiplier",
|
|
5130
6167
|
debugQueuePressure: "Queue pressure",
|
|
5131
6168
|
debugMaxReveal: "Maximum reveal",
|
|
@@ -5172,6 +6209,14 @@ window.__ModuleLoader__.load({
|
|
|
5172
6209
|
enabledHint: "由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。",
|
|
5173
6210
|
controlScroll: "接管滚动",
|
|
5174
6211
|
controlScrollHint: "默认开启,由丝滑流式写入会话滚动位置;关闭后贴底滚动交给 Harness。",
|
|
6212
|
+
preset: "手感预设",
|
|
6213
|
+
presetHint: "文字吐字节奏与视口跟随阻尼风格。",
|
|
6214
|
+
presetRealtime: "激进快速 (realtime)",
|
|
6215
|
+
presetRealtimeHint: "缓冲低、流速快,紧跟模型生成。",
|
|
6216
|
+
presetBalanced: "均衡自然 (balanced)",
|
|
6217
|
+
presetBalancedHint: "官方默认,平衡流速与节奏感。",
|
|
6218
|
+
presetSilky: "极致丝滑 (silky)",
|
|
6219
|
+
presetSilkyHint: "字符积压缓冲加大、阻尼更高、EMA 平滑度更好,视觉呼吸感最强。",
|
|
5175
6220
|
motionPreference: "动效偏好",
|
|
5176
6221
|
motionPreferenceHint: "流式渲染如何响应系统的「减少动态效果」设置。",
|
|
5177
6222
|
motionAuto: "跟随系统",
|
|
@@ -5215,6 +6260,15 @@ window.__ModuleLoader__.load({
|
|
|
5215
6260
|
debugReserve: "预留空间",
|
|
5216
6261
|
debugCapacity: "安全容量",
|
|
5217
6262
|
debugAppliedScale: "实际倍率",
|
|
6263
|
+
debugTerminalPhase: "收尾阶段",
|
|
6264
|
+
debugOwnedRunway: "实体留白",
|
|
6265
|
+
debugTerminalBudget: "收尾预算",
|
|
6266
|
+
debugBaselineShift: "基线位移",
|
|
6267
|
+
debugAnchorDelta: "锚点位移",
|
|
6268
|
+
debugPhaseLive: "流式中",
|
|
6269
|
+
debugPhaseDrain: "收尾排空",
|
|
6270
|
+
debugPhaseCascade: "宿主交接",
|
|
6271
|
+
debugPhaseNatural: "自然态",
|
|
5218
6272
|
debugRevealMultiplier: "揭示倍率",
|
|
5219
6273
|
debugQueuePressure: "队列压力",
|
|
5220
6274
|
debugMaxReveal: "最大揭示速度",
|
|
@@ -5275,12 +6329,33 @@ window.__ModuleLoader__.load({
|
|
|
5275
6329
|
* generic follow boundary. This is deliberately keyed by the owner that
|
|
5276
6330
|
* provides the renderer, not by individual tool names, so new Context,
|
|
5277
6331
|
* Command, and Tool rows are covered automatically.
|
|
6332
|
+
*
|
|
6333
|
+
* Host chrome and controllers are excluded on a different rule: `turn-process`
|
|
6334
|
+
* (the completed-turn process summary control) and `turn-tail` (the turn usage
|
|
6335
|
+
* footer) are not Agent output. The Host renders them as bare controls and
|
|
6336
|
+
* flips their visibility itself, so wrapping them has three measured costs and
|
|
6337
|
+
* no benefit:
|
|
6338
|
+
*
|
|
6339
|
+
* 1. The wrapper element defeats ChatView's `.flowItem:empty { display: none }`
|
|
6340
|
+
* rule. A `turn-process` renderer returns null whenever the Host's fold is
|
|
6341
|
+
* unavailable (e.g. `processWindowReady === false`), and that null render is
|
|
6342
|
+
* exactly what the `:empty` rule exists to erase; with a wrapper the row
|
|
6343
|
+
* stays in flow and still consumes the 16px column gap, painting an empty
|
|
6344
|
+
* band where the fold control should be.
|
|
6345
|
+
* 2. The summary label (`N tool calls · M messages`) is not streamed model
|
|
6346
|
+
* text, so pushing it through the character-reveal engine is wrong by
|
|
6347
|
+
* construction.
|
|
6348
|
+
* 3. The row would inherit the generic entrance animation (opacity + clip-path)
|
|
6349
|
+
* on a row the Host may mount and hide inside a single commit.
|
|
5278
6350
|
*/
|
|
5279
6351
|
const SKIP_WRAP = /* @__PURE__ */ new Set([
|
|
5280
6352
|
"assistant-step",
|
|
5281
6353
|
"user",
|
|
5282
6354
|
"steering",
|
|
5283
|
-
"command-input"
|
|
6355
|
+
"command-input",
|
|
6356
|
+
"turn-process",
|
|
6357
|
+
"turn-tail",
|
|
6358
|
+
"tool-call"
|
|
5284
6359
|
]);
|
|
5285
6360
|
/** React function/class or an exotic component such as memo/forwardRef/lazy. */
|
|
5286
6361
|
function isWrappableComponent(value) {
|
|
@@ -5376,7 +6451,7 @@ window.__ModuleLoader__.load({
|
|
|
5376
6451
|
refresh() {
|
|
5377
6452
|
const next = this.read();
|
|
5378
6453
|
const pending = this.card?.getSnapshot().status === "loading";
|
|
5379
|
-
if (pending === this.pending && next.enabled === this.value.enabled && next.controlScroll === this.value.controlScroll && next.motionPreference === this.value.motionPreference && next.thinkAutoExpand === this.value.thinkAutoExpand && next.logarithmicFade === this.value.logarithmicFade && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
|
|
6454
|
+
if (pending === this.pending && next.enabled === this.value.enabled && next.controlScroll === this.value.controlScroll && next.preset === this.value.preset && next.motionPreference === this.value.motionPreference && next.thinkAutoExpand === this.value.thinkAutoExpand && next.logarithmicFade === this.value.logarithmicFade && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
|
|
5380
6455
|
this.pending = pending;
|
|
5381
6456
|
this.value = next;
|
|
5382
6457
|
for (const listener of this.listeners) listener();
|
|
@@ -5385,6 +6460,15 @@ window.__ModuleLoader__.load({
|
|
|
5385
6460
|
takeoverEnabled() {
|
|
5386
6461
|
return !this.pending && this.value.enabled;
|
|
5387
6462
|
}
|
|
6463
|
+
/**
|
|
6464
|
+
* True only once an attached settings service has produced its loaded values.
|
|
6465
|
+
* Until then — and when no settings service is composed at all — `value` is
|
|
6466
|
+
* still the shared client default, so callers that must not let that default
|
|
6467
|
+
* shadow the install-time config have to ask before trusting it.
|
|
6468
|
+
*/
|
|
6469
|
+
ready() {
|
|
6470
|
+
return this.card?.getSnapshot().status === "ready";
|
|
6471
|
+
}
|
|
5388
6472
|
getSnapshot = () => this.value;
|
|
5389
6473
|
subscribe = (listener) => {
|
|
5390
6474
|
this.listeners.add(listener);
|
|
@@ -5408,6 +6492,49 @@ window.__ModuleLoader__.load({
|
|
|
5408
6492
|
const config = readBootConfig();
|
|
5409
6493
|
const settings = new SettingsCell();
|
|
5410
6494
|
const useControlScroll = () => (0, react.useSyncExternalStore)(settings.subscribe, () => settings.getSnapshot().controlScroll, () => settings.getSnapshot().controlScroll);
|
|
6495
|
+
/**
|
|
6496
|
+
* Layered `t` for the assistant renderer.
|
|
6497
|
+
*
|
|
6498
|
+
* The renderer's keys have no single owner: `conversation` owns the
|
|
6499
|
+
* `image.*` family in every Harness version, `chat` (0.1.5+) owns
|
|
6500
|
+
* `message.think`, and three `message.*` keys MOVED from `conversation` to
|
|
6501
|
+
* `chat` between the version this package pins and the current one. Binding
|
|
6502
|
+
* the slot to either namespace alone degrades a real slice of the UI to raw
|
|
6503
|
+
* keys, which is the defect this replaces.
|
|
6504
|
+
*
|
|
6505
|
+
* The order is deliberate: `conversation` first, so the pinned Harness keeps
|
|
6506
|
+
* resolving the keys it still owns there; then `chat` for what moved or is
|
|
6507
|
+
* new; then this plugin's own namespace for keys no Harness version
|
|
6508
|
+
* provides. A namespace that is not registered returns its key unchanged
|
|
6509
|
+
* (`LocaleRuntime.bind` does not throw), so an older Harness without `chat`
|
|
6510
|
+
* falls straight through.
|
|
6511
|
+
*
|
|
6512
|
+
* The reference is built once and held stable: the seat feeds a memoized
|
|
6513
|
+
* renderer, and a fresh identity per render would defeat that memoization.
|
|
6514
|
+
* Until the locale service arrives the seat's own binding stays in use.
|
|
6515
|
+
*/
|
|
6516
|
+
let assistantT;
|
|
6517
|
+
ctx.inject(["locale"], (localeCtx) => {
|
|
6518
|
+
localeCtx.effect(() => localeCtx.locale.register(CHAT_NS, {
|
|
6519
|
+
zh: chatZh,
|
|
6520
|
+
en: chatEn
|
|
6521
|
+
}), "dsh-smooth-stream: conversation fallback dictionary");
|
|
6522
|
+
const conversationT = localeCtx.locale.bind("conversation");
|
|
6523
|
+
const chatT = localeCtx.locale.bind("chat");
|
|
6524
|
+
const fallbackT = localeCtx.locale.bind(CHAT_NS);
|
|
6525
|
+
const merged = (key, params) => {
|
|
6526
|
+
const primary = conversationT(key, params);
|
|
6527
|
+
if (primary !== key) return primary;
|
|
6528
|
+
const secondary = chatT(key, params);
|
|
6529
|
+
if (secondary !== key) return secondary;
|
|
6530
|
+
return fallbackT(key, params);
|
|
6531
|
+
};
|
|
6532
|
+
const bound = merged;
|
|
6533
|
+
assistantT = bound;
|
|
6534
|
+
return () => {
|
|
6535
|
+
if (assistantT === bound) assistantT = void 0;
|
|
6536
|
+
};
|
|
6537
|
+
});
|
|
5411
6538
|
ctx.inject([
|
|
5412
6539
|
"slots",
|
|
5413
6540
|
"locale",
|
|
@@ -5470,8 +6597,9 @@ window.__ModuleLoader__.load({
|
|
|
5470
6597
|
const preferences = (0, react.useSyncExternalStore)(settings.subscribe, settings.getSnapshot, settings.getSnapshot);
|
|
5471
6598
|
return (0, react.createElement)(TypewriterAssistantNodeView, {
|
|
5472
6599
|
...props,
|
|
6600
|
+
...assistantT === void 0 ? {} : { t: assistantT },
|
|
5473
6601
|
mode: config.mode,
|
|
5474
|
-
preset: config.preset,
|
|
6602
|
+
preset: settings.ready() ? preferences.preset ?? config.preset : config.preset,
|
|
5475
6603
|
revealCharsPerSec: config.revealCharsPerSec,
|
|
5476
6604
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
5477
6605
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
@@ -5483,6 +6611,7 @@ window.__ModuleLoader__.load({
|
|
|
5483
6611
|
};
|
|
5484
6612
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
5485
6613
|
let releaseTakeover;
|
|
6614
|
+
let usedSwap = false;
|
|
5486
6615
|
const syncTakeover = () => {
|
|
5487
6616
|
if (!settings.takeoverEnabled()) {
|
|
5488
6617
|
releaseTakeover?.();
|
|
@@ -5491,7 +6620,16 @@ window.__ModuleLoader__.load({
|
|
|
5491
6620
|
}
|
|
5492
6621
|
if (releaseTakeover !== void 0) return;
|
|
5493
6622
|
const unwrap = wrapAgentChatRows(ctx, useControlScroll);
|
|
5494
|
-
const
|
|
6623
|
+
const builtin = ctx.slots.entries("conversation.chat.node").find((entry) => entry.options.key === "assistant-step" && entry.options.registrant !== "dsh-smooth-stream" && entry.component !== configured);
|
|
6624
|
+
let unshadow;
|
|
6625
|
+
if (builtin !== void 0 && isWrappableComponent(builtin.component)) {
|
|
6626
|
+
usedSwap = true;
|
|
6627
|
+
const inner = builtin.component;
|
|
6628
|
+
builtin.component = configured;
|
|
6629
|
+
unshadow = () => {
|
|
6630
|
+
if (builtin.component === configured) builtin.component = inner;
|
|
6631
|
+
};
|
|
6632
|
+
} else unshadow = ctx.slots.register({
|
|
5495
6633
|
name: "conversation.chat.node",
|
|
5496
6634
|
key: "assistant-step",
|
|
5497
6635
|
priority: -100,
|
|
@@ -5503,10 +6641,18 @@ window.__ModuleLoader__.load({
|
|
|
5503
6641
|
unshadow();
|
|
5504
6642
|
};
|
|
5505
6643
|
};
|
|
6644
|
+
const offSlotChange = ctx.on("slots/changed", (key) => {
|
|
6645
|
+
if (key !== "conversation.chat.node" || usedSwap || releaseTakeover === void 0) return;
|
|
6646
|
+
if (!settings.takeoverEnabled()) return;
|
|
6647
|
+
releaseTakeover();
|
|
6648
|
+
releaseTakeover = void 0;
|
|
6649
|
+
syncTakeover();
|
|
6650
|
+
});
|
|
5506
6651
|
const unsubscribe = settings.subscribe(syncTakeover);
|
|
5507
6652
|
syncTakeover();
|
|
5508
6653
|
return () => {
|
|
5509
6654
|
unsubscribe();
|
|
6655
|
+
offSlotChange();
|
|
5510
6656
|
releaseTakeover?.();
|
|
5511
6657
|
};
|
|
5512
6658
|
});
|