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/lib/index.js CHANGED
@@ -1,4 +1,9 @@
1
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
1
2
  import Schema from "@deepseek-ai/schemastery";
3
+ import { readFileSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawn } from "node:child_process";
2
7
  //#region src/config.ts
3
8
  /** Defaults shared by the Host schema and the client-side fallback. */
4
9
  const DEFAULT_STREAM_CONFIG = {
@@ -41,6 +46,122 @@ function injectStreamConfig(html, config) {
41
46
  return `${html.slice(0, at)}${script}${html.slice(at)}`;
42
47
  }
43
48
  //#endregion
49
+ //#region src/package-meta.ts
50
+ /** Host-only package metadata. The package manifest remains the source of truth. */
51
+ function packageManifestPath() {
52
+ try {
53
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
54
+ } catch {}
55
+ return join(process.cwd(), "package.json");
56
+ }
57
+ const manifest = JSON.parse(readFileSync(packageManifestPath(), "utf8"));
58
+ function readRequiredString(field) {
59
+ const value = manifest[field];
60
+ if (typeof value !== "string" || value.length === 0) throw new Error(`dsh-smooth-stream: package.json must contain a non-empty ${field}`);
61
+ return value;
62
+ }
63
+ /** npm package name, read from this plugin's manifest. */
64
+ const STREAM_PACKAGE_NAME = readRequiredString("name");
65
+ /** Version of the code currently loaded by the Host, read from its manifest. */
66
+ const STREAM_PACKAGE_VERSION = readRequiredString("version");
67
+ //#endregion
68
+ //#region src/profile-installation.ts
69
+ /** Host-only profile source inspection and fixed npm update runner. */
70
+ function record(value) {
71
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
72
+ }
73
+ function profileDirectory(baseUrl) {
74
+ if (baseUrl === void 0) return void 0;
75
+ try {
76
+ const url = new URL(baseUrl);
77
+ return url.protocol === "file:" ? fileURLToPath(url) : void 0;
78
+ } catch {
79
+ return;
80
+ }
81
+ }
82
+ function hasBundle(manifest, packageName) {
83
+ const bundles = record(record(manifest.dsh)?.profile)?.bundles;
84
+ return Array.isArray(bundles) && bundles.includes(packageName);
85
+ }
86
+ function isLocalSpecifier(specifier) {
87
+ return specifier.startsWith("link:") || specifier.startsWith("file:") || specifier.startsWith(".") || isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier);
88
+ }
89
+ function isRegistrySpecifier(specifier) {
90
+ return specifier.length > 0 && !specifier.includes(":") && !/[\\/]/.test(specifier);
91
+ }
92
+ function isNpmSpecifier(specifier, packageName) {
93
+ if (!specifier.startsWith("npm:")) return isRegistrySpecifier(specifier);
94
+ const aliased = specifier.slice(4);
95
+ if (aliased === packageName) return true;
96
+ const prefix = `${packageName}@`;
97
+ return aliased.startsWith(prefix) && isRegistrySpecifier(aliased.slice(prefix.length));
98
+ }
99
+ /**
100
+ * Read only the profile manifest anchored by the current Cordis config tree.
101
+ * A malformed or unrelated tree never receives an update affordance.
102
+ */
103
+ function inspectProfileInstallation(baseUrl, packageName) {
104
+ const profileDir = profileDirectory(baseUrl);
105
+ if (profileDir === void 0) return { kind: "unmanaged" };
106
+ let manifest;
107
+ try {
108
+ manifest = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8"));
109
+ } catch {
110
+ return { kind: "unmanaged" };
111
+ }
112
+ const specifier = record(manifest.dependencies)?.[packageName];
113
+ if (typeof specifier !== "string" || !hasBundle(manifest, packageName)) return { kind: "unmanaged" };
114
+ if (isLocalSpecifier(specifier)) return { kind: "development" };
115
+ if (!isNpmSpecifier(specifier, packageName)) return { kind: "unmanaged" };
116
+ return {
117
+ kind: "npm",
118
+ profileDir,
119
+ profileName: basename(profileDir)
120
+ };
121
+ }
122
+ /** Run the same fixed package update operation that a profile user would invoke. */
123
+ function updateNpmProfilePackage(profileDir, packageName) {
124
+ const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
125
+ return new Promise((resolve, reject) => {
126
+ const child = spawn(command, ["update", packageName], {
127
+ cwd: profileDir,
128
+ stdio: "ignore",
129
+ shell: false,
130
+ windowsHide: true
131
+ });
132
+ child.once("error", reject);
133
+ child.once("exit", (code, signal) => {
134
+ if (code === 0) resolve();
135
+ else reject(/* @__PURE__ */ new Error(`dsh-smooth-stream: pnpm update failed (${signal ?? String(code)})`));
136
+ });
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/settings-api.ts
141
+ /** Shared wire vocabulary for the plugin-owned settings RPC channel. */
142
+ /** Dedicated, loopback-only RPC channel registered by the Host half. */
143
+ const STREAM_SETTINGS_RPC_CHANNEL = "/smooth-stream";
144
+ /** Endpoints accepted by {@link STREAM_SETTINGS_RPC_CHANNEL}. */
145
+ const STREAM_SETTINGS_RPC = {
146
+ read: "settings.read",
147
+ write: "settings.write",
148
+ upgrade: "plugin.upgrade"
149
+ };
150
+ //#endregion
151
+ //#region src/settings.ts
152
+ /**
153
+ * User-owned settings for the smooth-stream plugin, exposed to the Host
154
+ * settings service and edited from the Web Settings "plugin configuration"
155
+ * page. This is the runtime-editable complement to {@link StreamConfig}: that
156
+ * contract is composed at load and bridged once through the boot global, while
157
+ * these preferences live in the durable user-settings document and take effect
158
+ * live.
159
+ */
160
+ /** Settings namespace registered by the Host and served through the plugin RPC. */
161
+ const STREAM_SETTINGS_NS = "smooth-stream";
162
+ /** Defaults shared by the Host schema and the client-side fallback. */
163
+ const DEFAULT_STREAM_SETTINGS = { thinkAutoExpand: true };
164
+ //#endregion
44
165
  //#region src/plugin.ts
45
166
  /** Display name shown by the Host loader while the plugin is mounted. */
46
167
  const name = "dsh-smooth-stream";
@@ -56,6 +177,11 @@ const Config = Schema.object({
56
177
  maxScrollSpeedPxPerSec: Schema.number().min(1).max(2e3).default(DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec)
57
178
  });
58
179
  /**
180
+ * Schema of the user-owned settings section. The Host keeps it in the durable
181
+ * settings provider while the browser edits it through the plugin RPC below.
182
+ */
183
+ const StreamSettingsSchema = Schema.object({ thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand) });
184
+ /**
59
185
  * Host half: log the resolved configuration and bridge it to the browser
60
186
  * half. The web boot graph carries no per-entry config, so the validated
61
187
  * value is injected into every served index response as a boot global the
@@ -68,6 +194,109 @@ function apply(ctx, config) {
68
194
  ctx.inject(["webServer"], (httpCtx) => {
69
195
  httpCtx.effect(() => httpCtx.webServer.tapIndex((html) => injectStreamConfig(html, config)), "dsh-smooth-stream: boot config bridge");
70
196
  });
197
+ ctx.inject(["settings"], (settingsCtx) => {
198
+ const scope = settingsCtx.settings.register(settingsNamespace(STREAM_SETTINGS_NS), StreamSettingsSchema, { applies: "live" });
199
+ settingsCtx.inject(["connection"], (connectionCtx) => {
200
+ let upgrade;
201
+ const view = () => {
202
+ const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME);
203
+ return {
204
+ version: STREAM_PACKAGE_VERSION,
205
+ installation: installation.kind,
206
+ writable: connectionCtx.settings.writable,
207
+ thinkAutoExpand: scope.get().thinkAutoExpand,
208
+ canUpgrade: installation.kind === "npm"
209
+ };
210
+ };
211
+ const handle = async (endpoint, payload) => {
212
+ if (endpoint === STREAM_SETTINGS_RPC.read) return {
213
+ ok: true,
214
+ value: view()
215
+ };
216
+ if (endpoint === STREAM_SETTINGS_RPC.write) {
217
+ if (typeof payload !== "object" || payload === null || typeof payload.thinkAutoExpand !== "boolean") return {
218
+ ok: false,
219
+ error: {
220
+ code: "settings-rejected",
221
+ message: "thinkAutoExpand must be a boolean",
222
+ details: { ns: STREAM_SETTINGS_NS }
223
+ }
224
+ };
225
+ if (!connectionCtx.settings.writable) return {
226
+ ok: false,
227
+ error: {
228
+ code: "settings-rejected",
229
+ message: "smooth-stream settings are read-only",
230
+ details: { ns: STREAM_SETTINGS_NS }
231
+ }
232
+ };
233
+ try {
234
+ await scope.update({ thinkAutoExpand: payload.thinkAutoExpand });
235
+ } catch {
236
+ return {
237
+ ok: false,
238
+ error: {
239
+ code: "settings-rejected",
240
+ message: "smooth-stream settings update failed",
241
+ details: { ns: STREAM_SETTINGS_NS }
242
+ }
243
+ };
244
+ }
245
+ return {
246
+ ok: true,
247
+ value: view()
248
+ };
249
+ }
250
+ if (endpoint === STREAM_SETTINGS_RPC.upgrade) {
251
+ const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME);
252
+ if (installation.kind !== "npm") return {
253
+ ok: false,
254
+ error: {
255
+ code: "internal",
256
+ message: "smooth-stream is not an npm profile dependency",
257
+ details: {}
258
+ }
259
+ };
260
+ if (upgrade !== void 0) return {
261
+ ok: false,
262
+ error: {
263
+ code: "internal",
264
+ message: "smooth-stream update is already running",
265
+ details: {}
266
+ }
267
+ };
268
+ upgrade = updateNpmProfilePackage(installation.profileDir, STREAM_PACKAGE_NAME);
269
+ try {
270
+ await upgrade;
271
+ } catch {
272
+ return {
273
+ ok: false,
274
+ error: {
275
+ code: "internal",
276
+ message: "smooth-stream update failed",
277
+ details: {}
278
+ }
279
+ };
280
+ } finally {
281
+ upgrade = void 0;
282
+ }
283
+ return {
284
+ ok: true,
285
+ value: { restartRequired: true }
286
+ };
287
+ }
288
+ return {
289
+ ok: false,
290
+ error: {
291
+ code: "internal",
292
+ message: `unknown smooth-stream endpoint ${JSON.stringify(endpoint)}`,
293
+ details: {}
294
+ }
295
+ };
296
+ };
297
+ connectionCtx.effect(() => connectionCtx.connection.rpc.handle(STREAM_SETTINGS_RPC_CHANNEL, handle, { authority: "loopback" }), "dsh-smooth-stream: settings RPC");
298
+ });
299
+ });
71
300
  }
72
301
  //#endregion
73
302
  export { Config, apply, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-smooth-stream",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "DeepSeek Harness (dsh) plugin: silky streaming reveal, no flicker. dsh 丝滑流式渲染插件。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,7 +53,7 @@
53
53
  "src/**/*.css",
54
54
  "cordis.patch.yml",
55
55
  "README.md",
56
- "README.zh.md",
56
+ "README.en.md",
57
57
  "LICENSE"
58
58
  ],
59
59
  "dsh": {
@@ -65,7 +65,10 @@
65
65
  "inject": [
66
66
  "@deepseek-ai/dsh-client-runtime",
67
67
  "@deepseek-ai/dsh-client-ui-conversation",
68
- "@deepseek-ai/dsh-client-locale"
68
+ "@deepseek-ai/dsh-client-locale",
69
+ "@deepseek-ai/dsh-client-connection",
70
+ "@deepseek-ai/dsh-client-ui-settings",
71
+ "@deepseek-ai/dsh-client-ui-settings-plugins"
69
72
  ]
70
73
  }
71
74
  },
