@linbin-mk/dsh-brand-deepseek 0.1.4 → 0.2.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 CHANGED
@@ -20,13 +20,13 @@ It fills two single slots: `sidebar.brand.mark` (the whale mark) and `sidebar.br
20
20
  - a **custom style** toggle — off restores the official default brand (`DSH 本地构建` / `DSH Local Build` + build-version badge): the brand slots are unregistered, so the shell fallback renders;
21
21
  - a **hero headline** toggle — switches the blank-session title row independently (defaults to on);
22
22
  - **trajectory tab** and **session log button** toggles — show or hide those two pieces of harness chrome in the conversation header (both default to on). Implemented purely at the DOM level: no harness source is touched and the data behind them (trajectory records, export) is unaffected;
23
- - **ten brand colors** (the current scheme — official DeepSeek blue — is the default). Choices apply immediately and persist through the Host settings RPC, so they survive reloads.
23
+ - **ten brand colors** (the current scheme — official DeepSeek blue — is the default). Choices apply immediately and are written back to the Host configuration, so they survive reloads.
24
24
  - **Live preview** — the settings preview panel mirrors both states; with the custom style off it renders the shell fallback (fish mark, localized label and the build-version badge).
25
25
 
26
26
  ## Requirements
27
27
 
28
28
  - Node.js `^22.19` or `>=24`
29
- - DeepSeek Harness `0.1.5-rc.1` or a compatible `0.1.5` prerelease, on a **Web profile** that provides `ctx.slots`, `ctx.remote.settings` and `ctx.locale`
29
+ - DeepSeek Harness `0.1.7-alpha.1` or a compatible `0.1.7` prerelease, on a **Web profile** that provides `ctx.slots`, `ctx.configForms` and `ctx.locale`
30
30
  - The client modules the plugin declares (`@deepseek-ai/dsh-client-ui-primitives`, `-ui-sidebar`, `-ui-settings`, `-ui-renderer`, `-locale`, `dsh-api-remotes`) — the default Web profile already ships them
31
31
  - No path dependency on a Harness checkout
32
32
 
@@ -58,7 +58,7 @@ dsh plugin --profile web-brand remove @linbin-mk/dsh-brand-deepseek
58
58
 
59
59
  ## Settings
60
60
 
61
- The host half registers the `dsh-brand-deepseek` settings namespace. The fields default as follows and are written through the harness's revision-guarded settings transport, so changes take effect immediately with no restart. The namespace is **deliberately not the package name**: the package name is the loader identity (`@linbin-mk/dsh-brand-deepseek`), while the namespace only persists values renaming the package does not invalidate existing settings.
61
+ The host half declares the brand style as this plugin's Cordis `Config`: all five fields are `.volatile()`, so every one of them is live-editable, with the defaults below. The settings namespace is the loader row id `brand-deepseek` from `cordis.patch.yml` **deliberately not the package name**, which is the loader identity (`@linbin-mk/dsh-brand-deepseek`); renaming the package does not invalidate existing settings. The client half reads and writes that row's configuration form through `ctx.configForms.get('brand-deepseek')`, and the harness settings domain owns revision fencing and write serialization, so changes take effect immediately with no restart. On a non-loopback page (settings stay process-local) every write control is disabled instead of pretending to save.
62
62
 
63
63
  | Setting | Default | Effect |
64
64
  | --- | --- | --- |
@@ -70,7 +70,7 @@ The host half registers the `dsh-brand-deepseek` settings namespace. The fields
70
70
 
71
71
  ## How it works
72
72
 
73
- - The **host half** (`lib/index.js`) does exactly one thing: it registers the `dsh-brand-deepseek` settings namespace so the client half's `settings.describe` / `settings.mutate` calls are accepted and persisted.
73
+ - The **host half** (`lib/index.js`) does two things: it declares the brand-style Cordis `Config` (five `.volatile()` fields the whole of the `brand-deepseek` settings namespace) and registers `{ auto: false }` for its own row, since the plugin ships its own settings page and the sidebar Plugins list must not generate a second one.
74
74
  - The **client half** (`lib/client.js`) is a CJS bundle built by tsdown that self-registers through `window.__ModuleLoader__.load({ id, factory })`. The `id` must equal the **package name** — the harness keys its client module table by entry name, and a mismatch makes the row unresolvable and takes the whole GUI down while `--dump-config` still looks fine. `tsdown.config.ts` derives the id from `package.json#name` and `scripts/smoke-client.mjs` re-asserts the equality, so a rename cannot drift silently.
