dsh-smooth-stream 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +4 -1
- package/README.md +5 -1
- package/lib/client.js +1757 -282
- package/lib/client.js.map +1 -1
- package/lib/index.js +299 -3
- package/package.json +1 -1
- package/src/client/AgentRowEntrance.module.css +1 -4
- package/src/client/AnimatedDisclosure.tsx +3 -3
- package/src/client/DebugPanel.tsx +34 -15
- package/src/client/FrameCoordinator.ts +180 -0
- package/src/client/LogarithmicFade.module.css +160 -0
- package/src/client/SmoothStreamCard.tsx +41 -3
- package/src/client/StreamBuffer.ts +145 -0
- package/src/client/TypewriterAssistantNodeView.module.css +82 -12
- package/src/client/TypewriterAssistantNodeView.tsx +232 -104
- package/src/client/debugRuntime.ts +58 -0
- package/src/client/harnessIcons.ts +27 -0
- package/src/client/index.ts +153 -11
- package/src/client/locales.ts +90 -0
- package/src/client/smooth-stream-card-controller.ts +10 -3
- package/src/client/smooth-stream-settings-api.ts +7 -3
- package/src/client/teleprompterGlide.ts +499 -70
- package/src/client/useDecoupledMarkdown.ts +173 -0
- package/src/client/useFpsGuard.ts +9 -6
- package/src/client/useLogarithmicFade.ts +672 -0
- package/src/client/useProgressiveDomText.ts +27 -11
- package/src/client/useSmoothStreamContent.ts +5 -13
- package/src/config.ts +1 -1
- package/src/plugin.ts +99 -12
- package/src/settings-api.ts +4 -0
- package/src/settings-channel.ts +303 -0
- package/src/settings.ts +14 -0
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
/** Defaults shared by the Host schema and the client-side fallback. */
|
|
8
8
|
const DEFAULT_STREAM_CONFIG = {
|
|
9
9
|
mode: "typewriter",
|
|
10
|
-
preset: "
|
|
10
|
+
preset: "silky",
|
|
11
11
|
revealCharsPerSec: 80,
|
|
12
12
|
scrollSpeedPxPerSec: 48,
|
|
13
13
|
maxScrollSpeedPxPerSec: 1e3
|
|
@@ -136,6 +136,214 @@ function updateNpmProfilePackage(profileDir, packageName) {
|
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
//#endregion
|
|
139
|
+
//#region src/settings-channel.ts
|
|
140
|
+
/** Largest request body this channel buffers; settings payloads are tiny. */
|
|
141
|
+
const MAX_REQUEST_BYTES = 1 << 20;
|
|
142
|
+
/** Endpoint segment shape accepted by the Connection router, mirrored here. */
|
|
143
|
+
const ENDPOINT_SEGMENT = /^[A-Za-z0-9_$.-]+$/;
|
|
144
|
+
/** Envelope discriminator the browser caller sends. */
|
|
145
|
+
const CLIENT_REQUEST = "client-request";
|
|
146
|
+
/** Envelope discriminator the browser caller expects back. */
|
|
147
|
+
const SERVER_RESPONSE = "server-response";
|
|
148
|
+
/**
|
|
149
|
+
* Register the settings RPC channel on the most capable path the kernel has.
|
|
150
|
+
* The registration is an effect of `ctx`, so the caller's fiber owns the
|
|
151
|
+
* channel's lifetime on either path.
|
|
152
|
+
* @param ctx - Host context carrying `connection` and, when composed, `webServer`.
|
|
153
|
+
* @param channel - absolute channel prefix, e.g. `/smooth-stream`.
|
|
154
|
+
* @param handler - decoded endpoint handler for every endpoint on the channel.
|
|
155
|
+
* @returns Disposer that withdraws the channel.
|
|
156
|
+
*/
|
|
157
|
+
function registerSettingsChannel(ctx, channel, handler) {
|
|
158
|
+
let connection;
|
|
159
|
+
try {
|
|
160
|
+
connection = ctx.get("connection");
|
|
161
|
+
} catch {
|
|
162
|
+
connection = void 0;
|
|
163
|
+
}
|
|
164
|
+
return ctx.effect(() => {
|
|
165
|
+
try {
|
|
166
|
+
const direct = mountDirectRoute(ctx, connection, channel, handler);
|
|
167
|
+
if (direct !== void 0) return direct;
|
|
168
|
+
} catch {}
|
|
169
|
+
const viaService = tryServiceChannel(connection, channel, handler);
|
|
170
|
+
if (viaService !== void 0) return viaService;
|
|
171
|
+
return mountDirectRoute(ctx, connection, channel, handler);
|
|
172
|
+
}, `dsh-smooth-stream: ${channel} RPC channel`);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Try the service-owned registration, which is the only path that also carries
|
|
176
|
+
* the kernel's own channel policy.
|
|
177
|
+
* @returns Disposer on success, `undefined` when the kernel cannot mount it.
|
|
178
|
+
*/
|
|
179
|
+
function tryServiceChannel(connection, channel, handler) {
|
|
180
|
+
const rpc = connection?.rpc;
|
|
181
|
+
if (rpc === void 0 || typeof rpc.handle !== "function") return void 0;
|
|
182
|
+
try {
|
|
183
|
+
const release = rpc.handle(channel, handler, { authority: "loopback" });
|
|
184
|
+
if (typeof release !== "function") return void 0;
|
|
185
|
+
return () => {
|
|
186
|
+
release();
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.warn(`[dsh-smooth-stream] connection.rpc.handle() could not mount ${channel}; falling back to a directly registered route`, error);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Mount the channel prefix on the Web server.
|
|
195
|
+
*
|
|
196
|
+
* With the connection service reachable the route sits behind the connection
|
|
197
|
+
* trust fence. On dsh 0.2.x the connection service is unresolvable from a
|
|
198
|
+
* plugin scope, so the route is then served unfenced (loopback + the app's
|
|
199
|
+
* own token gate only) with a warning — the same trade-off the skill center
|
|
200
|
+
* plugin ships on 0.2.x.
|
|
201
|
+
* @throws fail-closed when the connection service is reachable but cannot
|
|
202
|
+
* fence, so the channel is never exposed without the trust policy.
|
|
203
|
+
*/
|
|
204
|
+
function mountDirectRoute(ctx, connection, channel, handler) {
|
|
205
|
+
const webServer = ctx.get("webServer");
|
|
206
|
+
const reject = connection?.requestRejection;
|
|
207
|
+
if (webServer === void 0 || typeof webServer.register !== "function") throw new Error(`dsh-smooth-stream: webServer is unavailable, so ${channel} cannot be mounted`);
|
|
208
|
+
if (typeof reject !== "function") {
|
|
209
|
+
if (connection !== void 0) throw new Error(`dsh-smooth-stream: connection.requestRejection is unavailable, so ${channel} cannot be mounted behind the connection trust fence`);
|
|
210
|
+
console.warn(`[dsh-smooth-stream] ${channel} mounted without the connection trust fence (dsh 0.2.x: the connection service is not injectable here); loopback and the app token gate remain the only protections`);
|
|
211
|
+
}
|
|
212
|
+
return webServer.register({
|
|
213
|
+
kind: "prefix",
|
|
214
|
+
path: channel,
|
|
215
|
+
handler: async (req, res) => {
|
|
216
|
+
const rejection = reject?.call(connection, req);
|
|
217
|
+
if (rejection !== void 0) {
|
|
218
|
+
res.writeHead(rejection);
|
|
219
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
await answer(req, res, channel, handler);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Decode one request envelope, dispatch it, and write the decoded reply.
|
|
228
|
+
* Mirrors the Connection router's own carrier rules: method and endpoint must
|
|
229
|
+
* agree, the body must be JSON, and only endpoint failures are results while
|
|
230
|
+
* transport failures are statuses.
|
|
231
|
+
*/
|
|
232
|
+
async function answer(req, res, channel, handler) {
|
|
233
|
+
const endpoint = endpointOf(channel, req.url);
|
|
234
|
+
if (req.method !== "POST" || endpoint === void 0) {
|
|
235
|
+
res.writeHead(404);
|
|
236
|
+
res.end("not found");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if ((req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
|
|
240
|
+
res.writeHead(415);
|
|
241
|
+
res.end("content type must be application/json");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const body = await readBody(req);
|
|
245
|
+
if (body.kind === "too-large") {
|
|
246
|
+
res.writeHead(413);
|
|
247
|
+
res.end("request body too large");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (body.kind === "invalid") {
|
|
251
|
+
res.writeHead(400);
|
|
252
|
+
res.end("body is not JSON");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const envelope = requestEnvelope(body.value);
|
|
256
|
+
if (envelope === void 0) {
|
|
257
|
+
write(res, rpcIdOf(body.value), {
|
|
258
|
+
ok: false,
|
|
259
|
+
error: {
|
|
260
|
+
code: "gateway/bad-request",
|
|
261
|
+
message: "invalid client-request message",
|
|
262
|
+
details: { issues: [] }
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (envelope.method !== endpoint) {
|
|
268
|
+
write(res, envelope.rpcId, {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: {
|
|
271
|
+
code: "gateway/bad-request",
|
|
272
|
+
message: `method ${JSON.stringify(envelope.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
|
|
273
|
+
details: { issues: [] }
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const controller = new AbortController();
|
|
279
|
+
res.on("close", () => {
|
|
280
|
+
controller.abort();
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
write(res, envelope.rpcId, await handler(endpoint, envelope.payload, controller.signal));
|
|
284
|
+
} catch (error) {
|
|
285
|
+
res.writeHead(500);
|
|
286
|
+
res.end(`handler failure: ${String(error)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Resolve the channel-relative endpoint for one request path.
|
|
291
|
+
* @returns The endpoint, or `undefined` when the path is outside the channel
|
|
292
|
+
* or carries a segment the Connection router would refuse.
|
|
293
|
+
*/
|
|
294
|
+
function endpointOf(channel, rawUrl) {
|
|
295
|
+
const pathname = new URL(rawUrl ?? "/", "http://dsh.internal").pathname;
|
|
296
|
+
if (!pathname.startsWith(`${channel}/`)) return void 0;
|
|
297
|
+
const endpoint = pathname.slice(channel.length + 1);
|
|
298
|
+
if (endpoint.split("/").some((segment) => segment === "" || segment === "." || segment === ".." || !ENDPOINT_SEGMENT.test(segment))) return;
|
|
299
|
+
return endpoint;
|
|
300
|
+
}
|
|
301
|
+
/** Structure one decoded request, rejecting anything the caller could not have sent. */
|
|
302
|
+
function requestEnvelope(value) {
|
|
303
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
304
|
+
const record = value;
|
|
305
|
+
if (record.type !== CLIENT_REQUEST) return void 0;
|
|
306
|
+
if (typeof record.rpcId !== "string" || typeof record.method !== "string") return void 0;
|
|
307
|
+
return {
|
|
308
|
+
rpcId: record.rpcId,
|
|
309
|
+
method: record.method,
|
|
310
|
+
payload: record.payload
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** Recover the correlation id of a malformed envelope so the caller can match it. */
|
|
314
|
+
function rpcIdOf(value) {
|
|
315
|
+
const raw = value?.rpcId;
|
|
316
|
+
return typeof raw === "string" ? raw : "invalid-request";
|
|
317
|
+
}
|
|
318
|
+
/** Write one decoded reply in the envelope the browser caller parses. */
|
|
319
|
+
function write(res, rpcId, result) {
|
|
320
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
321
|
+
res.end(JSON.stringify({
|
|
322
|
+
type: SERVER_RESPONSE,
|
|
323
|
+
rpcId,
|
|
324
|
+
result
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
/** Buffer one request body, refusing both malformed JSON and unbounded input. */
|
|
328
|
+
async function readBody(req) {
|
|
329
|
+
const chunks = [];
|
|
330
|
+
let bytes = 0;
|
|
331
|
+
for await (const chunk of req) {
|
|
332
|
+
const buffer = chunk;
|
|
333
|
+
bytes += buffer.length;
|
|
334
|
+
if (bytes > MAX_REQUEST_BYTES) return { kind: "too-large" };
|
|
335
|
+
chunks.push(buffer);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
return {
|
|
339
|
+
kind: "value",
|
|
340
|
+
value: JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
341
|
+
};
|
|
342
|
+
} catch {
|
|
343
|
+
return { kind: "invalid" };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
139
347
|
//#region src/settings-api.ts
|
|
140
348
|
/** Shared wire vocabulary for the plugin-owned settings RPC channel. */
|
|
141
349
|
/** Dedicated, loopback-only RPC channel registered by the Host half. */
|
|
@@ -164,8 +372,10 @@ const STREAM_SETTINGS_NS = "smooth-stream";
|
|
|
164
372
|
const DEFAULT_STREAM_SETTINGS = {
|
|
165
373
|
enabled: true,
|
|
166
374
|
controlScroll: true,
|
|
375
|
+
preset: "silky",
|
|
167
376
|
motionPreference: "auto",
|
|
168
377
|
thinkAutoExpand: true,
|
|
378
|
+
logarithmicFade: true,
|
|
169
379
|
debugEnabled: false,
|
|
170
380
|
debugTuning: {
|
|
171
381
|
revealScale: 1,
|
|
@@ -201,12 +411,18 @@ const Config = Schema.object({
|
|
|
201
411
|
const StreamSettingsSchema = Schema.object({
|
|
202
412
|
enabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.enabled),
|
|
203
413
|
controlScroll: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.controlScroll),
|
|
414
|
+
preset: Schema.union([
|
|
415
|
+
Schema.const("realtime"),
|
|
416
|
+
Schema.const("balanced"),
|
|
417
|
+
Schema.const("silky")
|
|
418
|
+
]).default(DEFAULT_STREAM_SETTINGS.preset),
|
|
204
419
|
motionPreference: Schema.union([
|
|
205
420
|
Schema.const("auto"),
|
|
206
421
|
Schema.const("force-smooth"),
|
|
207
422
|
Schema.const("force-reduced")
|
|
208
423
|
]).default(DEFAULT_STREAM_SETTINGS.motionPreference),
|
|
209
424
|
thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand),
|
|
425
|
+
logarithmicFade: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.logarithmicFade),
|
|
210
426
|
debugEnabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.debugEnabled),
|
|
211
427
|
debugTuning: Schema.object({
|
|
212
428
|
revealScale: Schema.number().min(.25).max(2).default(DEFAULT_STREAM_SETTINGS.debugTuning.revealScale),
|
|
@@ -234,8 +450,68 @@ function apply(ctx, config) {
|
|
|
234
450
|
httpCtx.effect(() => httpCtx.webServer.tapIndex((html) => injectStreamConfig(html, config)), "dsh-smooth-stream: boot config bridge");
|
|
235
451
|
});
|
|
236
452
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
453
|
+
if (typeof settingsCtx.settings.register !== "function") {
|
|
454
|
+
const view02 = () => ({
|
|
455
|
+
version: STREAM_PACKAGE_VERSION,
|
|
456
|
+
installation: inspectProfileInstallation(ctx.baseUrl, STREAM_PACKAGE_NAME).kind,
|
|
457
|
+
writable: false,
|
|
458
|
+
enabled: DEFAULT_STREAM_SETTINGS.enabled,
|
|
459
|
+
controlScroll: DEFAULT_STREAM_SETTINGS.controlScroll,
|
|
460
|
+
preset: config.preset,
|
|
461
|
+
motionPreference: DEFAULT_STREAM_SETTINGS.motionPreference,
|
|
462
|
+
thinkAutoExpand: DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
463
|
+
logarithmicFade: DEFAULT_STREAM_SETTINGS.logarithmicFade,
|
|
464
|
+
canUpgrade: false
|
|
465
|
+
});
|
|
466
|
+
const handle02 = async (endpoint) => {
|
|
467
|
+
if (endpoint === STREAM_SETTINGS_RPC.read) return {
|
|
468
|
+
ok: true,
|
|
469
|
+
value: view02()
|
|
470
|
+
};
|
|
471
|
+
if (endpoint === STREAM_SETTINGS_RPC.debugRead) return {
|
|
472
|
+
ok: true,
|
|
473
|
+
value: {
|
|
474
|
+
debugEnabled: DEFAULT_STREAM_SETTINGS.debugEnabled,
|
|
475
|
+
tuning: DEFAULT_STREAM_SETTINGS.debugTuning
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
if (endpoint === STREAM_SETTINGS_RPC.upgrade) {
|
|
479
|
+
if (inspectProfileInstallation(ctx.baseUrl, STREAM_PACKAGE_NAME).kind !== "npm") return {
|
|
480
|
+
ok: false,
|
|
481
|
+
error: {
|
|
482
|
+
code: "internal",
|
|
483
|
+
message: "smooth-stream is not an npm profile dependency",
|
|
484
|
+
details: {}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
ok: false,
|
|
489
|
+
error: {
|
|
490
|
+
code: "internal",
|
|
491
|
+
message: "upgrade is unavailable until the 0.2.x settings migration lands",
|
|
492
|
+
details: {}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
error: {
|
|
499
|
+
code: "settings-rejected",
|
|
500
|
+
message: "smooth-stream settings are read-only on dsh 0.2.x; durable storage moved to profile entry config",
|
|
501
|
+
details: { ns: STREAM_SETTINGS_NS }
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
506
|
+
registerSettingsChannel(webCtx, STREAM_SETTINGS_RPC_CHANNEL, handle02);
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
237
510
|
const settingsNamespace = STREAM_SETTINGS_NS;
|
|
238
|
-
const scope = settingsCtx.settings.register(settingsNamespace, StreamSettingsSchema, {
|
|
511
|
+
const scope = settingsCtx.settings.register(settingsNamespace, StreamSettingsSchema, {
|
|
512
|
+
base: { preset: config.preset },
|
|
513
|
+
applies: "live"
|
|
514
|
+
});
|
|
239
515
|
settingsCtx.inject(["connection"], (connectionCtx) => {
|
|
240
516
|
let upgrade;
|
|
241
517
|
const view = () => {
|
|
@@ -247,8 +523,10 @@ function apply(ctx, config) {
|
|
|
247
523
|
writable: connectionCtx.settings.writable,
|
|
248
524
|
enabled: settings.enabled,
|
|
249
525
|
controlScroll: settings.controlScroll,
|
|
526
|
+
preset: settings.preset ?? config.preset,
|
|
250
527
|
motionPreference: settings.motionPreference,
|
|
251
528
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
529
|
+
logarithmicFade: settings.logarithmicFade,
|
|
252
530
|
canUpgrade: installation.kind === "npm"
|
|
253
531
|
};
|
|
254
532
|
};
|
|
@@ -288,6 +566,14 @@ function apply(ctx, config) {
|
|
|
288
566
|
};
|
|
289
567
|
try {
|
|
290
568
|
const next = payload;
|
|
569
|
+
if (next.preset !== void 0 && next.preset !== "realtime" && next.preset !== "balanced" && next.preset !== "silky") return {
|
|
570
|
+
ok: false,
|
|
571
|
+
error: {
|
|
572
|
+
code: "settings-rejected",
|
|
573
|
+
message: "preset must be one of realtime | balanced | silky",
|
|
574
|
+
details: { ns: STREAM_SETTINGS_NS }
|
|
575
|
+
}
|
|
576
|
+
};
|
|
291
577
|
if (next.motionPreference !== void 0 && next.motionPreference !== "auto" && next.motionPreference !== "force-smooth" && next.motionPreference !== "force-reduced") return {
|
|
292
578
|
ok: false,
|
|
293
579
|
error: {
|
|
@@ -296,6 +582,14 @@ function apply(ctx, config) {
|
|
|
296
582
|
details: { ns: STREAM_SETTINGS_NS }
|
|
297
583
|
}
|
|
298
584
|
};
|
|
585
|
+
if (next.logarithmicFade !== void 0 && typeof next.logarithmicFade !== "boolean") return {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: {
|
|
588
|
+
code: "settings-rejected",
|
|
589
|
+
message: "logarithmicFade must be a boolean",
|
|
590
|
+
details: { ns: STREAM_SETTINGS_NS }
|
|
591
|
+
}
|
|
592
|
+
};
|
|
299
593
|
const hasDebug = next.debugEnabled !== void 0 || next.debugTuning !== void 0;
|
|
300
594
|
if (hasDebug && (typeof next.debugEnabled !== "boolean" || !validDebugTuning(next.debugTuning))) return {
|
|
301
595
|
ok: false,
|
|
@@ -308,8 +602,10 @@ function apply(ctx, config) {
|
|
|
308
602
|
await scope.update({
|
|
309
603
|
enabled: next.enabled,
|
|
310
604
|
controlScroll: next.controlScroll,
|
|
605
|
+
...next.preset === void 0 ? {} : { preset: next.preset },
|
|
311
606
|
...next.motionPreference === void 0 ? {} : { motionPreference: next.motionPreference },
|
|
312
607
|
thinkAutoExpand: next.thinkAutoExpand,
|
|
608
|
+
...next.logarithmicFade === void 0 ? {} : { logarithmicFade: next.logarithmicFade },
|
|
313
609
|
...hasDebug ? {
|
|
314
610
|
debugEnabled: next.debugEnabled,
|
|
315
611
|
debugTuning: next.debugTuning
|
|
@@ -427,7 +723,7 @@ function apply(ctx, config) {
|
|
|
427
723
|
}
|
|
428
724
|
};
|
|
429
725
|
};
|
|
430
|
-
connectionCtx
|
|
726
|
+
registerSettingsChannel(connectionCtx, STREAM_SETTINGS_RPC_CHANNEL, handle);
|
|
431
727
|
});
|
|
432
728
|
});
|
|
433
729
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-smooth-stream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
.surface[data-entrance='active'] {
|
|
12
12
|
animation: dsh-smooth-stream-agent-row-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
13
|
-
will-change: opacity
|
|
13
|
+
will-change: opacity;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
@keyframes dsh-smooth-stream-agent-row-in {
|
|
17
17
|
0% {
|
|
18
18
|
opacity: 0;
|
|
19
|
-
transform: translate3d(0, 4px, 0);
|
|
20
19
|
clip-path: inset(0 100% 0 0);
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -27,7 +26,6 @@
|
|
|
27
26
|
|
|
28
27
|
100% {
|
|
29
28
|
opacity: 1;
|
|
30
|
-
transform: translate3d(0, 0, 0);
|
|
31
29
|
clip-path: inset(0 0 0 0);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
@@ -36,7 +34,6 @@
|
|
|
36
34
|
.surface[data-entrance='active'] {
|
|
37
35
|
animation: none;
|
|
38
36
|
opacity: 1;
|
|
39
|
-
transform: none;
|
|
40
37
|
clip-path: none;
|
|
41
38
|
will-change: auto;
|
|
42
39
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// row attribute and the Think chrome keep working unchanged.
|
|
9
9
|
|
|
10
10
|
import { type KeyboardEvent, type ReactNode } from 'react'
|
|
11
|
-
import {
|
|
11
|
+
import { IconChevronDown } from './harnessIcons.ts'
|
|
12
12
|
import css from './TypewriterAssistantNodeView.module.css'
|
|
13
13
|
|
|
14
14
|
/** Class-name join for optional overlay classes over the chrome defaults. */
|
|
@@ -74,11 +74,11 @@ export function AnimatedDisclosure({
|
|
|
74
74
|
>
|
|
75
75
|
<span className={cx(css.disclosureLeading, leadingClassName)}>
|
|
76
76
|
{open
|
|
77
|
-
? <
|
|
77
|
+
? <IconChevronDown className={chevronClassName} />
|
|
78
78
|
: (
|
|
79
79
|
<>
|
|
80
80
|
<span className={css.disclosureIconIdle}>{icon}</span>
|
|
81
|
-
<
|
|
81
|
+
<IconChevronDown className={cx(chevronClassName, css.disclosureChevronHover)} />
|
|
82
82
|
</>
|
|
83
83
|
)}
|
|
84
84
|
</span>
|
|
@@ -2,20 +2,26 @@ import { useEffect, useState } from 'react'
|
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
4
4
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
IconCodeOutline16,
|
|
8
|
-
IconCopyOutline16,
|
|
9
|
-
IconQuestionOutline14,
|
|
10
|
-
IconRefreshOutline16,
|
|
11
|
-
Tooltip,
|
|
12
|
-
writeClipboard,
|
|
13
|
-
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
|
+
import { Tooltip, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
6
|
+
import { IconClose, IconCode, IconCopy, IconQuestion, IconRefreshSmall } from './harnessIcons.ts'
|
|
14
7
|
import type { StreamDebugTuning } from '../settings.ts'
|
|
15
|
-
import type { DebugPanelFace, DebugRuntimeState } from './debugRuntime.ts'
|
|
8
|
+
import type { DebugPanelFace, DebugRuntimeState, FollowTerminalPhase } from './debugRuntime.ts'
|
|
16
9
|
import type { NS } from './locales.ts'
|
|
17
10
|
import css from './DebugPanel.module.css'
|
|
18
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Locale key per terminal-follow phase. `terminal-drain` and `host-cascade`
|
|
14
|
+
* are the two that must never be confused: the first is the engine finishing
|
|
15
|
+
* ordinary text, the second is a hostile host commit that needs temporary
|
|
16
|
+
* credit.
|
|
17
|
+
*/
|
|
18
|
+
const PHASE_LABEL: Record<FollowTerminalPhase, Parameters<PropsLocale<typeof NS>['t']>[0]> = {
|
|
19
|
+
'live': 'debugPhaseLive',
|
|
20
|
+
'terminal-drain': 'debugPhaseDrain',
|
|
21
|
+
'host-cascade': 'debugPhaseCascade',
|
|
22
|
+
'natural': 'debugPhaseNatural',
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
export type DebugPanelProps =
|
|
20
26
|
PropsRuntime<'conversation.session.header.utilities'>
|
|
21
27
|
& PropsLocale<typeof NS>
|
|
@@ -93,7 +99,7 @@ function TuningField({
|
|
|
93
99
|
aria-label={label}
|
|
94
100
|
title={t(control.tip)}
|
|
95
101
|
>
|
|
96
|
-
<
|
|
102
|
+
<IconQuestion />
|
|
97
103
|
</button>
|
|
98
104
|
</Tooltip>
|
|
99
105
|
</span>
|
|
@@ -165,7 +171,7 @@ export function DebugPanel(props: DebugPanelProps) {
|
|
|
165
171
|
<span className={css.state}>{t(live ? 'debugLive' : 'debugIdle')}</span>
|
|
166
172
|
{state.dirty ? <span className={css.unsaved}>{t('debugUnsaved')}</span> : null}
|
|
167
173
|
<button className={css.iconButton} type="button" title={t('debugCopy')} aria-label={t('debugCopy')} onClick={() => { void copyDiagnostics() }}>
|
|
168
|
-
<
|
|
174
|
+
<IconCopy />
|
|
169
175
|
</button>
|
|
170
176
|
<button
|
|
171
177
|
className={css.iconButton}
|
|
@@ -179,7 +185,7 @@ export function DebugPanel(props: DebugPanelProps) {
|
|
|
179
185
|
props.save()
|
|
180
186
|
}}
|
|
181
187
|
>
|
|
182
|
-
<
|
|
188
|
+
<IconClose />
|
|
183
189
|
</button>
|
|
184
190
|
<span className={css.visuallyHidden} aria-live="polite">{copied ? t('debugCopied') : ''}</span>
|
|
185
191
|
</header>
|
|
@@ -199,6 +205,19 @@ export function DebugPanel(props: DebugPanelProps) {
|
|
|
199
205
|
<Metric label={t('debugVelocity')} value={`${fixed(metrics.followVelocityPxPerSec, 0)} px/s`} />
|
|
200
206
|
<Metric label={t('debugReserve')} value={`${fixed(metrics.followReservePx)} px`} />
|
|
201
207
|
<Metric label={t('debugCapacity')} value={`${fixed(metrics.followCapacityPx)} px`} />
|
|
208
|
+
<Metric
|
|
209
|
+
label={t('debugTerminalPhase')}
|
|
210
|
+
value={t(PHASE_LABEL[metrics.followTerminalPhase])}
|
|
211
|
+
tone={metrics.followTerminalPhase === 'host-cascade' ? 'warn' : undefined}
|
|
212
|
+
/>
|
|
213
|
+
<Metric label={t('debugOwnedRunway')} value={`${fixed(metrics.followRunwayPx)} px`} />
|
|
214
|
+
<Metric label={t('debugTerminalBudget')} value={`${fixed(metrics.followTerminalBudgetPx)} px`} />
|
|
215
|
+
<Metric label={t('debugBaselineShift')} value={`${fixed(metrics.followBaselineShiftPx)} px`} />
|
|
216
|
+
<Metric
|
|
217
|
+
label={t('debugAnchorDelta')}
|
|
218
|
+
value={metrics.followAnchorDeltaPx === null ? '—' : `${fixed(metrics.followAnchorDeltaPx)} px`}
|
|
219
|
+
tone={(metrics.followAnchorDeltaPx ?? 0) > 0.35 ? 'warn' : undefined}
|
|
220
|
+
/>
|
|
202
221
|
<Metric label={t('debugAppliedScale')} value={`${fixed(metrics.followRevealScale, 2)}x`} />
|
|
203
222
|
</dl>
|
|
204
223
|
</section>
|
|
@@ -220,7 +239,7 @@ export function DebugPanel(props: DebugPanelProps) {
|
|
|
220
239
|
|
|
221
240
|
<footer className={css.footer}>
|
|
222
241
|
<button className={css.secondaryButton} type="button" disabled={!state.writable} onClick={props.reset}>
|
|
223
|
-
<
|
|
242
|
+
<IconRefreshSmall />
|
|
224
243
|
{t('debugReset')}
|
|
225
244
|
</button>
|
|
226
245
|
<span className={css.footerSpacer} />
|
|
@@ -240,7 +259,7 @@ export function DebugPanel(props: DebugPanelProps) {
|
|
|
240
259
|
title={t('debugPanelToggle')}
|
|
241
260
|
onClick={() => { setOpen(current => !current) }}
|
|
242
261
|
>
|
|
243
|
-
<
|
|
262
|
+
<IconCode />
|
|
244
263
|
</button>
|
|
245
264
|
{typeof document === 'undefined' || panel === null ? null : createPortal(panel, document.body)}
|
|
246
265
|
</>
|