dsh-smooth-stream 0.3.4 → 0.4.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 CHANGED
@@ -9,41 +9,42 @@ window.__ModuleLoader__.load({
9
9
  let _deepseek_ai_dsh_client_ui_attachment = require("@deepseek-ai/dsh-client-ui-attachment");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
12
+ let react_dom = require("react-dom");
12
13
  //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/TypewriterAssistantNodeView.module.css.mjs
13
- const css$1 = ".I17U7q_root{min-width:0;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:16px;line-height:28px;display:flex}.I17U7q_body{flex-direction:column;gap:16px;min-width:0;display:flex}.I17U7q_think{flex-direction:column;display:flex}.I17U7q_thinkRow{position:relative;overflow:hidden}.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{content:\"\";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite I17U7q_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes I17U7q_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.I17U7q_thinkLeading{flex-shrink:0}.I17U7q_thinkChevron{color:var(--dsw-alias-label-secondary)}.I17U7q_thinkTitle{font-weight:400}.I17U7q_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.I17U7q_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.I17U7q_thinkSummary[data-follow-end]{text-overflow:clip}.I17U7q_thinkBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.I17U7q_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.I17U7q_disclosureRow{cursor:pointer;align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.I17U7q_disclosureLeading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.I17U7q_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.I17U7q_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureIconIdle{opacity:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureChevronHover{opacity:1}.I17U7q_disclosureTitle{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.I17U7q_disclosureContent{visibility:visible;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s;grid-template-rows:1fr;display:grid}.I17U7q_disclosureContent[data-collapsed]{visibility:hidden;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s var(--ds-transition-duration,.2s);grid-template-rows:0fr}.I17U7q_disclosureContent>*{min-height:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{animation:none}.I17U7q_disclosureContent,.I17U7q_disclosureContent[data-collapsed]{transition:none}}.I17U7q_stopped{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:6px;align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}.I17U7q_visuallyHidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.I17U7q_follow{contain:layout style;min-width:0}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,li,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
14
- const tagId$1 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
14
+ const css$2 = ".I17U7q_root{min-width:0;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:16px;line-height:28px;display:flex}.I17U7q_body{flex-direction:column;gap:16px;min-width:0;display:flex}.I17U7q_think{flex-direction:column;display:flex}.I17U7q_thinkRow{position:relative;overflow:hidden}.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{content:\"\";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite I17U7q_dsh-smooth-stream-think-sweep;position:absolute;left:0}@keyframes I17U7q_dsh-smooth-stream-think-sweep{0%{left:-300px}90%,to{left:100%}}.I17U7q_thinkLeading{flex-shrink:0}.I17U7q_thinkChevron{color:var(--dsw-alias-label-secondary)}.I17U7q_thinkTitle{font-weight:400}.I17U7q_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.I17U7q_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.I17U7q_thinkSummary[data-follow-end]{text-overflow:clip}.I17U7q_thinkBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.I17U7q_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.I17U7q_disclosureRow{cursor:pointer;align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.I17U7q_disclosureLeading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.I17U7q_disclosureIconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.I17U7q_disclosureChevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureIconIdle{opacity:0}.I17U7q_disclosureRow:hover .I17U7q_disclosureChevronHover{opacity:1}.I17U7q_disclosureTitle{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.I17U7q_disclosureContent{visibility:visible;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s;grid-template-rows:1fr;display:grid}.I17U7q_disclosureContent[data-collapsed]{visibility:hidden;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s var(--ds-transition-duration,.2s);grid-template-rows:0fr}.I17U7q_disclosureContent>*{min-height:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.I17U7q_think[data-state=running] .I17U7q_thinkRow:after{animation:none}.I17U7q_disclosureContent,.I17U7q_disclosureContent[data-collapsed]{transition:none}}.I17U7q_stopped{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:6px;align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}.I17U7q_visuallyHidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.I17U7q_follow{contain:layout style;min-width:0}@supports (text-box-trim:trim-both){.I17U7q_root :is(p,h1,h2,h3,h4,h5,h6,li,blockquote){text-box-trim:trim-both;text-box-edge:text}}";
15
+ const tagId$2 = "dsh-smooth-stream/TypewriterAssistantNodeView.module.css";
15
16
  if (typeof document !== "undefined") {
16
- let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
17
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
17
18
  if (tag === null) {
18
19
  tag = document.createElement("style");
19
20
  tag.dataset.plugin = "dsh-smooth-stream";
20
- tag.dataset.pluginCss = tagId$1;
21
+ tag.dataset.pluginCss = tagId$2;
21
22
  document.head.appendChild(tag);
22
23
  }
23
- tag.textContent = css$1;
24
+ tag.textContent = css$2;
24
25
  }
25
26
  var TypewriterAssistantNodeView_module_css_default = {
27
+ "think": "I17U7q_think",
28
+ "thinkTitle": "I17U7q_thinkTitle",
29
+ "thinkSummary": "I17U7q_thinkSummary",
26
30
  "thinkBody": "I17U7q_thinkBody",
27
- "body": "I17U7q_body",
28
- "thinkRow": "I17U7q_thinkRow",
29
- "thinkLeading": "I17U7q_thinkLeading",
30
31
  "disclosureLeading": "I17U7q_disclosureLeading",
31
- "disclosureTitle": "I17U7q_disclosureTitle",
32
+ "disclosureIconIdle": "I17U7q_disclosureIconIdle",
33
+ "disclosureChevronHover": "I17U7q_disclosureChevronHover",
32
34
  "follow": "I17U7q_follow",
33
35
  "disclosureRoot": "I17U7q_disclosureRoot",
34
- "disclosureIconIdle": "I17U7q_disclosureIconIdle",
35
- "thinkTitle": "I17U7q_thinkTitle",
36
36
  "disclosureRow": "I17U7q_disclosureRow",
37
- "disclosureChevronHover": "I17U7q_disclosureChevronHover",
38
- "visuallyHidden": "I17U7q_visuallyHidden",
39
- "thinkSeparator": "I17U7q_thinkSeparator",
40
- "root": "I17U7q_root",
41
- "think": "I17U7q_think",
37
+ "thinkLeading": "I17U7q_thinkLeading",
42
38
  "dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
43
39
  "thinkChevron": "I17U7q_thinkChevron",
40
+ "root": "I17U7q_root",
41
+ "body": "I17U7q_body",
42
+ "visuallyHidden": "I17U7q_visuallyHidden",
44
43
  "stopped": "I17U7q_stopped",
45
- "thinkSummary": "I17U7q_thinkSummary",
46
- "disclosureContent": "I17U7q_disclosureContent"
44
+ "thinkSeparator": "I17U7q_thinkSeparator",
45
+ "disclosureTitle": "I17U7q_disclosureTitle",
46
+ "disclosureContent": "I17U7q_disclosureContent",
47
+ "thinkRow": "I17U7q_thinkRow"
47
48
  };
48
49
  //#endregion
49
50
  //#region src/client/AnimatedDisclosure.tsx
@@ -98,6 +99,285 @@ window.__ModuleLoader__.load({
98
99
  });
99
100
  }
100
101
  //#endregion
102
+ //#region src/settings.ts
103
+ /**
104
+ * User-owned settings for the smooth-stream plugin, exposed to the Host
105
+ * settings service and edited from the Web Settings "plugin configuration"
106
+ * page. This is the runtime-editable complement to {@link StreamConfig}: that
107
+ * contract is composed at load and bridged once through the boot global, while
108
+ * these preferences live in the durable user-settings document and take effect
109
+ * live.
110
+ */
111
+ /** Settings namespace registered by the Host and served through the plugin RPC. */
112
+ const STREAM_SETTINGS_NS = "smooth-stream";
113
+ /** Defaults preserve the production engine exactly. */
114
+ const DEFAULT_STREAM_DEBUG_TUNING = {
115
+ revealScale: 1,
116
+ queuePressure: .85,
117
+ maxRevealCps: 600,
118
+ springStiffness: 130,
119
+ springDamping: 24,
120
+ springMass: 1,
121
+ runwayPx: 48,
122
+ reserveResponseMs: 180,
123
+ backpressureMinScale: .55
124
+ };
125
+ /** Defaults shared by the Host schema and the client-side fallback. */
126
+ const DEFAULT_STREAM_SETTINGS = {
127
+ enabled: true,
128
+ thinkAutoExpand: true,
129
+ autoCollapse: true,
130
+ debugEnabled: false,
131
+ debugTuning: DEFAULT_STREAM_DEBUG_TUNING
132
+ };
133
+ //#endregion
134
+ //#region src/client/debugRuntime.ts
135
+ /**
136
+ * Shared browser-side diagnostics state.
137
+ *
138
+ * The settings card owns persistence. This module is the live bridge used by
139
+ * the renderer and the chat-side panel: renderer loops publish measurements,
140
+ * while panel edits are staged through the settings-card controller.
141
+ */
142
+ const EMPTY_METRICS = {
143
+ fps: null,
144
+ frameMs: null,
145
+ fpsDegraded: false,
146
+ streamActive: false,
147
+ streamBacklog: 0,
148
+ streamSpeedCps: 0,
149
+ streamTargetChars: 0,
150
+ streamDisplayedChars: 0,
151
+ followActive: false,
152
+ followLagPx: 0,
153
+ followVelocityPxPerSec: 0,
154
+ followReservePx: 0,
155
+ followCapacityPx: 0,
156
+ followRevealScale: 1,
157
+ followFollowing: false,
158
+ followConstrained: false,
159
+ scrollTop: null,
160
+ scrollHeight: null,
161
+ clientHeight: null,
162
+ lastUpdatedMs: null
163
+ };
164
+ const INITIAL_STATE = {
165
+ available: false,
166
+ enabled: false,
167
+ writable: false,
168
+ dirty: false,
169
+ status: "loading",
170
+ tuning: { ...DEFAULT_STREAM_DEBUG_TUNING },
171
+ metrics: EMPTY_METRICS
172
+ };
173
+ const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(INITIAL_STATE);
174
+ const streamMetrics = /* @__PURE__ */ new Map();
175
+ const followMetrics = /* @__PURE__ */ new Map();
176
+ let actions;
177
+ let lastMetricPublish = 0;
178
+ function resetMetrics() {
179
+ streamMetrics.clear();
180
+ followMetrics.clear();
181
+ lastMetricPublish = 0;
182
+ }
183
+ function resetRuntime() {
184
+ actions = void 0;
185
+ resetMetrics();
186
+ store.set({
187
+ ...INITIAL_STATE,
188
+ metrics: EMPTY_METRICS
189
+ });
190
+ }
191
+ function now() {
192
+ return typeof performance === "undefined" ? Date.now() : performance.now();
193
+ }
194
+ function sameTuning(left, right) {
195
+ return left.revealScale === right.revealScale && left.queuePressure === right.queuePressure && left.maxRevealCps === right.maxRevealCps && left.springStiffness === right.springStiffness && left.springDamping === right.springDamping && left.springMass === right.springMass && left.runwayPx === right.runwayPx && left.reserveResponseMs === right.reserveResponseMs && left.backpressureMinScale === right.backpressureMinScale;
196
+ }
197
+ function currentMetrics(timestamp) {
198
+ let stream;
199
+ for (const candidate of streamMetrics.values()) if (stream === void 0 || candidate.updatedAt > stream.updatedAt) stream = candidate;
200
+ let follow;
201
+ for (const candidate of followMetrics.values()) if (follow === void 0 || candidate.updatedAt > follow.updatedAt) follow = candidate;
202
+ return {
203
+ fps: store.getSnapshot().metrics.fps,
204
+ frameMs: store.getSnapshot().metrics.frameMs,
205
+ fpsDegraded: store.getSnapshot().metrics.fpsDegraded,
206
+ streamActive: stream?.active ?? false,
207
+ streamBacklog: stream?.backlog ?? 0,
208
+ streamSpeedCps: stream?.speedCps ?? 0,
209
+ streamTargetChars: stream?.targetChars ?? 0,
210
+ streamDisplayedChars: stream?.displayedChars ?? 0,
211
+ followActive: follow?.active ?? false,
212
+ followLagPx: follow?.lagPx ?? 0,
213
+ followVelocityPxPerSec: follow?.velocityPxPerSec ?? 0,
214
+ followReservePx: follow?.reservePx ?? 0,
215
+ followCapacityPx: follow?.capacityPx ?? 0,
216
+ followRevealScale: follow?.revealScale ?? 1,
217
+ followFollowing: follow?.following ?? false,
218
+ followConstrained: follow?.constrained ?? false,
219
+ scrollTop: follow?.scrollTop ?? null,
220
+ scrollHeight: follow?.scrollHeight ?? null,
221
+ clientHeight: follow?.clientHeight ?? null,
222
+ lastUpdatedMs: timestamp
223
+ };
224
+ }
225
+ function publishMetrics(force = false) {
226
+ const timestamp = now();
227
+ if (!force && timestamp - lastMetricPublish < 80) return;
228
+ lastMetricPublish = timestamp;
229
+ store.set({
230
+ ...store.getSnapshot(),
231
+ metrics: currentMetrics(timestamp)
232
+ });
233
+ }
234
+ const debugRuntime = {
235
+ store,
236
+ getSnapshot() {
237
+ return store.getSnapshot();
238
+ },
239
+ subscribe(listener) {
240
+ return store.subscribe(listener);
241
+ },
242
+ /** Whether hot-path instrumentation should do any work. */
243
+ isEnabled() {
244
+ return store.getSnapshot().enabled;
245
+ },
246
+ tuning() {
247
+ return store.getSnapshot().tuning;
248
+ },
249
+ /** Production values remain untouched until the user explicitly enables diagnostics. */
250
+ activeTuning() {
251
+ return store.getSnapshot().enabled ? store.getSnapshot().tuning : DEFAULT_STREAM_DEBUG_TUNING;
252
+ },
253
+ bindSettings(nextActions) {
254
+ actions = nextActions;
255
+ return () => {
256
+ if (actions !== nextActions) return;
257
+ resetRuntime();
258
+ };
259
+ },
260
+ syncSettings(input) {
261
+ const current = store.getSnapshot();
262
+ const available = input.available ?? true;
263
+ const enabled = available && input.enabled;
264
+ const availabilityChanged = current.available !== available;
265
+ const enabledChanged = current.enabled !== enabled;
266
+ if (availabilityChanged || enabledChanged) resetMetrics();
267
+ const tuning = input.tuning === void 0 ? current.tuning : {
268
+ ...DEFAULT_STREAM_DEBUG_TUNING,
269
+ ...input.tuning
270
+ };
271
+ if (current.available === available && current.enabled === enabled && current.writable === input.writable && current.dirty === input.dirty && current.status === input.status && sameTuning(current.tuning, tuning)) return;
272
+ store.set({
273
+ ...current,
274
+ available,
275
+ enabled,
276
+ writable: input.writable,
277
+ dirty: input.dirty,
278
+ status: input.status,
279
+ tuning,
280
+ metrics: availabilityChanged || enabledChanged || !enabled ? EMPTY_METRICS : current.metrics
281
+ });
282
+ },
283
+ edit(patch) {
284
+ const current = store.getSnapshot();
285
+ const enabled = patch.debugEnabled ?? current.enabled;
286
+ const enabledChanged = current.enabled !== enabled;
287
+ if (enabledChanged) resetMetrics();
288
+ const tuning = patch.debugTuning === void 0 ? current.tuning : {
289
+ ...current.tuning,
290
+ ...patch.debugTuning
291
+ };
292
+ store.set({
293
+ ...current,
294
+ enabled,
295
+ dirty: true,
296
+ tuning,
297
+ metrics: enabledChanged || !enabled ? EMPTY_METRICS : current.metrics
298
+ });
299
+ actions?.edit({
300
+ ...patch.debugEnabled === void 0 ? {} : { debugEnabled: patch.debugEnabled },
301
+ ...patch.debugTuning === void 0 ? {} : { debugTuning: tuning }
302
+ });
303
+ },
304
+ save() {
305
+ actions?.save();
306
+ },
307
+ discard() {
308
+ actions?.discard();
309
+ },
310
+ reset() {
311
+ this.edit({ debugTuning: { ...DEFAULT_STREAM_DEBUG_TUNING } });
312
+ },
313
+ reportStream(id, metric) {
314
+ if (!this.isEnabled()) return;
315
+ if (metric === null) streamMetrics.delete(id);
316
+ else streamMetrics.set(id, {
317
+ ...metric,
318
+ updatedAt: now()
319
+ });
320
+ publishMetrics(metric === null);
321
+ },
322
+ reportFollow(port, metric) {
323
+ if (!this.isEnabled()) return;
324
+ if (metric === null) followMetrics.delete(port);
325
+ else followMetrics.set(port, {
326
+ ...metric,
327
+ updatedAt: now()
328
+ });
329
+ publishMetrics(metric === null);
330
+ },
331
+ reportFps(fps, frameMs, degraded) {
332
+ if (!this.isEnabled()) return;
333
+ const current = store.getSnapshot();
334
+ store.set({
335
+ ...current,
336
+ metrics: {
337
+ ...current.metrics,
338
+ fps,
339
+ frameMs,
340
+ fpsDegraded: degraded,
341
+ lastUpdatedMs: now()
342
+ }
343
+ });
344
+ },
345
+ clearFps() {
346
+ if (!this.isEnabled()) return;
347
+ const current = store.getSnapshot();
348
+ store.set({
349
+ ...current,
350
+ metrics: {
351
+ ...current.metrics,
352
+ fps: null,
353
+ frameMs: null,
354
+ fpsDegraded: false,
355
+ lastUpdatedMs: now()
356
+ }
357
+ });
358
+ },
359
+ panelFace() {
360
+ return {
361
+ hooks: { debugRuntime: store },
362
+ edit: (patch) => {
363
+ this.edit(patch);
364
+ },
365
+ save: () => {
366
+ this.save();
367
+ },
368
+ discard: () => {
369
+ this.discard();
370
+ },
371
+ reset: () => {
372
+ this.reset();
373
+ }
374
+ };
375
+ },
376
+ resetRuntime() {
377
+ resetRuntime();
378
+ }
379
+ };
380
+ //#endregion
101
381
  //#region src/client/useSmoothStreamContent.ts
102
382
  /**
103
383
  * Stream-smoothing reveal hook.
@@ -161,20 +441,19 @@ window.__ModuleLoader__.load({
161
441
  }
162
442
  };
163
443
  const QUEUE_ACCEL_EXPONENT = 1.25;
164
- const QUEUE_PRESSURE_FACTOR = .85;
165
444
  const CATCHUP_SECONDS = .15;
166
445
  const clamp = (value, min, max) => {
167
446
  return Math.min(max, Math.max(min, value));
168
447
  };
169
448
  /** Float-debt queue integration from `ultimate_stream_physics_scroller.html`. */
170
- function computeAdaptiveQueueStep(backlog, dtMs, debt, revealScale = 1) {
449
+ function computeAdaptiveQueueStep(backlog, dtMs, debt, revealScale = 1, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
171
450
  if (backlog <= 0 || dtMs <= 0) return {
172
451
  revealChars: 0,
173
452
  debt: 0,
174
453
  speedCps: 0
175
454
  };
176
- const speedCps = Math.min(600, 90 + Math.pow(backlog, QUEUE_ACCEL_EXPONENT) * QUEUE_PRESSURE_FACTOR);
177
- const effectiveScale = clamp(revealScale, .05, 1);
455
+ const speedCps = Math.min(tuning.maxRevealCps, 90 + Math.pow(backlog, QUEUE_ACCEL_EXPONENT) * tuning.queuePressure);
456
+ const effectiveScale = clamp(revealScale * tuning.revealScale, .05, 2);
178
457
  const accumulated = Math.max(0, debt) + speedCps * effectiveScale * (dtMs / 1e3);
179
458
  const revealChars = Math.min(backlog, Math.floor(accumulated));
180
459
  return {
@@ -268,6 +547,7 @@ window.__ModuleLoader__.load({
268
547
  revealScaleOutRef.current = revealScaleRef;
269
548
  const inputCompleteRef = (0, react.useRef)(inputComplete);
270
549
  inputCompleteRef.current = inputComplete;
550
+ const streamIdRef = (0, react.useRef)(`stream-${Math.random().toString(36).slice(2)}`);
271
551
  (0, react.useEffect)(() => {
272
552
  holdBackRef.current = shouldHoldBack;
273
553
  }, [shouldHoldBack]);
@@ -319,6 +599,7 @@ window.__ModuleLoader__.load({
319
599
  settleCpsRef.current = null;
320
600
  const speedOut = speedOutRef.current;
321
601
  if (speedOut !== void 0) speedOut.current = seedCps;
602
+ debugRuntime.reportStream(streamIdRef.current, null);
322
603
  stopFrameLoop();
323
604
  return;
324
605
  }
@@ -332,6 +613,7 @@ window.__ModuleLoader__.load({
332
613
  lastFrameTsRef.current = now;
333
614
  const idleMs = now - lastInputTsRef.current;
334
615
  const producerComplete = inputCompleteRef.current;
616
+ const debugTuning = debugRuntime.activeTuning();
335
617
  const inputActive = !producerComplete && idleMs <= config.activeInputWindowMs;
336
618
  const settling = producerComplete || !inputActive && idleMs >= config.settleAfterMs;
337
619
  if (!producerComplete) settleCpsRef.current = null;
@@ -355,14 +637,21 @@ window.__ModuleLoader__.load({
355
637
  settling,
356
638
  steadyCps
357
639
  }, dtSeconds);
358
- revealChars = Math.min(step.revealChars, backlog);
640
+ revealChars = Math.min(Math.round(step.revealChars * debugTuning.revealScale), backlog);
359
641
  revealSpeedCps = frameIntervalMs > 0 ? revealChars * 1e3 / frameIntervalMs : 0;
360
642
  } else {
361
- const step = computeAdaptiveQueueStep(backlog, frameIntervalMs, queueDebtRef.current, revealScaleOutRef.current?.current ?? 1);
643
+ const step = computeAdaptiveQueueStep(backlog, frameIntervalMs, queueDebtRef.current, revealScaleOutRef.current?.current ?? 1, debugTuning);
362
644
  revealChars = step.revealChars;
363
645
  revealSpeedCps = step.speedCps;
364
646
  nextQueueDebt = step.debt;
365
647
  }
648
+ debugRuntime.reportStream(streamIdRef.current, {
649
+ backlog,
650
+ speedCps: revealSpeedCps,
651
+ targetChars: targetCount,
652
+ displayedChars: displayedCount,
653
+ active: !producerComplete
654
+ });
366
655
  if (holdBackRef.current?.() === true) {
367
656
  rafRef.current = requestAnimationFrame(tick);
368
657
  return;
@@ -439,6 +728,7 @@ window.__ModuleLoader__.load({
439
728
  (0, react.useEffect)(() => {
440
729
  return () => {
441
730
  stopFrameLoop();
731
+ debugRuntime.reportStream(streamIdRef.current, null);
442
732
  };
443
733
  }, [stopFrameLoop]);
444
734
  return displayedContent;
@@ -470,6 +760,7 @@ window.__ModuleLoader__.load({
470
760
  (0, react.useEffect)(() => {
471
761
  if (!active) return;
472
762
  let rafId = 0;
763
+ let lastDebugReport = 0;
473
764
  const frame = (now) => {
474
765
  rafId = requestAnimationFrame(frame);
475
766
  const fps = fpsRef.current;
@@ -480,13 +771,18 @@ window.__ModuleLoader__.load({
480
771
  const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs));
481
772
  fps.lastMs = now;
482
773
  fps.emaMs = fps.emaMs === 0 ? delta : fps.emaMs + FPS_ALPHA * (delta - fps.emaMs);
483
- if (1e3 / fps.emaMs < FPS_THRESHOLD) {
774
+ const currentFps = 1e3 / fps.emaMs;
775
+ if (currentFps < FPS_THRESHOLD) {
484
776
  fps.healthyRun = 0;
485
777
  fps.degraded = true;
486
778
  } else if (fps.degraded) {
487
779
  fps.healthyRun += 1;
488
780
  if (fps.healthyRun >= RECOVER_FRAMES) fps.degraded = false;
489
781
  }
782
+ if (now - lastDebugReport >= 100) {
783
+ debugRuntime.reportFps(currentFps, fps.emaMs, fps.degraded);
784
+ lastDebugReport = now;
785
+ }
490
786
  };
491
787
  rafId = requestAnimationFrame(frame);
492
788
  return () => {
@@ -497,6 +793,7 @@ window.__ModuleLoader__.load({
497
793
  healthyRun: 0,
498
794
  degraded: false
499
795
  };
796
+ debugRuntime.clearFps();
500
797
  };
501
798
  }, [active]);
502
799
  const ref = (0, react.useCallback)((element) => {
@@ -519,6 +816,805 @@ window.__ModuleLoader__.load({
519
816
  };
520
817
  }
521
818
  //#endregion
819
+ //#region src/client/auto-collapse-controller.ts
820
+ /**
821
+ * Auto-collapse coordinator: folds the work process of finished turns behind
822
+ * one summary row so only the model's final answer stays visible.
823
+ *
824
+ * The Harness ChatView lays a conversation out as a flat list of flow items
825
+ * (`[data-chat-flow] > [data-chat-flow-kind=…]`). This controller watches that
826
+ * list, splits it into turn segments at `user`/`steering` boundaries closed by
827
+ * `turn-tail`, and — once a segment has settled — hides its thinking rows,
828
+ * tool/command/context seats and intermediate replies, leaving one clickable
829
+ * `已处理 {时长}` row plus the final answer. Clicking the row expands the full
830
+ * process again; the choice survives later renders because it lives in the
831
+ * controller, not the DOM.
832
+ *
833
+ * Adapted from the standalone `dsh-auto-collapse` plugin (level-1 folding
834
+ * only): no second-level chips, no merged thinking rows, no status-text
835
+ * rewrite, no transition animation — the collapse decision itself stays
836
+ * instantaneous and reversible.
837
+ *
838
+ * React coexistence follows the upstream design: injected rows are plain
839
+ * siblings/prepend children the vdom diff never claims, native rows are only
840
+ * ever touched through an inline-`display` ledger that records the precise
841
+ * original value and restores it on stop, session switch, or foreign takeover
842
+ * (a written-value comparison plus an ownership sentinel catches another
843
+ * script rewriting styles behind our back). A MutationObserver batches real
844
+ * structure changes into rAF passes; a low-frequency self-rearming audit pass
845
+ * reconciles unobservable style writes; a setTimeout fallback keeps passes
846
+ * flowing when background tabs suspend rAF.
847
+ */
848
+ const STYLE_ID = "dshss-auto-collapse-style";
849
+ /** Class of the injected per-turn summary row; also consumed by the follow
850
+ * engine so the row rides the same lag transform as native message rows. */
851
+ const PROCESSED_CLASS = "dshss-processed";
852
+ /** Inline custom property marking elements whose display this plugin owns. */
853
+ const DISPLAY_OWNED_PROP = "--dshss-display-owned";
854
+ /** Unobservable external style writes are reconciled by this audit cadence. */
855
+ const AUDIT_TICK_MS = 1e3;
856
+ /** Background-tab fallback that flushes a scheduled pass when rAF is frozen. */
857
+ const PASS_FALLBACK_MS = 60;
858
+ /**
859
+ * Fold motion, deliberately minimal: layout changes land synchronously (the
860
+ * display toggle happens in the same tick as the decision) and nothing
861
+ * animates geometry at all — the earlier container-height squeeze was removed
862
+ * because real sessions showed residual drift after the pin released (other
863
+ * layout participants keep settling around the fold). What remains is one
864
+ * paint-only flourish: the summary row fades its opacity in. A fold is
865
+ * therefore a single reflow followed by a fade — the most stable shape
866
+ * possible.
867
+ */
868
+ const FOLD_ROW_FADE_MS = 160;
869
+ const FOLD_EASE_OUT = "cubic-bezier(0.22, 0.61, 0.36, 1)";
870
+ const ZH_COPY = {
871
+ processedLabel: "已处理",
872
+ expandTitle: "展开工作过程",
873
+ collapseTitle: "收起工作过程",
874
+ formatDuration: formatZhDuration
875
+ };
876
+ const EN_COPY = {
877
+ processedLabel: "Processed",
878
+ expandTitle: "Show the work process",
879
+ collapseTitle: "Hide the work process",
880
+ formatDuration: formatEnDuration
881
+ };
882
+ function resolveFoldCopy() {
883
+ let lang = "";
884
+ try {
885
+ lang = document.documentElement.lang || navigator.language || "";
886
+ } catch {
887
+ lang = "";
888
+ }
889
+ return lang.toLowerCase().startsWith("zh") ? ZH_COPY : EN_COPY;
890
+ }
891
+ const FOLD_CSS = `
892
+ .${PROCESSED_CLASS} {
893
+ box-sizing: border-box;
894
+ display: inline-flex;
895
+ align-self: flex-start;
896
+ width: fit-content;
897
+ max-width: 100%;
898
+ align-items: center;
899
+ gap: 6px;
900
+ padding: 2px 0;
901
+ border: none;
902
+ background: none;
903
+ font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
904
+ color: var(--dsw-alias-label-tertiary, rgba(127, 127, 127, 0.9));
905
+ cursor: pointer;
906
+ user-select: none;
907
+ border-radius: 4px;
908
+ transition: color 0.15s ease;
909
+ }
910
+ .${PROCESSED_CLASS}:hover {
911
+ color: var(--dsw-alias-label-primary, inherit);
912
+ background: transparent;
913
+ }
914
+ .${PROCESSED_CLASS}:focus-visible {
915
+ outline: 2px solid var(--dsw-alias-state-focus-ring, rgba(77, 107, 254, 0.8));
916
+ outline-offset: 2px;
917
+ }
918
+ .${PROCESSED_CLASS}-chevron {
919
+ display: inline-flex;
920
+ flex: none;
921
+ width: 14px;
922
+ height: 14px;
923
+ opacity: 0.55;
924
+ transform: rotate(-90deg);
925
+ transition: transform 0.12s ease, opacity 0.1s ease;
926
+ }
927
+ .${PROCESSED_CLASS}:hover .${PROCESSED_CLASS}-chevron {
928
+ opacity: 0.9;
929
+ }
930
+ .${PROCESSED_CLASS}[aria-expanded="true"] .${PROCESSED_CLASS}-chevron {
931
+ transform: rotate(0deg);
932
+ }
933
+ @media (prefers-reduced-motion: reduce) {
934
+ .${PROCESSED_CLASS},
935
+ .${PROCESSED_CLASS}-chevron {
936
+ transition: none;
937
+ }
938
+ }
939
+ `;
940
+ var AutoCollapseController = class {
941
+ observer = null;
942
+ raf = 0;
943
+ timer = 0;
944
+ auditTimer = 0;
945
+ /** True between start() and stop(); the preference toggle re-enters both. */
946
+ running = false;
947
+ lastPassError = "";
948
+ flow = null;
949
+ /** Stable segment key → summary row + gesture state. */
950
+ segmentStates = /* @__PURE__ */ new Map();
951
+ /** First sighting of a running segment, for turns without a host duration. */
952
+ runningSince = /* @__PURE__ */ new Map();
953
+ /** Segments that settled once: a resumed run restarts local timing. */
954
+ completedOnce = /* @__PURE__ */ new Set();
955
+ /** Precise pre-plugin display values, restored on stop/switch/takeover. */
956
+ originalDisplay = /* @__PURE__ */ new WeakMap();
957
+ writtenDisplay = /* @__PURE__ */ new WeakMap();
958
+ controlledDisplay = /* @__PURE__ */ new Set();
959
+ /** Reply-presence verdicts cached per message until a mutation hits it. */
960
+ bodyTextCache = /* @__PURE__ */ new WeakMap();
961
+ dirtyMessages = /* @__PURE__ */ new Set();
962
+ onVisibilityChange = () => {
963
+ if (typeof document === "undefined" || document.hidden !== true) this.schedule();
964
+ };
965
+ /** Re-run a pass immediately (settings changes, diagnostics, tests).
966
+ * Reply-presence verdicts are dropped wholesale: manual refreshes are rare,
967
+ * and a conservative rebuild beats a stale classification. */
968
+ refresh() {
969
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
970
+ this.dirtyMessages.clear();
971
+ this.schedule();
972
+ }
973
+ start() {
974
+ if (this.running) return;
975
+ this.running = true;
976
+ injectStyle();
977
+ try {
978
+ this.observer = new MutationObserver((records) => {
979
+ if (this.shouldSchedule(records)) {
980
+ this.markDirty(records);
981
+ this.schedule();
982
+ }
983
+ });
984
+ this.observer.observe(document.body, {
985
+ childList: true,
986
+ subtree: true,
987
+ attributes: true,
988
+ attributeFilter: ["data-state"],
989
+ characterData: true
990
+ });
991
+ this.armAuditLoop();
992
+ this.schedule();
993
+ } catch (error) {
994
+ this.reportError(error);
995
+ throw error;
996
+ }
997
+ }
998
+ stop() {
999
+ this.running = false;
1000
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
1001
+ this.raf = 0;
1002
+ if (this.timer !== 0) clearTimeout(this.timer);
1003
+ this.timer = 0;
1004
+ if (this.auditTimer !== 0) clearTimeout(this.auditTimer);
1005
+ this.auditTimer = 0;
1006
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", this.onVisibilityChange);
1007
+ this.observer?.disconnect();
1008
+ this.observer = null;
1009
+ this.switchFlow(null);
1010
+ removeStyle();
1011
+ }
1012
+ /** Low-frequency reconciliation for style writes no observer can see. */
1013
+ armAuditLoop() {
1014
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") document.addEventListener("visibilitychange", this.onVisibilityChange);
1015
+ this.rearmAudit();
1016
+ }
1017
+ rearmAudit() {
1018
+ if (!this.running || this.auditTimer !== 0) return;
1019
+ this.auditTimer = setTimeout(() => {
1020
+ this.auditTimer = 0;
1021
+ if (!this.running) return;
1022
+ if (typeof document !== "undefined" && document.hidden === true) {
1023
+ this.rearmAudit();
1024
+ return;
1025
+ }
1026
+ this.schedule();
1027
+ this.rearmAudit();
1028
+ }, AUDIT_TICK_MS);
1029
+ }
1030
+ /** Body-level observations only need to wake us up near the active flow. */
1031
+ shouldSchedule(records) {
1032
+ if (records.length === 0 || this.flow === null || !this.flow.isConnected) return true;
1033
+ return records.some((record) => nodeWithin(record.target, this.flow) || nodeWithin(this.flow, record.target));
1034
+ }
1035
+ /** Walk each record to its flow child so reply-cache invalidation stays
1036
+ * targeted; anything unattributable invalidates conservatively. */
1037
+ markDirty(records) {
1038
+ const flow = this.flow;
1039
+ if (flow === null || !flow.isConnected) return;
1040
+ if (records.length === 0) {
1041
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
1042
+ this.dirtyMessages.clear();
1043
+ return;
1044
+ }
1045
+ for (const record of records) {
1046
+ let current = record.target;
1047
+ while (current !== null && current.parentNode !== flow) current = current.parentNode;
1048
+ if (!(current instanceof HTMLElement)) {
1049
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
1050
+ this.dirtyMessages.clear();
1051
+ return;
1052
+ }
1053
+ this.dirtyMessages.add(current);
1054
+ }
1055
+ }
1056
+ schedule() {
1057
+ if (!this.running || this.raf !== 0) return;
1058
+ this.raf = requestAnimationFrame(() => {
1059
+ this.raf = 0;
1060
+ if (this.timer !== 0) {
1061
+ clearTimeout(this.timer);
1062
+ this.timer = 0;
1063
+ }
1064
+ this.runPass();
1065
+ });
1066
+ if (this.timer !== 0) clearTimeout(this.timer);
1067
+ this.timer = setTimeout(() => {
1068
+ this.timer = 0;
1069
+ if (this.raf !== 0) {
1070
+ cancelAnimationFrame(this.raf);
1071
+ this.raf = 0;
1072
+ this.runPass();
1073
+ }
1074
+ }, PASS_FALLBACK_MS);
1075
+ }
1076
+ runPass() {
1077
+ try {
1078
+ this.pass();
1079
+ this.lastPassError = "";
1080
+ } catch (error) {
1081
+ this.reportError(error);
1082
+ }
1083
+ }
1084
+ reportError(error) {
1085
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1086
+ if (message === this.lastPassError) return;
1087
+ this.lastPassError = message;
1088
+ console.error("[dsh-smooth-stream] auto-collapse pass failed", error);
1089
+ }
1090
+ pass() {
1091
+ if (!this.running) return;
1092
+ const nextFlow = findFlow();
1093
+ if (nextFlow !== this.flow) this.switchFlow(nextFlow);
1094
+ const flow = this.flow;
1095
+ if (flow === null) return;
1096
+ for (const el of this.dirtyMessages) this.bodyTextCache.delete(el);
1097
+ this.dirtyMessages.clear();
1098
+ const segments = buildSegments(flow, (el) => this.hasBodyCached(el));
1099
+ const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
1100
+ for (const segment of segments) {
1101
+ if (!segment.running) continue;
1102
+ if (this.completedOnce.has(segment.key)) {
1103
+ this.completedOnce.delete(segment.key);
1104
+ this.runningSince.delete(segment.key);
1105
+ }
1106
+ if (!this.runningSince.has(segment.key)) this.runningSince.set(segment.key, Date.now());
1107
+ }
1108
+ const completedKeys = /* @__PURE__ */ new Set();
1109
+ for (const segment of segments) {
1110
+ if (!segment.closed || segment.running || !segment.hasWork) continue;
1111
+ completedKeys.add(segment.key);
1112
+ this.completedOnce.add(segment.key);
1113
+ let state = this.segmentStates.get(segment.key);
1114
+ if (state === void 0) {
1115
+ state = {
1116
+ key: segment.key,
1117
+ row: null,
1118
+ expanded: false,
1119
+ snapshot: segment
1120
+ };
1121
+ this.segmentStates.set(segment.key, state);
1122
+ } else state.snapshot = segment;
1123
+ const started = this.runningSince.get(segment.key);
1124
+ const parsed = segment.boundary === null ? void 0 : parseTurnDuration(segment.boundary);
1125
+ if (parsed !== void 0) state.duration = parsed;
1126
+ else if (state.duration === void 0 && started !== void 0) state.duration = Date.now() - started;
1127
+ if (state.row === null || !state.row.isConnected) state.row = this.createProcessedRow(state);
1128
+ this.syncProcessedRow(state);
1129
+ }
1130
+ for (const [key, state] of [...this.segmentStates]) {
1131
+ if (completedKeys.has(key)) continue;
1132
+ state.row?.remove();
1133
+ this.segmentStates.delete(key);
1134
+ }
1135
+ const desiredHidden = /* @__PURE__ */ new Set();
1136
+ for (const segment of segments) {
1137
+ const state = this.segmentStates.get(segment.key);
1138
+ if (state !== void 0 && !state.expanded) {
1139
+ for (const seat of segment.hideSeats) this.hideElement(seat, desiredHidden);
1140
+ for (const row of segment.finalThinkRows) this.hideElement(row, desiredHidden);
1141
+ if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
1142
+ } else {
1143
+ for (const seat of segment.hideSeats) this.restoreElement(seat);
1144
+ for (const row of segment.finalThinkRows) this.restoreElement(row);
1145
+ if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
1146
+ }
1147
+ }
1148
+ for (const segment of segments) {
1149
+ if (!hasVisibleSegmentWork(segment)) {
1150
+ const state = this.segmentStates.get(segment.key);
1151
+ if (state !== void 0 && state.row !== null) {
1152
+ state.row.remove();
1153
+ state.row = null;
1154
+ }
1155
+ }
1156
+ for (const seat of segment.hideSeats) this.retainDisplayControl(seat, desiredHidden);
1157
+ for (const row of segment.finalThinkRows) this.retainDisplayControl(row, desiredHidden);
1158
+ if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
1159
+ }
1160
+ this.restoreUnusedDisplays(desiredHidden);
1161
+ for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
1162
+ for (const key of [...this.runningSince.keys()]) if (!liveSegmentKeys.has(key)) this.runningSince.delete(key);
1163
+ for (const key of [...this.completedOnce]) if (!liveSegmentKeys.has(key)) this.completedOnce.delete(key);
1164
+ }
1165
+ /** Flow swap means a session switch: fully restore the old tree, rebuild. */
1166
+ switchFlow(next) {
1167
+ if (next === this.flow) return;
1168
+ for (const state of this.segmentStates.values()) state.row?.remove();
1169
+ this.segmentStates.clear();
1170
+ this.runningSince.clear();
1171
+ this.completedOnce.clear();
1172
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
1173
+ this.dirtyMessages.clear();
1174
+ this.restoreAllDisplays();
1175
+ this.flow = next;
1176
+ }
1177
+ /** Keep already-controlled elements in the desired-hidden set so the sweep
1178
+ * does not flip them back mid-flight of an external change. */
1179
+ retainDisplayControl(el, desiredHidden) {
1180
+ if (this.controlledDisplay.has(el)) desiredHidden.add(el);
1181
+ }
1182
+ /** Restore every element the ledger controls that this pass no longer wants
1183
+ * hidden; elements absent from `desired` come back to their true display. */
1184
+ restoreUnusedDisplays(desired) {
1185
+ for (const el of [...this.controlledDisplay]) if (!desired.has(el)) this.restoreElement(el);
1186
+ }
1187
+ /** Restore every element still under the display ledger, then retire the
1188
+ * ledger itself (stop / session switch). */
1189
+ restoreAllDisplays() {
1190
+ for (const el of [...this.controlledDisplay]) this.restoreElement(el);
1191
+ this.controlledDisplay.clear();
1192
+ this.originalDisplay = /* @__PURE__ */ new WeakMap();
1193
+ this.writtenDisplay = /* @__PURE__ */ new WeakMap();
1194
+ }
1195
+ createProcessedRow(state) {
1196
+ const row = createProcessedRowElement(state.duration);
1197
+ row.addEventListener("click", () => {
1198
+ state.expanded = !state.expanded;
1199
+ this.syncProcessedRow(state);
1200
+ this.schedule();
1201
+ });
1202
+ return row;
1203
+ }
1204
+ syncProcessedRow(state) {
1205
+ const row = state.row;
1206
+ if (row === null) return;
1207
+ const copy = resolveFoldCopy();
1208
+ const text = row.firstElementChild;
1209
+ const label = state.duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(state.duration)}`;
1210
+ if (text !== null && text.textContent !== label) text.textContent = label;
1211
+ const expanded = String(state.expanded);
1212
+ if (row.getAttribute("aria-expanded") !== expanded) row.setAttribute("aria-expanded", expanded);
1213
+ const title = state.expanded ? copy.collapseTitle : copy.expandTitle;
1214
+ if (row.title !== title) row.title = title;
1215
+ }
1216
+ placeProcessedRow(flow, state) {
1217
+ const row = state.row;
1218
+ if (row === null) return;
1219
+ if (!state.snapshot.hasWork || !hasVisibleSegmentWork(state.snapshot)) {
1220
+ row.remove();
1221
+ state.row = null;
1222
+ return;
1223
+ }
1224
+ let target = state.snapshot.firstWork ?? state.snapshot.finalStep ?? state.snapshot.boundary;
1225
+ if (target === null || target.parentElement !== flow) {
1226
+ row.remove();
1227
+ state.row = null;
1228
+ return;
1229
+ }
1230
+ if (row.parentElement !== flow || row.nextElementSibling !== target) {
1231
+ target.before(row);
1232
+ this.revealVisualOnce(row);
1233
+ }
1234
+ }
1235
+ hasBodyCached(el) {
1236
+ const cached = this.bodyTextCache.get(el);
1237
+ if (cached !== void 0) return cached;
1238
+ const value = hasBodyContent(el);
1239
+ this.bodyTextCache.set(el, value);
1240
+ return value;
1241
+ }
1242
+ /** True when the recorded display was changed (or the ownership sentinel
1243
+ * wiped) behind the ledger's back; callers hand control back to reality. */
1244
+ displayForeign(el) {
1245
+ const written = this.writtenDisplay.get(el);
1246
+ if (written === void 0) return false;
1247
+ return el.style.getPropertyValue(DISPLAY_OWNED_PROP) === "" || el.style.display !== written;
1248
+ }
1249
+ releaseDisplayLedger(el) {
1250
+ this.originalDisplay.delete(el);
1251
+ this.writtenDisplay.delete(el);
1252
+ this.controlledDisplay.delete(el);
1253
+ el.style.removeProperty(DISPLAY_OWNED_PROP);
1254
+ }
1255
+ hideElement(el, desired) {
1256
+ desired.add(el);
1257
+ if (!this.originalDisplay.has(el) && !isDisplayed(el)) return;
1258
+ if (!this.originalDisplay.has(el) || this.displayForeign(el)) {
1259
+ this.originalDisplay.set(el, el.style.display);
1260
+ this.writtenDisplay.set(el, el.style.display);
1261
+ el.style.setProperty(DISPLAY_OWNED_PROP, "1");
1262
+ }
1263
+ this.controlledDisplay.add(el);
1264
+ if (el.style.display === "none") return;
1265
+ el.style.display = "none";
1266
+ this.writtenDisplay.set(el, "none");
1267
+ }
1268
+ restoreElement(el) {
1269
+ if (!this.originalDisplay.has(el)) return;
1270
+ if (this.displayForeign(el)) {
1271
+ this.releaseDisplayLedger(el);
1272
+ return;
1273
+ }
1274
+ const original = this.originalDisplay.get(el);
1275
+ if (el.style.display !== original) el.style.display = original;
1276
+ this.releaseDisplayLedger(el);
1277
+ }
1278
+ /** Whether WAAPI exists and the user has not asked for reduced motion. */
1279
+ canAnimate(el) {
1280
+ if (typeof el.animate !== "function") return false;
1281
+ try {
1282
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
1283
+ } catch {
1284
+ return false;
1285
+ }
1286
+ return true;
1287
+ }
1288
+ /** First-appearance flourish for the plugin-owned summary row: a pure
1289
+ * opacity fade. Paint-only, so it cannot shift anyone's layout. */
1290
+ revealVisualOnce(row) {
1291
+ if (row.dataset.dshssShown === "1") return;
1292
+ row.dataset.dshssShown = "1";
1293
+ if (!this.canAnimate(row)) return;
1294
+ row.animate([{ opacity: "0" }, { opacity: "1" }], {
1295
+ duration: FOLD_ROW_FADE_MS,
1296
+ easing: FOLD_EASE_OUT
1297
+ });
1298
+ }
1299
+ };
1300
+ /** Visible chat flow container, falling back to the first one rendered. */
1301
+ function findFlow() {
1302
+ const flows = document.querySelectorAll("[data-chat-flow]");
1303
+ for (const flow of flows) if (flow.offsetParent !== null || flow.getBoundingClientRect().width > 0) return flow;
1304
+ return flows[0] ?? null;
1305
+ }
1306
+ /** parentNode walk that also accepts Text mutation targets. */
1307
+ function nodeWithin(node, ancestor) {
1308
+ for (let current = node; current !== null; current = current.parentNode) if (current === ancestor) return true;
1309
+ return false;
1310
+ }
1311
+ /** Top-level message order, excluding this plugin's own summary rows. */
1312
+ function flowItems(flow) {
1313
+ return [...flow.children].filter((el) => el instanceof HTMLElement && !el.classList.contains("dshss-processed"));
1314
+ }
1315
+ function isDisplayed(el) {
1316
+ if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
1317
+ return el.style.display !== "none";
1318
+ }
1319
+ function stableElementKey(el, fallbackIndex) {
1320
+ const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
1321
+ return `${kind}:${el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`}`;
1322
+ }
1323
+ /** Kinds that can open a history stretch lacking an explicit user marker. */
1324
+ function opensTurnWork(el) {
1325
+ const kind = el.getAttribute("data-chat-flow-kind");
1326
+ return kind === "assistant-step" || kind === "assistant" || kind === "tool-call" || kind === "command" || kind === "manual-compaction";
1327
+ }
1328
+ /**
1329
+ * Split the flow into turn segments. `user`/`steering` rows close the previous
1330
+ * segment and open the next; `turn-tail` closes and resets the opener. Leading
1331
+ * context before the first user row joins that user's segment, matching how
1332
+ * the host presents context injection.
1333
+ */
1334
+ function buildSegments(flow, hasBody) {
1335
+ const items = flowItems(flow);
1336
+ const itemIndex = new Map(items.map((el, index) => [el, index]));
1337
+ const snapshots = [];
1338
+ let contentStart = 0;
1339
+ let startMarker = null;
1340
+ const append = (end, boundary, closed) => {
1341
+ if (end < contentStart) return;
1342
+ const range = items.slice(contentStart, end);
1343
+ const hideSeats = /* @__PURE__ */ new Set();
1344
+ const bodySteps = [];
1345
+ let running = false;
1346
+ for (const el of range) {
1347
+ const classified = classifyItem(el, hasBody);
1348
+ if (classified.fold === "body") bodySteps.push(el);
1349
+ else if (classified.fold === "seat") hideSeats.add(el);
1350
+ if (classified.running) running = true;
1351
+ }
1352
+ const finalStep = bodySteps.at(-1) ?? null;
1353
+ const middleSteps = new Set(bodySteps.slice(0, -1));
1354
+ for (const step of middleSteps) hideSeats.add(step);
1355
+ const finalThinkRows = finalStep === null ? [] : thinkRowsIn(finalStep);
1356
+ const firstWork = range.find((el) => hideSeats.has(el)) ?? finalStep;
1357
+ const identity = startMarker ?? range.find((el) => opensTurnWork(el)) ?? boundary;
1358
+ const identityIndex = identity === null ? contentStart : itemIndex.get(identity) ?? contentStart;
1359
+ const key = `${startMarker === null ? "leading" : "segment"}:${identity === null ? `open:${contentStart}` : stableElementKey(identity, identityIndex)}`;
1360
+ snapshots.push({
1361
+ key,
1362
+ boundary,
1363
+ startMarker,
1364
+ hideSeats,
1365
+ middleSteps,
1366
+ finalStep,
1367
+ finalThinkRows,
1368
+ firstWork,
1369
+ closed,
1370
+ running,
1371
+ hasWork: hideSeats.size > 0 || finalThinkRows.length > 0
1372
+ });
1373
+ };
1374
+ items.forEach((el, index) => {
1375
+ const kind = el.getAttribute("data-chat-flow-kind");
1376
+ if (kind === "user" || kind === "steering") {
1377
+ if (startMarker !== null) {
1378
+ append(index, el, true);
1379
+ contentStart = index + 1;
1380
+ } else if (items.slice(contentStart, index).some((el) => opensTurnWork(el))) {
1381
+ append(index, el, true);
1382
+ contentStart = index + 1;
1383
+ }
1384
+ startMarker = el;
1385
+ return;
1386
+ }
1387
+ if (kind === "turn-tail") {
1388
+ append(index, el, true);
1389
+ contentStart = index + 1;
1390
+ startMarker = null;
1391
+ }
1392
+ });
1393
+ if (contentStart < items.length) append(items.length, null, false);
1394
+ return snapshots;
1395
+ }
1396
+ /**
1397
+ * Classify one top-level flow item against the Harness's own node vocabulary.
1398
+ * Reply-bearing assistant messages become `body` candidates (last one per
1399
+ * segment wins as the final answer); core work process — tool-call seats,
1400
+ * command/compaction cards, context injection, thinking-only messages —
1401
+ * becomes a hideable `seat`. Everything else is skipped untouched: boundaries,
1402
+ * empty decorations, and crucially any kind this module does not own, so rows
1403
+ * contributed by other plugins' slots are never folded or hidden.
1404
+ */
1405
+ function classifyItem(el, hasBody) {
1406
+ const kind = el.getAttribute("data-chat-flow-kind");
1407
+ if (kind === "user" || kind === "steering" || kind === "turn-tail") return {
1408
+ fold: "skip",
1409
+ running: false
1410
+ };
1411
+ if (kind === "assistant-step" || kind === "assistant") {
1412
+ if (hasBody(el)) return {
1413
+ fold: "body",
1414
+ running: subtreeRunning(el)
1415
+ };
1416
+ if (thinkRowsIn(el).length > 0) return {
1417
+ fold: "seat",
1418
+ running: subtreeRunning(el)
1419
+ };
1420
+ return {
1421
+ fold: "skip",
1422
+ running: false
1423
+ };
1424
+ }
1425
+ if (kind === "tool-call") {
1426
+ const rows = topCallRowsIn(el);
1427
+ if (rows.length === 0 || !rows.every((row) => hasNativeCardChrome(row))) return {
1428
+ fold: "skip",
1429
+ running: subtreeRunning(el)
1430
+ };
1431
+ return {
1432
+ fold: "seat",
1433
+ running: subtreeRunning(el)
1434
+ };
1435
+ }
1436
+ if (kind === "context" || kind === "command" || kind === "manual-compaction") return {
1437
+ fold: "seat",
1438
+ running: subtreeRunning(el)
1439
+ };
1440
+ return {
1441
+ fold: "skip",
1442
+ running: false
1443
+ };
1444
+ }
1445
+ /** Native work-card shape: every core card family stamps `data-variant`
1446
+ * together with `data-state`, whether composed through ToolRow or hand-rolled
1447
+ * (Bash). Custom toolviews use their own namespaced attributes instead. */
1448
+ function hasNativeCardChrome(row) {
1449
+ return row.querySelector("[data-variant][data-state]") !== null;
1450
+ }
1451
+ /** Whether anything inside the item reports a running lifecycle. Deliberately
1452
+ * broad: staying "running" too long only delays a collapse, collapsing early
1453
+ * would cut off live output. Native cards stamp `data-state` next to
1454
+ * `data-tool`/`data-variant`; custom views use their own vocabulary and are
1455
+ * intentionally invisible here. */
1456
+ function subtreeRunning(el) {
1457
+ if (el.getAttribute("data-state") === "running") return true;
1458
+ for (const root of el.querySelectorAll("[data-tool][data-state], [data-variant][data-state]")) if (root.getAttribute("data-state") === "running") return true;
1459
+ return false;
1460
+ }
1461
+ function hasVisibleSegmentWork(segment) {
1462
+ const candidates = [...segment.hideSeats, ...segment.middleSteps];
1463
+ if (segment.startMarker !== null) candidates.push(segment.startMarker);
1464
+ if (segment.finalStep !== null) candidates.push(segment.finalStep);
1465
+ return candidates.some(isDisplayed);
1466
+ }
1467
+ /** Top-level tool card rows of a seat (excluding sub-dispatch nests). */
1468
+ function topCallRowsIn(el) {
1469
+ const rows = [];
1470
+ for (const row of el.querySelectorAll("[data-chat-call-id]")) {
1471
+ if (row.closest("[data-subcalls]") !== null) continue;
1472
+ if (row.closest("[data-chat-call-id]") !== row) continue;
1473
+ rows.push(row);
1474
+ }
1475
+ return rows;
1476
+ }
1477
+ /** Reasoning rows of a message: native think variants outside tool cards. */
1478
+ function thinkRowsIn(el) {
1479
+ const rows = [];
1480
+ for (const row of el.querySelectorAll("[data-variant=\"think\"]:not([data-tool])")) {
1481
+ if (row.closest("[data-chat-call-id]") !== null) continue;
1482
+ if (row.closest("[data-subcalls]") !== null) continue;
1483
+ rows.push(row);
1484
+ }
1485
+ return rows;
1486
+ }
1487
+ /**
1488
+ * Reply detection beyond reasoning rows and tool cards: any non-empty text or
1489
+ * media counts, because Markdown paragraphs necessarily carry text nodes.
1490
+ * Rendered markdown class names are build-time hashes, so structure-based
1491
+ * detection is the only stable contract.
1492
+ */
1493
+ function hasBodyContent(el) {
1494
+ const kind = el.getAttribute("data-chat-flow-kind");
1495
+ if (kind === "command" || kind === "manual-compaction") return false;
1496
+ if (hasBodyText(el)) return true;
1497
+ const excluded = "[data-variant=\"think\"], [data-chat-call-id]";
1498
+ for (const media of el.querySelectorAll("img, video, audio, canvas")) if (media.closest(excluded) === null) return true;
1499
+ return false;
1500
+ }
1501
+ function hasBodyText(el) {
1502
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
1503
+ let node;
1504
+ while ((node = walker.nextNode()) !== null) {
1505
+ if (node.data.trim() === "") continue;
1506
+ const parent = node.parentElement;
1507
+ if (parent !== null && parent.closest("[data-variant=\"think\"], [data-chat-call-id], [data-variant=\"others\"][data-state]") !== null) continue;
1508
+ return true;
1509
+ }
1510
+ return false;
1511
+ }
1512
+ /**
1513
+ * Official turn duration from the closing element: localized tails read
1514
+ * `用时 33秒` / `用时 2分05秒` or `Ran for 33s` / `Ran for 2m 05s`; newer
1515
+ * tails only carry an end timestamp, which is diffed against the opening user
1516
+ * row's timestamp.
1517
+ */
1518
+ function parseTurnDuration(boundary) {
1519
+ const text = boundary.textContent ?? "";
1520
+ const zh = text.match(/用时\s*(\d+)分(\d+)秒|用时\s*(\d+)秒/);
1521
+ if (zh !== null) {
1522
+ if (zh[1] !== void 0 && zh[2] !== void 0) return Number(zh[1]) * 6e4 + Number(zh[2]) * 1e3;
1523
+ if (zh[3] !== void 0) return Number(zh[3]) * 1e3;
1524
+ return;
1525
+ }
1526
+ const en = text.match(/Ran for\s*(?:(\d+)m\s*)?(\d+)s/);
1527
+ if (en !== null) return ((en[1] === void 0 ? 0 : Number(en[1])) * 60 + Number(en[2])) * 1e3;
1528
+ const end = parseTimeText(text);
1529
+ const start = findTurnStart(boundary);
1530
+ if (end !== void 0 && start !== void 0 && end > start) return end - start;
1531
+ }
1532
+ /** DSH timestamp text (`8月14日 21:56` / `2026年8月14日 22:11`) → epoch ms. */
1533
+ function parseTimeText(text) {
1534
+ const m = text.match(/(?:(\d{4})年)?(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})/);
1535
+ if (m === null) return void 0;
1536
+ const year = m[1] !== void 0 ? Number(m[1]) : (/* @__PURE__ */ new Date()).getFullYear();
1537
+ const t = new Date(year, Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5])).getTime();
1538
+ return Number.isNaN(t) ? void 0 : t;
1539
+ }
1540
+ /** Latest turn-opening timestamp at or before the boundary element. */
1541
+ function findTurnStart(boundary) {
1542
+ const flow = boundary.parentElement;
1543
+ if (flow === null) return void 0;
1544
+ let best = null;
1545
+ for (const s of flow.querySelectorAll("[class*=\"timeStart\"]")) {
1546
+ const pos = s.compareDocumentPosition(boundary);
1547
+ if ((pos & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 || (pos & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0 || s === boundary) best = s;
1548
+ else break;
1549
+ }
1550
+ if (best === null) return void 0;
1551
+ return parseTimeText(best.textContent ?? "");
1552
+ }
1553
+ /** Compact duration pieces shared by both locales: seconds under a minute,
1554
+ * whole minutes drop the seconds, hours drop to minute granularity. */
1555
+ function durationParts(ms) {
1556
+ const s = Math.round(ms / 1e3);
1557
+ return {
1558
+ h: Math.floor(s / 3600),
1559
+ m: Math.floor(s % 3600 / 60),
1560
+ s: s % 60
1561
+ };
1562
+ }
1563
+ /** 毫秒 → 中文紧凑时长(14秒 / 2分05秒 / 15分 / 3小时2分)。 */
1564
+ function formatZhDuration(ms) {
1565
+ const { h, m, s } = durationParts(ms);
1566
+ if (h > 0) return m > 0 ? `${h}小时${m}分` : `${h}小时`;
1567
+ if (s < 60 && m === 0 && h === 0) return `${s}秒`;
1568
+ if (s === 0) return `${m}分`;
1569
+ return `${m}分${String(s).padStart(2, "0")}秒`;
1570
+ }
1571
+ /** Milliseconds → compact English duration (14s / 2m05s / 15m / 3h2m). */
1572
+ function formatEnDuration(ms) {
1573
+ const { h, m, s } = durationParts(ms);
1574
+ if (h > 0) return m > 0 ? `${h}h${m}m` : `${h}h`;
1575
+ if (m === 0 && h === 0) return `${s}s`;
1576
+ if (s === 0) return `${m}m`;
1577
+ return `${m}m${String(s).padStart(2, "0")}s`;
1578
+ }
1579
+ /** Native disclosure chevron (IconChevronDownOutline14, 14x14). */
1580
+ const CHEVRON_PATH = "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z";
1581
+ function createChevronIcon(className) {
1582
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1583
+ svg.setAttribute("width", "14");
1584
+ svg.setAttribute("height", "14");
1585
+ svg.setAttribute("class", className);
1586
+ svg.setAttribute("viewBox", "0 0 14 14");
1587
+ svg.setAttribute("fill", "none");
1588
+ svg.setAttribute("aria-hidden", "true");
1589
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1590
+ path.setAttribute("d", CHEVRON_PATH);
1591
+ path.setAttribute("fill", "currentColor");
1592
+ svg.appendChild(path);
1593
+ return svg;
1594
+ }
1595
+ function createProcessedRowElement(duration) {
1596
+ const copy = resolveFoldCopy();
1597
+ const btn = document.createElement("button");
1598
+ btn.type = "button";
1599
+ btn.className = PROCESSED_CLASS;
1600
+ btn.setAttribute("aria-expanded", "false");
1601
+ const text = document.createElement("span");
1602
+ text.textContent = duration === void 0 ? copy.processedLabel : `${copy.processedLabel} ${copy.formatDuration(duration)}`;
1603
+ btn.append(text, createChevronIcon(`${PROCESSED_CLASS}-chevron`));
1604
+ btn.title = copy.expandTitle;
1605
+ return btn;
1606
+ }
1607
+ function injectStyle() {
1608
+ if (document.getElementById(STYLE_ID) !== null) return;
1609
+ const style = document.createElement("style");
1610
+ style.id = STYLE_ID;
1611
+ style.textContent = FOLD_CSS;
1612
+ document.head.appendChild(style);
1613
+ }
1614
+ function removeStyle() {
1615
+ document.getElementById(STYLE_ID)?.remove();
1616
+ }
1617
+ //#endregion
522
1618
  //#region src/client/teleprompterGlide.ts
523
1619
  /**
524
1620
  * Conversation-port follow while an assistant reply streams.
@@ -554,8 +1650,6 @@ window.__ModuleLoader__.load({
554
1650
  const FOLLOW_OWNED_ATTR = "data-follow-owned";
555
1651
  /** Sub-pixel settle threshold; clearing below this cannot produce a visible rebound. */
556
1652
  const FOLLOW_SETTLE_EPSILON_PX = .25;
557
- /** Lowest reveal rate retained while the spring is short on paint room. */
558
- const FOLLOW_REVEAL_MIN_SCALE = .55;
559
1653
  /** Safe-lag occupancy band over which reveal pressure is progressively reduced. */
560
1654
  const FOLLOW_BACKPRESSURE_START_RATIO = .25;
561
1655
  const GESTURE_EVENTS = [
@@ -582,14 +1676,15 @@ window.__ModuleLoader__.load({
582
1676
  * a constrained paint lands at the minimum immediately so the next reveal
583
1677
  * commit cannot keep feeding an already-full visual buffer.
584
1678
  */
585
- function computeFollowRevealScale(lagPx, capacityPx, constrained = false) {
586
- if (constrained) return FOLLOW_REVEAL_MIN_SCALE;
1679
+ function computeFollowRevealScale(lagPx, capacityPx, constrained = false, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
1680
+ if (constrained) return tuning.backpressureMinScale;
587
1681
  if (!Number.isFinite(capacityPx)) return 1;
588
- if (capacityPx <= 0) return lagPx > 0 ? FOLLOW_REVEAL_MIN_SCALE : 1;
1682
+ if (capacityPx <= 0) return lagPx > 0 ? tuning.backpressureMinScale : 1;
589
1683
  const ratio = Math.min(1, Math.max(0, lagPx / capacityPx));
590
- if (ratio >= .75) return FOLLOW_REVEAL_MIN_SCALE;
1684
+ if (ratio >= .75) return tuning.backpressureMinScale;
591
1685
  const progress = Math.min(1, Math.max(0, (ratio - FOLLOW_BACKPRESSURE_START_RATIO) / .5));
592
- return 1 - .44999999999999996 * (progress * progress * (3 - 2 * progress));
1686
+ const eased = progress * progress * (3 - 2 * progress);
1687
+ return 1 - (1 - tuning.backpressureMinScale) * eased;
593
1688
  }
594
1689
  /**
595
1690
  * Semi-implicit spring integration with four substeps per <=32ms slice.
@@ -597,7 +1692,7 @@ window.__ModuleLoader__.load({
597
1692
  * @param input - Current visible lag and carried physics velocity.
598
1693
  * @returns The position advance, its fraction, and next velocity.
599
1694
  */
600
- function computeFollowStep(dtMs, input) {
1695
+ function computeFollowStep(dtMs, input, tuning = DEFAULT_STREAM_DEBUG_TUNING) {
601
1696
  if (input.lag <= .1 || dtMs <= 0) return {
602
1697
  advancePx: 0,
603
1698
  lerpStep: 0,
@@ -609,7 +1704,7 @@ window.__ModuleLoader__.load({
609
1704
  const slices = Math.max(1, Math.ceil(elapsedMs / 32));
610
1705
  const subDt = elapsedMs / 1e3 / slices / 4;
611
1706
  for (let slice = 0; slice < slices; slice += 1) for (let substep = 0; substep < 4; substep += 1) {
612
- const acceleration = (130 * lag - 24 * velocity) / 1;
1707
+ const acceleration = (tuning.springStiffness * lag - tuning.springDamping * velocity) / tuning.springMass;
613
1708
  velocity = Math.max(0, velocity + acceleration * subDt);
614
1709
  const advance = velocity * subDt;
615
1710
  if (advance >= lag) return {
@@ -631,10 +1726,11 @@ window.__ModuleLoader__.load({
631
1726
  return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
632
1727
  }
633
1728
  /** Outermost message surfaces; nested tool rows ride their parent. */
1729
+ /** @internal Test seam: the equal-lag transform set for one scrollport. */
634
1730
  function shiftSurfacesOf(port) {
635
1731
  const transcript = port.querySelector("[data-chat-transcript]");
636
1732
  if (transcript !== null) return [transcript];
637
- return [...port.querySelectorAll("[data-chat-anchor-key]")].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
1733
+ return [...port.querySelectorAll(`[data-chat-anchor-key], .${PROCESSED_CLASS}`)].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
638
1734
  }
639
1735
  function currentShiftOf(element) {
640
1736
  return Number(/translate3d\(0(?:px)?,\s*(-?[\d.]+)px,\s*0(?:px)?\)/.exec(element.style.transform)?.[1] ?? 0);
@@ -713,12 +1809,12 @@ window.__ModuleLoader__.load({
713
1809
  }
714
1810
  if (migrated) invalidatePaintLimit(port);
715
1811
  }
716
- function ensureRunway(port, surfaces) {
1812
+ function ensureRunway(port, surfaces, runwayPx = 48) {
717
1813
  const status = turnStatusOf(port);
718
1814
  const composer = port.querySelector("[data-composer-seat]");
719
1815
  migrateLegacyRunway(port, surfaces, status, composer);
720
1816
  const naturalHeight = Math.max(0, port.scrollHeight - runwayOffsetOf(port));
721
- if (port.clientHeight <= 0 || naturalHeight <= port.clientHeight) {
1817
+ if (runwayPx <= 0 || port.clientHeight <= 0 || naturalHeight <= port.clientHeight) {
722
1818
  restoreRunway(port);
723
1819
  return;
724
1820
  }
@@ -735,17 +1831,18 @@ window.__ModuleLoader__.load({
735
1831
  }
736
1832
  const element = target.element;
737
1833
  const current = followRunways.get(port);
738
- if (current?.element === element && current.property === target.property) return;
1834
+ if (current?.element === element && current.property === target.property && current.requestedPx === runwayPx) return;
739
1835
  restoreRunway(port);
740
1836
  const beforeHeight = port.scrollHeight;
741
1837
  const original = element.style[target.property];
742
- element.style[target.property] = original === "" ? `48px` : `calc(${original} + 48px)`;
1838
+ element.style[target.property] = original === "" ? `${runwayPx}px` : `calc(${original} + ${runwayPx}px)`;
743
1839
  const offset = Math.max(0, port.scrollHeight - beforeHeight);
744
1840
  followRunways.set(port, {
745
1841
  element,
746
1842
  offset,
747
1843
  property: target.property,
748
- original
1844
+ original,
1845
+ requestedPx: runwayPx
749
1846
  });
750
1847
  invalidatePaintLimit(port);
751
1848
  }
@@ -808,9 +1905,9 @@ window.__ModuleLoader__.load({
808
1905
  * same frame. Carrying that excess in `scrollTop` would move the transcript
809
1906
  * toward fixed chrome and also make the host expose jump-to-bottom.
810
1907
  */
811
- function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0) {
1908
+ function applyVisual(port, animatedH, reservePx, velocityPxPerSec = 0, runwayPx = 48) {
812
1909
  const surfaces = shiftSurfacesOf(port);
813
- ensureRunway(port, surfaces);
1910
+ ensureRunway(port, surfaces, runwayPx);
814
1911
  const contentHeight = Math.max(0, port.scrollHeight);
815
1912
  const runwayOffset = runwayOffsetOf(port);
816
1913
  const targetHeight = Math.max(0, contentHeight - runwayOffset);
@@ -956,9 +2053,10 @@ window.__ModuleLoader__.load({
956
2053
  };
957
2054
  const updateRevealScale = (next, elapsedMs, urgent = false) => {
958
2055
  if (revealScaleRef === void 0) return;
2056
+ const tuning = debugRuntime.activeTuning();
959
2057
  const state = followMotionStates.get(next);
960
- const target = state === void 0 ? 1 : computeFollowRevealScale(state.lagPx, state.capacityPx, state.constrained);
961
- const current = Math.min(1, Math.max(FOLLOW_REVEAL_MIN_SCALE, revealScaleRef.current));
2058
+ const target = state === void 0 ? 1 : computeFollowRevealScale(state.lagPx, state.capacityPx, state.constrained, tuning);
2059
+ const current = Math.min(1, Math.max(tuning.backpressureMinScale, revealScaleRef.current));
962
2060
  if (target < current || urgent) {
963
2061
  revealScaleRef.current = Math.min(current, target);
964
2062
  return;
@@ -969,6 +2067,22 @@ window.__ModuleLoader__.load({
969
2067
  const releaseRevealScale = () => {
970
2068
  if (revealScaleRef !== void 0) revealScaleRef.current = 1;
971
2069
  };
2070
+ const reportFollow = (next, isActive) => {
2071
+ const state = followMotionStates.get(next);
2072
+ debugRuntime.reportFollow(next, {
2073
+ lagPx: state?.lagPx ?? Math.max(0, next.scrollHeight - next.clientHeight - next.scrollTop),
2074
+ velocityPxPerSec: state?.velocityPxPerSec ?? 0,
2075
+ reservePx: state?.reservePx ?? 0,
2076
+ capacityPx: state?.capacityPx ?? 0,
2077
+ revealScale: revealScaleRef?.current ?? 1,
2078
+ following,
2079
+ constrained: state?.constrained ?? false,
2080
+ scrollTop: next.scrollTop,
2081
+ scrollHeight: next.scrollHeight,
2082
+ clientHeight: next.clientHeight,
2083
+ active: isActive
2084
+ });
2085
+ };
972
2086
  const isLeader = (next) => followLeaders.get(next)?.owner === owner;
973
2087
  const hold = (next) => {
974
2088
  followActivityAt.set(next, performance.now());
@@ -986,11 +2100,14 @@ window.__ModuleLoader__.load({
986
2100
  clearMotion(next);
987
2101
  followMotionStates.delete(next);
988
2102
  releaseRevealScale();
2103
+ debugRuntime.reportFollow(next, null);
989
2104
  }
990
2105
  };
991
2106
  const handBackVisual = (next) => {
992
2107
  const shift = currentShiftOf(shiftSurfacesOf(next).at(-1) ?? next);
993
2108
  const visualTop = Math.max(0, next.scrollTop - Math.max(0, shift));
2109
+ clearMotion(next);
2110
+ restoreRunway(next);
994
2111
  const floor = Math.max(0, next.scrollHeight - next.clientHeight);
995
2112
  next.scrollTop = Math.min(visualTop, Math.max(0, floor - 26));
996
2113
  followScrollLedgers.set(next, next.scrollTop);
@@ -1018,8 +2135,10 @@ window.__ModuleLoader__.load({
1018
2135
  const restoreBeforePaint = () => {
1019
2136
  if (!following || port === null || !isLeader(port)) return;
1020
2137
  invalidatePaintLimit(port);
1021
- animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec);
2138
+ const tuning = debugRuntime.activeTuning();
2139
+ animatedH = applyVisual(port, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1022
2140
  updateRevealScale(port, 0, true);
2141
+ reportFollow(port, activeRef.current);
1023
2142
  };
1024
2143
  const bindPort = (next) => {
1025
2144
  if (port === next) return;
@@ -1041,6 +2160,7 @@ window.__ModuleLoader__.load({
1041
2160
  rafId = requestAnimationFrame(frame);
1042
2161
  const elapsedMs = Math.max(.001, now - last);
1043
2162
  const dt = Math.min(32, elapsedMs);
2163
+ const tuning = debugRuntime.activeTuning();
1044
2164
  last = now;
1045
2165
  const root = rootRef.current;
1046
2166
  if (root === null) return;
@@ -1067,8 +2187,9 @@ window.__ModuleLoader__.load({
1067
2187
  if (following) {
1068
2188
  hold(nextPort);
1069
2189
  if (isLeader(nextPort)) {
1070
- animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec);
2190
+ animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1071
2191
  updateRevealScale(nextPort, elapsedMs);
2192
+ reportFollow(nextPort, activeRef.current);
1072
2193
  const runwayOffset = runwayOffsetOf(nextPort);
1073
2194
  if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffset) <= .25) finishEntrance();
1074
2195
  } else finishEntrance();
@@ -1076,7 +2197,7 @@ window.__ModuleLoader__.load({
1076
2197
  primed = true;
1077
2198
  return;
1078
2199
  }
1079
- if (!following && !interacting && reportedLag <= (readerReleased ? 1 : 25)) {
2200
+ if (!following && (!interacting || readerReleased && !readerGestureIntent && reportedLag <= 1) && reportedLag <= (readerReleased ? 1 : 25)) {
1080
2201
  following = true;
1081
2202
  readerReleased = false;
1082
2203
  animatedH = extent;
@@ -1097,6 +2218,7 @@ window.__ModuleLoader__.load({
1097
2218
  }
1098
2219
  if (!activeRef.current || !following) {
1099
2220
  followScrollLedgers.set(nextPort, nextPort.scrollTop);
2221
+ reportFollow(nextPort, activeRef.current);
1100
2222
  return;
1101
2223
  }
1102
2224
  hold(nextPort);
@@ -1108,14 +2230,14 @@ window.__ModuleLoader__.load({
1108
2230
  const contentHeight = nextPort.scrollHeight;
1109
2231
  const lag = Math.max(0, contentHeight - animatedH - runwayOffset);
1110
2232
  const predictGrowth = predictiveRef?.current ?? predictive;
1111
- const reserveTarget = !predictGrowth ? 0 : computeFollowReserve(speedCpsRef.current, runwayOffset);
1112
- const reserveStep = 1 - Math.exp(-elapsedMs / 180);
2233
+ const reserveTarget = !predictGrowth ? 0 : computeFollowReserve(speedCpsRef.current, tuning.runwayPx);
2234
+ const reserveStep = 1 - Math.exp(-elapsedMs / tuning.reserveResponseMs);
1113
2235
  reservePx += (reserveTarget - reservePx) * reserveStep;
1114
2236
  const step = computeFollowStep(dt, {
1115
2237
  lag,
1116
2238
  speedEma: speedCpsRef.current,
1117
2239
  velocityPxPerSec
1118
- });
2240
+ }, tuning);
1119
2241
  if (lag <= .1) {
1120
2242
  animatedH = contentHeight - runwayOffset;
1121
2243
  velocityPxPerSec = 0;
@@ -1124,8 +2246,9 @@ window.__ModuleLoader__.load({
1124
2246
  animatedH = Math.min(contentHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
1125
2247
  velocityPxPerSec = step.velocityPxPerSec;
1126
2248
  }
1127
- animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec);
2249
+ animatedH = applyVisual(nextPort, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
1128
2250
  updateRevealScale(nextPort, elapsedMs);
2251
+ reportFollow(nextPort, true);
1129
2252
  if (Math.max(0, nextPort.scrollHeight - animatedH - runwayOffsetOf(nextPort)) <= .25) finishEntrance();
1130
2253
  };
1131
2254
  frame(performance.now());
@@ -1143,6 +2266,7 @@ window.__ModuleLoader__.load({
1143
2266
  clearVisual(host);
1144
2267
  followLeaders.delete(host);
1145
2268
  releaseRevealScale();
2269
+ debugRuntime.reportFollow(host, null);
1146
2270
  return;
1147
2271
  }
1148
2272
  if (preserveReader) {
@@ -1150,19 +2274,23 @@ window.__ModuleLoader__.load({
1150
2274
  clearVisual(host);
1151
2275
  followLeaders.delete(host);
1152
2276
  releaseRevealScale();
2277
+ debugRuntime.reportFollow(host, null);
1153
2278
  return;
1154
2279
  }
1155
- ensureRunway(host, shiftSurfacesOf(host));
2280
+ const completionTuning = debugRuntime.activeTuning();
2281
+ ensureRunway(host, shiftSurfacesOf(host), completionTuning.runwayPx);
1156
2282
  const completionRunway = runwayOffsetOf(host);
1157
2283
  const completionMinimumLag = Math.max(0, reservePx);
1158
2284
  animatedH = Math.min(animatedH, host.scrollHeight - completionRunway - completionMinimumLag);
1159
2285
  settleAtFloor(host);
1160
- animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec);
2286
+ animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, completionTuning.runwayPx);
2287
+ reportFollow(host, false);
1161
2288
  const runwayOffset = runwayOffsetOf(host);
1162
2289
  if (Math.max(0, host.scrollHeight - animatedH - runwayOffset) <= .25 && reservePx <= .25) {
1163
2290
  finishAtNaturalFloor(host);
1164
2291
  followLeaders.delete(host);
1165
2292
  releaseRevealScale();
2293
+ debugRuntime.reportFollow(host, null);
1166
2294
  return;
1167
2295
  }
1168
2296
  for (const name of GESTURE_EVENTS) host.addEventListener(name, markGesture, { passive: true });
@@ -1185,14 +2313,16 @@ window.__ModuleLoader__.load({
1185
2313
  clearVisual(host);
1186
2314
  followLeaders.delete(host);
1187
2315
  releaseRevealScale();
2316
+ debugRuntime.reportFollow(host, null);
1188
2317
  stopSettleListeners();
1189
2318
  return;
1190
2319
  }
1191
2320
  const dt = Math.min(32, Math.max(0, now - settleLast));
2321
+ const tuning = debugRuntime.activeTuning();
1192
2322
  settleLast = now;
1193
2323
  const runwayOffset = runwayOffsetOf(host);
1194
2324
  const lag = Math.max(0, host.scrollHeight - animatedH - runwayOffset);
1195
- const reserveStep = 1 - Math.exp(-dt / 180);
2325
+ const reserveStep = 1 - Math.exp(-dt / tuning.reserveResponseMs);
1196
2326
  reservePx += (0 - reservePx) * reserveStep;
1197
2327
  if (lag <= .25 && reservePx <= .25) {
1198
2328
  animatedH = host.scrollHeight - runwayOffset;
@@ -1201,6 +2331,7 @@ window.__ModuleLoader__.load({
1201
2331
  finishAtNaturalFloor(host);
1202
2332
  followLeaders.delete(host);
1203
2333
  releaseRevealScale();
2334
+ debugRuntime.reportFollow(host, null);
1204
2335
  stopSettleListeners();
1205
2336
  return;
1206
2337
  }
@@ -1208,12 +2339,13 @@ window.__ModuleLoader__.load({
1208
2339
  lag,
1209
2340
  speedEma: speedCpsRef.current,
1210
2341
  velocityPxPerSec
1211
- });
2342
+ }, tuning);
1212
2343
  const minimumLag = Math.max(0, reservePx);
1213
2344
  animatedH = Math.min(host.scrollHeight - runwayOffset - minimumLag, animatedH + step.advancePx);
1214
2345
  velocityPxPerSec = step.velocityPxPerSec;
1215
2346
  settleAtFloor(host);
1216
- animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec);
2347
+ animatedH = applyVisual(host, animatedH, reservePx, velocityPxPerSec, tuning.runwayPx);
2348
+ reportFollow(host, false);
1217
2349
  requestAnimationFrame(settleFrame);
1218
2350
  };
1219
2351
  requestAnimationFrame(settleFrame);
@@ -1278,10 +2410,6 @@ window.__ModuleLoader__.load({
1278
2410
  */
1279
2411
  const STREAM_BOOT_GLOBAL = "__DSH_SMOOTH_STREAM_CONFIG__";
1280
2412
  //#endregion
1281
- //#region src/settings.ts
1282
- /** Defaults shared by the Host schema and the client-side fallback. */
1283
- const DEFAULT_STREAM_SETTINGS = { thinkAutoExpand: true };
1284
- //#endregion
1285
2413
  //#region src/client/TypewriterAssistantNodeView.tsx
1286
2414
  function usePrefersReducedMotion() {
1287
2415
  const [reduced, setReduced] = (0, react.useState)(() => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true);
@@ -1805,6 +2933,8 @@ window.__ModuleLoader__.load({
1805
2933
  * while the row belongs to the live Agent turn. No clip, mask, overlay, or
1806
2934
  * duplicate accessibility tree is introduced.
1807
2935
  */
2936
+ /** Last presented text per root, retained across follow lifecycle flips. */
2937
+ const ledgerByRoot = /* @__PURE__ */ new WeakMap();
1808
2938
  const SKIP_TEXT_SELECTOR = [
1809
2939
  "[aria-hidden=\"true\"]",
1810
2940
  "[aria-live]",
@@ -1832,10 +2962,56 @@ window.__ModuleLoader__.load({
1832
2962
  */
1833
2963
  function useProgressiveDomText(rootRef, enabled, revealInitial, speedCpsRef, onSettled) {
1834
2964
  (0, react.useLayoutEffect)(() => {
1835
- if (!enabled) return;
1836
2965
  const root = rootRef.current;
1837
2966
  if (root === null || typeof document === "undefined") return;
1838
- const records = /* @__PURE__ */ new Map();
2967
+ let records = ledgerByRoot.get(root);
2968
+ if (!enabled && records === void 0) return;
2969
+ if (records === void 0) {
2970
+ records = /* @__PURE__ */ new Map();
2971
+ ledgerByRoot.set(root, records);
2972
+ }
2973
+ const forEachText = (from, callback) => {
2974
+ if (from.nodeType === Node.TEXT_NODE) {
2975
+ callback(from);
2976
+ return;
2977
+ }
2978
+ const walker = document.createTreeWalker(from, NodeFilter.SHOW_TEXT);
2979
+ let current = walker.nextNode();
2980
+ while (current !== null) {
2981
+ callback(current);
2982
+ current = walker.nextNode();
2983
+ }
2984
+ };
2985
+ const settle = (text) => {
2986
+ if (!revealable(text, root)) return;
2987
+ const chars = [...text.data];
2988
+ records.set(text, {
2989
+ chars,
2990
+ full: text.data,
2991
+ shown: chars.length
2992
+ });
2993
+ };
2994
+ const snapshotVisible = () => {
2995
+ const current = /* @__PURE__ */ new Set();
2996
+ forEachText(root, (text) => {
2997
+ if (!revealable(text, root)) return;
2998
+ current.add(text);
2999
+ settle(text);
3000
+ });
3001
+ for (const text of records.keys()) if (!current.has(text)) records.delete(text);
3002
+ };
3003
+ if (!enabled) {
3004
+ snapshotVisible();
3005
+ const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(snapshotVisible);
3006
+ observer?.observe(root, {
3007
+ childList: true,
3008
+ characterData: true,
3009
+ subtree: true
3010
+ });
3011
+ return () => {
3012
+ observer?.disconnect();
3013
+ };
3014
+ }
1839
3015
  const pending = /* @__PURE__ */ new Set();
1840
3016
  const internalWrites = /* @__PURE__ */ new WeakMap();
1841
3017
  let rafId = 0;
@@ -1843,10 +3019,12 @@ window.__ModuleLoader__.load({
1843
3019
  let debt = 0;
1844
3020
  let stopped = false;
1845
3021
  let announcedSettled = false;
3022
+ const streamId = `dom-${Math.random().toString(36).slice(2)}`;
1846
3023
  const announceSettled = () => {
1847
3024
  lastFrame = null;
1848
3025
  debt = 0;
1849
3026
  speedCpsRef.current = 35;
3027
+ debugRuntime.reportStream(streamId, null);
1850
3028
  if (announcedSettled) return;
1851
3029
  announcedSettled = true;
1852
3030
  onSettled?.();
@@ -1873,18 +3051,21 @@ window.__ModuleLoader__.load({
1873
3051
  write(node, chars.slice(0, preserved).join(""));
1874
3052
  };
1875
3053
  const visit = (from, reveal) => {
1876
- if (from.nodeType === Node.TEXT_NODE) {
1877
- const text = from;
1878
- if (reveal) enqueue(text, text.data, records.get(text));
1879
- return;
1880
- }
1881
- const walker = document.createTreeWalker(from, NodeFilter.SHOW_TEXT);
1882
- let current = walker.nextNode();
1883
- while (current !== null) {
1884
- const text = current;
1885
- if (reveal) enqueue(text, text.data, records.get(text));
1886
- current = walker.nextNode();
1887
- }
3054
+ forEachText(from, (text) => {
3055
+ if (!revealable(text, root)) return;
3056
+ if (reveal) {
3057
+ enqueue(text, text.data, records.get(text));
3058
+ return;
3059
+ }
3060
+ settle(text);
3061
+ });
3062
+ };
3063
+ const forget = (from) => {
3064
+ forEachText(from, (text) => {
3065
+ if (root.contains(text)) return;
3066
+ records.delete(text);
3067
+ pending.delete(text);
3068
+ });
1888
3069
  };
1889
3070
  const scheduleFrame = () => {
1890
3071
  if (stopped || pending.size === 0 || rafId !== 0) return;
@@ -1910,7 +3091,7 @@ window.__ModuleLoader__.load({
1910
3091
  const record = records.get(node);
1911
3092
  if (record !== void 0) backlog += record.chars.length - record.shown;
1912
3093
  }
1913
- const step = computeAdaptiveQueueStep(backlog, elapsed, debt);
3094
+ const step = computeAdaptiveQueueStep(backlog, elapsed, debt, 1, debugRuntime.activeTuning());
1914
3095
  debt = step.debt;
1915
3096
  speedCpsRef.current = step.speedCps;
1916
3097
  let remaining = step.revealChars;
@@ -1919,6 +3100,7 @@ window.__ModuleLoader__.load({
1919
3100
  const record = records.get(node);
1920
3101
  if (record === void 0 || !node.isConnected) {
1921
3102
  pending.delete(node);
3103
+ records.delete(node);
1922
3104
  continue;
1923
3105
  }
1924
3106
  const amount = Math.min(remaining, record.chars.length - record.shown);
@@ -1932,10 +3114,30 @@ window.__ModuleLoader__.load({
1932
3114
  remaining -= amount;
1933
3115
  if (shown >= next.chars.length) pending.delete(node);
1934
3116
  }
3117
+ let targetChars = 0;
3118
+ let displayedChars = 0;
3119
+ let nextBacklog = 0;
3120
+ for (const [node, record] of records) {
3121
+ if (!node.isConnected) {
3122
+ records.delete(node);
3123
+ pending.delete(node);
3124
+ continue;
3125
+ }
3126
+ targetChars += record.chars.length;
3127
+ displayedChars += record.shown;
3128
+ if (pending.has(node)) nextBacklog += record.chars.length - record.shown;
3129
+ }
3130
+ debugRuntime.reportStream(streamId, {
3131
+ backlog: nextBacklog,
3132
+ speedCps: step.speedCps,
3133
+ targetChars,
3134
+ displayedChars,
3135
+ active: pending.size > 0
3136
+ });
1935
3137
  if (pending.size === 0) announceSettled();
1936
3138
  else scheduleFrame();
1937
3139
  };
1938
- if (revealInitial) visit(root, true);
3140
+ visit(root, revealInitial);
1939
3141
  const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver((mutations) => {
1940
3142
  for (const mutation of mutations) {
1941
3143
  if (mutation.type === "characterData") {
@@ -1947,6 +3149,7 @@ window.__ModuleLoader__.load({
1947
3149
  enqueue(node, node.data, records.get(node));
1948
3150
  continue;
1949
3151
  }
3152
+ for (const removed of mutation.removedNodes) forget(removed);
1950
3153
  for (const added of mutation.addedNodes) visit(added, true);
1951
3154
  }
1952
3155
  if (pending.size === 0) announceSettled();
@@ -1969,6 +3172,7 @@ window.__ModuleLoader__.load({
1969
3172
  }
1970
3173
  pending.clear();
1971
3174
  speedCpsRef.current = 35;
3175
+ debugRuntime.reportStream(streamId, null);
1972
3176
  };
1973
3177
  }, [
1974
3178
  enabled,
@@ -2151,50 +3355,51 @@ window.__ModuleLoader__.load({
2151
3355
  }
2152
3356
  //#endregion
2153
3357
  //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/SmoothStreamCard.module.css.mjs
2154
- const css = ".XNGeRG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.XNGeRG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.XNGeRG_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.XNGeRG_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.XNGeRG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.XNGeRG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.XNGeRG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.XNGeRG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.XNGeRG_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.XNGeRG_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.XNGeRG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.XNGeRG_chevronOpen{transform:rotate(180deg)}.XNGeRG_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.XNGeRG_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.XNGeRG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.XNGeRG_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.XNGeRG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.XNGeRG_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_toggle:disabled{cursor:default}.XNGeRG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.XNGeRG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.XNGeRG_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.XNGeRG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.XNGeRG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.XNGeRG_save,.XNGeRG_discard,.XNGeRG_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.XNGeRG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.XNGeRG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.XNGeRG_discard:hover:not(:disabled),.XNGeRG_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.XNGeRG_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.XNGeRG_discard:disabled,.XNGeRG_save:disabled,.XNGeRG_update:disabled{opacity:.4;cursor:default}.XNGeRG_discard:focus-visible,.XNGeRG_save:focus-visible,.XNGeRG_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
2155
- const tagId = "dsh-smooth-stream/SmoothStreamCard.module.css";
3358
+ const css$1 = ".XNGeRG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.XNGeRG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.XNGeRG_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.XNGeRG_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.XNGeRG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.XNGeRG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.XNGeRG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.XNGeRG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.XNGeRG_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.XNGeRG_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.XNGeRG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.XNGeRG_chevronOpen{transform:rotate(180deg)}.XNGeRG_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.XNGeRG_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.XNGeRG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.XNGeRG_field+.XNGeRG_field{border-top:1px solid var(--dsw-alias-border-l2)}.XNGeRG_fieldDisabled{opacity:.5}.XNGeRG_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.XNGeRG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.XNGeRG_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.XNGeRG_toggle:disabled{cursor:default}.XNGeRG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.XNGeRG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.XNGeRG_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.XNGeRG_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.XNGeRG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.XNGeRG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.XNGeRG_save,.XNGeRG_discard,.XNGeRG_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.XNGeRG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.XNGeRG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.XNGeRG_discard:hover:not(:disabled),.XNGeRG_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.XNGeRG_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.XNGeRG_discard:disabled,.XNGeRG_save:disabled,.XNGeRG_update:disabled{opacity:.4;cursor:default}.XNGeRG_discard:focus-visible,.XNGeRG_save:focus-visible,.XNGeRG_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
3359
+ const tagId$1 = "dsh-smooth-stream/SmoothStreamCard.module.css";
2156
3360
  if (typeof document !== "undefined") {
2157
- let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
3361
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
2158
3362
  if (tag === null) {
2159
3363
  tag = document.createElement("style");
2160
3364
  tag.dataset.plugin = "dsh-smooth-stream";
2161
- tag.dataset.pluginCss = tagId;
3365
+ tag.dataset.pluginCss = tagId$1;
2162
3366
  document.head.appendChild(tag);
2163
3367
  }
2164
- tag.textContent = css;
3368
+ tag.textContent = css$1;
2165
3369
  }
2166
3370
  var SmoothStreamCard_module_css_default = {
2167
- "header": "XNGeRG_header",
2168
- "version": "XNGeRG_version",
2169
- "readOnly": "XNGeRG_readOnly",
2170
- "footer": "XNGeRG_footer",
2171
- "label": "XNGeRG_label",
3371
+ "updateRow": "XNGeRG_updateRow",
3372
+ "fieldDisabled": "XNGeRG_fieldDisabled",
2172
3373
  "hint": "XNGeRG_hint",
2173
- "fieldHead": "XNGeRG_fieldHead",
3374
+ "chevron": "XNGeRG_chevron",
3375
+ "cardOpen": "XNGeRG_cardOpen",
3376
+ "version": "XNGeRG_version",
3377
+ "pending": "XNGeRG_pending",
2174
3378
  "failure": "XNGeRG_failure",
2175
- "update": "XNGeRG_update",
2176
- "field": "XNGeRG_field",
3379
+ "header": "XNGeRG_header",
3380
+ "updateCopy": "XNGeRG_updateCopy",
3381
+ "failed": "XNGeRG_failed",
3382
+ "discard": "XNGeRG_discard",
3383
+ "name": "XNGeRG_name",
2177
3384
  "save": "XNGeRG_save",
2178
- "pending": "XNGeRG_pending",
2179
- "card": "XNGeRG_card",
2180
- "cardOpen": "XNGeRG_cardOpen",
2181
- "chevron": "XNGeRG_chevron",
3385
+ "fieldHead": "XNGeRG_fieldHead",
3386
+ "update": "XNGeRG_update",
3387
+ "headText": "XNGeRG_headText",
2182
3388
  "toggle": "XNGeRG_toggle",
2183
3389
  "body": "XNGeRG_body",
2184
- "name": "XNGeRG_name",
2185
- "failed": "XNGeRG_failed",
2186
- "updateRow": "XNGeRG_updateRow",
2187
- "discard": "XNGeRG_discard",
3390
+ "readOnly": "XNGeRG_readOnly",
2188
3391
  "description": "XNGeRG_description",
2189
- "headText": "XNGeRG_headText",
2190
- "updateCopy": "XNGeRG_updateCopy",
2191
- "chevronOpen": "XNGeRG_chevronOpen"
3392
+ "chevronOpen": "XNGeRG_chevronOpen",
3393
+ "card": "XNGeRG_card",
3394
+ "field": "XNGeRG_field",
3395
+ "label": "XNGeRG_label",
3396
+ "footer": "XNGeRG_footer"
2192
3397
  };
2193
3398
  //#endregion
2194
3399
  //#region src/client/SmoothStreamCard.tsx
2195
3400
  /**
2196
3401
  * The smooth-stream plugin configuration card, rendered inside the Web
2197
- * Settings "plugin configuration" page. One toggle, staged until the user
3402
+ * Settings "plugin configuration" page. Preferences are staged until the user
2198
3403
  * saves — the same shape as the Host-shipped cards, hand-drawn because the
2199
3404
  * Host cards' chrome is not exported for reuse.
2200
3405
  */
@@ -2264,6 +3469,27 @@ window.__ModuleLoader__.load({
2264
3469
  }) : null,
2265
3470
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2266
3471
  className: SmoothStreamCard_module_css_default.field,
3472
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3473
+ className: SmoothStreamCard_module_css_default.fieldHead,
3474
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3475
+ className: SmoothStreamCard_module_css_default.label,
3476
+ children: t("enabled")
3477
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3478
+ type: "checkbox",
3479
+ className: SmoothStreamCard_module_css_default.toggle,
3480
+ checked: state.enabled,
3481
+ disabled: !state.writable || state.saving,
3482
+ onChange: (event) => {
3483
+ props.edit({ enabled: event.target.checked });
3484
+ }
3485
+ })]
3486
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3487
+ className: SmoothStreamCard_module_css_default.hint,
3488
+ children: t("enabledHint")
3489
+ })]
3490
+ }),
3491
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3492
+ className: state.enabled ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
2267
3493
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2268
3494
  className: SmoothStreamCard_module_css_default.fieldHead,
2269
3495
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2273,9 +3499,9 @@ window.__ModuleLoader__.load({
2273
3499
  type: "checkbox",
2274
3500
  className: SmoothStreamCard_module_css_default.toggle,
2275
3501
  checked: state.thinkAutoExpand,
2276
- disabled: !state.writable,
3502
+ disabled: !state.writable || state.saving || !state.enabled,
2277
3503
  onChange: (event) => {
2278
- props.edit(event.target.checked);
3504
+ props.edit({ thinkAutoExpand: event.target.checked });
2279
3505
  }
2280
3506
  })]
2281
3507
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2283,6 +3509,48 @@ window.__ModuleLoader__.load({
2283
3509
  children: t("thinkAutoExpandHint")
2284
3510
  })]
2285
3511
  }),
3512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3513
+ className: SmoothStreamCard_module_css_default.field,
3514
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3515
+ className: SmoothStreamCard_module_css_default.fieldHead,
3516
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3517
+ className: SmoothStreamCard_module_css_default.label,
3518
+ children: t("autoCollapse")
3519
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3520
+ type: "checkbox",
3521
+ className: SmoothStreamCard_module_css_default.toggle,
3522
+ checked: state.autoCollapse,
3523
+ disabled: !state.writable || state.saving,
3524
+ onChange: (event) => {
3525
+ props.edit({ autoCollapse: event.target.checked });
3526
+ }
3527
+ })]
3528
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3529
+ className: SmoothStreamCard_module_css_default.hint,
3530
+ children: t("autoCollapseHint")
3531
+ })]
3532
+ }),
3533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3534
+ className: state.debugAvailable ? SmoothStreamCard_module_css_default.field : `${SmoothStreamCard_module_css_default.field} ${SmoothStreamCard_module_css_default.fieldDisabled}`,
3535
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3536
+ className: SmoothStreamCard_module_css_default.fieldHead,
3537
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3538
+ className: SmoothStreamCard_module_css_default.label,
3539
+ children: t("debugEnabled")
3540
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3541
+ type: "checkbox",
3542
+ className: SmoothStreamCard_module_css_default.toggle,
3543
+ checked: state.debugEnabled,
3544
+ disabled: !state.debugAvailable || !state.writable || state.saving,
3545
+ onChange: (event) => {
3546
+ props.edit({ debugEnabled: event.target.checked });
3547
+ }
3548
+ })]
3549
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3550
+ className: SmoothStreamCard_module_css_default.hint,
3551
+ children: state.debugAvailable ? t("debugEnabledHint") : t("debugUnavailable")
3552
+ })]
3553
+ }),
2286
3554
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2287
3555
  className: SmoothStreamCard_module_css_default.updateRow,
2288
3556
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -2348,7 +3616,9 @@ window.__ModuleLoader__.load({
2348
3616
  api;
2349
3617
  store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.projection());
2350
3618
  loaded;
2351
- staged;
3619
+ loadedDebug;
3620
+ stagedBase;
3621
+ stagedDebug;
2352
3622
  saving = false;
2353
3623
  failed = false;
2354
3624
  upgrading = false;
@@ -2379,8 +3649,22 @@ window.__ModuleLoader__.load({
2379
3649
  inject() {
2380
3650
  return {
2381
3651
  hooks: { smoothStreamCard: this.store },
2382
- edit: (value) => {
2383
- this.staged = value;
3652
+ edit: (patch) => {
3653
+ if (this.saving) return;
3654
+ if (patch.enabled !== void 0 || patch.thinkAutoExpand !== void 0 || patch.autoCollapse !== void 0) this.stagedBase = {
3655
+ ...this.baseValues(),
3656
+ ...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
3657
+ ...patch.thinkAutoExpand === void 0 ? {} : { thinkAutoExpand: patch.thinkAutoExpand },
3658
+ ...patch.autoCollapse === void 0 ? {} : { autoCollapse: patch.autoCollapse }
3659
+ };
3660
+ if (this.loadedDebug !== void 0 && (patch.debugEnabled !== void 0 || patch.debugTuning !== void 0)) this.stagedDebug = {
3661
+ ...this.debugValues(),
3662
+ ...patch.debugEnabled === void 0 ? {} : { debugEnabled: patch.debugEnabled },
3663
+ ...patch.debugTuning === void 0 ? {} : { debugTuning: {
3664
+ ...this.debugValues().debugTuning,
3665
+ ...patch.debugTuning
3666
+ } }
3667
+ };
2384
3668
  this.failed = false;
2385
3669
  this.publish();
2386
3670
  },
@@ -2388,8 +3672,9 @@ window.__ModuleLoader__.load({
2388
3672
  this.save();
2389
3673
  },
2390
3674
  discard: () => {
2391
- if (this.staged === void 0 && !this.failed) return;
2392
- this.staged = void 0;
3675
+ if (this.stagedBase === void 0 && this.stagedDebug === void 0 && !this.failed) return;
3676
+ this.stagedBase = void 0;
3677
+ this.stagedDebug = void 0;
2393
3678
  this.failed = false;
2394
3679
  this.publish();
2395
3680
  },
@@ -2405,10 +3690,12 @@ window.__ModuleLoader__.load({
2405
3690
  return {
2406
3691
  status: this.loadStatus,
2407
3692
  writable: this.loaded?.writable ?? false,
2408
- dirty: this.staged !== void 0,
3693
+ dirty: this.stagedBase !== void 0 || this.stagedDebug !== void 0,
2409
3694
  saving: this.saving,
2410
3695
  failed: this.failed,
2411
- thinkAutoExpand: this.staged ?? this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
3696
+ ...this.baseValues(),
3697
+ ...this.debugValues(),
3698
+ debugAvailable: this.loadedDebug !== void 0,
2412
3699
  version: this.loaded?.version,
2413
3700
  installation: this.loaded?.installation ?? "unmanaged",
2414
3701
  canUpgrade: this.loaded?.canUpgrade ?? false,
@@ -2417,32 +3704,81 @@ window.__ModuleLoader__.load({
2417
3704
  restartRequired: this.restartRequired
2418
3705
  };
2419
3706
  }
3707
+ baseValues() {
3708
+ return this.stagedBase ?? {
3709
+ enabled: this.loaded?.enabled ?? DEFAULT_STREAM_SETTINGS.enabled,
3710
+ thinkAutoExpand: this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
3711
+ autoCollapse: this.loaded?.autoCollapse ?? DEFAULT_STREAM_SETTINGS.autoCollapse
3712
+ };
3713
+ }
3714
+ debugValues() {
3715
+ return this.stagedDebug ?? {
3716
+ debugEnabled: this.loadedDebug?.debugEnabled ?? DEFAULT_STREAM_SETTINGS.debugEnabled,
3717
+ debugTuning: this.loadedDebug?.tuning ?? DEFAULT_STREAM_DEBUG_TUNING
3718
+ };
3719
+ }
3720
+ /** Complete settings projection consumed by the live SettingsCell bridge. */
3721
+ values() {
3722
+ return {
3723
+ ...this.baseValues(),
3724
+ ...this.debugValues()
3725
+ };
3726
+ }
2420
3727
  async load() {
2421
3728
  const generation = ++this.loadGeneration;
2422
3729
  this.loadStatus = "loading";
2423
3730
  this.loaded = void 0;
3731
+ this.loadedDebug = void 0;
2424
3732
  this.publish();
2425
3733
  try {
2426
3734
  const view = await this.api.read();
2427
3735
  if (generation !== this.loadGeneration) return;
2428
3736
  this.loaded = view;
2429
3737
  this.loadStatus = "ready";
3738
+ this.publish();
3739
+ try {
3740
+ const debug = await this.api.readDebug();
3741
+ if (generation !== this.loadGeneration) return;
3742
+ this.loadedDebug = debug;
3743
+ } catch {
3744
+ if (generation !== this.loadGeneration) return;
3745
+ this.loadedDebug = void 0;
3746
+ }
2430
3747
  } catch {
2431
3748
  if (generation !== this.loadGeneration) return;
2432
3749
  this.loaded = void 0;
3750
+ this.loadedDebug = void 0;
2433
3751
  this.loadStatus = "unavailable";
2434
3752
  }
2435
3753
  this.publish();
2436
3754
  }
2437
3755
  async save() {
2438
- if (this.staged === void 0 || this.saving || this.loaded?.writable !== true) return;
2439
- const value = this.staged;
3756
+ if (this.stagedBase === void 0 && this.stagedDebug === void 0 || this.saving || this.loaded?.writable !== true) return;
3757
+ const base = this.stagedBase;
3758
+ const debug = this.stagedDebug;
2440
3759
  this.saving = true;
2441
3760
  this.failed = false;
2442
3761
  this.publish();
2443
3762
  try {
2444
- this.loaded = await this.api.write(value);
2445
- this.staged = void 0;
3763
+ if (base !== void 0) {
3764
+ const combined = debug !== void 0 && this.loadedDebug !== void 0;
3765
+ this.loaded = await this.api.write(combined ? {
3766
+ ...base,
3767
+ ...debug
3768
+ } : base);
3769
+ this.stagedBase = void 0;
3770
+ if (combined) {
3771
+ this.loadedDebug = {
3772
+ debugEnabled: debug.debugEnabled,
3773
+ tuning: { ...debug.debugTuning }
3774
+ };
3775
+ this.stagedDebug = void 0;
3776
+ }
3777
+ }
3778
+ if (debug !== void 0 && this.loadedDebug !== void 0 && this.stagedDebug !== void 0) {
3779
+ this.loadedDebug = await this.api.writeDebug(debug);
3780
+ this.stagedDebug = void 0;
3781
+ }
2446
3782
  } catch {
2447
3783
  this.failed = true;
2448
3784
  }
@@ -2477,6 +3813,8 @@ window.__ModuleLoader__.load({
2477
3813
  const STREAM_SETTINGS_RPC = {
2478
3814
  read: "settings.read",
2479
3815
  write: "settings.write",
3816
+ debugRead: "debug.read",
3817
+ debugWrite: "debug.write",
2480
3818
  upgrade: "plugin.upgrade"
2481
3819
  };
2482
3820
  //#endregion
@@ -2490,13 +3828,22 @@ window.__ModuleLoader__.load({
2490
3828
  "npm",
2491
3829
  "development",
2492
3830
  "unmanaged"
2493
- ].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.thinkAutoExpand !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
3831
+ ].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.enabled !== "boolean" || typeof data.thinkAutoExpand !== "boolean" || typeof data.autoCollapse !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-smooth-stream: malformed settings response");
2494
3832
  return data;
2495
3833
  }
2496
3834
  function upgradeView(value) {
2497
3835
  if (record(value)?.restartRequired !== true) throw new Error("dsh-smooth-stream: malformed update response");
2498
3836
  return { restartRequired: true };
2499
3837
  }
3838
+ function debugSettingsView(value) {
3839
+ const data = record(value);
3840
+ const tuning = record(data?.tuning);
3841
+ if (data === void 0 || typeof data.debugEnabled !== "boolean" || tuning === void 0 || typeof tuning.revealScale !== "number" || typeof tuning.queuePressure !== "number" || typeof tuning.maxRevealCps !== "number" || typeof tuning.springStiffness !== "number" || typeof tuning.springDamping !== "number" || typeof tuning.springMass !== "number" || typeof tuning.runwayPx !== "number" || typeof tuning.reserveResponseMs !== "number" || typeof tuning.backpressureMinScale !== "number") throw new Error("dsh-smooth-stream: malformed debug settings response");
3842
+ return {
3843
+ debugEnabled: data.debugEnabled,
3844
+ tuning
3845
+ };
3846
+ }
2500
3847
  function accepted(result) {
2501
3848
  if (!result.ok) throw new Error(result.error.message);
2502
3849
  return result.value;
@@ -2507,8 +3854,25 @@ window.__ModuleLoader__.load({
2507
3854
  async read() {
2508
3855
  return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.read, {})));
2509
3856
  },
2510
- async write(thinkAutoExpand) {
2511
- return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, { thinkAutoExpand })));
3857
+ async write(settings) {
3858
+ return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.write, {
3859
+ enabled: settings.enabled,
3860
+ thinkAutoExpand: settings.thinkAutoExpand,
3861
+ autoCollapse: settings.autoCollapse,
3862
+ ...settings.debugEnabled === void 0 || settings.debugTuning === void 0 ? {} : {
3863
+ debugEnabled: settings.debugEnabled,
3864
+ debugTuning: settings.debugTuning
3865
+ }
3866
+ })));
3867
+ },
3868
+ async readDebug() {
3869
+ return debugSettingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.debugRead, {})));
3870
+ },
3871
+ async writeDebug(settings) {
3872
+ return debugSettingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.debugWrite, {
3873
+ debugEnabled: settings.debugEnabled,
3874
+ tuning: settings.debugTuning
3875
+ })));
2512
3876
  },
2513
3877
  async upgrade() {
2514
3878
  return upgradeView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.upgrade, {})));
@@ -2516,6 +3880,433 @@ window.__ModuleLoader__.load({
2516
3880
  };
2517
3881
  }
2518
3882
  //#endregion
3883
+ //#region \0dsh-css:/Users/dzlin/work/project/dsh-smooth-stream/src/client/DebugPanel.module.css.mjs
3884
+ const css = ".m4_tdG_trigger,.m4_tdG_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}.m4_tdG_trigger:hover,.m4_tdG_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.m4_tdG_iconButton:disabled{opacity:.45;cursor:not-allowed}.m4_tdG_triggerActive{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-business-primary)}.m4_tdG_trigger:focus-visible,.m4_tdG_iconButton:focus-visible,.m4_tdG_secondaryButton:focus-visible,.m4_tdG_primaryButton:focus-visible,.m4_tdG_number:focus-visible,.m4_tdG_range:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.m4_tdG_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}.m4_tdG_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}.m4_tdG_statusDot{background:var(--dsw-alias-label-caption);border-radius:50%;flex:none;width:7px;height:7px}.m4_tdG_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)}.m4_tdG_title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px;overflow:hidden}.m4_tdG_state,.m4_tdG_unsaved{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:10px;line-height:16px}.m4_tdG_unsaved{color:var(--dsw-alias-state-warn-primary)}.m4_tdG_scrollArea{overscroll-behavior:contain;min-height:0;overflow-y:auto}.m4_tdG_section{padding:14px}.m4_tdG_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}.m4_tdG_section+.m4_tdG_section{border-top:1px solid var(--dsw-alias-border-l2)}.m4_tdG_section h2{color:var(--dsw-alias-label-secondary);margin:0 0 10px;font-size:12px;font-weight:600;line-height:18px}.m4_tdG_metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:0 16px;margin:0;display:grid}.m4_tdG_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}.m4_tdG_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}.m4_tdG_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}.m4_tdG_metric dd[data-tone=good]{color:var(--dsw-alias-state-success-primary)}.m4_tdG_metric dd[data-tone=warn]{color:var(--dsw-alias-state-warn-primary)}.m4_tdG_control{flex-direction:column;gap:5px;padding:6px 0;display:flex}.m4_tdG_controlHead{color:var(--dsw-alias-label-secondary);justify-content:space-between;align-items:center;gap:10px;font-size:11px;line-height:18px;display:flex}.m4_tdG_controlLabel{align-items:center;gap:4px;min-width:0;display:inline-flex}.m4_tdG_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}.m4_tdG_infoButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.m4_tdG_infoButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.m4_tdG_numberWrap{flex:none;align-items:center;gap:4px;display:inline-flex}.m4_tdG_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}.m4_tdG_unit{width:30px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px}.m4_tdG_range{width:100%;height:18px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer;margin:0}.m4_tdG_range:disabled,.m4_tdG_number:disabled{opacity:.5;cursor:default}.m4_tdG_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}.m4_tdG_footerSpacer{flex:1}.m4_tdG_secondaryButton,.m4_tdG_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}.m4_tdG_primaryButton{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:#0000}.m4_tdG_secondaryButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.m4_tdG_secondaryButton:disabled,.m4_tdG_primaryButton:disabled{opacity:.4;cursor:default}.m4_tdG_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){.m4_tdG_trigger,.m4_tdG_iconButton{width:44px;height:44px}.m4_tdG_panel{top:auto;right:12px;bottom:max(12px, env(safe-area-inset-bottom));width:auto;max-height:min(70dvh,640px);left:12px}.m4_tdG_secondaryButton,.m4_tdG_primaryButton{min-height:44px}}@media (prefers-reduced-motion:reduce){.m4_tdG_statusLive{box-shadow:none}}";
3885
+ const tagId = "dsh-smooth-stream/DebugPanel.module.css";
3886
+ if (typeof document !== "undefined") {
3887
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
3888
+ if (tag === null) {
3889
+ tag = document.createElement("style");
3890
+ tag.dataset.plugin = "dsh-smooth-stream";
3891
+ tag.dataset.pluginCss = tagId;
3892
+ document.head.appendChild(tag);
3893
+ }
3894
+ tag.textContent = css;
3895
+ }
3896
+ var DebugPanel_module_css_default = {
3897
+ "infoButton": "m4_tdG_infoButton",
3898
+ "unit": "m4_tdG_unit",
3899
+ "range": "m4_tdG_range",
3900
+ "secondaryButton": "m4_tdG_secondaryButton",
3901
+ "footerSpacer": "m4_tdG_footerSpacer",
3902
+ "visuallyHidden": "m4_tdG_visuallyHidden",
3903
+ "title": "m4_tdG_title",
3904
+ "number": "m4_tdG_number",
3905
+ "numberWrap": "m4_tdG_numberWrap",
3906
+ "footer": "m4_tdG_footer",
3907
+ "triggerActive": "m4_tdG_triggerActive",
3908
+ "iconButton": "m4_tdG_iconButton",
3909
+ "scrollArea": "m4_tdG_scrollArea",
3910
+ "section": "m4_tdG_section",
3911
+ "trigger": "m4_tdG_trigger",
3912
+ "statusDot": "m4_tdG_statusDot",
3913
+ "state": "m4_tdG_state",
3914
+ "panelHeader": "m4_tdG_panelHeader",
3915
+ "panel": "m4_tdG_panel",
3916
+ "primaryButton": "m4_tdG_primaryButton",
3917
+ "unsaved": "m4_tdG_unsaved",
3918
+ "guide": "m4_tdG_guide",
3919
+ "metric": "m4_tdG_metric",
3920
+ "controlHead": "m4_tdG_controlHead",
3921
+ "controlLabel": "m4_tdG_controlLabel",
3922
+ "statusLive": "m4_tdG_statusLive",
3923
+ "control": "m4_tdG_control",
3924
+ "metrics": "m4_tdG_metrics"
3925
+ };
3926
+ //#endregion
3927
+ //#region src/client/DebugPanel.tsx
3928
+ const REVEAL_CONTROLS = [
3929
+ {
3930
+ key: "revealScale",
3931
+ label: "debugRevealMultiplier",
3932
+ tip: "debugTipRevealMultiplier",
3933
+ min: .25,
3934
+ max: 2,
3935
+ step: .05,
3936
+ unit: "x"
3937
+ },
3938
+ {
3939
+ key: "queuePressure",
3940
+ label: "debugQueuePressure",
3941
+ tip: "debugTipQueuePressure",
3942
+ min: 0,
3943
+ max: 2,
3944
+ step: .05,
3945
+ unit: "x"
3946
+ },
3947
+ {
3948
+ key: "maxRevealCps",
3949
+ label: "debugMaxReveal",
3950
+ tip: "debugTipMaxReveal",
3951
+ min: 120,
3952
+ max: 1e3,
3953
+ step: 10,
3954
+ unit: "cps"
3955
+ }
3956
+ ];
3957
+ const FOLLOW_CONTROLS = [
3958
+ {
3959
+ key: "springStiffness",
3960
+ label: "debugSpringStiffness",
3961
+ tip: "debugTipSpringStiffness",
3962
+ min: 40,
3963
+ max: 320,
3964
+ step: 5,
3965
+ unit: ""
3966
+ },
3967
+ {
3968
+ key: "springDamping",
3969
+ label: "debugSpringDamping",
3970
+ tip: "debugTipSpringDamping",
3971
+ min: 8,
3972
+ max: 80,
3973
+ step: 1,
3974
+ unit: ""
3975
+ },
3976
+ {
3977
+ key: "springMass",
3978
+ label: "debugSpringMass",
3979
+ tip: "debugTipSpringMass",
3980
+ min: .5,
3981
+ max: 3,
3982
+ step: .05,
3983
+ unit: ""
3984
+ },
3985
+ {
3986
+ key: "runwayPx",
3987
+ label: "debugRunway",
3988
+ tip: "debugTipRunway",
3989
+ min: 0,
3990
+ max: 120,
3991
+ step: 2,
3992
+ unit: "px"
3993
+ },
3994
+ {
3995
+ key: "reserveResponseMs",
3996
+ label: "debugReserveResponse",
3997
+ tip: "debugTipReserveResponse",
3998
+ min: 60,
3999
+ max: 600,
4000
+ step: 10,
4001
+ unit: "ms"
4002
+ },
4003
+ {
4004
+ key: "backpressureMinScale",
4005
+ label: "debugBackpressureMin",
4006
+ tip: "debugTipBackpressureMin",
4007
+ min: .25,
4008
+ max: 1,
4009
+ step: .05,
4010
+ unit: "x"
4011
+ }
4012
+ ];
4013
+ function fixed(value, digits = 1) {
4014
+ return value === null || !Number.isFinite(value) ? "-" : value.toFixed(digits);
4015
+ }
4016
+ function Metric({ label, value, tone }) {
4017
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4018
+ className: DebugPanel_module_css_default.metric,
4019
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
4020
+ "data-tone": tone,
4021
+ children: value
4022
+ })]
4023
+ });
4024
+ }
4025
+ function TuningField({ control, state, edit, label, t }) {
4026
+ const value = state.tuning[control.key];
4027
+ const labelId = `smooth-stream-debug-${control.key}`;
4028
+ const update = (next) => {
4029
+ if (!Number.isFinite(next)) return;
4030
+ const clamped = Math.min(control.max, Math.max(control.min, next));
4031
+ edit({ debugTuning: {
4032
+ ...state.tuning,
4033
+ [control.key]: clamped
4034
+ } });
4035
+ };
4036
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4037
+ className: DebugPanel_module_css_default.control,
4038
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4039
+ className: DebugPanel_module_css_default.controlHead,
4040
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4041
+ className: DebugPanel_module_css_default.controlLabel,
4042
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4043
+ id: labelId,
4044
+ children: label
4045
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4046
+ label: t(control.tip),
4047
+ side: "right",
4048
+ maxWidth: 300,
4049
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4050
+ className: DebugPanel_module_css_default.infoButton,
4051
+ type: "button",
4052
+ "aria-label": label,
4053
+ title: t(control.tip),
4054
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, {})
4055
+ })
4056
+ })]
4057
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4058
+ className: DebugPanel_module_css_default.numberWrap,
4059
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4060
+ className: DebugPanel_module_css_default.number,
4061
+ type: "number",
4062
+ "aria-labelledby": labelId,
4063
+ min: control.min,
4064
+ max: control.max,
4065
+ step: control.step,
4066
+ value,
4067
+ disabled: !state.writable,
4068
+ onChange: (event) => {
4069
+ update(event.currentTarget.valueAsNumber);
4070
+ }
4071
+ }), control.unit === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4072
+ className: DebugPanel_module_css_default.unit,
4073
+ children: control.unit
4074
+ })]
4075
+ })]
4076
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4077
+ className: DebugPanel_module_css_default.range,
4078
+ type: "range",
4079
+ "aria-labelledby": labelId,
4080
+ min: control.min,
4081
+ max: control.max,
4082
+ step: control.step,
4083
+ value,
4084
+ disabled: !state.writable,
4085
+ onChange: (event) => {
4086
+ update(event.currentTarget.valueAsNumber);
4087
+ }
4088
+ })]
4089
+ });
4090
+ }
4091
+ function DebugPanel(props) {
4092
+ const { t } = props;
4093
+ const state = props.useDebugRuntime((snapshot) => snapshot);
4094
+ const [open, setOpen] = (0, react.useState)(true);
4095
+ const [copied, setCopied] = (0, react.useState)(false);
4096
+ (0, react.useEffect)(() => {
4097
+ if (!copied) return;
4098
+ const timer = setTimeout(() => {
4099
+ setCopied(false);
4100
+ }, 1400);
4101
+ return () => {
4102
+ clearTimeout(timer);
4103
+ };
4104
+ }, [copied]);
4105
+ (0, react.useEffect)(() => {
4106
+ if (state.enabled) setOpen(true);
4107
+ }, [state.enabled]);
4108
+ if (!state.available || !state.enabled || !open) return null;
4109
+ const metrics = state.metrics;
4110
+ const live = metrics.streamActive || metrics.followActive;
4111
+ const progress = metrics.streamTargetChars <= 0 ? "-" : `${String(metrics.streamDisplayedChars)} / ${String(metrics.streamTargetChars)}`;
4112
+ const copyDiagnostics = async () => {
4113
+ if (await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(JSON.stringify({
4114
+ tuning: state.tuning,
4115
+ metrics: state.metrics
4116
+ }, null, 2))) setCopied(true);
4117
+ };
4118
+ const panel = !state.enabled || !open ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
4119
+ className: DebugPanel_module_css_default.panel,
4120
+ role: "complementary",
4121
+ "aria-label": t("debugPanelTitle"),
4122
+ children: [
4123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4124
+ className: DebugPanel_module_css_default.panelHeader,
4125
+ children: [
4126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4127
+ className: live ? `${DebugPanel_module_css_default.statusDot} ${DebugPanel_module_css_default.statusLive}` : DebugPanel_module_css_default.statusDot,
4128
+ "aria-hidden": true
4129
+ }),
4130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4131
+ className: DebugPanel_module_css_default.title,
4132
+ children: t("debugPanelTitle")
4133
+ }),
4134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4135
+ className: DebugPanel_module_css_default.state,
4136
+ children: t(live ? "debugLive" : "debugIdle")
4137
+ }),
4138
+ state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4139
+ className: DebugPanel_module_css_default.unsaved,
4140
+ children: t("debugUnsaved")
4141
+ }) : null,
4142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4143
+ className: DebugPanel_module_css_default.iconButton,
4144
+ type: "button",
4145
+ title: t("debugCopy"),
4146
+ "aria-label": t("debugCopy"),
4147
+ onClick: () => {
4148
+ copyDiagnostics();
4149
+ },
4150
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {})
4151
+ }),
4152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4153
+ className: DebugPanel_module_css_default.iconButton,
4154
+ type: "button",
4155
+ title: t("debugPanelClose"),
4156
+ "aria-label": t("debugPanelClose"),
4157
+ disabled: !state.writable,
4158
+ onClick: () => {
4159
+ setOpen(false);
4160
+ props.edit({ debugEnabled: false });
4161
+ props.save();
4162
+ },
4163
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
4164
+ }),
4165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4166
+ className: DebugPanel_module_css_default.visuallyHidden,
4167
+ "aria-live": "polite",
4168
+ children: copied ? t("debugCopied") : ""
4169
+ })
4170
+ ]
4171
+ }),
4172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4173
+ className: DebugPanel_module_css_default.scrollArea,
4174
+ children: [
4175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4176
+ className: DebugPanel_module_css_default.guide,
4177
+ children: t("debugGuide")
4178
+ }),
4179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4180
+ className: DebugPanel_module_css_default.section,
4181
+ "aria-labelledby": "smooth-stream-live-heading",
4182
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4183
+ id: "smooth-stream-live-heading",
4184
+ children: t("debugSectionLive")
4185
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
4186
+ className: DebugPanel_module_css_default.metrics,
4187
+ children: [
4188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4189
+ label: t("debugFps"),
4190
+ value: fixed(metrics.fps, 0),
4191
+ tone: (metrics.fps ?? 60) < 45 ? "warn" : "good"
4192
+ }),
4193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4194
+ label: t("debugFrameTime"),
4195
+ value: `${fixed(metrics.frameMs)} ms`
4196
+ }),
4197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4198
+ label: t("debugBacklog"),
4199
+ value: String(metrics.streamBacklog),
4200
+ tone: metrics.streamBacklog > 32 ? "warn" : void 0
4201
+ }),
4202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4203
+ label: t("debugRevealSpeed"),
4204
+ value: `${fixed(metrics.streamSpeedCps, 0)} cps`
4205
+ }),
4206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4207
+ label: t("debugProgress"),
4208
+ value: progress
4209
+ }),
4210
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4211
+ label: t("debugFollowState"),
4212
+ value: t(metrics.followFollowing ? "debugFollowing" : "debugReleased")
4213
+ }),
4214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4215
+ label: t("debugLag"),
4216
+ value: `${fixed(metrics.followLagPx)} px`,
4217
+ tone: metrics.followConstrained ? "warn" : void 0
4218
+ }),
4219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4220
+ label: t("debugVelocity"),
4221
+ value: `${fixed(metrics.followVelocityPxPerSec, 0)} px/s`
4222
+ }),
4223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4224
+ label: t("debugReserve"),
4225
+ value: `${fixed(metrics.followReservePx)} px`
4226
+ }),
4227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4228
+ label: t("debugCapacity"),
4229
+ value: `${fixed(metrics.followCapacityPx)} px`
4230
+ }),
4231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
4232
+ label: t("debugAppliedScale"),
4233
+ value: `${fixed(metrics.followRevealScale, 2)}x`
4234
+ })
4235
+ ]
4236
+ })]
4237
+ }),
4238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4239
+ className: DebugPanel_module_css_default.section,
4240
+ "aria-labelledby": "smooth-stream-reveal-heading",
4241
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4242
+ id: "smooth-stream-reveal-heading",
4243
+ children: t("debugSectionReveal")
4244
+ }), REVEAL_CONTROLS.map((control) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TuningField, {
4245
+ control,
4246
+ state,
4247
+ edit: props.edit,
4248
+ label: t(control.label),
4249
+ t
4250
+ }, control.key))]
4251
+ }),
4252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4253
+ className: DebugPanel_module_css_default.section,
4254
+ "aria-labelledby": "smooth-stream-follow-heading",
4255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4256
+ id: "smooth-stream-follow-heading",
4257
+ children: t("debugSectionFollow")
4258
+ }), FOLLOW_CONTROLS.map((control) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TuningField, {
4259
+ control,
4260
+ state,
4261
+ edit: props.edit,
4262
+ label: t(control.label),
4263
+ t
4264
+ }, control.key))]
4265
+ })
4266
+ ]
4267
+ }),
4268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
4269
+ className: DebugPanel_module_css_default.footer,
4270
+ children: [
4271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4272
+ className: DebugPanel_module_css_default.secondaryButton,
4273
+ type: "button",
4274
+ disabled: !state.writable,
4275
+ onClick: props.reset,
4276
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {}), t("debugReset")]
4277
+ }),
4278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: DebugPanel_module_css_default.footerSpacer }),
4279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4280
+ className: DebugPanel_module_css_default.secondaryButton,
4281
+ type: "button",
4282
+ disabled: !state.writable || !state.dirty,
4283
+ onClick: props.discard,
4284
+ children: t("debugDiscard")
4285
+ }),
4286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4287
+ className: DebugPanel_module_css_default.primaryButton,
4288
+ type: "button",
4289
+ disabled: !state.writable || !state.dirty,
4290
+ onClick: props.save,
4291
+ children: t("debugSave")
4292
+ })
4293
+ ]
4294
+ })
4295
+ ]
4296
+ });
4297
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4298
+ type: "button",
4299
+ className: state.enabled && open ? `${DebugPanel_module_css_default.trigger} ${DebugPanel_module_css_default.triggerActive}` : DebugPanel_module_css_default.trigger,
4300
+ "aria-expanded": state.enabled && open,
4301
+ "aria-label": t("debugPanelToggle"),
4302
+ title: t("debugPanelToggle"),
4303
+ onClick: () => {
4304
+ setOpen((current) => !current);
4305
+ },
4306
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCodeOutline16, {})
4307
+ }), typeof document === "undefined" || panel === null ? null : (0, react_dom.createPortal)(panel, document.body)] });
4308
+ }
4309
+ //#endregion
2519
4310
  //#region src/client/locales.ts