75
75
  - The **hero style sheet** targets the harness's CSS-module locals by their `<hash>_<local>` shape, scoped to the conversation shell's `[data-composer-seat]` and anchored on the hero's own `_fishHitbox` descendant, so the shared `.headline` locals of other modules (ContextMeter, ApprovalPanel) are never touched. The official default title is hidden under **every** local it has shipped with — `_headlineText` in `0.1.5-rc.1` and the `_titleGroup` wrapper (title plus badge) introduced in `0.1.5-rc.2`; on a harness that never had a given local the matching selector is simply a no-op. Should those names change again, the plugin degrades to the brand title rendering next to the official one instead of erroring or taking the page down.
76
76
  - The **two harness-chrome toggles** locate elements by class-name suffix with the localized label as a fallback, and a `MutationObserver` re-applies the state after React remounts the header. If both the class and the label change, the element stays visible rather than being hidden by mistake.
@@ -81,10 +81,11 @@ The host half registers the `dsh-brand-deepseek` settings namespace. The fields
81
81
  pnpm install
82
82
  pnpm build # src/ -> lib/ (lib/ is committed; installs need no build step)
83
83
  pnpm build:check # rebuild and assert lib/ matches what is committed
84
- pnpm test # both smoke tests
84
+ pnpm test # four smoke tests
85
+ npx tsc -p tsconfig.json --noEmit # type check (build.mjs does not run tsc)
85
86
  ```
86
87
 
87
- `pnpm test` runs `scripts/smoke-client.mjs` (loads `lib/client.js` the way the browser loader does, asserting bundle id == package name plus the export and `inject` contract) and `scripts/smoke-visibility.mjs` (mounts `apply()` on a jsdom document and verifies that both persisted toggles drive the DOM, including the observer re-apply after a remount and the label fallback locator). jsdom is a development dependency of this package only; the plugin never needs it at runtime.
88
+ `pnpm test` runs `scripts/smoke-client.mjs` (loads `lib/client.js` the way the browser loader does, asserting bundle id == package name plus the export and `inject` contract), `scripts/smoke-host.mjs` (loads `lib/index.js` and asserts all five fields are volatile with the documented defaults, and that the host half registers `{ auto: false }` for its **own fiber**), `scripts/smoke-visibility.mjs` (mounts `apply()` on a jsdom document and verifies that both persisted toggles drive the DOM, including the observer re-apply after a remount and the label fallback locator) and `scripts/smoke-config.mjs` (mounts `apply()` over a fake configuration form and verifies that the form is fetched by the `brand-deepseek` row id, that the accepted section drives the runtime, that toggles write through `form.set`, and that a refused write or a non-writable page never pretends to save). jsdom is a development dependency of this package only; the plugin never needs it at runtime.
88
89
 
89
90
  ## Troubleshooting
90
91
 
@@ -105,7 +106,7 @@ pnpm test # both smoke tests
105
106
 
106
107
  ## Privacy and scope
107
108
 
108
- The plugin makes no network requests, reads no conversation content and reports no telemetry. It does three things only: register slot occupants, read and write the `dsh-brand-deepseek` settings namespace, and hide two DOM elements in the conversation header (the trajectory tab and the session log button) according to your settings. Values live in the harness profile settings and never leave the machine. Hiding affects rendering only — trajectory data and export are untouched.
109
+ The plugin makes no network requests, reads no conversation content and reports no telemetry. It does three things only: register slot occupants, read and write the `brand-deepseek` configuration row, and hide two DOM elements in the conversation header (the trajectory tab and the session log button) according to your settings. Values live in the harness profile configuration and never leave the machine. Hiding affects rendering only — trajectory data and export are untouched.
109
110
 
110
111
  ## License
111
112
 
package/README.md CHANGED
@@ -20,13 +20,13 @@
20
20
  - **自定义样式**开关 —— 关闭即恢复官方默认品牌(`DSH 本地构建` / `DSH Local Build` + 构建版本徽章):品牌插槽整体注销,由外壳回退渲染;
21
21
  - **会话页标题**开关 —— 独立切换标题行(默认开启);
22
22
  - **轨迹页签**与 **Session 日志按钮**开关 —— 独立显示/隐藏会话页头部这两处外壳元素(默认都开启),纯 DOM 层操作,不影响背后的轨迹数据与导出功能;
23
- - **十种品牌颜色**可选(当前配色 = 官方 DeepSeek 蓝,为默认项),选择后立即生效并通过 Host 设置 RPC 持久化,刷新后仍在。
23
+ - **十种品牌颜色**可选(当前配色 = 官方 DeepSeek 蓝,为默认项),选择后立即生效并写回 Host 配置,刷新后仍在。
24
24
  - **实时预览** —— 设置页的预览区跟随两个状态渲染:自定义样式关闭时展示当前外壳回退品牌(鱼形标志、本地化标签与构建版本徽章)。
25
25
 
26
26
  ## 要求
27
27
 
28
28
  - Node.js `^22.19` 或 `>=24`
29
- - DeepSeek Harness `0.1.5-rc.1` 或兼容的 `0.1.5` 预发布版本,以及提供 `ctx.slots`、`ctx.remote.settings` 与 `ctx.locale` 的 **Web profile**
29
+ - DeepSeek Harness `0.1.7-alpha.1` 或兼容的 `0.1.7` 预发布版本,以及提供 `ctx.slots`、`ctx.configForms` 与 `ctx.locale` 的 **Web profile**
30
30
  - Web profile 中需带有插件声明的客户端模块(`@deepseek-ai/dsh-client-ui-primitives`、`-ui-sidebar`、`-ui-settings`、`-ui-renderer`、`-locale`、`dsh-api-remotes`);默认 Web profile 已包含
31
31
  - 产物不含任何指向 Harness checkout 的路径依赖
32
32
 
@@ -58,7 +58,7 @@ dsh plugin --profile web-brand remove @linbin-mk/dsh-brand-deepseek
58
58
 
59
59
  ## 设置
60
60
 
61
- Host 半侧注册 `dsh-brand-deepseek` 设置命名空间,字段默认值如下,通过 Harness 带修订保护的设置传输写入,因此修改立即生效、无需重启 Harness。命名空间名与包名**故意不同**:包名是加载标识(`@linbin-mk/dsh-brand-deepseek`),命名空间只用于持久化,改包名不会让已有设置失效。
61
+ Host 半侧把品牌样式声明成本插件的 Cordis `Config`:五个字段全部 `.volatile()`,因此都是可在运行时改写的活配置,默认值如下。设置命名空间就是 `cordis.patch.yml` 里的 loader id `brand-deepseek`——它与包名(加载标识 `@linbin-mk/dsh-brand-deepseek`)**故意不同**,改包名不会让已有设置失效。客户端半侧通过 `ctx.configForms.get('brand-deepseek')` 读写该行的配置表单,写入由 Harness 的设置域做修订号保护与串行化,因此修改立即生效、无需重启 Harness;非 loopback 页面(设置只能留在进程内存里)下写入控件整体禁用,不会假装已保存。
62
62
 
63
63
  | 设置 | 默认值 | 作用 |
64
64
  | --- | --- | --- |
@@ -70,7 +70,7 @@ Host 半侧注册 `dsh-brand-deepseek` 设置命名空间,字段默认值如
70
70
 
71
71
  ## 工作原理
72
72
 
73
- - **Host 半侧**(`lib/index.js`)只做一件事:注册 `dsh-brand-deepseek` 设置命名空间,让客户端半侧的 `settings.describe` / `settings.mutate` 调用被接受并持久化。
73
+ - **Host 半侧**(`lib/index.js`)只做两件事:声明品牌样式的 Cordis `Config`(五个 `.volatile()` 字段,即设置命名空间 `brand-deepseek` 的全部内容),并为自己这一行声明 `{ auto: false }`——本插件自带设置页,不需要侧边栏「插件」列表再自动生成一个。
74
74
  - **客户端半侧**(`lib/client.js`)是一个由 tsdown 打包、以 `window.__ModuleLoader__.load({ id, factory })` 自注册的 CJS bundle。`id` 必须等于**包名**——Harness 用入口名索引客户端模块表,名字不一致时该行解析失败、整个 GUI 起不来(而 `--dump-config` 仍显示正常)。`tsdown.config.ts` 直接从 `package.json` 取 `name` 写进 banner,`scripts/smoke-client.mjs` 再断言一次两者相等,因此重命名不会静默漂移。
75
75
  - **标题行样式**按 CSS Module 的 `<hash>_<local>` 形状匹配 Harness 的局部类名,并用会话外壳的 `[data-composer-seat]` 限定作用域、锚定在标题行自己的 `_fishHitbox` 后代上,因此不会波及其他模块(ContextMeter、ApprovalPanel)的同名 `.headline`。官方默认标题按它用过的**每个**局部名隐藏——`0.1.5-rc.1` 的 `_headlineText`,以及 `0.1.5-rc.2` 起包裹标题与徽章的 `_titleGroup`;没出现过该局部名的版本上,对应选择器只是空操作。若将来这些名字被再次改掉,插件退化为「品牌标题 + 官方标题并排显示」,而不是报错或让整个页面起不来。
76
76
  - **两个外壳开关**(轨迹页签、Session 日志按钮)按类名后缀定位元素、用本地化文案兜底,并用 `MutationObserver` 在 React 重挂载后重新应用;若两者都改名,元素保持可见,而不是误隐藏。
@@ -81,10 +81,11 @@ Host 半侧注册 `dsh-brand-deepseek` 设置命名空间,字段默认值如
81
81
  pnpm install
82
82
  pnpm build # src/ -> lib/(lib/ 已提交,安装后无需构建)
83
83
  pnpm build:check # 重新构建并断言 lib/ 与提交内容一致
84
- pnpm test # 两个冒烟测试
84
+ pnpm test # 四个冒烟测试
85
+ npx tsc -p tsconfig.json --noEmit # 类型检查(build.mjs 不跑 tsc)
85
86
  ```