@@ -75,13 +78,19 @@
75
78
  "test": "vitest run",
76
79
  "typecheck": "tsc -p tsconfig.json --pretty false"
77
80
  },
81
+ "dependencies": {
82
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.6"
83
+ },
78
84
  "peerDependencies": {
79
85
  "@deepseek-ai/cordis": "^4.0.1",
80
86
  "@deepseek-ai/dsh-client-locale": "*",
81
87
  "@deepseek-ai/dsh-client-runtime": "*",
82
88
  "@deepseek-ai/dsh-client-ui-attachment": "*",
89
+ "@deepseek-ai/dsh-client-connection": "*",
83
90
  "@deepseek-ai/dsh-client-ui-conversation": "*",
84
91
  "@deepseek-ai/dsh-client-ui-primitives": "*",
92
+ "@deepseek-ai/dsh-client-ui-settings": "*",
93
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "*",
85
94
  "@deepseek-ai/dsh-client-ui-slots": "*",
86
95
  "@deepseek-ai/dsh-host-webserver": "*",
87
96
  "@deepseek-ai/schemastery": "*",
@@ -97,12 +106,21 @@
97
106
  "@deepseek-ai/dsh-client-ui-attachment": {
98
107
  "optional": true
99
108
  },
109
+ "@deepseek-ai/dsh-client-connection": {
110
+ "optional": true
111
+ },
100
112
  "@deepseek-ai/dsh-client-ui-conversation": {
101
113
  "optional": true
102
114
  },
103
115
  "@deepseek-ai/dsh-client-ui-primitives": {
104
116
  "optional": true
105
117
  },
118
+ "@deepseek-ai/dsh-client-ui-settings": {
119
+ "optional": true
120
+ },
121
+ "@deepseek-ai/dsh-client-ui-settings-plugins": {
122
+ "optional": true
123
+ },
106
124
  "@deepseek-ai/dsh-client-ui-slots": {
107
125
  "optional": true
108
126
  },
@@ -115,10 +133,14 @@
115
133
  "@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
116
134
  "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
117
135
  "@deepseek-ai/dsh-client-ui-attachment": "0.1.0-rc.6",
136
+ "@deepseek-ai/dsh-client-connection": "0.1.0-rc.6",
118
137
  "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6",
119
138
  "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6",
139
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6",
140
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.0-rc.6",
120
141
  "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
121
142
  "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.6",
143
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.6",
122
144
  "@deepseek-ai/schemastery": "3.18.1",
123
145
  "@testing-library/react": "^16.3.0",
124
146
  "@types/node": "^22.20.0",
@@ -0,0 +1,253 @@
1
+ /* Mirrors the shipped PluginCard chrome without importing an internal module
2
+ from another client package. */
3
+
4
+ .card {
5
+ list-style: none;
6
+ display: flex;
7
+ flex-direction: column;
8
+ border: 1px solid var(--dsw-alias-border-l2);
9
+ border-radius: 12px;
10
+ background: var(--dsw-alias-bg-layer-3);
11
+ transition: border-color .16s, background .16s;
12
+ }
13
+
14
+ .card:hover {
15
+ border-color: var(--dsw-alias-label-dimmed);
16
+ }
17
+
18
+ .cardOpen {
19
+ border-color: var(--dsw-alias-label-dimmed);
20
+ background: var(--dsw-alias-bg-layer-2);
21
+ }
22
+
23
+ .header {
24
+ appearance: none;
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 12px;
28
+ width: 100%;
29
+ padding: 14px 16px;
30
+ border: 0;
31
+ border-radius: 12px;
32
+ background: none;
33
+ cursor: pointer;
34
+ text-align: left;
35
+ font: inherit;
36
+ color: inherit;
37
+ }
38
+
39
+ .header:focus-visible {
40
+ outline: 2px solid var(--dsw-alias-brand-primary);
41
+ outline-offset: -2px;
42
+ }
43
+
44
+ .headText {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 4px;
48
+ min-width: 0;
49
+ flex: 1;
50
+ }
51
+
52
+ .name {
53
+ font-weight: 600;
54
+ font-size: 15px;
55
+ line-height: 1.4;
56
+ color: var(--dsw-alias-label-primary);
57
+ }
58
+
59
+ .description {
60
+ font-size: 13px;
61
+ line-height: 1.5;
62
+ color: var(--dsw-alias-label-tertiary);
63
+ }
64
+
65
+ .version {
66
+ flex: none;
67
+ max-width: 16rem;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ white-space: nowrap;
71
+ font-size: 12px;
72
+ line-height: 1.5;
73
+ color: var(--dsw-alias-label-tertiary);
74
+ }
75
+
76
+ .pending {
77
+ flex: none;
78
+ padding: 1px 8px;
79
+ border-radius: 999px;
80
+ background: var(--dsw-alias-bg-module-platform);
81
+ color: var(--dsw-alias-label-secondary);
82
+ font-size: 11px;
83
+ font-weight: 500;
84
+ line-height: 17px;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .chevron {
89
+ flex: none;
90
+ color: var(--dsw-alias-label-tertiary);
91
+ transition: transform .16s;
92
+ }
93
+
94
+ .chevronOpen {
95
+ transform: rotate(180deg);
96
+ }
97
+
98
+ .body {
99
+ display: flex;
100
+ flex-direction: column;
101
+ margin: 0 16px;
102
+ padding-bottom: 8px;
103
+ border-top: 1px solid var(--dsw-alias-border-l2);
104
+ }
105
+
106
+ .readOnly {
107
+ margin: 12px 0 0;
108
+ font-size: 12px;
109
+ line-height: 1.5;
110
+ color: var(--dsw-alias-label-tertiary);
111
+ }
112
+
113
+ .field {
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 6px;
117
+ padding: 12px 0;
118
+ }
119
+
120
+ .fieldHead {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: space-between;
124
+ gap: 8px;
125
+ }
126
+
127
+ .label {
128
+ flex: 1;
129
+ min-width: 0;
130
+ font-size: 13px;
131
+ font-weight: 500;
132
+ line-height: 1.5;
133
+ color: var(--dsw-alias-label-primary);
134
+ }
135
+
136
+ .toggle {
137
+ width: 16px;
138
+ height: 16px;
139
+ accent-color: var(--dsw-alias-brand-primary);
140
+ cursor: pointer;
141
+ }
142
+
143
+ .toggle:disabled {
144
+ cursor: default;
145
+ }
146
+
147
+ .toggle:focus-visible {
148
+ outline: 2px solid var(--dsw-alias-brand-primary);
149
+ outline-offset: 2px;
150
+ }
151
+
152
+ .hint {
153
+ margin: 0;
154
+ font-size: 12px;
155
+ line-height: 1.5;
156
+ color: var(--dsw-alias-label-tertiary);
157
+ }
158
+
159
+ .failure {
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: space-between;
163
+ gap: 12px;
164
+ padding: 12px 0;
165
+ }
166
+
167
+ .updateRow {
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: space-between;
171
+ gap: 12px;
172
+ padding: 12px 0;
173
+ border-top: 1px solid var(--dsw-alias-border-l2);
174
+ }
175
+
176
+ .updateCopy {
177
+ display: flex;
178
+ flex-direction: column;
179
+ min-width: 0;
180
+ gap: 4px;
181
+ }
182
+
183
+ .footer {
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: flex-end;
187
+ gap: 8px;
188
+ padding: 12px 0 4px;
189
+ border-top: 1px solid var(--dsw-alias-border-l2);
190
+ }
191
+
192
+ .failed {
193
+ flex: 1;
194
+ min-width: 0;
195
+ margin: 0;
196
+ font-size: 12px;
197
+ line-height: 1.5;
198
+ color: var(--dsw-alias-label-error);
199
+ }
200
+
201
+ .save,
202
+ .discard,
203
+ .update {
204
+ appearance: none;
205
+ border: 1px solid transparent;
206
+ border-radius: 8px;
207
+ font: inherit;
208
+ font-size: 13px;
209
+ line-height: 1.5;
210
+ padding: 5px 14px;
211
+ cursor: pointer;
212
+ }
213
+
214
+ .save {
215
+ background: var(--dsw-alias-label-primary);
216
+ color: var(--dsw-alias-bg-layer-3);
217
+ }
218
+
219
+ .discard {
220
+ border-color: var(--dsw-alias-border-l2);
221
+ background: none;
222
+ color: var(--dsw-alias-label-secondary);
223
+ }
224
+
225
+ .discard:hover:not(:disabled),
226
+ .update:hover:not(:disabled) {
227
+ border-color: var(--dsw-alias-label-dimmed);
228
+ color: var(--dsw-alias-label-primary);
229
+ }
230
+
231
+ .update {
232
+ display: inline-flex;
233
+ align-items: center;
234
+ flex: none;
235
+ gap: 6px;
236
+ border-color: var(--dsw-alias-border-l2);
237
+ background: none;
238
+ color: var(--dsw-alias-label-secondary);
239
+ }
240
+
241
+ .discard:disabled,
242
+ .save:disabled,
243
+ .update:disabled {
244
+ opacity: 0.4;
245
+ cursor: default;
246
+ }
247
+
248
+ .discard:focus-visible,
249
+ .save:focus-visible,
250
+ .update:focus-visible {
251
+ outline: 2px solid var(--dsw-alias-brand-primary);
252
+ outline-offset: 1px;
253
+ }