dsh-smooth-stream 0.3.4 → 0.4.1

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.
@@ -14,6 +14,8 @@
14
14
  */
15
15
 
16
16
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
17
+ import { DEFAULT_STREAM_DEBUG_TUNING, type StreamDebugTuning } from '../settings.ts'
18
+ import { debugRuntime } from './debugRuntime.ts'
17
19
 
18
20
  export type StreamSmoothingPreset = 'realtime' | 'balanced' | 'silky'
19
21
 
@@ -117,13 +119,14 @@ export function computeAdaptiveQueueStep(
117
119
  dtMs: number,
118
120
  debt: number,
119
121
  revealScale = 1,
122
+ tuning: StreamDebugTuning = DEFAULT_STREAM_DEBUG_TUNING,
120
123
  ): AdaptiveQueueStep {
121
124
  if (backlog <= 0 || dtMs <= 0) return { revealChars: 0, debt: 0, speedCps: 0 }
122
125
  const speedCps = Math.min(
123
- QUEUE_MAX_SPEED_CPS,
124
- QUEUE_BASE_SPEED_CPS + Math.pow(backlog, QUEUE_ACCEL_EXPONENT) * QUEUE_PRESSURE_FACTOR,
126
+ tuning.maxRevealCps,
127
+ QUEUE_BASE_SPEED_CPS + Math.pow(backlog, QUEUE_ACCEL_EXPONENT) * tuning.queuePressure,
125
128
  )
126
- const effectiveScale = clamp(revealScale, 0.05, 1)
129
+ const effectiveScale = clamp(revealScale * tuning.revealScale, 0.05, 2)
127
130
  const accumulated = Math.max(0, debt) + speedCps * effectiveScale * (dtMs / 1000)
128
131
  const revealChars = Math.min(backlog, Math.floor(accumulated))
129
132
  return { revealChars, debt: revealChars >= backlog ? 0 : accumulated - revealChars, speedCps }
@@ -313,6 +316,7 @@ export function useSmoothStreamContent(
313
316
  revealScaleOutRef.current = revealScaleRef
314
317
  const inputCompleteRef = useRef(inputComplete)
315
318
  inputCompleteRef.current = inputComplete
319
+ const streamIdRef = useRef(`stream-${Math.random().toString(36).slice(2)}`)
316
320
 
317
321
  useEffect(() => {
318
322
  holdBackRef.current = shouldHoldBack
@@ -374,6 +378,7 @@ export function useSmoothStreamContent(
374
378
  settleCpsRef.current = null
375
379
  const speedOut = speedOutRef.current
376
380
  if (speedOut !== undefined) speedOut.current = seedCps
381
+ debugRuntime.reportStream(streamIdRef.current, null)
377
382
  stopFrameLoop()
378
383
  return
379
384
  }
@@ -390,6 +395,7 @@ export function useSmoothStreamContent(
390
395
 
391
396
  const idleMs = now - lastInputTsRef.current
392
397
  const producerComplete = inputCompleteRef.current
398
+ const debugTuning = debugRuntime.activeTuning()
393
399
  const inputActive = !producerComplete && idleMs <= config.activeInputWindowMs
394
400
  const settling = producerComplete || (!inputActive && idleMs >= config.settleAfterMs)
395
401
  if (!producerComplete) settleCpsRef.current = null
@@ -422,7 +428,7 @@ export function useSmoothStreamContent(
422
428
  },
423
429
  dtSeconds,
424
430
  )
425
- revealChars = Math.min(step.revealChars, backlog)
431
+ revealChars = Math.min(Math.round(step.revealChars * debugTuning.revealScale), backlog)
426
432
  revealSpeedCps = frameIntervalMs > 0 ? (revealChars * 1000) / frameIntervalMs : 0
427
433
  } else {
428
434
  const step = computeAdaptiveQueueStep(
@@ -430,12 +436,21 @@ export function useSmoothStreamContent(
430
436
  frameIntervalMs,
431
437
  queueDebtRef.current,
432
438
  revealScaleOutRef.current?.current ?? 1,
439
+ debugTuning,
433
440
  )
434
441
  revealChars = step.revealChars
435
442
  revealSpeedCps = step.speedCps
436
443
  nextQueueDebt = step.debt
437
444
  }
438
445
 
446
+ debugRuntime.reportStream(streamIdRef.current, {
447
+ backlog,
448
+ speedCps: revealSpeedCps,
449
+ targetChars: targetCount,
450
+ displayedChars: displayedCount,
451
+ active: !producerComplete,
452
+ })
453
+
439
454
  // Performance guard: while degraded and the reply is offscreen, skip
440
455
  // the DOM commit — the backlog keeps accumulating and flushes when the
441
456
  // guard clears or the reply scrolls into view.
@@ -527,6 +542,7 @@ export function useSmoothStreamContent(
527
542
  useEffect(() => {
528
543
  return () => {
529
544
  stopFrameLoop()
545
+ debugRuntime.reportStream(streamIdRef.current, null)
530
546
  }
531
547
  }, [stopFrameLoop])
532
548
 
package/src/plugin.ts CHANGED
@@ -7,8 +7,18 @@ import { DEFAULT_STREAM_CONFIG, type StreamConfig } from './config.ts'
7
7
  import { injectStreamConfig } from './boot-config.ts'
8
8
  import { STREAM_PACKAGE_NAME, STREAM_PACKAGE_VERSION } from './package-meta.ts'
9
9
  import { inspectProfileInstallation, updateNpmProfilePackage } from './profile-installation.ts'
10
- import { STREAM_SETTINGS_RPC, STREAM_SETTINGS_RPC_CHANNEL, type StreamSettingsView } from './settings-api.ts'
11
- import { DEFAULT_STREAM_SETTINGS, STREAM_SETTINGS_NS, type StreamSettings } from './settings.ts'
10
+ import {
11
+ STREAM_SETTINGS_RPC,
12
+ STREAM_SETTINGS_RPC_CHANNEL,
13
+ type StreamDebugSettingsView,
14
+ type StreamSettingsView,
15
+ } from './settings-api.ts'
16
+ import {
17
+ DEFAULT_STREAM_SETTINGS,
18
+ STREAM_SETTINGS_NS,
19
+ type StreamDebugTuning,
20
+ type StreamSettings,
21
+ } from './settings.ts'
12
22
 
13
23
  /** Display name shown by the Host loader while the plugin is mounted. */
14
24
  export const name = 'dsh-smooth-stream'
@@ -42,7 +52,21 @@ export const Config: Schema<Config> = Schema.object({
42
52
  * settings provider while the browser edits it through the plugin RPC below.
43
53
  */
44
54
  export const StreamSettingsSchema: Schema<StreamSettings> = Schema.object({
55
+ enabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.enabled),
45
56
  thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand),
57
+ autoCollapse: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.autoCollapse),
58
+ debugEnabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.debugEnabled),
59
+ debugTuning: Schema.object({
60
+ revealScale: Schema.number().min(0.25).max(2).default(DEFAULT_STREAM_SETTINGS.debugTuning.revealScale),
61
+ queuePressure: Schema.number().min(0).max(2).default(DEFAULT_STREAM_SETTINGS.debugTuning.queuePressure),
62
+ maxRevealCps: Schema.number().min(120).max(1000).default(DEFAULT_STREAM_SETTINGS.debugTuning.maxRevealCps),
63
+ springStiffness: Schema.number().min(40).max(320).default(DEFAULT_STREAM_SETTINGS.debugTuning.springStiffness),
64
+ springDamping: Schema.number().min(8).max(80).default(DEFAULT_STREAM_SETTINGS.debugTuning.springDamping),
65
+ springMass: Schema.number().min(0.5).max(3).default(DEFAULT_STREAM_SETTINGS.debugTuning.springMass),
66
+ runwayPx: Schema.number().min(0).max(120).default(DEFAULT_STREAM_SETTINGS.debugTuning.runwayPx),
67
+ reserveResponseMs: Schema.number().min(60).max(600).default(DEFAULT_STREAM_SETTINGS.debugTuning.reserveResponseMs),
68
+ backpressureMinScale: Schema.number().min(0.25).max(1).default(DEFAULT_STREAM_SETTINGS.debugTuning.backpressureMinScale),
69
+ }),
46
70
  })
47
71
 
48
72
  /**
@@ -79,25 +103,61 @@ export function apply(ctx: Context, config: Config): void {
79
103
 
80
104
  const view = (): StreamSettingsView => {
81
105
  const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME)
106
+ const settings = scope.get()
82
107
  return {
83
108
  version: STREAM_PACKAGE_VERSION,
84
109
  installation: installation.kind,
85
110
  writable: connectionCtx.settings.writable,
86
- thinkAutoExpand: scope.get().thinkAutoExpand,
111
+ enabled: settings.enabled,
112
+ thinkAutoExpand: settings.thinkAutoExpand,
113
+ autoCollapse: settings.autoCollapse,
87
114
  canUpgrade: installation.kind === 'npm',
88
115
  }
89
116
  }
90
117
 
118
+ const debugView = (): StreamDebugSettingsView => {
119
+ const settings = scope.get()
120
+ return {
121
+ debugEnabled: settings.debugEnabled,
122
+ tuning: { ...settings.debugTuning },
123
+ }
124
+ }
125
+
126
+ const validDebugTuning = (value: unknown): value is StreamDebugTuning => {
127
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
128
+ const tuning = value as Record<string, unknown>
129
+ return typeof tuning.revealScale === 'number'
130
+ && tuning.revealScale >= 0.25 && tuning.revealScale <= 2
131
+ && typeof tuning.queuePressure === 'number'
132
+ && tuning.queuePressure >= 0 && tuning.queuePressure <= 2
133
+ && typeof tuning.maxRevealCps === 'number'
134
+ && tuning.maxRevealCps >= 120 && tuning.maxRevealCps <= 1000
135
+ && typeof tuning.springStiffness === 'number'
136
+ && tuning.springStiffness >= 40 && tuning.springStiffness <= 320
137
+ && typeof tuning.springDamping === 'number'
138
+ && tuning.springDamping >= 8 && tuning.springDamping <= 80
139
+ && typeof tuning.springMass === 'number'
140
+ && tuning.springMass >= 0.5 && tuning.springMass <= 3
141
+ && typeof tuning.runwayPx === 'number'
142
+ && tuning.runwayPx >= 0 && tuning.runwayPx <= 120
143
+ && typeof tuning.reserveResponseMs === 'number'
144
+ && tuning.reserveResponseMs >= 60 && tuning.reserveResponseMs <= 600
145
+ && typeof tuning.backpressureMinScale === 'number'
146
+ && tuning.backpressureMinScale >= 0.25 && tuning.backpressureMinScale <= 1
147
+ }
148
+
91
149
  const handle: ConnectionRpcHandler = async (endpoint, payload) => {
92
150
  if (endpoint === STREAM_SETTINGS_RPC.read) return { ok: true, value: view() }
93
151
  if (endpoint === STREAM_SETTINGS_RPC.write) {
94
- if (typeof payload !== 'object' || payload === null
95
- || typeof (payload as { thinkAutoExpand?: unknown }).thinkAutoExpand !== 'boolean') {
152
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)
153
+ || typeof (payload as { enabled?: unknown }).enabled !== 'boolean'
154
+ || typeof (payload as { thinkAutoExpand?: unknown }).thinkAutoExpand !== 'boolean'
155
+ || typeof (payload as { autoCollapse?: unknown }).autoCollapse !== 'boolean') {
96
156
  return {
97
157
  ok: false,
98
158
  error: {
99
159
  code: 'settings-rejected',
100
- message: 'thinkAutoExpand must be a boolean',
160
+ message: 'enabled, thinkAutoExpand and autoCollapse must be booleans',
101
161
  details: { ns: STREAM_SETTINGS_NS },
102
162
  },
103
163
  }
@@ -113,7 +173,30 @@ export function apply(ctx: Context, config: Config): void {
113
173
  }
114
174
  }
115
175
  try {
116
- await scope.update({ thinkAutoExpand: (payload as { thinkAutoExpand: boolean }).thinkAutoExpand })
176
+ const next = payload as {
177
+ enabled: boolean
178
+ thinkAutoExpand: boolean
179
+ autoCollapse: boolean
180
+ debugEnabled?: unknown
181
+ debugTuning?: unknown
182
+ }
183
+ const hasDebug = next.debugEnabled !== undefined || next.debugTuning !== undefined
184
+ if (hasDebug && (typeof next.debugEnabled !== 'boolean' || !validDebugTuning(next.debugTuning))) {
185
+ return {
186
+ ok: false,
187
+ error: {
188
+ code: 'settings-rejected',
189
+ message: 'debugEnabled and debugTuning must be provided together and be valid',
190
+ details: { ns: STREAM_SETTINGS_NS },
191
+ },
192
+ }
193
+ }
194
+ await scope.update({
195
+ enabled: next.enabled,
196
+ thinkAutoExpand: next.thinkAutoExpand,
197
+ autoCollapse: next.autoCollapse,
198
+ ...(hasDebug ? { debugEnabled: next.debugEnabled, debugTuning: next.debugTuning } : {}),
199
+ })
117
200
  } catch {
118
201
  return {
119
202
  ok: false,
@@ -126,6 +209,53 @@ export function apply(ctx: Context, config: Config): void {
126
209
  }
127
210
  return { ok: true, value: view() }
128
211
  }
212
+ if (endpoint === STREAM_SETTINGS_RPC.debugRead) return { ok: true, value: debugView() }
213
+ if (endpoint === STREAM_SETTINGS_RPC.debugWrite) {
214
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
215
+ return {
216
+ ok: false,
217
+ error: {
218
+ code: 'settings-rejected',
219
+ message: 'debug settings must be an object',
220
+ details: { ns: STREAM_SETTINGS_NS },
221
+ },
222
+ }
223
+ }
224
+ const next = payload as { debugEnabled?: unknown; tuning?: unknown }
225
+ if (typeof next.debugEnabled !== 'boolean' || !validDebugTuning(next.tuning)) {
226
+ return {
227
+ ok: false,
228
+ error: {
229
+ code: 'settings-rejected',
230
+ message: 'debugEnabled and tuning are malformed',
231
+ details: { ns: STREAM_SETTINGS_NS },
232
+ },
233
+ }
234
+ }
235
+ if (!connectionCtx.settings.writable) {
236
+ return {
237
+ ok: false,
238
+ error: {
239
+ code: 'settings-rejected',
240
+ message: 'smooth-stream debug settings are read-only',
241
+ details: { ns: STREAM_SETTINGS_NS },
242
+ },
243
+ }
244
+ }
245
+ try {
246
+ await scope.update({ debugEnabled: next.debugEnabled, debugTuning: next.tuning })
247
+ } catch {
248
+ return {
249
+ ok: false,
250
+ error: {
251
+ code: 'settings-rejected',
252
+ message: 'smooth-stream debug settings update failed',
253
+ details: { ns: STREAM_SETTINGS_NS },
254
+ },
255
+ }
256
+ }
257
+ return { ok: true, value: debugView() }
258
+ }
129
259
  if (endpoint === STREAM_SETTINGS_RPC.upgrade) {
130
260
  const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME)
131
261
  if (installation.kind !== 'npm') {
@@ -7,6 +7,8 @@ export const STREAM_SETTINGS_RPC_CHANNEL = '/smooth-stream'
7
7
  export const STREAM_SETTINGS_RPC = {
8
8
  read: 'settings.read',
9
9
  write: 'settings.write',
10
+ debugRead: 'debug.read',
11
+ debugWrite: 'debug.write',
10
12
  upgrade: 'plugin.upgrade',
11
13
  } as const
12
14
 
@@ -21,12 +23,22 @@ export interface StreamSettingsView {
21
23
  installation: StreamInstallationKind
22
24
  /** Whether the Host settings provider permits a write. */
23
25
  writable: boolean
26
+ /** Whether smooth-stream currently owns conversation rendering. */
27
+ enabled: boolean
24
28
  /** Current resolved preference. */
25
29
  thinkAutoExpand: boolean
30
+ /** Whether finished turns fold their work process behind a summary row. */
31
+ autoCollapse: boolean
26
32
  /** Whether a fixed npm update command is safe to offer. */
27
33
  canUpgrade: boolean
28
34
  }
29
35
 
36
+ /** Debug-only settings returned by the diagnostics RPC. */
37
+ export interface StreamDebugSettingsView {
38
+ debugEnabled: boolean
39
+ tuning: import('./settings.ts').StreamDebugTuning
40
+ }
41
+
30
42
  /** Successful package update acknowledgement; loading the new code needs a restart. */
31
43
  export interface StreamUpgradeView {
32
44
  restartRequired: true
package/src/settings.ts CHANGED
@@ -10,21 +10,76 @@
10
10
  /** Settings namespace registered by the Host and served through the plugin RPC. */
11
11
  export const STREAM_SETTINGS_NS = 'smooth-stream'
12
12
 
13
+ /** Runtime knobs exposed only while the diagnostics switch is enabled. */
14
+ export interface StreamDebugTuning {
15
+ /** Multiplier applied to the reveal cadence (1 = preset default). */
16
+ revealScale: number
17
+ /** Multiplier for backlog pressure in the adaptive reveal queue. */
18
+ queuePressure: number
19
+ /** Upper bound for reveal cadence, in characters per second. */
20
+ maxRevealCps: number
21
+ /** Spring stiffness used by the conversation follower. */
22
+ springStiffness: number
23
+ /** Spring damping used by the conversation follower. */
24
+ springDamping: number
25
+ /** Spring mass used by the conversation follower. */
26
+ springMass: number
27
+ /** Predictive runway before the fixed conversation chrome, in pixels. */
28
+ runwayPx: number
29
+ /** Response time for opening/closing the predictive runway, in ms. */
30
+ reserveResponseMs: number
31
+ /** Lowest reveal multiplier when visual lag fills the safe paint room. */
32
+ backpressureMinScale: number
33
+ }
34
+
35
+ /** Defaults preserve the production engine exactly. */
36
+ export const DEFAULT_STREAM_DEBUG_TUNING: StreamDebugTuning = {
37
+ revealScale: 1,
38
+ queuePressure: 0.85,
39
+ maxRevealCps: 600,
40
+ springStiffness: 130,
41
+ springDamping: 24,
42
+ springMass: 1,
43
+ runwayPx: 48,
44
+ reserveResponseMs: 180,
45
+ backpressureMinScale: 0.55,
46
+ }
47
+
13
48
  /**
14
49
  * Preferences a user may set. Deliberately separate from {@link StreamConfig}
15
50
  * because the two change at different times: composition-time values go
16
51
  * through the boot global, a live UI edit goes through the protected plugin RPC.
17
52
  */
18
53
  export interface StreamSettings {
54
+ /**
55
+ * Whether this plugin replaces and wraps Harness conversation renderers.
56
+ * Off returns all rendering ownership to the built-in UI.
57
+ */
58
+ enabled: boolean
19
59
  /**
20
60
  * Whether a reasoning ("Think") block auto-expands while it is the
21
61
  * streaming tail. Off keeps the block collapsed — the user can still open
22
62
  * it by hand — and stops the running state from re-owning the disclosure.
23
63
  */
24
64
  thinkAutoExpand: boolean
65
+ /**
66
+ * Whether a finished turn folds its work process (thinking, tool calls,
67
+ * context injection, intermediate replies) behind one "已处理" summary row,
68
+ * leaving only the final answer visible. The summary row can always be
69
+ * toggled by hand, and turns still expand live while they stream.
70
+ */
71
+ autoCollapse: boolean
72
+ /** Whether the live renderer diagnostics panel is enabled. */
73
+ debugEnabled: boolean
74
+ /** Values edited by the diagnostics panel. */
75
+ debugTuning: StreamDebugTuning
25
76
  }
26
77
 
27
78
  /** Defaults shared by the Host schema and the client-side fallback. */
28
79
  export const DEFAULT_STREAM_SETTINGS: StreamSettings = {
80
+ enabled: true,
29
81
  thinkAutoExpand: true,
82
+ autoCollapse: true,
83
+ debugEnabled: false,
84
+ debugTuning: DEFAULT_STREAM_DEBUG_TUNING,
30
85
  }