86
87
 
87
- `pnpm test` 依次跑:`scripts/smoke-client.mjs`(按浏览器加载器的真实方式加载 `lib/client.js`,断言 bundle id == 包名、导出与 `inject` 契约)和 `scripts/smoke-visibility.mjs`(在 jsdom 里挂载 `apply()`,验证两个持久化开关驱动 DOM,包括重挂载后的观察者补偿与文案兜底定位)。jsdom 只是本包的开发依赖,运行时不需要。
88
+ `pnpm test` 依次跑:`scripts/smoke-client.mjs`(按浏览器加载器的真实方式加载 `lib/client.js`,断言 bundle id == 包名、导出与 `inject` 契约)、`scripts/smoke-host.mjs`(加载 `lib/index.js`,断言五个字段都是 volatile、默认值正确,且 Host 半侧以**自己的 fiber** 注册 `{ auto: false }`)、`scripts/smoke-visibility.mjs`(在 jsdom 里挂载 `apply()`,验证两个持久化开关驱动 DOM,包括重挂载后的观察者补偿与文案兜底定位)和 `scripts/smoke-config.mjs`(用假的配置表单挂载 `apply()`,验证按行 id `brand-deepseek` 取表单、读到的配置驱动运行时、开关经 `form.set` 写回、Host 拒绝写入或页面不可写时不假装已保存)。jsdom 只是本包的开发依赖,运行时不需要。
88
89
 
