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/index.js CHANGED
@@ -145,6 +145,8 @@ const STREAM_SETTINGS_RPC_CHANNEL = "/smooth-stream";
145
145
  const STREAM_SETTINGS_RPC = {
146
146
  read: "settings.read",
147
147
  write: "settings.write",
148
+ debugRead: "debug.read",
149
+ debugWrite: "debug.write",
148
150
  upgrade: "plugin.upgrade"
149
151
  };
150
152
  //#endregion
@@ -160,7 +162,23 @@ const STREAM_SETTINGS_RPC = {
160
162
  /** Settings namespace registered by the Host and served through the plugin RPC. */
161
163
  const STREAM_SETTINGS_NS = "smooth-stream";
162
164
  /** Defaults shared by the Host schema and the client-side fallback. */
163
- const DEFAULT_STREAM_SETTINGS = { thinkAutoExpand: true };
165
+ const DEFAULT_STREAM_SETTINGS = {
166
+ enabled: true,
167
+ thinkAutoExpand: true,
168
+ autoCollapse: true,
169
+ debugEnabled: false,
170
+ debugTuning: {
171
+ revealScale: 1,
172
+ queuePressure: .85,
173
+ maxRevealCps: 600,
174
+ springStiffness: 130,
175
+ springDamping: 24,
176
+ springMass: 1,
177
+ runwayPx: 48,
178
+ reserveResponseMs: 180,
179
+ backpressureMinScale: .55
180
+ }
181
+ };
164
182
  //#endregion
165
183
  //#region src/plugin.ts
166
184
  /** Display name shown by the Host loader while the plugin is mounted. */
@@ -180,7 +198,23 @@ const Config = Schema.object({
180
198
  * Schema of the user-owned settings section. The Host keeps it in the durable
181
199
  * settings provider while the browser edits it through the plugin RPC below.
182
200
  */
183
- const StreamSettingsSchema = Schema.object({ thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand) });
201
+ const StreamSettingsSchema = Schema.object({
202
+ enabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.enabled),
203
+ thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand),
204
+ autoCollapse: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.autoCollapse),
205
+ debugEnabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.debugEnabled),
206
+ debugTuning: Schema.object({
207
+ revealScale: Schema.number().min(.25).max(2).default(DEFAULT_STREAM_SETTINGS.debugTuning.revealScale),
208
+ queuePressure: Schema.number().min(0).max(2).default(DEFAULT_STREAM_SETTINGS.debugTuning.queuePressure),
209
+ maxRevealCps: Schema.number().min(120).max(1e3).default(DEFAULT_STREAM_SETTINGS.debugTuning.maxRevealCps),
210
+ springStiffness: Schema.number().min(40).max(320).default(DEFAULT_STREAM_SETTINGS.debugTuning.springStiffness),
211
+ springDamping: Schema.number().min(8).max(80).default(DEFAULT_STREAM_SETTINGS.debugTuning.springDamping),
212
+ springMass: Schema.number().min(.5).max(3).default(DEFAULT_STREAM_SETTINGS.debugTuning.springMass),
213
+ runwayPx: Schema.number().min(0).max(120).default(DEFAULT_STREAM_SETTINGS.debugTuning.runwayPx),
214
+ reserveResponseMs: Schema.number().min(60).max(600).default(DEFAULT_STREAM_SETTINGS.debugTuning.reserveResponseMs),
215
+ backpressureMinScale: Schema.number().min(.25).max(1).default(DEFAULT_STREAM_SETTINGS.debugTuning.backpressureMinScale)
216
+ })
217
+ });
184
218
  /**
185
219
  * Host half: log the resolved configuration and bridge it to the browser
186
220
  * half. The web boot graph carries no per-entry config, so the validated
@@ -200,25 +234,40 @@ function apply(ctx, config) {
200
234
  let upgrade;
201
235
  const view = () => {
202
236
  const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME);
237
+ const settings = scope.get();
203
238
  return {
204
239
  version: STREAM_PACKAGE_VERSION,
205
240
  installation: installation.kind,
206
241
  writable: connectionCtx.settings.writable,
207
- thinkAutoExpand: scope.get().thinkAutoExpand,
242
+ enabled: settings.enabled,
243
+ thinkAutoExpand: settings.thinkAutoExpand,
244
+ autoCollapse: settings.autoCollapse,
208
245
  canUpgrade: installation.kind === "npm"
209
246
  };
210
247
  };
248
+ const debugView = () => {
249
+ const settings = scope.get();
250
+ return {
251
+ debugEnabled: settings.debugEnabled,
252
+ tuning: { ...settings.debugTuning }
253
+ };
254
+ };
255
+ const validDebugTuning = (value) => {
256
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
257
+ const tuning = value;
258
+ return typeof tuning.revealScale === "number" && tuning.revealScale >= .25 && tuning.revealScale <= 2 && typeof tuning.queuePressure === "number" && tuning.queuePressure >= 0 && tuning.queuePressure <= 2 && typeof tuning.maxRevealCps === "number" && tuning.maxRevealCps >= 120 && tuning.maxRevealCps <= 1e3 && typeof tuning.springStiffness === "number" && tuning.springStiffness >= 40 && tuning.springStiffness <= 320 && typeof tuning.springDamping === "number" && tuning.springDamping >= 8 && tuning.springDamping <= 80 && typeof tuning.springMass === "number" && tuning.springMass >= .5 && tuning.springMass <= 3 && typeof tuning.runwayPx === "number" && tuning.runwayPx >= 0 && tuning.runwayPx <= 120 && typeof tuning.reserveResponseMs === "number" && tuning.reserveResponseMs >= 60 && tuning.reserveResponseMs <= 600 && typeof tuning.backpressureMinScale === "number" && tuning.backpressureMinScale >= .25 && tuning.backpressureMinScale <= 1;
259
+ };
211
260
  const handle = async (endpoint, payload) => {
212
261
  if (endpoint === STREAM_SETTINGS_RPC.read) return {
213
262
  ok: true,
214
263
  value: view()
215
264
  };
216
265
  if (endpoint === STREAM_SETTINGS_RPC.write) {
217
- if (typeof payload !== "object" || payload === null || typeof payload.thinkAutoExpand !== "boolean") return {
266
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload) || typeof payload.enabled !== "boolean" || typeof payload.thinkAutoExpand !== "boolean" || typeof payload.autoCollapse !== "boolean") return {
218
267
  ok: false,
219
268
  error: {
220
269
  code: "settings-rejected",
221
- message: "thinkAutoExpand must be a boolean",
270
+ message: "enabled, thinkAutoExpand and autoCollapse must be booleans",
222
271
  details: { ns: STREAM_SETTINGS_NS }
223
272
  }
224
273
  };
@@ -231,7 +280,25 @@ function apply(ctx, config) {
231
280
  }
232
281
  };
233
282
  try {
234
- await scope.update({ thinkAutoExpand: payload.thinkAutoExpand });
283
+ const next = payload;
284
+ const hasDebug = next.debugEnabled !== void 0 || next.debugTuning !== void 0;
285
+ if (hasDebug && (typeof next.debugEnabled !== "boolean" || !validDebugTuning(next.debugTuning))) return {
286
+ ok: false,
287
+ error: {
288
+ code: "settings-rejected",
289
+ message: "debugEnabled and debugTuning must be provided together and be valid",
290
+ details: { ns: STREAM_SETTINGS_NS }
291
+ }
292
+ };
293
+ await scope.update({
294
+ enabled: next.enabled,
295
+ thinkAutoExpand: next.thinkAutoExpand,
296
+ autoCollapse: next.autoCollapse,
297
+ ...hasDebug ? {
298
+ debugEnabled: next.debugEnabled,
299
+ debugTuning: next.debugTuning
300
+ } : {}
301
+ });
235
302
  } catch {
236
303
  return {
237
304
  ok: false,
@@ -247,6 +314,56 @@ function apply(ctx, config) {
247
314
  value: view()
248
315
  };
249
316
  }
317
+ if (endpoint === STREAM_SETTINGS_RPC.debugRead) return {
318
+ ok: true,
319
+ value: debugView()
320
+ };
321
+ if (endpoint === STREAM_SETTINGS_RPC.debugWrite) {
322
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return {
323
+ ok: false,
324
+ error: {
325
+ code: "settings-rejected",
326
+ message: "debug settings must be an object",
327
+ details: { ns: STREAM_SETTINGS_NS }
328
+ }
329
+ };
330
+ const next = payload;
331
+ if (typeof next.debugEnabled !== "boolean" || !validDebugTuning(next.tuning)) return {
332
+ ok: false,
333
+ error: {
334
+ code: "settings-rejected",
335
+ message: "debugEnabled and tuning are malformed",
336
+ details: { ns: STREAM_SETTINGS_NS }
337
+ }
338
+ };
339
+ if (!connectionCtx.settings.writable) return {
340
+ ok: false,
341
+ error: {
342
+ code: "settings-rejected",
343
+ message: "smooth-stream debug settings are read-only",
344
+ details: { ns: STREAM_SETTINGS_NS }
345
+ }
346
+ };
347
+ try {
348
+ await scope.update({
349
+ debugEnabled: next.debugEnabled,
350
+ debugTuning: next.tuning
351
+ });
352
+ } catch {
353
+ return {
354
+ ok: false,
355
+ error: {
356
+ code: "settings-rejected",
357
+ message: "smooth-stream debug settings update failed",
358
+ details: { ns: STREAM_SETTINGS_NS }
359
+ }
360
+ };
361
+ }
362
+ return {
363
+ ok: true,
364
+ value: debugView()
365
+ };
366
+ }
250
367
  if (endpoint === STREAM_SETTINGS_RPC.upgrade) {
251
368
  const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME);
252
369
  if (installation.kind !== "npm") return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-smooth-stream",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Fluid streaming rendering and silky scrolling for DeepSeek Harness replies, including Markdown, code blocks, tables, and tool results.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,13 +77,6 @@
77
77
  ]
78
78
  }
79
79
  },
80
- "scripts": {
81
- "build": "tsdown",
82
- "build:repro": "esbuild repro/main.tsx --bundle --format=esm --platform=browser --loader:.css=local-css --outfile=repro/bundle.js",
83
- "prepare": "tsdown",
84
- "test": "vitest run",
85
- "typecheck": "tsc -p tsconfig.json --pretty false"
86
- },
87
80
  "dependencies": {
88
81
  "@deepseek-ai/dsh-settings": "0.1.0-rc.6"
89
82
  },
@@ -160,5 +153,12 @@
160
153
  "tsdown": "^0.22.2",
161
154
  "typescript": "^5.9.2",
162
155
  "vitest": "^4.1.8"
156
+ },
157
+ "scripts": {
158
+ "benchmark": "node scripts/run-stream-benchmark.mjs",
159
+ "build": "tsdown",
160
+ "build:repro": "esbuild repro/main.tsx --bundle --format=esm --platform=browser --loader:.css=local-css --outfile=repro/bundle.js",
161
+ "test": "vitest run",
162
+ "typecheck": "tsc -p tsconfig.json --pretty false"
163
163
  }
164
- }
164
+ }
@@ -0,0 +1,370 @@
1
+ .trigger,
2
+ .iconButton {
3
+ appearance: none;
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: 32px;
8
+ height: 32px;
9
+ padding: 0;
10
+ border: 1px solid transparent;
11
+ border-radius: 6px;
12
+ background: transparent;
13
+ color: var(--dsw-alias-label-tertiary);
14
+ cursor: pointer;
15
+ }
16
+
17
+ .trigger:hover,
18
+ .iconButton:hover:not(:disabled) {
19
+ background: var(--dsw-alias-interactive-bg-hover);
20
+ color: var(--dsw-alias-label-primary);
21
+ }
22
+
23
+ .iconButton:disabled {
24
+ opacity: 0.45;
25
+ cursor: not-allowed;
26
+ }
27
+
28
+ .triggerActive {
29
+ border-color: var(--dsw-alias-border-l2);
30
+ background: var(--dsw-alias-bg-layer-2);
31
+ color: var(--dsw-alias-state-business-primary);
32
+ }
33
+
34
+ .trigger:focus-visible,
35
+ .iconButton:focus-visible,
36
+ .secondaryButton:focus-visible,
37
+ .primaryButton:focus-visible,
38
+ .number:focus-visible,
39
+ .range:focus-visible {
40
+ outline: 2px solid var(--dsw-alias-brand-primary);
41
+ outline-offset: 1px;
42
+ }
43
+
44
+ .panel {
45
+ position: fixed;
46
+ top: 88px;
47
+ right: 16px;
48
+ bottom: 16px;
49
+ z-index: 30;
50
+ display: flex;
51
+ flex-direction: column;
52
+ width: min(328px, calc(100vw - 32px));
53
+ min-height: 280px;
54
+ overflow: hidden;
55
+ border: 1px solid var(--dsw-alias-border-l2);
56
+ border-radius: 8px;
57
+ background: var(--dsw-alias-bg-layer-3);
58
+ box-shadow: 0 12px 32px color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent);
59
+ color: var(--dsw-alias-label-primary);
60
+ }
61
+
62
+ .panelHeader {
63
+ display: flex;
64
+ flex: none;
65
+ align-items: center;
66
+ gap: 8px;
67
+ min-height: 48px;
68
+ padding: 0 8px 0 14px;
69
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
70
+ }
71
+
72
+ .statusDot {
73
+ flex: none;
74
+ width: 7px;
75
+ height: 7px;
76
+ border-radius: 50%;
77
+ background: var(--dsw-alias-label-caption);
78
+ }
79
+
80
+ .statusLive {
81
+ background: var(--dsw-alias-state-success-primary);
82
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent);
83
+ }
84
+
85
+ .title {
86
+ min-width: 0;
87
+ overflow: hidden;
88
+ flex: 1;
89
+ font-size: 14px;
90
+ font-weight: 600;
91
+ line-height: 20px;
92
+ text-overflow: ellipsis;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .state,
97
+ .unsaved {
98
+ flex: none;
99
+ font-size: 10px;
100
+ line-height: 16px;
101
+ color: var(--dsw-alias-label-tertiary);
102
+ white-space: nowrap;
103
+ }
104
+
105
+ .unsaved {
106
+ color: var(--dsw-alias-state-warn-primary);
107
+ }
108
+
109
+ .scrollArea {
110
+ min-height: 0;
111
+ overflow-y: auto;
112
+ overscroll-behavior: contain;
113
+ }
114
+
115
+ .section {
116
+ padding: 14px;
117
+ }
118
+
119
+ .guide {
120
+ margin: 0;
121
+ padding: 12px 14px;
122
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
123
+ color: var(--dsw-alias-label-tertiary);
124
+ font-size: 11px;
125
+ line-height: 1.55;
126
+ }
127
+
128
+ .section + .section {
129
+ border-top: 1px solid var(--dsw-alias-border-l2);
130
+ }
131
+
132
+ .section h2 {
133
+ margin: 0 0 10px;
134
+ font-size: 12px;
135
+ font-weight: 600;
136
+ line-height: 18px;
137
+ color: var(--dsw-alias-label-secondary);
138
+ }
139
+
140
+ .metrics {
141
+ display: grid;
142
+ grid-template-columns: repeat(2, minmax(0, 1fr));
143
+ gap: 0 16px;
144
+ margin: 0;
145
+ }
146
+
147
+ .metric {
148
+ display: flex;
149
+ align-items: baseline;
150
+ justify-content: space-between;
151
+ gap: 8px;
152
+ min-width: 0;
153
+ padding: 5px 0;
154
+ border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent);
155
+ }
156
+
157
+ .metric dt {
158
+ min-width: 0;
159
+ overflow: hidden;
160
+ font-size: 11px;
161
+ line-height: 16px;
162
+ color: var(--dsw-alias-label-tertiary);
163
+ text-overflow: ellipsis;
164
+ white-space: nowrap;
165
+ }
166
+
167
+ .metric dd {
168
+ min-width: 0;
169
+ margin: 0;
170
+ overflow: hidden;
171
+ color: var(--dsw-alias-label-primary);
172
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
173
+ font-size: 11px;
174
+ font-variant-numeric: tabular-nums;
175
+ line-height: 16px;
176
+ text-overflow: ellipsis;
177
+ white-space: nowrap;
178
+ }
179
+
180
+ .metric dd[data-tone='good'] {
181
+ color: var(--dsw-alias-state-success-primary);
182
+ }
183
+
184
+ .metric dd[data-tone='warn'] {
185
+ color: var(--dsw-alias-state-warn-primary);
186
+ }
187
+
188
+ .control {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 5px;
192
+ padding: 6px 0;
193
+ }
194
+
195
+ .controlHead {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ gap: 10px;
200
+ font-size: 11px;
201
+ line-height: 18px;
202
+ color: var(--dsw-alias-label-secondary);
203
+ }
204
+
205
+ .controlLabel {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ min-width: 0;
209
+ gap: 4px;
210
+ }
211
+
212
+ .infoButton {
213
+ appearance: none;
214
+ display: inline-flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ width: 20px;
218
+ height: 20px;
219
+ padding: 0;
220
+ border: 0;
221
+ border-radius: 50%;
222
+ background: transparent;
223
+ color: var(--dsw-alias-label-tertiary);
224
+ cursor: help;
225
+ }
226
+
227
+ .infoButton:hover {
228
+ color: var(--dsw-alias-label-primary);
229
+ background: var(--dsw-alias-interactive-bg-hover);
230
+ }
231
+
232
+ .infoButton:focus-visible {
233
+ outline: 2px solid var(--dsw-alias-brand-primary);
234
+ outline-offset: 1px;
235
+ }
236
+
237
+ .numberWrap {
238
+ display: inline-flex;
239
+ align-items: center;
240
+ flex: none;
241
+ gap: 4px;
242
+ }
243
+
244
+ .number {
245
+ box-sizing: border-box;
246
+ width: 68px;
247
+ height: 26px;
248
+ padding: 2px 6px;
249
+ border: 1px solid var(--dsw-alias-border-l2);
250
+ border-radius: 5px;
251
+ background: var(--dsw-alias-bg-layer-2);
252
+ color: var(--dsw-alias-label-primary);
253
+ font: inherit;
254
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
255
+ font-variant-numeric: tabular-nums;
256
+ text-align: right;
257
+ }
258
+
259
+ .unit {
260
+ width: 30px;
261
+ color: var(--dsw-alias-label-tertiary);
262
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
263
+ font-size: 10px;
264
+ }
265
+
266
+ .range {
267
+ width: 100%;
268
+ height: 18px;
269
+ margin: 0;
270
+ accent-color: var(--dsw-alias-state-business-primary);
271
+ cursor: pointer;
272
+ }
273
+
274
+ .range:disabled,
275
+ .number:disabled {
276
+ opacity: 0.5;
277
+ cursor: default;
278
+ }
279
+
280
+ .footer {
281
+ display: flex;
282
+ flex: none;
283
+ align-items: center;
284
+ gap: 6px;
285
+ min-height: 50px;
286
+ padding: 8px 10px;
287
+ border-top: 1px solid var(--dsw-alias-border-l2);
288
+ background: var(--dsw-alias-bg-layer-2);
289
+ }
290
+
291
+ .footerSpacer {
292
+ flex: 1;
293
+ }
294
+
295
+ .secondaryButton,
296
+ .primaryButton {
297
+ appearance: none;
298
+ display: inline-flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ gap: 5px;
302
+ min-height: 30px;
303
+ padding: 4px 9px;
304
+ border: 1px solid var(--dsw-alias-border-l2);
305
+ border-radius: 6px;
306
+ background: transparent;
307
+ color: var(--dsw-alias-label-secondary);
308
+ font: inherit;
309
+ font-size: 11px;
310
+ line-height: 18px;
311
+ cursor: pointer;
312
+ }
313
+
314
+ .primaryButton {
315
+ border-color: transparent;
316
+ background: var(--dsw-alias-label-primary);
317
+ color: var(--dsw-alias-bg-layer-3);
318
+ }
319
+
320
+ .secondaryButton:hover:not(:disabled) {
321
+ color: var(--dsw-alias-label-primary);
322
+ border-color: var(--dsw-alias-label-dimmed);
323
+ }
324
+
325
+ .secondaryButton:disabled,
326
+ .primaryButton:disabled {
327
+ opacity: 0.4;
328
+ cursor: default;
329
+ }
330
+
331
+ .visuallyHidden {
332
+ position: absolute;
333
+ overflow: hidden;
334
+ width: 1px;
335
+ height: 1px;
336
+ padding: 0;
337
+ border: 0;
338
+ margin: -1px;
339
+ clip: rect(0 0 0 0);
340
+ clip-path: inset(50%);
341
+ white-space: nowrap;
342
+ }
343
+
344
+ @media (max-width: 900px) {
345
+ .trigger,
346
+ .iconButton {
347
+ width: 44px;
348
+ height: 44px;
349
+ }
350
+
351
+ .panel {
352
+ top: auto;
353
+ right: 12px;
354
+ bottom: max(12px, env(safe-area-inset-bottom));
355
+ left: 12px;
356
+ width: auto;
357
+ max-height: min(70dvh, 640px);
358
+ }
359
+
360
+ .secondaryButton,
361
+ .primaryButton {
362
+ min-height: 44px;
363
+ }
364
+ }
365
+
366
+ @media (prefers-reduced-motion: reduce) {
367
+ .statusLive {
368
+ box-shadow: none;
369
+ }
370
+ }