2520
4311
  /** Locale bundles for the smooth-stream plugin configuration card. */
2521
4312
  /** Dictionary namespace owned by this plugin's settings card. */
@@ -2524,8 +4315,61 @@ window.__ModuleLoader__.load({
2524
4315
  const en = {
2525
4316
  title: "Smooth stream",
2526
4317
  description: "How replies are revealed while they stream.",
4318
+ enabled: "Enable smooth streaming",
4319
+ enabledHint: "Let this plugin render and follow streaming replies. Turn off to use the built-in Harness renderer.",
2527
4320
  thinkAutoExpand: "Auto-expand thinking",
2528
4321
  thinkAutoExpandHint: "Open the thinking block while it streams. Turn off to keep it collapsed.",
4322
+ autoCollapse: "Collapse finished work",
4323
+ autoCollapseHint: "When a reply finishes, fold its thinking, tools, and intermediate output behind one “Processed” summary so only the final answer shows. Click the summary to expand again; works with streaming on or off.",
4324
+ debugEnabled: "Show render diagnostics",
4325
+ debugEnabledHint: "Show live streaming and scroll metrics on the right side of the chat. Tune values there, then save them here.",
4326
+ debugUnavailable: "Live diagnostics require a newer plugin Host.",
4327
+ debugPanelTitle: "Render diagnostics",
4328
+ debugPanelToggle: "Toggle render diagnostics",
4329
+ debugPanelClose: "Hide diagnostics panel",
4330
+ debugGuide: "Tune one value at a time while a reply streams. Keep FPS stable and backlog near zero. If text lags, raise the reveal multiplier, queue pressure, or maximum reveal; if a blank gap appears, reduce Predictive runway. Increase damping when the scroll feels springy. Save only after the behavior is stable; Reset restores the production defaults.",
4331
+ debugLive: "Streaming",
4332
+ debugIdle: "Idle",
4333
+ debugUnsaved: "Unsaved tuning",
4334
+ debugSave: "Save tuning",
4335
+ debugDiscard: "Discard changes",
4336
+ debugReset: "Reset tuning",
4337
+ debugCopy: "Copy diagnostics",
4338
+ debugCopied: "Copied",
4339
+ debugSectionLive: "Live renderer",
4340
+ debugSectionReveal: "Reveal tuning",
4341
+ debugSectionFollow: "Scroll tuning",
4342
+ debugFps: "FPS",
4343
+ debugFrameTime: "Frame",
4344
+ debugBacklog: "Backlog",
4345
+ debugRevealSpeed: "Reveal",
4346
+ debugProgress: "Progress",
4347
+ debugFollowState: "Follow",
4348
+ debugFollowing: "Pinned",
4349
+ debugReleased: "Released",
4350
+ debugLag: "Visual lag",
4351
+ debugVelocity: "Velocity",
4352
+ debugReserve: "Reserve",
4353
+ debugCapacity: "Capacity",
4354
+ debugAppliedScale: "Applied scale",
4355
+ debugRevealMultiplier: "Reveal multiplier",
4356
+ debugQueuePressure: "Queue pressure",
4357
+ debugMaxReveal: "Maximum reveal",
4358
+ debugSpringStiffness: "Spring stiffness",
4359
+ debugSpringDamping: "Spring damping",
4360
+ debugSpringMass: "Spring mass",
4361
+ debugRunway: "Predictive runway",
4362
+ debugReserveResponse: "Runway response",
4363
+ debugBackpressureMin: "Minimum backpressure",
4364
+ debugTipRevealMultiplier: "Overall reveal speed multiplier. Higher reveals text faster and clears backlog sooner, but can feel jumpy. Lower is smoother but takes longer to finish.",
4365
+ debugTipQueuePressure: "Backlog acceleration strength. Higher catches up to a growing queue more aggressively; lower keeps speed steadier but may leave backlog.",
4366
+ debugTipMaxReveal: "Hard cap for reveal speed in characters per second. Higher allows faster catch-up; lower limits bursts and keeps motion calmer.",
4367
+ debugTipSpringStiffness: "Scroll spring strength. Higher closes visual lag faster but can feel sharp; lower feels softer but follows more slowly.",
4368
+ debugTipSpringDamping: "Scroll energy damping. Higher suppresses overshoot and jitter but feels heavier; lower feels livelier but may oscillate.",
4369
+ debugTipSpringMass: "Scroll inertia. Higher makes movement slower and heavier; lower makes it react faster but can feel abrupt.",
4370
+ debugTipRunway: "Predictive blank space reserved while content grows. Higher absorbs growth and protects the bottom follow, but can create a larger visible gap; lower reduces blank space but leaves less room to absorb lag.",
4371
+ debugTipReserveResponse: "How quickly the reserved runway opens or closes. Higher changes more gradually; lower reacts faster but can look abrupt.",
4372
+ debugTipBackpressureMin: "Slowest reveal multiplier under scroll pressure. Higher keeps text moving but may increase visual lag; lower slows text more to protect smooth following.",
2529
4373
  readOnly: "This deployment stores settings read-only.",
2530
4374
  loading: "Loading plugin settings…",
2531
4375
  unavailable: "Plugin settings are unavailable in this connection.",
@@ -2550,8 +4394,61 @@ window.__ModuleLoader__.load({
2550
4394
  const zh = {
2551
4395
  title: "丝滑流式",
2552
4396
  description: "回复在流式输出时如何逐字展现。",
4397
+ enabled: "启用丝滑流式渲染",
4398
+ enabledHint: "由本插件渲染并跟随流式回复;关闭后使用 Harness 内置渲染。",
2553
4399
  thinkAutoExpand: "自动展开思考",
2554
4400
  thinkAutoExpandHint: "思考块在流式时自动展开;关闭后保持折叠,可手动展开。",
4401
+ autoCollapse: "完成后自动折叠",
4402
+ autoCollapseHint: "回复处理完成后,把思考、工具与过程输出折叠为一行“已处理”摘要,只展示最终回复;点击摘要可再次展开。开启或关闭流式渲染均可用,默认开启。",
4403
+ debugEnabled: "显示渲染调试面板",
4404
+ debugEnabledHint: "在聊天右侧显示流式渲染和滚动的实时参数,可在面板中调节并在这里保存。",
4405
+ debugUnavailable: "当前 Host 版本不支持实时调试,请先更新插件 Host。",
4406
+ debugPanelTitle: "渲染诊断",
4407
+ debugPanelToggle: "显示或隐藏渲染诊断",
4408
+ debugPanelClose: "收起诊断面板",
4409
+ debugGuide: "流式输出时一次只调一个参数,观察帧率、积压和视觉滞后。优先保持帧率稳定、积压接近 0;如果文字滞后,提高揭示倍率、队列压力或最大揭示速度;如果底部出现空白,降低“预测预留空间”。滚动有回弹或抖动时提高阻尼。确认表现稳定后再保存;“恢复默认参数”会回到生产默认值。",
4410
+ debugLive: "正在流式输出",
4411
+ debugIdle: "空闲",
4412
+ debugUnsaved: "参数尚未保存",
4413
+ debugSave: "保存参数",
4414
+ debugDiscard: "放弃修改",
4415
+ debugReset: "恢复默认参数",
4416
+ debugCopy: "复制诊断数据",
4417
+ debugCopied: "已复制",
4418
+ debugSectionLive: "实时渲染",
4419
+ debugSectionReveal: "流式参数",
4420
+ debugSectionFollow: "滚动参数",
4421
+ debugFps: "帧率",
4422
+ debugFrameTime: "帧耗时",
4423
+ debugBacklog: "积压字符",
4424
+ debugRevealSpeed: "揭示速度",
4425
+ debugProgress: "渲染进度",
4426
+ debugFollowState: "跟随状态",
4427
+ debugFollowing: "跟随底部",
4428
+ debugReleased: "用户已释放",
4429
+ debugLag: "视觉滞后",
4430
+ debugVelocity: "滚动速度",
4431
+ debugReserve: "预留空间",
4432
+ debugCapacity: "安全容量",
4433
+ debugAppliedScale: "实际倍率",
4434
+ debugRevealMultiplier: "揭示倍率",
4435
+ debugQueuePressure: "队列压力",
4436
+ debugMaxReveal: "最大揭示速度",
4437
+ debugSpringStiffness: "弹簧刚度",
4438
+ debugSpringDamping: "弹簧阻尼",
4439
+ debugSpringMass: "弹簧质量",
4440
+ debugRunway: "预测预留空间",
4441
+ debugReserveResponse: "预留响应时间",
4442
+ debugBackpressureMin: "最小背压倍率",
4443
+ debugTipRevealMultiplier: "整体文字揭示速度倍率。调大能更快清空积压,但可能显得跳;调小更平滑,但完成回复需要更久。",
4444
+ debugTipQueuePressure: "积压对加速的影响强度。调大能更积极追赶增长中的队列;调小速度更稳定,但积压可能持续。",
4445
+ debugTipMaxReveal: "每秒揭示字符数上限。调大允许更快追赶;调小限制突发速度,让动作更平稳。",
4446
+ debugTipSpringStiffness: "滚动弹簧刚度。调大更快收拢视觉滞后,但感觉更硬;调小更柔和,但跟随更慢。",
4447
+ debugTipSpringDamping: "滚动能量阻尼。调大能抑制过冲和抖动,但感觉更沉;调小更灵活,但可能回弹。",
4448
+ debugTipSpringMass: "滚动惯性。调大移动更慢更沉;调小反应更快,但可能显得突兀。",
4449
+ debugTipRunway: "内容增长时预留的预测空白。调大更能吸收增长、保护底部跟随,但可能产生更大的可见空区;调小能减少空白,但可吸收滞后的空间也更少。",
4450
+ debugTipReserveResponse: "预留空间打开或关闭的响应时间。调大变化更渐进;调小反应更快,但可能显得突变。",
4451
+ debugTipBackpressureMin: "滚动压力下允许的最低揭示倍率。调大文字仍会较快前进,但视觉滞后可能增加;调小会更积极减速,以保护跟随平滑。",
2555
4452
  readOnly: "本部署的设置为只读。",
2556
4453
  loading: "正在加载插件设置…",
2557
4454
  unavailable: "当前连接无法访问插件设置。",
@@ -2664,15 +4561,15 @@ window.__ModuleLoader__.load({
2664
4561
  };
2665
4562
  }
2666
4563
  /**
2667
- * A live preference cell read by `useSyncExternalStore`. It starts on the
2668
- * shared default and, once the plugin-owned settings controller is attached,
2669
- * tracks the resolved `thinkAutoExpand` value. A composition (or test) that
2670
- * ships no settings surface keeps the cell on the default.
4564
+ * A live settings cell shared by the renderer lifecycle and React views. It
4565
+ * starts on the shared defaults and follows the plugin-owned controller once
4566
+ * the optional settings services arrive.
2671
4567
  */
2672
- var PreferenceCell = class {
4568
+ var SettingsCell = class {
2673
4569
  listeners = /* @__PURE__ */ new Set();
2674
4570
  card;
2675
- value = DEFAULT_STREAM_SETTINGS.thinkAutoExpand;
4571
+ value = DEFAULT_STREAM_SETTINGS;
4572
+ pending = false;
2676
4573
  /** Re-point the cell at the plugin-owned settings controller. */
2677
4574
  attach(card) {
2678
4575
  this.card = card;
@@ -2688,14 +4585,26 @@ window.__ModuleLoader__.load({
2688
4585
  };
2689
4586
  }
2690
4587
  read() {
2691
- return this.card?.getSnapshot().thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand;
4588
+ const snapshot = this.card?.getSnapshot();
4589
+ if (snapshot === void 0 || snapshot.status !== "ready") return this.value;
4590
+ return this.card?.values() ?? this.value;
2692
4591
  }
2693
4592
  refresh() {
2694
4593
  const next = this.read();
2695
- if (next === this.value) return;
4594
+ const pending = this.card?.getSnapshot().status === "loading";
4595
+ if (pending === this.pending && next.enabled === this.value.enabled && next.thinkAutoExpand === this.value.thinkAutoExpand && next.autoCollapse === this.value.autoCollapse && next.debugEnabled === this.value.debugEnabled && next.debugTuning === this.value.debugTuning) return;
4596
+ this.pending = pending;
2696
4597
  this.value = next;
2697
4598
  for (const listener of this.listeners) listener();
2698
4599
  }
4600
+ /** False while an available settings service is resolving its authority. */
4601
+ takeoverEnabled() {
4602
+ return !this.pending && this.value.enabled;
4603
+ }
4604
+ /** Whether finished turns should fold behind a summary row right now. */
4605
+ autoCollapseActive() {
4606
+ return !this.pending && this.value.autoCollapse;
4607
+ }
2699
4608
  getSnapshot = () => this.value;
2700
4609
  subscribe = (listener) => {
2701
4610
  this.listeners.add(listener);
@@ -2717,14 +4626,51 @@ window.__ModuleLoader__.load({
2717
4626
  */
2718
4627
  function apply(ctx) {
2719
4628
  const config = readBootConfig();
2720
- const preference = new PreferenceCell();
4629
+ const settings = new SettingsCell();
4630
+ ctx.inject(["slots"], () => {
4631
+ const autoCollapse = new AutoCollapseController();
4632
+ const syncAutoCollapse = () => {
4633
+ if (settings.autoCollapseActive()) autoCollapse.start();
4634
+ else autoCollapse.stop();
4635
+ };
4636
+ const unsubscribe = settings.subscribe(syncAutoCollapse);
4637
+ syncAutoCollapse();
4638
+ return () => {
4639
+ unsubscribe();
4640
+ autoCollapse.stop();
4641
+ };
4642
+ });
2721
4643
  ctx.inject([
2722
4644
  "slots",
2723
4645
  "locale",
2724
4646
  "connection"
2725
4647
  ], (settingsCtx) => {
2726
4648
  const card = new SmoothStreamCardController(createSmoothStreamSettingsApi(settingsCtx.get("connection")));
2727
- const detachPreference = preference.attach(card);
4649
+ const detachSettings = settings.attach(card);
4650
+ const syncDebug = () => {
4651
+ const snapshot = card.getSnapshot();
4652
+ debugRuntime.syncSettings({
4653
+ available: snapshot.debugAvailable,
4654
+ enabled: snapshot.debugEnabled,
4655
+ writable: snapshot.writable && !snapshot.saving,
4656
+ dirty: snapshot.dirty,
4657
+ status: snapshot.status,
4658
+ tuning: snapshot.debugTuning
4659
+ });
4660
+ };
4661
+ const detachBinding = debugRuntime.bindSettings({
4662
+ edit: (patch) => {
4663
+ card.inject().edit(patch);
4664
+ },
4665
+ save: () => {
4666
+ card.inject().save();
4667
+ },
4668
+ discard: () => {
4669
+ card.inject().discard();
4670
+ }
4671
+ });
4672
+ const detachDebug = card.subscribe(syncDebug);
4673
+ syncDebug();
2728
4674
  card.start();
2729
4675
  settingsCtx.effect(() => settingsCtx.locale.register(NS, {
2730
4676
  zh,
@@ -2733,17 +4679,27 @@ window.__ModuleLoader__.load({
2733
4679
  settingsCtx.slots.inject("settings.plugin.item", () => settingsCtx.slots.register({
2734
4680
  name: "settings.plugin.item",
2735
4681
  id: "smooth-stream",
4682
+ key: STREAM_SETTINGS_NS,
2736
4683
  order: 30,
2737
4684
  locale: NS,
2738
4685
  inject: () => card.inject()
2739
4686
  }, SmoothStreamCard));
4687
+ settingsCtx.slots.inject("conversation.session.header.utilities", () => settingsCtx.slots.register({
4688
+ name: "conversation.session.header.utilities",
4689
+ id: "smooth-stream-debug",
4690
+ order: 40,
4691
+ locale: NS,
4692
+ inject: () => debugRuntime.panelFace()
4693
+ }, DebugPanel));
2740
4694
  return () => {
2741
4695
  card.stop();
2742
- detachPreference();
4696
+ detachDebug();
4697
+ detachSettings();
4698
+ detachBinding();
2743
4699
  };
2744
4700
  });
2745
4701
  const configured = function StreamConfiguredView(props) {
2746
- const thinkAutoExpand = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot, preference.getSnapshot);
4702
+ const preferences = (0, react.useSyncExternalStore)(settings.subscribe, settings.getSnapshot, settings.getSnapshot);
2747
4703
  return (0, react.createElement)(TypewriterAssistantNodeView, {
2748
4704
  ...props,
2749
4705
  mode: config.mode,
@@ -2751,21 +4707,36 @@ window.__ModuleLoader__.load({
2751
4707
  revealCharsPerSec: config.revealCharsPerSec,
2752
4708
  scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
2753
4709
  maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
2754
- thinkAutoExpand
4710
+ thinkAutoExpand: preferences.thinkAutoExpand
2755
4711
  });
2756
4712
  };
2757
4713
  ctx.slots.inject("conversation.chat.node", () => {
2758
- const unwrap = wrapAgentChatRows(ctx);
2759
- const unshadow = ctx.slots.register({
2760
- name: "conversation.chat.node",
2761
- key: "assistant-step",
2762
- priority: -100,
2763
- locale: "conversation",
2764
- registrant: "dsh-smooth-stream"
2765
- }, configured);
4714
+ let releaseTakeover;
4715
+ const syncTakeover = () => {
4716
+ if (!settings.takeoverEnabled()) {
4717
+ releaseTakeover?.();
4718
+ releaseTakeover = void 0;
4719
+ return;
4720
+ }
4721
+ if (releaseTakeover !== void 0) return;
4722
+ const unwrap = wrapAgentChatRows(ctx);
4723
+ const unshadow = ctx.slots.register({
4724
+ name: "conversation.chat.node",
4725
+ key: "assistant-step",
4726
+ priority: -100,
4727
+ locale: "conversation",
4728
+ registrant: "dsh-smooth-stream"
4729
+ }, configured);
4730
+ releaseTakeover = () => {
4731
+ unwrap();
4732
+ unshadow();
4733
+ };
4734
+ };
4735
+ const unsubscribe = settings.subscribe(syncTakeover);
4736
+ syncTakeover();
2766
4737
  return () => {
2767
- unshadow();
2768
- unwrap();
4738
+ unsubscribe();
4739
+ releaseTakeover?.();
2769
4740
  };
2770
4741
  });
2771
4742
  }