89
90
  ## 常见问题
90
91
 
@@ -105,7 +106,7 @@ pnpm test # 两个冒烟测试
105
106
 
106
107
  ## 隐私与作用域
107
108
 
108
- 插件不发起任何网络请求、不读取会话内容、不上报遥测。它只做三件事:注册插槽占位、读写 `dsh-brand-deepseek` 设置命名空间、在会话页头部按设置隐藏两个 DOM 元素(轨迹页签、Session 日志按钮)。设置值保存在 Harness 的 profile 设置里,不离开本机。隐藏元素只影响渲染,轨迹数据与导出功能不受影响。
109
+ 插件不发起任何网络请求、不读取会话内容、不上报遥测。它只做三件事:注册插槽占位、读写本插件 `brand-deepseek` 这一行的配置、在会话页头部按设置隐藏两个 DOM 元素(轨迹页签、Session 日志按钮)。设置值保存在 Harness 的 profile 配置里,不离开本机。隐藏元素只影响渲染,轨迹数据与导出功能不受影响。
109
110
 
110
111
  ## 许可证
111
112
 
package/lib/client.js CHANGED
@@ -196,6 +196,7 @@ window.__ModuleLoader__.load({
196
196
  trajectoryTab: true,
197
197
  sessionLogButton: true,
198
198
  color: DEFAULT_BRAND_COLOR,
199
+ writable: false,
199
200
  busy: false,
200
201
  error: null
201
202
  };
@@ -209,9 +210,24 @@ window.__ModuleLoader__.load({
209
210
  };
210
211
  };
211
212
  /**
212
- * Reload the persisted style. The load handler also applies the side
213
- * effects (brand slot registration and the color variable), so both the
214
- * boot refresh and every page visit converge on the persisted state.
213
+ * Adopt one Host-accepted style without a write (the first read, a write
214
+ * folded back, or an edit made from elsewhere). In-flight or failed
215
+ * operations keep their own status, so an accepted push never masks an error.
216
+ * @param state - accepted brand-style values.
217
+ * @param writable - whether the Host document accepts writes from this page.
218
+ */
219
+ adopt = (state, writable) => {
220
+ this.state = {
221
+ ...this.state,
222
+ ...state,
223
+ writable
224
+ };
225
+ this.emit();
226
+ };
227
+ /**
228
+ * Reload the accepted style. The load handler also applies the side effects
229
+ * (brand slot registration and the color variable), so both the boot refresh
230
+ * and every page visit converge on the accepted state.
215
231
  */
216
232
  refresh = async () => {
217
233
  const load = this.handlers?.load;
@@ -223,14 +239,10 @@ window.__ModuleLoader__.load({
223
239
  };
224
240
  this.emit();
225
241
  try {
226
- const { enabled, hero, trajectoryTab, sessionLogButton, color } = await load();
242
+ const accepted = await load();
227
243
  this.state = {
228
244
  ...this.state,
229
- enabled,
230
- hero,
231
- trajectoryTab,
232
- sessionLogButton,
233
- color,
245
+ ...accepted,
234
246
  busy: false,
235
247
  error: null
236
248
  };
@@ -254,10 +266,10 @@ window.__ModuleLoader__.load({
254
266
  };
255
267
  this.emit();
256
268
  try {
257
- await handler(enabled);
269
+ const accepted = await handler(enabled);
258
270
  this.state = {
259
271
  ...this.state,
260
- enabled,
272
+ ...accepted,
261
273
  busy: false,
262
274
  error: null
263
275
  };
@@ -281,10 +293,10 @@ window.__ModuleLoader__.load({
281
293
  };
282
294
  this.emit();
283
295
  try {
284
- await handler(hero);
296
+ const accepted = await handler(hero);
285
297
  this.state = {
286
298
  ...this.state,
287
- hero,
299
+ ...accepted,
288
300
  busy: false,
289
301
  error: null
290
302
  };
@@ -308,10 +320,10 @@ window.__ModuleLoader__.load({
308
320
  };
309
321
  this.emit();
310
322
  try {
311
- await handler(visible);
323
+ const accepted = await handler(visible);
312
324
  this.state = {
313
325
  ...this.state,
314
- trajectoryTab: visible,
326
+ ...accepted,
315
327
  busy: false,
316
328
  error: null
317
329
  };
@@ -335,10 +347,10 @@ window.__ModuleLoader__.load({
335
347
  };
336
348
  this.emit();
337
349
  try {
338
- await handler(visible);
350
+ const accepted = await handler(visible);
339
351
  this.state = {
340
352
  ...this.state,
341
- sessionLogButton: visible,
353
+ ...accepted,
342
354
  busy: false,
343
355
  error: null
344
356
  };
@@ -362,10 +374,10 @@ window.__ModuleLoader__.load({
362
374
  };
363
375
  this.emit();
364
376
  try {
365
- await handler(color);
377
+ const accepted = await handler(color);
366
378
  this.state = {
367
379
  ...this.state,
368
- color,
380
+ ...accepted,
369
381
  busy: false,
370
382
  error: null
371
383
  };
@@ -414,8 +426,11 @@ window.__ModuleLoader__.load({
414
426
  * the sidebar preview.
415
427
  *
416
428
  * The page remounts on every visit (the settings shell renders only the
417
- * active section), so it always starts from a fresh read of the persisted
418
- * document. Writes go through the Host settings RPC, owned by the apply half.
429
+ * active section), so it always starts from a fresh adoption of the accepted
430
+ * Config section. Writes go through the plugin's Config form, owned by the
431
+ * apply half; every control is disabled while a write is in flight and when
432
+ * the Host document cannot accept writes from this page (a non-loopback page
433
+ * keeps no durable settings).
419
434
  */
420
435
  /**
421
436
  * The sidebar's local-build badge format (ui-sidebar `localBuildVersion()`):
@@ -574,7 +589,7 @@ window.__ModuleLoader__.load({
574
589
  window.clearTimeout(timer);
575
590
  };
576
591
  }, [state.enabled]);
577
- const disabled = state.busy;
592
+ const disabled = state.busy || !state.writable;
578
593
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
579
594
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("settings.title") }),
580
595
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.intro") }),
@@ -1028,20 +1043,31 @@ window.__ModuleLoader__.load({
1028
1043
  "brand.heroDefaultBadge": "Preview"
1029
1044
  };
1030
1045
  //#endregion
1046
+ //#region src/settings.ts
1047
+ /**
1048
+ * Brand-style configuration shared by the plugin's two faces.
1049
+ *
1050
+ * The host half declares its Cordis Config as the volatile projection of this
1051
+ * shape, and the loader row id becomes the settings namespace; the client half
1052
+ * addresses that entry's live form through {@link BRAND_DEEPSEEK_ENTRY_ID}.
1053
+ * The module carries no imports of its own, so the client bundle inlines
1054
+ * declarations plus one string constant and nothing else.
1055
+ */
1056
+ /** Loader entry id (`cordis.patch.yml`) owning the brand-style settings. */
1057
+ const BRAND_DEEPSEEK_ENTRY_ID = "brand-deepseek";
1058
+ //#endregion
1031
1059
  //#region src/client/index.ts
1032
1060
  const inject = [
1033
1061
  "slots",
1034
1062
  "remote",
1035
- "remote.settings",
1063
+ "configForms",
1036
1064
  "locale"
1037
1065
  ];
1038
- /** Settings namespace holding the persisted brand style. */
1039
- const NAMESPACE = "dsh-brand-deepseek";
1040
1066
  /** Document-root color variable the brand SVGs fill from. */
1041
1067
  const COLOR_VAR = "--dsh-brand-deepseek-color";
1042
1068
  /** DOM id of the injected hero-headline sheet. */
1043
1069
  const HERO_STYLE_ID = "dsh-brand-deepseek-hero-style";
1044
- /** Read the persisted brand style; the defaults keep today's look. */
1070
+ /** Read the accepted brand style; the defaults keep today's look. */
1045
1071
  function defaultsOf(stored) {
1046
1072
  const value = stored ?? {};
1047
1073
  return {
@@ -1053,7 +1079,7 @@ window.__ModuleLoader__.load({
1053
1079
  };
1054
1080
  }
1055
1081
  /**
1056
- * Client plugin body: locale dictionaries, the persisted brand-style wiring
1082
+ * Client plugin body: locale dictionaries, the live brand-style wiring
1057
1083
  * (brand slot registration + color variable), and the settings page entry.
1058
1084
  * @param ctx - client root context.
1059
1085
  */
@@ -1062,8 +1088,8 @@ window.__ModuleLoader__.load({
1062
1088
  zh,
1063
1089
  en
1064
1090
  }), "dsh-brand-deepseek: dictionaries");
1065
- const api = ctx.remote.settings;
1066
1091
  const t = ctx.locale.bind("dsh-brand-deepseek");
1092
+ const form = ctx.configForms.get(BRAND_DEEPSEEK_ENTRY_ID);
1067
1093
  let brandDispose;
1068
1094
  const installBrand = () => {
1069
1095
  if (brandDispose !== void 0) return;
@@ -1108,7 +1134,7 @@ window.__ModuleLoader__.load({
1108
1134
  };
1109
1135
  const visibility = createVisibilityController();
1110
1136
  visibility.start();
1111
- /** Reconcile the runtime with one persisted style (idempotent). */
1137
+ /** Reconcile the runtime with one brand style (idempotent). */
1112
1138
  const applyState = (state) => {
1113
1139
  if (state.enabled) installBrand();
1114
1140
  else uninstallBrand();
@@ -1119,87 +1145,56 @@ window.__ModuleLoader__.load({
1119
1145
  visibility.setTrajectoryTab(state.trajectoryTab);
1120
1146
  visibility.setSessionLogButton(state.sessionLogButton);
1121
1147
  };
1122
- const readPersisted = async () => {
1123
- const response = await api.describe();
1124
- if (!response.ok) throw new Error(response.error.message);
1125
- return defaultsOf(response.value.namespaces.find((view) => view.ns === NAMESPACE)?.value);
1148
+ /** Whether the Host document accepts writes from this page. */
1149
+ const writable = () => {
1150
+ const snapshot = form.getSnapshot();
1151
+ return snapshot.status === "ready" && snapshot.writable && snapshot.mode === "host";
1126
1152
  };
1127
- const mutate = async (ops) => {
1128
- const response = await api.mutate(NAMESPACE, ops, void 0);
1129
- if (!response.ok) throw new Error(response.error.message);
1153
+ /**
1154
+ * Adopt the section the Host last accepted (the schema defaults while none
1155
+ * stands): apply its side effects and publish it to the settings page.
1156
+ */
1157
+ const adopt = () => {
1158
+ const state = defaultsOf(form.getSnapshot().value);
1159
+ applyState(state);
1160
+ brandStyle.adopt(state, writable());
1161
+ return state;
1130
1162
  };
1131
- const currentOf = () => {
1132
- const current = brandStyle.getSnapshot();
1133
- return {
1134
- enabled: current.enabled,
1135
- hero: current.hero,
1136
- trajectoryTab: current.trajectoryTab,
1137
- sessionLogButton: current.sessionLogButton,
1138
- color: current.color
1139
- };
1163
+ /**
1164
+ * Write one field. A page the Host refuses to persist never pretends to:
1165
+ * the write is skipped, and a refused write leaves the accepted section
1166
+ * standing for the page to re-adopt.
1167
+ */
1168
+ const write = async (field, value) => {
1169
+ if (!writable()) throw new Error("this page cannot persist plugin settings");
1170
+ if (!await form.set(field, value)) throw new Error(`the Host refused the "${field}" change`);
1140
1171
  };
1141
1172
  brandStyle.handlers = {
1142
- load: async () => {
1143
- const state = await readPersisted();
1144
- applyState(state);
1145
- return state;
1146
- },
1173
+ load: async () => adopt(),
1147
1174
  setEnabled: async (enabled) => {
1148
- await mutate([{
1149
- op: "set",
1150
- path: ["enabled"],
1151
- value: enabled
1152
- }]);
1153
- applyState({
1154
- ...currentOf(),
1155
- enabled
1156
- });
1175
+ await write("enabled", enabled);
1176
+ return adopt();
1157
1177
  },
1158
1178
  setHero: async (hero) => {
1159
- await mutate([{
1160
- op: "set",
1161
- path: ["hero"],
1162
- value: hero
1163
- }]);
1164
- applyState({
1165
- ...currentOf(),
1166
- hero
1167
- });
1179
+ await write("hero", hero);
1180
+ return adopt();
1168
1181
  },
1169
1182
  setTrajectoryTab: async (trajectoryTab) => {
1170
- await mutate([{
1171
- op: "set",
1172
- path: ["trajectoryTab"],
1173
- value: trajectoryTab
1174
- }]);
1175
- applyState({
1176
- ...currentOf(),
1177
- trajectoryTab
1178
- });
1183
+ await write("trajectoryTab", trajectoryTab);
1184
+ return adopt();
1179
1185
  },
1180
1186
  setSessionLogButton: async (sessionLogButton) => {
1181
- await mutate([{
1182
- op: "set",
1183
- path: ["sessionLogButton"],
1184
- value: sessionLogButton
1185
- }]);
1186
- applyState({
1187
- ...currentOf(),
1188
- sessionLogButton
1189
- });
1187
+ await write("sessionLogButton", sessionLogButton);
1188
+ return adopt();
1190
1189
  },
1191
1190
  setColor: async (color) => {
1192
- await mutate([{
1193
- op: "set",
1194
- path: ["color"],
1195
- value: color
1196
- }]);
1197
- applyState({
1198
- ...currentOf(),
1199
- color
1200
- });
1191
+ await write("color", color);
1192
+ return adopt();
1201
1193
  }
1202
1194
  };
1195
+ ctx.effect(() => form.subscribe(() => {
1196
+ adopt();
1197
+ }), "dsh-brand-deepseek: config form adoption");
1203
1198
  brandStyle.refresh();
1204
1199
  ctx.effect(() => () => {
1205
1200
  visibility.dispose();
package/lib/index.js CHANGED
@@ -1,26 +1,22 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  //#region src/index.ts
3
- const inject = ["settings"];
4
- /** Settings namespace holding the persisted brand style. */
5
- const NS = "dsh-brand-deepseek";
6
- /** Host plugin body — registers the namespace, nothing else. */
3
+ /** Config schema; the defaults match the plugin's shipped look. */
4
+ const Config = z.object({
5
+ enabled: z.boolean().default(true).volatile(),
6
+ hero: z.boolean().default(true).volatile(),
7
+ trajectoryTab: z.boolean().default(true).volatile(),
8
+ sessionLogButton: z.boolean().default(true).volatile(),
9
+ color: z.string().default("#4176e6").volatile()
10
+ });
11
+ /**
12
+ * Host plugin body: declare this instance's settings presentation. The client
13
+ * ships the Settings page, so the registry generates none of its own.
14
+ * @param ctx - host plugin context.
15
+ */
7
16
  function apply(ctx) {
8
- ctx.effect(() => {
9
- ctx.settings.register(NS, z.object({
10
- enabled: z.boolean().default(true),
11
- hero: z.boolean().default(true),
12
- trajectoryTab: z.boolean().default(true),
13
- sessionLogButton: z.boolean().default(true),
14
- color: z.string().default("#4176e6")
15
- }), { base: {
16
- enabled: true,
17
- hero: true,
18
- trajectoryTab: true,
19
- sessionLogButton: true,
20
- color: "#4176e6"
21
- } });
22
- return () => {};
23
- }, "dsh-brand-deepseek: settings namespace");
17
+ ctx.inject(["settings"], (child) => {
18
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
19
+ });
24
20
  }
25
21
  //#endregion
26
- export { apply, inject };
22
+ export { Config, apply };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linbin-mk/dsh-brand-deepseek",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Third-party DeepSeek Harness brand plugin: whale mark plus a minimal 'deepseek' wordmark for the Web client sidebar.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -32,7 +32,7 @@
32
32
  "scripts": {
33
33
  "build": "node build.mjs",
34
34
  "build:check": "node build.mjs && git diff --exit-code -- lib",
35
- "test": "node scripts/smoke-client.mjs && node scripts/smoke-visibility.mjs"
35
+ "test": "node scripts/smoke-client.mjs && node scripts/smoke-host.mjs && node scripts/smoke-visibility.mjs && node scripts/smoke-config.mjs"
36
36
  },
37
37
  "files": [
38
38
  "lib/**/*.js",
@@ -47,43 +47,65 @@
47
47
  },
48
48
  "packageManager": "pnpm@11.7.0",
49
49
  "dependencies": {
50
- "@deepseek-ai/schemastery": "^3.18.2"
50
+ "@deepseek-ai/schemastery": "^3.18.3"
51
51
  },
52
52
  "peerDependencies": {
53
- "@deepseek-ai/cordis": "^4.0.2",
54
- "@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.1",
55
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
56
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
57
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
58
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
59
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
60
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.1",
61
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
62
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1"
53
+ "@deepseek-ai/cordis": "^4.0.3",
54
+ "@deepseek-ai/dsh-api-remotes": "^0.1.7-alpha.1",
55
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1",
56
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-alpha.1",
57
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
58
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-alpha.1",
59
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-alpha.1",
60
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-alpha.1",
61
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
62
+ "@deepseek-ai/dsh-settings": "^0.1.7-alpha.1"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
- "@deepseek-ai/cordis": { "optional": true },
66
- "@deepseek-ai/dsh-api-remotes": { "optional": true },
67
- "@deepseek-ai/dsh-client-locale": { "optional": true },
68
- "@deepseek-ai/dsh-client-ui-conversation": { "optional": true },
69
- "@deepseek-ai/dsh-client-ui-primitives": { "optional": true },
70
- "@deepseek-ai/dsh-client-ui-renderer": { "optional": true },
71
- "@deepseek-ai/dsh-client-ui-settings": { "optional": true },
72
- "@deepseek-ai/dsh-client-ui-sidebar": { "optional": true },
73
- "@deepseek-ai/dsh-client-ui-slots": { "optional": true },
74
- "@deepseek-ai/dsh-settings": { "optional": true }
65
+ "@deepseek-ai/cordis": {
66
+ "optional": true
67
+ },
68
+ "@deepseek-ai/dsh-api-remotes": {
69
+ "optional": true
70
+ },
71
+ "@deepseek-ai/dsh-client-locale": {
72
+ "optional": true
73
+ },
74
+ "@deepseek-ai/dsh-client-ui-conversation": {
75
+ "optional": true
76
+ },
77
+ "@deepseek-ai/dsh-client-ui-primitives": {
78
+ "optional": true
79
+ },
80
+ "@deepseek-ai/dsh-client-ui-renderer": {
81
+ "optional": true
82
+ },
83
+ "@deepseek-ai/dsh-client-ui-settings": {
84
+ "optional": true
85
+ },
86
+ "@deepseek-ai/dsh-client-ui-sidebar": {
87
+ "optional": true
88
+ },
89
+ "@deepseek-ai/dsh-client-ui-slots": {
90
+ "optional": true
91
+ },
92
+ "@deepseek-ai/dsh-settings": {
93
+ "optional": true
94
+ }
75
95
  },
76
96
  "devDependencies": {
77
- "@deepseek-ai/cordis": "^4.0.2",
78
- "@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.1",
79
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
80
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
81
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
82
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
83
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
84
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.1",
85
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
86
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
97
+ "@deepseek-ai/cordis": "^4.0.3",
98
+ "@deepseek-ai/dsh-api-remotes": "^0.1.7-alpha.1",
99
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1",
100
+ "@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1",
101
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-alpha.1",
102
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
103
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-alpha.1",
104
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-alpha.1",
105
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-alpha.1",
106
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
107
+ "@deepseek-ai/dsh-settings": "^0.1.7-alpha.1",
108
+ "@types/node": "^22.20.4",
87
109
  "@types/react": "^18.3.1",
88
110
  "jsdom": "^25.0.1",
89
111
  "react": "^18.2.0",