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/README.en.md +14 -4
- package/README.md +14 -4
- package/lib/client.js +2111 -140
- package/lib/client.js.map +1 -1
- package/lib/index.js +123 -6
- package/package.json +9 -9
- package/src/client/DebugPanel.module.css +370 -0
- package/src/client/DebugPanel.tsx +248 -0
- package/src/client/SmoothStreamCard.module.css +8 -0
- package/src/client/SmoothStreamCard.tsx +42 -3
- package/src/client/auto-collapse-controller.ts +977 -0
- package/src/client/debugRuntime.ts +355 -0
- package/src/client/index.ts +121 -30
- package/src/client/locales.ts +122 -0
- package/src/client/smooth-stream-card-controller.ts +97 -14
- package/src/client/smooth-stream-settings-api.ts +53 -3
- package/src/client/teleprompterGlide.ts +97 -31
- package/src/client/useFpsGuard.ts +7 -0
- package/src/client/useProgressiveDomText.ts +99 -17
- package/src/client/useSmoothStreamContent.ts +20 -4
- package/src/plugin.ts +137 -7
- package/src/settings-api.ts +12 -0
- package/src/settings.ts +55 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared browser-side diagnostics state.
|
|
3
|
+
*
|
|
4
|
+
* The settings card owns persistence. This module is the live bridge used by
|
|
5
|
+
* the renderer and the chat-side panel: renderer loops publish measurements,
|
|
6
|
+
* while panel edits are staged through the settings-card controller.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_STREAM_DEBUG_TUNING,
|
|
12
|
+
type StreamDebugTuning,
|
|
13
|
+
type StreamSettings,
|
|
14
|
+
} from '../settings.ts'
|
|
15
|
+
|
|
16
|
+
export interface DebugMetrics {
|
|
17
|
+
fps: number | null
|
|
18
|
+
frameMs: number | null
|
|
19
|
+
fpsDegraded: boolean
|
|
20
|
+
streamActive: boolean
|
|
21
|
+
streamBacklog: number
|
|
22
|
+
streamSpeedCps: number
|
|
23
|
+
streamTargetChars: number
|
|
24
|
+
streamDisplayedChars: number
|
|
25
|
+
followActive: boolean
|
|
26
|
+
followLagPx: number
|
|
27
|
+
followVelocityPxPerSec: number
|
|
28
|
+
followReservePx: number
|
|
29
|
+
followCapacityPx: number
|
|
30
|
+
followRevealScale: number
|
|
31
|
+
followFollowing: boolean
|
|
32
|
+
followConstrained: boolean
|
|
33
|
+
scrollTop: number | null
|
|
34
|
+
scrollHeight: number | null
|
|
35
|
+
clientHeight: number | null
|
|
36
|
+
lastUpdatedMs: number | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DebugRuntimeState {
|
|
40
|
+
available: boolean
|
|
41
|
+
enabled: boolean
|
|
42
|
+
writable: boolean
|
|
43
|
+
dirty: boolean
|
|
44
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
45
|
+
tuning: StreamDebugTuning
|
|
46
|
+
metrics: DebugMetrics
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DebugSettingsPatch = Partial<Pick<StreamSettings, 'debugEnabled' | 'debugTuning'>>
|
|
50
|
+
|
|
51
|
+
export interface DebugPanelFace {
|
|
52
|
+
hooks: {
|
|
53
|
+
debugRuntime: SnapshotStore<DebugRuntimeState>
|
|
54
|
+
}
|
|
55
|
+
edit: (patch: DebugSettingsPatch) => void
|
|
56
|
+
save: () => void
|
|
57
|
+
discard: () => void
|
|
58
|
+
reset: () => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DebugSettingsActions {
|
|
62
|
+
edit: (patch: DebugSettingsPatch) => void
|
|
63
|
+
save: () => void
|
|
64
|
+
discard: () => void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface StreamMetric {
|
|
68
|
+
backlog: number
|
|
69
|
+
speedCps: number
|
|
70
|
+
targetChars: number
|
|
71
|
+
displayedChars: number
|
|
72
|
+
active: boolean
|
|
73
|
+
updatedAt: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface FollowMetric {
|
|
77
|
+
lagPx: number
|
|
78
|
+
velocityPxPerSec: number
|
|
79
|
+
reservePx: number
|
|
80
|
+
capacityPx: number
|
|
81
|
+
revealScale: number
|
|
82
|
+
following: boolean
|
|
83
|
+
constrained: boolean
|
|
84
|
+
scrollTop: number
|
|
85
|
+
scrollHeight: number
|
|
86
|
+
clientHeight: number
|
|
87
|
+
active: boolean
|
|
88
|
+
updatedAt: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const EMPTY_METRICS: DebugMetrics = {
|
|
92
|
+
fps: null,
|
|
93
|
+
frameMs: null,
|
|
94
|
+
fpsDegraded: false,
|
|
95
|
+
streamActive: false,
|
|
96
|
+
streamBacklog: 0,
|
|
97
|
+
streamSpeedCps: 0,
|
|
98
|
+
streamTargetChars: 0,
|
|
99
|
+
streamDisplayedChars: 0,
|
|
100
|
+
followActive: false,
|
|
101
|
+
followLagPx: 0,
|
|
102
|
+
followVelocityPxPerSec: 0,
|
|
103
|
+
followReservePx: 0,
|
|
104
|
+
followCapacityPx: 0,
|
|
105
|
+
followRevealScale: 1,
|
|
106
|
+
followFollowing: false,
|
|
107
|
+
followConstrained: false,
|
|
108
|
+
scrollTop: null,
|
|
109
|
+
scrollHeight: null,
|
|
110
|
+
clientHeight: null,
|
|
111
|
+
lastUpdatedMs: null,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const INITIAL_STATE: DebugRuntimeState = {
|
|
115
|
+
available: false,
|
|
116
|
+
enabled: false,
|
|
117
|
+
writable: false,
|
|
118
|
+
dirty: false,
|
|
119
|
+
status: 'loading',
|
|
120
|
+
tuning: { ...DEFAULT_STREAM_DEBUG_TUNING },
|
|
121
|
+
metrics: EMPTY_METRICS,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const store = createSnapshotStore<DebugRuntimeState>(INITIAL_STATE)
|
|
125
|
+
const streamMetrics = new Map<string, StreamMetric>()
|
|
126
|
+
const followMetrics = new Map<HTMLElement, FollowMetric>()
|
|
127
|
+
let actions: DebugSettingsActions | undefined
|
|
128
|
+
let lastMetricPublish = 0
|
|
129
|
+
|
|
130
|
+
function resetMetrics(): void {
|
|
131
|
+
streamMetrics.clear()
|
|
132
|
+
followMetrics.clear()
|
|
133
|
+
lastMetricPublish = 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resetRuntime(): void {
|
|
137
|
+
actions = undefined
|
|
138
|
+
resetMetrics()
|
|
139
|
+
store.set({ ...INITIAL_STATE, metrics: EMPTY_METRICS })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function now(): number {
|
|
143
|
+
return typeof performance === 'undefined' ? Date.now() : performance.now()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sameTuning(left: StreamDebugTuning, right: StreamDebugTuning): boolean {
|
|
147
|
+
return left.revealScale === right.revealScale
|
|
148
|
+
&& left.queuePressure === right.queuePressure
|
|
149
|
+
&& left.maxRevealCps === right.maxRevealCps
|
|
150
|
+
&& left.springStiffness === right.springStiffness
|
|
151
|
+
&& left.springDamping === right.springDamping
|
|
152
|
+
&& left.springMass === right.springMass
|
|
153
|
+
&& left.runwayPx === right.runwayPx
|
|
154
|
+
&& left.reserveResponseMs === right.reserveResponseMs
|
|
155
|
+
&& left.backpressureMinScale === right.backpressureMinScale
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function currentMetrics(timestamp: number): DebugMetrics {
|
|
159
|
+
let stream: StreamMetric | undefined
|
|
160
|
+
for (const candidate of streamMetrics.values()) {
|
|
161
|
+
if (stream === undefined || candidate.updatedAt > stream.updatedAt) stream = candidate
|
|
162
|
+
}
|
|
163
|
+
let follow: FollowMetric | undefined
|
|
164
|
+
for (const candidate of followMetrics.values()) {
|
|
165
|
+
if (follow === undefined || candidate.updatedAt > follow.updatedAt) follow = candidate
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
fps: store.getSnapshot().metrics.fps,
|
|
169
|
+
frameMs: store.getSnapshot().metrics.frameMs,
|
|
170
|
+
fpsDegraded: store.getSnapshot().metrics.fpsDegraded,
|
|
171
|
+
streamActive: stream?.active ?? false,
|
|
172
|
+
streamBacklog: stream?.backlog ?? 0,
|
|
173
|
+
streamSpeedCps: stream?.speedCps ?? 0,
|
|
174
|
+
streamTargetChars: stream?.targetChars ?? 0,
|
|
175
|
+
streamDisplayedChars: stream?.displayedChars ?? 0,
|
|
176
|
+
followActive: follow?.active ?? false,
|
|
177
|
+
followLagPx: follow?.lagPx ?? 0,
|
|
178
|
+
followVelocityPxPerSec: follow?.velocityPxPerSec ?? 0,
|
|
179
|
+
followReservePx: follow?.reservePx ?? 0,
|
|
180
|
+
followCapacityPx: follow?.capacityPx ?? 0,
|
|
181
|
+
followRevealScale: follow?.revealScale ?? 1,
|
|
182
|
+
followFollowing: follow?.following ?? false,
|
|
183
|
+
followConstrained: follow?.constrained ?? false,
|
|
184
|
+
scrollTop: follow?.scrollTop ?? null,
|
|
185
|
+
scrollHeight: follow?.scrollHeight ?? null,
|
|
186
|
+
clientHeight: follow?.clientHeight ?? null,
|
|
187
|
+
lastUpdatedMs: timestamp,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function publishMetrics(force = false): void {
|
|
192
|
+
const timestamp = now()
|
|
193
|
+
if (!force && timestamp - lastMetricPublish < 80) return
|
|
194
|
+
lastMetricPublish = timestamp
|
|
195
|
+
store.set({ ...store.getSnapshot(), metrics: currentMetrics(timestamp) })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const debugRuntime = {
|
|
199
|
+
store,
|
|
200
|
+
|
|
201
|
+
getSnapshot(): DebugRuntimeState {
|
|
202
|
+
return store.getSnapshot()
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
subscribe(listener: () => void): () => void {
|
|
206
|
+
return store.subscribe(listener)
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/** Whether hot-path instrumentation should do any work. */
|
|
210
|
+
isEnabled(): boolean {
|
|
211
|
+
return store.getSnapshot().enabled
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
tuning(): StreamDebugTuning {
|
|
215
|
+
return store.getSnapshot().tuning
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/** Production values remain untouched until the user explicitly enables diagnostics. */
|
|
219
|
+
activeTuning(): StreamDebugTuning {
|
|
220
|
+
return store.getSnapshot().enabled ? store.getSnapshot().tuning : DEFAULT_STREAM_DEBUG_TUNING
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
bindSettings(nextActions: DebugSettingsActions | undefined): () => void {
|
|
224
|
+
actions = nextActions
|
|
225
|
+
return () => {
|
|
226
|
+
if (actions !== nextActions) return
|
|
227
|
+
resetRuntime()
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
syncSettings(input: {
|
|
232
|
+
available?: boolean
|
|
233
|
+
enabled: boolean
|
|
234
|
+
writable: boolean
|
|
235
|
+
dirty: boolean
|
|
236
|
+
status: DebugRuntimeState['status']
|
|
237
|
+
tuning?: StreamDebugTuning
|
|
238
|
+
}): void {
|
|
239
|
+
const current = store.getSnapshot()
|
|
240
|
+
// Callers predating the Host capability flag are local diagnostics tests
|
|
241
|
+
// and renderer probes; preserve their enabled behavior unless the wiring
|
|
242
|
+
// explicitly reports that the RPC is unavailable.
|
|
243
|
+
const available = input.available ?? true
|
|
244
|
+
const enabled = available && input.enabled
|
|
245
|
+
const availabilityChanged = current.available !== available
|
|
246
|
+
const enabledChanged = current.enabled !== enabled
|
|
247
|
+
if (availabilityChanged || enabledChanged) resetMetrics()
|
|
248
|
+
const tuning = input.tuning === undefined
|
|
249
|
+
? current.tuning
|
|
250
|
+
: { ...DEFAULT_STREAM_DEBUG_TUNING, ...input.tuning }
|
|
251
|
+
if (
|
|
252
|
+
current.available === available
|
|
253
|
+
&& current.enabled === enabled
|
|
254
|
+
&& current.writable === input.writable
|
|
255
|
+
&& current.dirty === input.dirty
|
|
256
|
+
&& current.status === input.status
|
|
257
|
+
&& sameTuning(current.tuning, tuning)
|
|
258
|
+
) return
|
|
259
|
+
store.set({
|
|
260
|
+
...current,
|
|
261
|
+
available,
|
|
262
|
+
enabled,
|
|
263
|
+
writable: input.writable,
|
|
264
|
+
dirty: input.dirty,
|
|
265
|
+
status: input.status,
|
|
266
|
+
tuning,
|
|
267
|
+
metrics: availabilityChanged || enabledChanged || !enabled ? EMPTY_METRICS : current.metrics,
|
|
268
|
+
})
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
edit(patch: DebugSettingsPatch): void {
|
|
272
|
+
const current = store.getSnapshot()
|
|
273
|
+
const enabled = patch.debugEnabled ?? current.enabled
|
|
274
|
+
const enabledChanged = current.enabled !== enabled
|
|
275
|
+
if (enabledChanged) resetMetrics()
|
|
276
|
+
const tuning = patch.debugTuning === undefined
|
|
277
|
+
? current.tuning
|
|
278
|
+
: { ...current.tuning, ...patch.debugTuning }
|
|
279
|
+
store.set({
|
|
280
|
+
...current,
|
|
281
|
+
enabled,
|
|
282
|
+
dirty: true,
|
|
283
|
+
tuning,
|
|
284
|
+
metrics: enabledChanged || !enabled ? EMPTY_METRICS : current.metrics,
|
|
285
|
+
})
|
|
286
|
+
actions?.edit({
|
|
287
|
+
...(patch.debugEnabled === undefined ? {} : { debugEnabled: patch.debugEnabled }),
|
|
288
|
+
...(patch.debugTuning === undefined ? {} : { debugTuning: tuning }),
|
|
289
|
+
})
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
save(): void {
|
|
293
|
+
actions?.save()
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
discard(): void {
|
|
297
|
+
actions?.discard()
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
reset(): void {
|
|
301
|
+
this.edit({ debugTuning: { ...DEFAULT_STREAM_DEBUG_TUNING } })
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
reportStream(id: string, metric: Omit<StreamMetric, 'updatedAt'> | null): void {
|
|
305
|
+
if (!this.isEnabled()) return
|
|
306
|
+
if (metric === null) streamMetrics.delete(id)
|
|
307
|
+
else streamMetrics.set(id, { ...metric, updatedAt: now() })
|
|
308
|
+
publishMetrics(metric === null)
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
reportFollow(port: HTMLElement, metric: Omit<FollowMetric, 'updatedAt'> | null): void {
|
|
312
|
+
if (!this.isEnabled()) return
|
|
313
|
+
if (metric === null) followMetrics.delete(port)
|
|
314
|
+
else followMetrics.set(port, { ...metric, updatedAt: now() })
|
|
315
|
+
publishMetrics(metric === null)
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
reportFps(fps: number, frameMs: number, degraded: boolean): void {
|
|
319
|
+
if (!this.isEnabled()) return
|
|
320
|
+
const current = store.getSnapshot()
|
|
321
|
+
store.set({
|
|
322
|
+
...current,
|
|
323
|
+
metrics: { ...current.metrics, fps, frameMs, fpsDegraded: degraded, lastUpdatedMs: now() },
|
|
324
|
+
})
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
clearFps(): void {
|
|
328
|
+
if (!this.isEnabled()) return
|
|
329
|
+
const current = store.getSnapshot()
|
|
330
|
+
store.set({
|
|
331
|
+
...current,
|
|
332
|
+
metrics: {
|
|
333
|
+
...current.metrics,
|
|
334
|
+
fps: null,
|
|
335
|
+
frameMs: null,
|
|
336
|
+
fpsDegraded: false,
|
|
337
|
+
lastUpdatedMs: now(),
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
panelFace(): DebugPanelFace {
|
|
343
|
+
return {
|
|
344
|
+
hooks: { debugRuntime: store },
|
|
345
|
+
edit: patch => { this.edit(patch) },
|
|
346
|
+
save: () => { this.save() },
|
|
347
|
+
discard: () => { this.discard() },
|
|
348
|
+
reset: () => { this.reset() },
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
resetRuntime(): void {
|
|
353
|
+
resetRuntime()
|
|
354
|
+
},
|
|
355
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -7,13 +7,16 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client
|
|
|
7
7
|
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
8
8
|
import type { ChatNodeViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
9
9
|
import { TypewriterAssistantNodeView } from './TypewriterAssistantNodeView.tsx'
|
|
10
|
+
import { AutoCollapseController } from './auto-collapse-controller.ts'
|
|
10
11
|
import { wrapFollowNodeView, type FollowWrapProps } from './TypewriterToolNodeView.tsx'
|
|
11
12
|
import { SmoothStreamCard } from './SmoothStreamCard.tsx'
|
|
12
13
|
import { SmoothStreamCardController } from './smooth-stream-card-controller.ts'
|
|
13
14
|
import { createSmoothStreamSettingsApi } from './smooth-stream-settings-api.ts'
|
|
15
|
+
import { DebugPanel } from './DebugPanel.tsx'
|
|
16
|
+
import { debugRuntime } from './debugRuntime.ts'
|
|
14
17
|
import { NS as SETTINGS_NS, en, zh } from './locales.ts'
|
|
15
18
|
import { DEFAULT_STREAM_CONFIG, STREAM_BOOT_GLOBAL, type StreamConfig } from '../config.ts'
|
|
16
|
-
import { DEFAULT_STREAM_SETTINGS } from '../settings.ts'
|
|
19
|
+
import { DEFAULT_STREAM_SETTINGS, STREAM_SETTINGS_NS, type StreamSettings } from '../settings.ts'
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Cordis services required by the browser half. Only `slots` is load-bearing
|
|
@@ -116,15 +119,15 @@ function wrapAgentChatRows(ctx: ClientContext): () => void {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
|
-
* A live
|
|
120
|
-
* shared
|
|
121
|
-
*
|
|
122
|
-
* ships no settings surface keeps the cell on the default.
|
|
122
|
+
* A live settings cell shared by the renderer lifecycle and React views. It
|
|
123
|
+
* starts on the shared defaults and follows the plugin-owned controller once
|
|
124
|
+
* the optional settings services arrive.
|
|
123
125
|
*/
|
|
124
|
-
class
|
|
126
|
+
class SettingsCell {
|
|
125
127
|
private readonly listeners = new Set<() => void>()
|
|
126
128
|
private card: SmoothStreamCardController | undefined
|
|
127
|
-
private value = DEFAULT_STREAM_SETTINGS
|
|
129
|
+
private value: StreamSettings = DEFAULT_STREAM_SETTINGS
|
|
130
|
+
private pending = false
|
|
128
131
|
|
|
129
132
|
/** Re-point the cell at the plugin-owned settings controller. */
|
|
130
133
|
attach(card: SmoothStreamCardController): () => void {
|
|
@@ -139,18 +142,39 @@ class PreferenceCell {
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
private read():
|
|
143
|
-
|
|
145
|
+
private read(): StreamSettings {
|
|
146
|
+
const snapshot = this.card?.getSnapshot()
|
|
147
|
+
if (snapshot === undefined || snapshot.status !== 'ready') return this.value
|
|
148
|
+
return this.card?.values() ?? this.value
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
private refresh(): void {
|
|
147
152
|
const next = this.read()
|
|
148
|
-
|
|
153
|
+
const pending = this.card?.getSnapshot().status === 'loading'
|
|
154
|
+
if (
|
|
155
|
+
pending === this.pending
|
|
156
|
+
&& next.enabled === this.value.enabled
|
|
157
|
+
&& next.thinkAutoExpand === this.value.thinkAutoExpand
|
|
158
|
+
&& next.autoCollapse === this.value.autoCollapse
|
|
159
|
+
&& next.debugEnabled === this.value.debugEnabled
|
|
160
|
+
&& next.debugTuning === this.value.debugTuning
|
|
161
|
+
) return
|
|
162
|
+
this.pending = pending
|
|
149
163
|
this.value = next
|
|
150
164
|
for (const listener of this.listeners) listener()
|
|
151
165
|
}
|
|
152
166
|
|
|
153
|
-
|
|
167
|
+
/** False while an available settings service is resolving its authority. */
|
|
168
|
+
takeoverEnabled(): boolean {
|
|
169
|
+
return !this.pending && this.value.enabled
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Whether finished turns should fold behind a summary row right now. */
|
|
173
|
+
autoCollapseActive(): boolean {
|
|
174
|
+
return !this.pending && this.value.autoCollapse
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
readonly getSnapshot = (): StreamSettings => this.value
|
|
154
178
|
|
|
155
179
|
readonly subscribe = (listener: () => void): () => void => {
|
|
156
180
|
this.listeners.add(listener)
|
|
@@ -171,7 +195,26 @@ class PreferenceCell {
|
|
|
171
195
|
*/
|
|
172
196
|
export function apply(ctx: ClientContext): void {
|
|
173
197
|
const config = readBootConfig()
|
|
174
|
-
const
|
|
198
|
+
const settings = new SettingsCell()
|
|
199
|
+
|
|
200
|
+
// Auto-collapse works at the DOM level and is independent of the renderer
|
|
201
|
+
// takeover: it also folds finished turns when the built-in Harness renderer
|
|
202
|
+
// owns the conversation. The lifecycle rides the guaranteed `slots` service
|
|
203
|
+
// (this entry's own inject list) and follows the same live settings cell, so
|
|
204
|
+
// flipping the preference restores every folded row immediately.
|
|
205
|
+
ctx.inject(['slots'], () => {
|
|
206
|
+
const autoCollapse = new AutoCollapseController()
|
|
207
|
+
const syncAutoCollapse = (): void => {
|
|
208
|
+
if (settings.autoCollapseActive()) autoCollapse.start()
|
|
209
|
+
else autoCollapse.stop()
|
|
210
|
+
}
|
|
211
|
+
const unsubscribe = settings.subscribe(syncAutoCollapse)
|
|
212
|
+
syncAutoCollapse()
|
|
213
|
+
return () => {
|
|
214
|
+
unsubscribe()
|
|
215
|
+
autoCollapse.stop()
|
|
216
|
+
}
|
|
217
|
+
})
|
|
175
218
|
|
|
176
219
|
// The card talks to the plugin-owned loopback RPC, so the core settings
|
|
177
220
|
// namespace allowlist cannot make it disappear. The stream still applies
|
|
@@ -183,27 +226,57 @@ export function apply(ctx: ClientContext): void {
|
|
|
183
226
|
// handle, so narrow through unknown to its client contract here.
|
|
184
227
|
createSmoothStreamSettingsApi(settingsCtx.get('connection') as unknown as ConnectionHandle),
|
|
185
228
|
)
|
|
186
|
-
const
|
|
229
|
+
const detachSettings = settings.attach(card)
|
|
230
|
+
const syncDebug = (): void => {
|
|
231
|
+
const snapshot = card.getSnapshot()
|
|
232
|
+
debugRuntime.syncSettings({
|
|
233
|
+
available: snapshot.debugAvailable,
|
|
234
|
+
enabled: snapshot.debugEnabled,
|
|
235
|
+
writable: snapshot.writable && !snapshot.saving,
|
|
236
|
+
dirty: snapshot.dirty,
|
|
237
|
+
status: snapshot.status,
|
|
238
|
+
tuning: snapshot.debugTuning,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
const detachBinding = debugRuntime.bindSettings({
|
|
242
|
+
edit: patch => { card.inject().edit(patch) },
|
|
243
|
+
save: () => { card.inject().save() },
|
|
244
|
+
discard: () => { card.inject().discard() },
|
|
245
|
+
})
|
|
246
|
+
const detachDebug = card.subscribe(syncDebug)
|
|
247
|
+
syncDebug()
|
|
187
248
|
card.start()
|
|
188
249
|
settingsCtx.effect(() => settingsCtx.locale.register(SETTINGS_NS, { zh, en }), 'dsh-smooth-stream: settings dictionaries')
|
|
189
250
|
settingsCtx.slots.inject('settings.plugin.item', () => settingsCtx.slots.register({
|
|
190
251
|
name: 'settings.plugin.item',
|
|
191
252
|
id: 'smooth-stream',
|
|
253
|
+
key: STREAM_SETTINGS_NS,
|
|
192
254
|
order: 30,
|
|
193
255
|
locale: SETTINGS_NS,
|
|
194
256
|
inject: () => card.inject(),
|
|
195
|
-
|
|
257
|
+
// Older Harness declares this slot as a list (`id`); newer Harness uses a
|
|
258
|
+
// keyed slot filtered by the Host settings namespace (`key`).
|
|
259
|
+
} as never, SmoothStreamCard))
|
|
260
|
+
settingsCtx.slots.inject('conversation.session.header.utilities', () => settingsCtx.slots.register({
|
|
261
|
+
name: 'conversation.session.header.utilities',
|
|
262
|
+
id: 'smooth-stream-debug',
|
|
263
|
+
order: 40,
|
|
264
|
+
locale: SETTINGS_NS,
|
|
265
|
+
inject: () => debugRuntime.panelFace(),
|
|
266
|
+
}, DebugPanel))
|
|
196
267
|
return () => {
|
|
197
268
|
card.stop()
|
|
198
|
-
|
|
269
|
+
detachDebug()
|
|
270
|
+
detachSettings()
|
|
271
|
+
detachBinding()
|
|
199
272
|
}
|
|
200
273
|
})
|
|
201
274
|
|
|
202
275
|
const configured = function StreamConfiguredView(props: AssistantProps) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
276
|
+
const preferences = useSyncExternalStore(
|
|
277
|
+
settings.subscribe,
|
|
278
|
+
settings.getSnapshot,
|
|
279
|
+
settings.getSnapshot,
|
|
207
280
|
)
|
|
208
281
|
return createElement(TypewriterAssistantNodeView, {
|
|
209
282
|
...props,
|
|
@@ -212,21 +285,39 @@ export function apply(ctx: ClientContext): void {
|
|
|
212
285
|
revealCharsPerSec: config.revealCharsPerSec,
|
|
213
286
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
214
287
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
215
|
-
thinkAutoExpand,
|
|
288
|
+
thinkAutoExpand: preferences.thinkAutoExpand,
|
|
216
289
|
})
|
|
217
290
|
}
|
|
218
291
|
ctx.slots.inject('conversation.chat.node', () => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
292
|
+
let releaseTakeover: (() => void) | undefined
|
|
293
|
+
|
|
294
|
+
const syncTakeover = (): void => {
|
|
295
|
+
if (!settings.takeoverEnabled()) {
|
|
296
|
+
releaseTakeover?.()
|
|
297
|
+
releaseTakeover = undefined
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
if (releaseTakeover !== undefined) return
|
|
301
|
+
const unwrap = wrapAgentChatRows(ctx)
|
|
302
|
+
const unshadow = ctx.slots.register({
|
|
303
|
+
name: 'conversation.chat.node',
|
|
304
|
+
key: 'assistant-step',
|
|
305
|
+
priority: -100,
|
|
306
|
+
locale: 'conversation',
|
|
307
|
+
registrant: 'dsh-smooth-stream',
|
|
308
|
+
}, configured)
|
|
309
|
+
releaseTakeover = () => {
|
|
310
|
+
// Stop observing slot changes before the shadow entry is removed.
|
|
311
|
+
unwrap()
|
|
312
|
+
unshadow()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const unsubscribe = settings.subscribe(syncTakeover)
|
|
317
|
+
syncTakeover()
|
|
227
318
|
return () => {
|
|
228
|
-
|
|
229
|
-
|
|
319
|
+
unsubscribe()
|
|
320
|
+
releaseTakeover?.()
|
|
230
321
|
}
|
|
231
322
|
})
|
|
232
323
|
}
|