dsh-smooth-stream 0.1.0 → 0.3.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.
- package/README.en.md +103 -0
- package/README.md +62 -47
- package/lib/client.js +545 -44
- package/lib/client.js.map +1 -1
- package/lib/index.js +229 -0
- package/package.json +25 -3
- package/src/client/SmoothStreamCard.module.css +253 -0
- package/src/client/SmoothStreamCard.tsx +122 -0
- package/src/client/TypewriterAssistantNodeView.tsx +11 -3
- package/src/client/index.ts +96 -3
- package/src/client/locales.ts +74 -0
- package/src/client/smooth-stream-card-controller.ts +162 -0
- package/src/client/smooth-stream-settings-api.ts +65 -0
- package/src/package-meta.ts +39 -0
- package/src/plugin.ts +101 -0
- package/src/profile-installation.ts +121 -0
- package/src/settings-api.ts +33 -0
- package/src/settings.ts +30 -0
- package/README.zh.md +0 -88
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smooth-stream plugin configuration card, rendered inside the Web
|
|
3
|
+
* Settings "plugin configuration" page. One toggle, staged until the user
|
|
4
|
+
* saves — the same shape as the Host-shipped cards, hand-drawn because the
|
|
5
|
+
* Host cards' chrome is not exported for reuse.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
10
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
11
|
+
import { IconChevronDownOutline14, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
12
|
+
import type { SmoothStreamCardFace } from './smooth-stream-card-controller.ts'
|
|
13
|
+
import css from './SmoothStreamCard.module.css'
|
|
14
|
+
|
|
15
|
+
/** Props the renderer binds for the smooth-stream card. */
|
|
16
|
+
export type SmoothStreamCardProps =
|
|
17
|
+
PropsRuntime<'settings.plugin.item'>
|
|
18
|
+
& PropsLocale<'settings.smoothStream'>
|
|
19
|
+
& InjectFace<SmoothStreamCardFace>
|
|
20
|
+
|
|
21
|
+
/** Render the smooth-stream card independently of the core settings namespace allowlist. */
|
|
22
|
+
export function SmoothStreamCard(props: SmoothStreamCardProps) {
|
|
23
|
+
const { t } = props
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
const state = props.useSmoothStreamCard(snapshot => snapshot)
|
|
26
|
+
const blocked = !state.dirty || state.saving || state.status !== 'ready'
|
|
27
|
+
const versionLabel = state.version === undefined
|
|
28
|
+
? null
|
|
29
|
+
: t(state.installation === 'development' ? 'developmentVersion' : 'version')
|
|
30
|
+
.replace('{version}', state.version)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<li className={open ? `${css.card} ${css.cardOpen}` : css.card}>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className={css.header}
|
|
37
|
+
aria-expanded={open}
|
|
38
|
+
onClick={() => { setOpen(!open) }}
|
|
39
|
+
>
|
|
40
|
+
<span className={css.headText}>
|
|
41
|
+
<span className={css.name}>{t('title')}</span>
|
|
42
|
+
<span className={css.description}>{t('description')}</span>
|
|
43
|
+
</span>
|
|
44
|
+
{versionLabel === null ? null : <span className={css.version}>{versionLabel}</span>}
|
|
45
|
+
{state.dirty ? <span className={css.pending}>{t('unsaved')}</span> : null}
|
|
46
|
+
<IconChevronDownOutline14 className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron} />
|
|
47
|
+
</button>
|
|
48
|
+
{open
|
|
49
|
+
? (
|
|
50
|
+
<div className={css.body}>
|
|
51
|
+
{state.status === 'loading' ? <p className={css.readOnly} role="status">{t('loading')}</p> : null}
|
|
52
|
+
{state.status === 'unavailable' ? (
|
|
53
|
+
<div className={css.failure}>
|
|
54
|
+
<p className={css.readOnly} role="status">{t('unavailable')}</p>
|
|
55
|
+
<button type="button" className={css.discard} onClick={props.reload}>{t('retry')}</button>
|
|
56
|
+
</div>
|
|
57
|
+
) : null}
|
|
58
|
+
{state.status === 'ready' ? (
|
|
59
|
+
<>
|
|
60
|
+
{!state.writable ? <p className={css.readOnly} role="status">{t('readOnly')}</p> : null}
|
|
61
|
+
<label className={css.field}>
|
|
62
|
+
<span className={css.fieldHead}>
|
|
63
|
+
<span className={css.label}>{t('thinkAutoExpand')}</span>
|
|
64
|
+
<input
|
|
65
|
+
type="checkbox"
|
|
66
|
+
className={css.toggle}
|
|
67
|
+
checked={state.thinkAutoExpand}
|
|
68
|
+
disabled={!state.writable}
|
|
69
|
+
onChange={(event) => { props.edit(event.target.checked) }}
|
|
70
|
+
/>
|
|
71
|
+
</span>
|
|
72
|
+
<span className={css.hint}>{t('thinkAutoExpandHint')}</span>
|
|
73
|
+
</label>
|
|
74
|
+
<div className={css.updateRow}>
|
|
75
|
+
<span className={css.updateCopy}>
|
|
76
|
+
<span className={css.label}>{t('updates')}</span>
|
|
77
|
+
<span className={css.hint}>
|
|
78
|
+
{state.restartRequired
|
|
79
|
+
? t('restartRequired')
|
|
80
|
+
: state.installation === 'npm' ? t('updateHint')
|
|
81
|
+
: state.installation === 'development' ? t('developmentBuild') : t('updateUnavailable')}
|
|
82
|
+
</span>
|
|
83
|
+
</span>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
className={css.update}
|
|
87
|
+
disabled={!state.canUpgrade || state.upgrading || state.restartRequired}
|
|
88
|
+
title={state.canUpgrade ? undefined : t('updateUnavailable')}
|
|
89
|
+
onClick={props.upgrade}
|
|
90
|
+
>
|
|
91
|
+
<span aria-hidden="true"><IconRefreshOutline14 /></span>
|
|
92
|
+
{t(state.upgrading ? 'updating' : 'update')}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
{state.upgradeFailed ? <p className={css.failed} role="status">{t('updateFailed')}</p> : null}
|
|
96
|
+
<div className={css.footer}>
|
|
97
|
+
{state.failed ? <p className={css.failed} role="status">{t('saveFailed')}</p> : null}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className={css.discard}
|
|
101
|
+
disabled={!state.dirty || state.saving}
|
|
102
|
+
onClick={props.discard}
|
|
103
|
+
>
|
|
104
|
+
{t('discard')}
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className={css.save}
|
|
109
|
+
disabled={blocked}
|
|
110
|
+
onClick={props.save}
|
|
111
|
+
>
|
|
112
|
+
{t(state.saving ? 'saving' : 'save')}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
: null}
|
|
120
|
+
</li>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -7,6 +7,7 @@ import { useSmoothStreamContent, type StreamSmoothingPreset } from './useSmoothS
|
|
|
7
7
|
import { useFpsGuard } from './useFpsGuard.ts'
|
|
8
8
|
import { FollowHost } from './FollowHost.tsx'
|
|
9
9
|
import { DEFAULT_STREAM_CONFIG, type StreamMode } from '../config.ts'
|
|
10
|
+
import { DEFAULT_STREAM_SETTINGS } from '../settings.ts'
|
|
10
11
|
import css from './TypewriterAssistantNodeView.module.css'
|
|
11
12
|
|
|
12
13
|
type AssistantProps = ChatNodeViewProps<'assistant-step'>
|
|
@@ -125,17 +126,19 @@ function AnimatedReasoning({
|
|
|
125
126
|
text,
|
|
126
127
|
running,
|
|
127
128
|
preset,
|
|
129
|
+
thinkAutoExpand,
|
|
128
130
|
shouldHoldBack,
|
|
129
131
|
t,
|
|
130
132
|
}: {
|
|
131
133
|
text: string
|
|
132
134
|
running: boolean
|
|
133
135
|
preset: StreamSmoothingPreset
|
|
136
|
+
thinkAutoExpand: boolean
|
|
134
137
|
shouldHoldBack: () => boolean
|
|
135
138
|
t: AssistantProps['t']
|
|
136
139
|
}) {
|
|
137
140
|
const reduced = usePrefersReducedMotion()
|
|
138
|
-
const [expanded, setExpanded] = useState(running)
|
|
141
|
+
const [expanded, setExpanded] = useState(running && thinkAutoExpand)
|
|
139
142
|
const summaryRef = useRef<HTMLSpanElement>(null)
|
|
140
143
|
const speedCpsRef = useRef(35)
|
|
141
144
|
const displayed = useSmoothStreamContent(text, {
|
|
@@ -148,8 +151,10 @@ function AnimatedReasoning({
|
|
|
148
151
|
const summary = running ? latestLine(shown) : firstLine(text)
|
|
149
152
|
|
|
150
153
|
useEffect(() => {
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
// Only the running state owns disclosure while auto-expand is on; with it
|
|
155
|
+
// off, a manual toggle is never wrestled back by the stream.
|
|
156
|
+
if (thinkAutoExpand) setExpanded(running)
|
|
157
|
+
}, [running, thinkAutoExpand])
|
|
153
158
|
|
|
154
159
|
useEffect(() => {
|
|
155
160
|
const element = summaryRef.current
|
|
@@ -200,6 +205,7 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
200
205
|
revealCharsPerSec: _revealCharsPerSec = DEFAULT_STREAM_CONFIG.revealCharsPerSec,
|
|
201
206
|
scrollSpeedPxPerSec: _scrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.scrollSpeedPxPerSec,
|
|
202
207
|
maxScrollSpeedPxPerSec: _maxScrollSpeedPxPerSec = DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec,
|
|
208
|
+
thinkAutoExpand = DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
203
209
|
node,
|
|
204
210
|
useTurnData,
|
|
205
211
|
openFile,
|
|
@@ -212,6 +218,7 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
212
218
|
revealCharsPerSec?: number
|
|
213
219
|
scrollSpeedPxPerSec?: number
|
|
214
220
|
maxScrollSpeedPxPerSec?: number
|
|
221
|
+
thinkAutoExpand?: boolean
|
|
215
222
|
}) {
|
|
216
223
|
const data = node.data
|
|
217
224
|
const streaming = data.status === 'running'
|
|
@@ -272,6 +279,7 @@ export const TypewriterAssistantNodeView = memo(function TypewriterAssistantNode
|
|
|
272
279
|
text={block.text}
|
|
273
280
|
running={streaming && index === last}
|
|
274
281
|
preset={preset}
|
|
282
|
+
thinkAutoExpand={thinkAutoExpand}
|
|
275
283
|
shouldHoldBack={shouldHoldBack}
|
|
276
284
|
t={t}
|
|
277
285
|
/>,
|
package/src/client/index.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import { createElement, type ComponentType } from 'react'
|
|
1
|
+
import { createElement, useSyncExternalStore, type ComponentType } from 'react'
|
|
2
2
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
3
3
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
4
|
+
// Type-only: the connection Context merge and the plugins section's SlotMap
|
|
5
|
+
// entry ('settings.plugin.item').
|
|
6
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
7
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
4
8
|
import type { ChatNodeViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
5
9
|
import { TypewriterAssistantNodeView } from './TypewriterAssistantNodeView.tsx'
|
|
6
10
|
import { wrapFollowNodeView, type FollowWrapProps } from './TypewriterToolNodeView.tsx'
|
|
11
|
+
import { SmoothStreamCard } from './SmoothStreamCard.tsx'
|
|
12
|
+
import { SmoothStreamCardController } from './smooth-stream-card-controller.ts'
|
|
13
|
+
import { createSmoothStreamSettingsApi } from './smooth-stream-settings-api.ts'
|
|
14
|
+
import { NS as SETTINGS_NS, en, zh } from './locales.ts'
|
|
7
15
|
import { DEFAULT_STREAM_CONFIG, STREAM_BOOT_GLOBAL, type StreamConfig } from '../config.ts'
|
|
16
|
+
import { DEFAULT_STREAM_SETTINGS } from '../settings.ts'
|
|
8
17
|
|
|
9
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Cordis services required by the browser half. Only `slots` is load-bearing
|
|
20
|
+
* for the stream itself; locale and Connection power the configuration card
|
|
21
|
+
* and are wired through `ctx.inject` below so a deployment without them still
|
|
22
|
+
* streams with defaults.
|
|
23
|
+
*/
|
|
10
24
|
export const inject = ['slots']
|
|
11
25
|
|
|
12
26
|
type AssistantProps = ChatNodeViewProps<'assistant-step'>
|
|
@@ -85,17 +99,95 @@ function wrapGrowingChatRows(ctx: ClientContext): () => void {
|
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
102
|
+
/**
|
|
103
|
+
* A live preference cell read by `useSyncExternalStore`. It starts on the
|
|
104
|
+
* shared default and, once the plugin-owned settings controller is attached,
|
|
105
|
+
* tracks the resolved `thinkAutoExpand` value. A composition (or test) that
|
|
106
|
+
* ships no settings surface keeps the cell on the default.
|
|
107
|
+
*/
|
|
108
|
+
class PreferenceCell {
|
|
109
|
+
private readonly listeners = new Set<() => void>()
|
|
110
|
+
private card: SmoothStreamCardController | undefined
|
|
111
|
+
private value = DEFAULT_STREAM_SETTINGS.thinkAutoExpand
|
|
112
|
+
|
|
113
|
+
/** Re-point the cell at the plugin-owned settings controller. */
|
|
114
|
+
attach(card: SmoothStreamCardController): () => void {
|
|
115
|
+
this.card = card
|
|
116
|
+
this.refresh()
|
|
117
|
+
const unsubscribe = card.subscribe(() => { this.refresh() })
|
|
118
|
+
return () => {
|
|
119
|
+
unsubscribe()
|
|
120
|
+
if (this.card !== card) return
|
|
121
|
+
this.card = undefined
|
|
122
|
+
this.refresh()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private read(): boolean {
|
|
127
|
+
return this.card?.getSnapshot().thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private refresh(): void {
|
|
131
|
+
const next = this.read()
|
|
132
|
+
if (next === this.value) return
|
|
133
|
+
this.value = next
|
|
134
|
+
for (const listener of this.listeners) listener()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
readonly getSnapshot = (): boolean => this.value
|
|
138
|
+
|
|
139
|
+
readonly subscribe = (listener: () => void): () => void => {
|
|
140
|
+
this.listeners.add(listener)
|
|
141
|
+
return () => { this.listeners.delete(listener) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
88
145
|
/**
|
|
89
146
|
* Register the typewriter renderer after the conversation package declares the
|
|
90
147
|
* keyed Chat node seat. A lower priority shadows the built-in assistant row;
|
|
91
148
|
* every other growing row is wrapped in place so Tool cards, retries, and
|
|
92
149
|
* workflow runs share conversation follow. The Host-bridged configuration
|
|
93
|
-
* selects the render direction, smoothing preset, and glide speed
|
|
150
|
+
* selects the render direction, smoothing preset, and glide speed; the
|
|
151
|
+
* plugin-owned settings RPC supplies the live auto-expand preference when the
|
|
152
|
+
* settings surface is composed.
|
|
94
153
|
* @param ctx - Browser context carrying the shared slot registry.
|
|
95
154
|
*/
|
|
96
155
|
export function apply(ctx: ClientContext): void {
|
|
97
156
|
const config = readBootConfig()
|
|
157
|
+
const preference = new PreferenceCell()
|
|
158
|
+
|
|
159
|
+
// The card talks to the plugin-owned loopback RPC, so the core settings
|
|
160
|
+
// namespace allowlist cannot make it disappear. The stream still applies
|
|
161
|
+
// with defaults when the optional Settings UI or Connection is absent.
|
|
162
|
+
ctx.inject(['slots', 'locale', 'connection'], (settingsCtx) => {
|
|
163
|
+
const card = new SmoothStreamCardController(
|
|
164
|
+
// The shared Context augmentation also carries the Host-side Connection
|
|
165
|
+
// shape. This browser entry runs after the client provider installs its
|
|
166
|
+
// handle, so narrow through unknown to its client contract here.
|
|
167
|
+
createSmoothStreamSettingsApi(settingsCtx.get('connection') as unknown as ConnectionHandle),
|
|
168
|
+
)
|
|
169
|
+
const detachPreference = preference.attach(card)
|
|
170
|
+
card.start()
|
|
171
|
+
settingsCtx.effect(() => settingsCtx.locale.register(SETTINGS_NS, { zh, en }), 'dsh-smooth-stream: settings dictionaries')
|
|
172
|
+
settingsCtx.slots.inject('settings.plugin.item', () => settingsCtx.slots.register({
|
|
173
|
+
name: 'settings.plugin.item',
|
|
174
|
+
id: 'smooth-stream',
|
|
175
|
+
order: 30,
|
|
176
|
+
locale: SETTINGS_NS,
|
|
177
|
+
inject: () => card.inject(),
|
|
178
|
+
}, SmoothStreamCard))
|
|
179
|
+
return () => {
|
|
180
|
+
card.stop()
|
|
181
|
+
detachPreference()
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
98
185
|
const configured = function StreamConfiguredView(props: AssistantProps) {
|
|
186
|
+
const thinkAutoExpand = useSyncExternalStore(
|
|
187
|
+
preference.subscribe,
|
|
188
|
+
preference.getSnapshot,
|
|
189
|
+
preference.getSnapshot,
|
|
190
|
+
)
|
|
99
191
|
return createElement(TypewriterAssistantNodeView, {
|
|
100
192
|
...props,
|
|
101
193
|
mode: config.mode,
|
|
@@ -103,6 +195,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
103
195
|
revealCharsPerSec: config.revealCharsPerSec,
|
|
104
196
|
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
105
197
|
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
198
|
+
thinkAutoExpand,
|
|
106
199
|
})
|
|
107
200
|
}
|
|
108
201
|
ctx.slots.inject('conversation.chat.node', () => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Locale bundles for the smooth-stream plugin configuration card. */
|
|
2
|
+
|
|
3
|
+
/** Dictionary namespace owned by this plugin's settings card. */
|
|
4
|
+
export const NS = 'settings.smoothStream'
|
|
5
|
+
|
|
6
|
+
/** Locale keys the card renders. */
|
|
7
|
+
export type SmoothStreamLocaleKey =
|
|
8
|
+
| 'title' | 'description'
|
|
9
|
+
| 'thinkAutoExpand' | 'thinkAutoExpandHint'
|
|
10
|
+
| 'readOnly' | 'loading' | 'unavailable' | 'retry'
|
|
11
|
+
| 'version' | 'developmentVersion'
|
|
12
|
+
| 'updates' | 'updateHint' | 'developmentBuild' | 'updateUnavailable'
|
|
13
|
+
| 'update' | 'updating' | 'restartRequired' | 'updateFailed'
|
|
14
|
+
| 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed'
|
|
15
|
+
|
|
16
|
+
/** English copy. */
|
|
17
|
+
export const en: Record<SmoothStreamLocaleKey, string> = {
|
|
18
|
+
title: 'Smooth stream',
|
|
19
|
+
description: 'How replies are revealed while they stream.',
|
|
20
|
+
thinkAutoExpand: 'Auto-expand thinking',
|
|
21
|
+
thinkAutoExpandHint: 'Open the thinking block while it streams. Turn off to keep it collapsed.',
|
|
22
|
+
readOnly: 'This deployment stores settings read-only.',
|
|
23
|
+
loading: 'Loading plugin settings…',
|
|
24
|
+
unavailable: 'Plugin settings are unavailable in this connection.',
|
|
25
|
+
retry: 'Retry',
|
|
26
|
+
version: 'Version {version}',
|
|
27
|
+
developmentVersion: 'Development version {version}',
|
|
28
|
+
updates: 'Updates',
|
|
29
|
+
updateHint: 'Install the newest npm version, then restart Harness.',
|
|
30
|
+
developmentBuild: 'Linked source; updates are managed in the checkout.',
|
|
31
|
+
updateUnavailable: 'Updates are available only for an npm profile installation.',
|
|
32
|
+
update: 'Update',
|
|
33
|
+
updating: 'Updating…',
|
|
34
|
+
restartRequired: 'Updated. Restart Harness to load the new version.',
|
|
35
|
+
updateFailed: 'The package update failed; your current version is unchanged.',
|
|
36
|
+
save: 'Save',
|
|
37
|
+
saving: 'Saving…',
|
|
38
|
+
discard: 'Discard',
|
|
39
|
+
unsaved: 'Unsaved',
|
|
40
|
+
saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Simplified Chinese copy. */
|
|
44
|
+
export const zh: Record<SmoothStreamLocaleKey, string> = {
|
|
45
|
+
title: '丝滑流式',
|
|
46
|
+
description: '回复在流式输出时如何逐字展现。',
|
|
47
|
+
thinkAutoExpand: '自动展开思考',
|
|
48
|
+
thinkAutoExpandHint: '思考块在流式时自动展开;关闭后保持折叠,可手动展开。',
|
|
49
|
+
readOnly: '本部署的设置为只读。',
|
|
50
|
+
loading: '正在加载插件设置…',
|
|
51
|
+
unavailable: '当前连接无法访问插件设置。',
|
|
52
|
+
retry: '重试',
|
|
53
|
+
version: '版本 {version}',
|
|
54
|
+
developmentVersion: '开发版本 {version}',
|
|
55
|
+
updates: '更新',
|
|
56
|
+
updateHint: '安装最新 npm 版本后重启 Harness。',
|
|
57
|
+
developmentBuild: '当前为本地链接版本,请在源码目录管理更新。',
|
|
58
|
+
updateUnavailable: '只有 profile 使用 npm 包时才能更新。',
|
|
59
|
+
update: '更新',
|
|
60
|
+
updating: '更新中…',
|
|
61
|
+
restartRequired: '已更新;重启 Harness 后加载新版本。',
|
|
62
|
+
updateFailed: '包更新失败,当前版本未改变。',
|
|
63
|
+
save: '保存',
|
|
64
|
+
saving: '保存中…',
|
|
65
|
+
discard: '放弃修改',
|
|
66
|
+
unsaved: '未保存',
|
|
67
|
+
saveFailed: '本部署没有接受这些值,已保留供你修改。',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
71
|
+
interface LocaleNamespaceMap {
|
|
72
|
+
'settings.smoothStream': SmoothStreamLocaleKey
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Staged form state for the plugin-owned smooth-stream settings RPC. */
|
|
2
|
+
|
|
3
|
+
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
4
|
+
import { DEFAULT_STREAM_SETTINGS } from '../settings.ts'
|
|
5
|
+
import type { StreamInstallationKind, StreamSettingsView } from '../settings-api.ts'
|
|
6
|
+
import type { SmoothStreamSettingsApi } from './smooth-stream-settings-api.ts'
|
|
7
|
+
|
|
8
|
+
/** What the smooth-stream card renders. It remains visible while its Host RPC loads. */
|
|
9
|
+
export interface SmoothStreamCardState {
|
|
10
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
11
|
+
writable: boolean
|
|
12
|
+
dirty: boolean
|
|
13
|
+
saving: boolean
|
|
14
|
+
failed: boolean
|
|
15
|
+
thinkAutoExpand: boolean
|
|
16
|
+
version: string | undefined
|
|
17
|
+
installation: StreamInstallationKind
|
|
18
|
+
canUpgrade: boolean
|
|
19
|
+
upgrading: boolean
|
|
20
|
+
upgradeFailed: boolean
|
|
21
|
+
restartRequired: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The registration-side face injected into the settings slot renderer. */
|
|
25
|
+
export interface SmoothStreamCardFace {
|
|
26
|
+
hooks: {
|
|
27
|
+
smoothStreamCard: SnapshotStore<SmoothStreamCardState>
|
|
28
|
+
}
|
|
29
|
+
edit: (value: boolean) => void
|
|
30
|
+
save: () => void
|
|
31
|
+
discard: () => void
|
|
32
|
+
reload: () => void
|
|
33
|
+
upgrade: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Bridges the plugin's protected Host interface onto a staged settings form. */
|
|
37
|
+
export class SmoothStreamCardController {
|
|
38
|
+
private readonly store = createSnapshotStore<SmoothStreamCardState>(this.projection())
|
|
39
|
+
private loaded: StreamSettingsView | undefined
|
|
40
|
+
private staged: boolean | undefined
|
|
41
|
+
private saving = false
|
|
42
|
+
private failed = false
|
|
43
|
+
private upgrading = false
|
|
44
|
+
private upgradeFailed = false
|
|
45
|
+
private restartRequired = false
|
|
46
|
+
private loadGeneration = 0
|
|
47
|
+
private loadStatus: 'loading' | 'ready' | 'unavailable' = 'loading'
|
|
48
|
+
|
|
49
|
+
constructor(private readonly api: SmoothStreamSettingsApi) {}
|
|
50
|
+
|
|
51
|
+
/** Begin the background read after the card has been registered. */
|
|
52
|
+
start(): void {
|
|
53
|
+
void this.load()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Ignore a late response after the surrounding optional services unload. */
|
|
57
|
+
stop(): void {
|
|
58
|
+
this.loadGeneration += 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Current card snapshot, also consumed by the streaming preference cell. */
|
|
62
|
+
getSnapshot(): SmoothStreamCardState {
|
|
63
|
+
return this.store.getSnapshot()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Subscribe to state changes. */
|
|
67
|
+
subscribe(listener: () => void): () => void {
|
|
68
|
+
return this.store.subscribe(listener)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Build the face consumed by the settings slot renderer. */
|
|
72
|
+
inject(): SmoothStreamCardFace {
|
|
73
|
+
return {
|
|
74
|
+
hooks: { smoothStreamCard: this.store },
|
|
75
|
+
edit: (value) => {
|
|
76
|
+
this.staged = value
|
|
77
|
+
this.failed = false
|
|
78
|
+
this.publish()
|
|
79
|
+
},
|
|
80
|
+
save: () => { void this.save() },
|
|
81
|
+
discard: () => {
|
|
82
|
+
if (this.staged === undefined && !this.failed) return
|
|
83
|
+
this.staged = undefined
|
|
84
|
+
this.failed = false
|
|
85
|
+
this.publish()
|
|
86
|
+
},
|
|
87
|
+
reload: () => { void this.load() },
|
|
88
|
+
upgrade: () => { void this.upgrade() },
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private projection(): SmoothStreamCardState {
|
|
93
|
+
return {
|
|
94
|
+
status: this.loadStatus,
|
|
95
|
+
writable: this.loaded?.writable ?? false,
|
|
96
|
+
dirty: this.staged !== undefined,
|
|
97
|
+
saving: this.saving,
|
|
98
|
+
failed: this.failed,
|
|
99
|
+
thinkAutoExpand: this.staged ?? this.loaded?.thinkAutoExpand ?? DEFAULT_STREAM_SETTINGS.thinkAutoExpand,
|
|
100
|
+
version: this.loaded?.version,
|
|
101
|
+
installation: this.loaded?.installation ?? 'unmanaged',
|
|
102
|
+
canUpgrade: this.loaded?.canUpgrade ?? false,
|
|
103
|
+
upgrading: this.upgrading,
|
|
104
|
+
upgradeFailed: this.upgradeFailed,
|
|
105
|
+
restartRequired: this.restartRequired,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async load(): Promise<void> {
|
|
110
|
+
const generation = ++this.loadGeneration
|
|
111
|
+
this.loadStatus = 'loading'
|
|
112
|
+
this.loaded = undefined
|
|
113
|
+
this.publish()
|
|
114
|
+
try {
|
|
115
|
+
const view = await this.api.read()
|
|
116
|
+
if (generation !== this.loadGeneration) return
|
|
117
|
+
this.loaded = view
|
|
118
|
+
this.loadStatus = 'ready'
|
|
119
|
+
} catch {
|
|
120
|
+
if (generation !== this.loadGeneration) return
|
|
121
|
+
this.loaded = undefined
|
|
122
|
+
this.loadStatus = 'unavailable'
|
|
123
|
+
}
|
|
124
|
+
this.publish()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async save(): Promise<void> {
|
|
128
|
+
if (this.staged === undefined || this.saving || this.loaded?.writable !== true) return
|
|
129
|
+
const value = this.staged
|
|
130
|
+
this.saving = true
|
|
131
|
+
this.failed = false
|
|
132
|
+
this.publish()
|
|
133
|
+
try {
|
|
134
|
+
this.loaded = await this.api.write(value)
|
|
135
|
+
this.staged = undefined
|
|
136
|
+
} catch {
|
|
137
|
+
this.failed = true
|
|
138
|
+
}
|
|
139
|
+
this.saving = false
|
|
140
|
+
this.publish()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async upgrade(): Promise<void> {
|
|
144
|
+
if (this.loaded?.canUpgrade !== true || this.upgrading) return
|
|
145
|
+
this.upgrading = true
|
|
146
|
+
this.upgradeFailed = false
|
|
147
|
+
this.restartRequired = false
|
|
148
|
+
this.publish()
|
|
149
|
+
try {
|
|
150
|
+
const result = await this.api.upgrade()
|
|
151
|
+
this.restartRequired = result.restartRequired
|
|
152
|
+
} catch {
|
|
153
|
+
this.upgradeFailed = true
|
|
154
|
+
}
|
|
155
|
+
this.upgrading = false
|
|
156
|
+
this.publish()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private publish(): void {
|
|
160
|
+
this.store.set(this.projection())
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Browser adapter for the Host-owned smooth-stream settings RPC. */
|
|
2
|
+
|
|
3
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
4
|
+
import {
|
|
5
|
+
STREAM_SETTINGS_RPC,
|
|
6
|
+
STREAM_SETTINGS_RPC_CHANNEL,
|
|
7
|
+
type StreamSettingsView,
|
|
8
|
+
type StreamUpgradeView,
|
|
9
|
+
} from '../settings-api.ts'
|
|
10
|
+
|
|
11
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
13
|
+
? value as Record<string, unknown>
|
|
14
|
+
: undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function settingsView(value: unknown): StreamSettingsView {
|
|
18
|
+
const data = record(value)
|
|
19
|
+
if (data === undefined
|
|
20
|
+
|| typeof data.version !== 'string'
|
|
21
|
+
|| !['npm', 'development', 'unmanaged'].includes(data.installation as string)
|
|
22
|
+
|| typeof data.writable !== 'boolean'
|
|
23
|
+
|| typeof data.thinkAutoExpand !== 'boolean'
|
|
24
|
+
|| typeof data.canUpgrade !== 'boolean') {
|
|
25
|
+
throw new Error('dsh-smooth-stream: malformed settings response')
|
|
26
|
+
}
|
|
27
|
+
return data as unknown as StreamSettingsView
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function upgradeView(value: unknown): StreamUpgradeView {
|
|
31
|
+
const data = record(value)
|
|
32
|
+
if (data?.restartRequired !== true) throw new Error('dsh-smooth-stream: malformed update response')
|
|
33
|
+
return { restartRequired: true }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function accepted(result: Awaited<ReturnType<ConnectionHandle['rpc']['call']>>): unknown {
|
|
37
|
+
if (!result.ok) throw new Error(result.error.message)
|
|
38
|
+
return result.value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Narrow client contract consumed by the staged settings-card controller. */
|
|
42
|
+
export interface SmoothStreamSettingsApi {
|
|
43
|
+
read(): Promise<StreamSettingsView>
|
|
44
|
+
write(thinkAutoExpand: boolean): Promise<StreamSettingsView>
|
|
45
|
+
upgrade(): Promise<StreamUpgradeView>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build the typed facade over the generic Connection RPC service. */
|
|
49
|
+
export function createSmoothStreamSettingsApi(connection: ConnectionHandle): SmoothStreamSettingsApi {
|
|
50
|
+
return {
|
|
51
|
+
async read(): Promise<StreamSettingsView> {
|
|
52
|
+
return settingsView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.read, {})))
|
|
53
|
+
},
|
|
54
|
+
async write(thinkAutoExpand: boolean): Promise<StreamSettingsView> {
|
|
55
|
+
return settingsView(accepted(await connection.rpc.call(
|
|
56
|
+
STREAM_SETTINGS_RPC_CHANNEL,
|
|
57
|
+
STREAM_SETTINGS_RPC.write,
|
|
58
|
+
{ thinkAutoExpand },
|
|
59
|
+
)))
|
|
60
|
+
},
|
|
61
|
+
async upgrade(): Promise<StreamUpgradeView> {
|
|
62
|
+
return upgradeView(accepted(await connection.rpc.call(STREAM_SETTINGS_RPC_CHANNEL, STREAM_SETTINGS_RPC.upgrade, {})))
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Host-only package metadata. The package manifest remains the source of truth. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
interface PackageManifest {
|
|
8
|
+
name?: unknown
|
|
9
|
+
version?: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function packageManifestPath(): string {
|
|
13
|
+
// Production loads this Node entry from its installed file. Vite's test
|
|
14
|
+
// transform instead supplies an http URL, where the project manifest is the
|
|
15
|
+
// closest equivalent source of truth.
|
|
16
|
+
try {
|
|
17
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
18
|
+
} catch {
|
|
19
|
+
// Fall through to the project root when a browser-oriented test transform
|
|
20
|
+
// has replaced import.meta.url with a non-file URL.
|
|
21
|
+
}
|
|
22
|
+
return join(process.cwd(), 'package.json')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manifest = JSON.parse(readFileSync(packageManifestPath(), 'utf8')) as PackageManifest
|
|
26
|
+
|
|
27
|
+
function readRequiredString(field: keyof PackageManifest): string {
|
|
28
|
+
const value = manifest[field]
|
|
29
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
30
|
+
throw new Error(`dsh-smooth-stream: package.json must contain a non-empty ${field}`)
|
|
31
|
+
}
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** npm package name, read from this plugin's manifest. */
|
|
36
|
+
export const STREAM_PACKAGE_NAME = readRequiredString('name')
|
|
37
|
+
|
|
38
|
+
/** Version of the code currently loaded by the Host, read from its manifest. */
|
|
39
|
+
export const STREAM_PACKAGE_VERSION = readRequiredString('version')
|