dsh-web-icon-indicator 0.5.0 → 0.5.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
@@ -40,17 +40,17 @@
40
40
  * Configuration object (all optional):
41
41
  * iconsDir Absolute directory holding base.svg. Default `<package>/icons/`.
42
42
  * statusPath JSON status endpoint path. Default `/dsh-web-icon-status.json`.
43
- * Registration-time: composition entry only (never settings.yaml).
43
+ * Registration-time (non-volatile): composition entry only.
44
44
  * iconPathPrefix URL prefix for static icon files. Default `/dsh-web-icon-indicator`.
45
- * Registration-time: composition entry only (never settings.yaml).
45
+ * Registration-time (non-volatile): composition entry only.
46
46
  * askingHoldMs Minimum visibility for the asking state (ms). Default 3500.
47
47
  * doneHoldMs How long the done state stays before falling back to idle.
48
48
  * Default 5000.
49
49
  * defaultColor Default icon color — the idle whale's primary color, as
50
50
  * `#rgb` / `#rrggbb`. Absent = the idle state's own colors[0]
51
51
  * (today's behavior). Give each DSH instance (its own profile /
52
- * settings.yaml) a different value to tell their browser tabs
53
- * apart. A value perceptually too close to another state's
52
+ * user settings layer) a different value to tell their browser
53
+ * tabs apart. A value perceptually too close to another state's
54
54
  * color raises a WARNING — surfaced in the settings card, in
55
55
  * the host log, and as `warnings` on the status endpoint — but
56
56
  * is still honored.
@@ -67,12 +67,23 @@
67
67
  * - speed: optional per-state cycle in ms (blink toggle interval too).
68
68
  * Default 1200.
69
69
  *
70
- * The whole config surface is also registered with the DSH settings service
71
- * (namespace `web-icon-indicator`): validated against a schemastery schema,
72
- * persisted to the profile's `settings.yaml`, and editable from
73
- * 设置 → 插件 → 插件配置 in the Web GUI (see the `dsh.client` browser half).
74
- * While the profile composes no settings service, the plugin keeps working
75
- * exactly as before, reading the composition entry directly.
70
+ * The whole config surface is the plugin's Cordis `Config` schema (exported as
71
+ * both `Config` and `CONFIG_SCHEMA`), and the same schema is registered with
72
+ * the LEGACY settings service when the running host still exposes
73
+ * `installSection` — one bundle serves every published host generation:
74
+ *
75
+ * - DSH ≥ 0.1.7: the settings service projects the schema's `.volatile()`
76
+ * fields into a live form, persisted into the profile patch
77
+ * (`~/.dsh/profiles/<profile>/cordis.patch.yml`); a write mutates the
78
+ * running plugin's config references in place and emits
79
+ * `loader/volatile-update` instead of restarting it.
80
+ * - DSH ≤ 0.1.6-alpha.1: the plugin registers the namespace itself under
81
+ * `web-icon-indicator` and the service drives it through
82
+ * `setSource`/`onChange`, persisted into `~/.dsh/settings.yaml`.
83
+ *
84
+ * `statusPath` / `iconPathPrefix` stay non-volatile in both. When the profile
85
+ * composes no settings service at all, the plugin keeps working on the
86
+ * composition entry + defaults it was mounted with.
76
87
  *
77
88
  * @module dsh-web-icon-indicator
78
89
  */
@@ -104,21 +115,54 @@ const EFFECT_NAMES = ["static", "blink", "breath", "rainbow", "heartbeat", "boun
104
115
  const STATE_NAMES = ["idle", "running", "asking", "done"];
105
116
 
106
117
  /**
107
- * Settings namespace under which this plugin's config is registered (the
108
- * `web-icon-indicator:` section of the profile's settings.yaml, surfaced in
109
- * 设置 → 插件 → 插件配置). Lowercase kebab, per the settings domain's rule.
118
+ * Settings namespace of this plugin on the MODERN host line (DSH ≥ 0.1.7):
119
+ * the settings namespace IS the profile entry id (`schema(entry)` reads
120
+ * `entry.fiber.runtime.Config` and `describe()` keys every form by
121
+ * `entry.options.id`), and this plugin's bundle patch declares exactly one row:
122
+ * `dsh-web-icon-indicator`. The browser half reads the same namespace through
123
+ * `ctx.configForms.get(...)`, so the two halves must keep this literal in sync
124
+ * (it is also the row id in `cordis.patch.yml`).
125
+ */
126
+ const SETTINGS_NAMESPACE = "dsh-web-icon-indicator";
127
+
128
+ /**
129
+ * Settings namespace on the LEGACY host line (DSH ≤ 0.1.6-alpha.1), where the
130
+ * consumer-owned namespace is an arbitrary string it passes to
131
+ * `settings.installSection` and the settings document section is named after
132
+ * it. Kept at the historical value so an existing 0.5.x user's
133
+ * `web-icon-indicator:` section keeps working after this upgrade; the browser
134
+ * half binds its legacy scope to the same string.
135
+ */
136
+ const LEGACY_SETTINGS_NAMESPACE = "web-icon-indicator";
137
+
138
+ /**
139
+ * Mark one field as LIVE when the resolved schemastery supports it.
110
140
  *
111
- * Since DSH 0.1.2 the namespace is a plain string literal (the type-level
112
- * `SettingsNamespace` brand exists only at compile time; the runtime value is
113
- * the same string), so no helper import is needed here.
141
+ * `.volatile()` exists since schemastery 3.18.3 and is what makes the modern
142
+ * settings service render a field at all (`volatileForm()` keeps volatile
143
+ * fields only) and what makes a settings write mutate the running plugin's
144
+ * references + emit `loader/volatile-update` instead of restarting it. On the
145
+ * legacy host line the installed schemastery (3.18.2) has no `.volatile()`,
146
+ * and the legacy settings service owns the whole live-form lifecycle through
147
+ * `installSection`/`setSource`/`onChange`, so the same schema object works
148
+ * unwrapped there.
114
149
  */
115
- const SETTINGS_NAMESPACE = "web-icon-indicator";
150
+ const LIVE = (schema) => (typeof schema.volatile === "function" ? schema.volatile() : schema);
116
151
 
117
152
  /**
118
- * Schemastery schema mirroring `DEFAULTS`. Registered with the settings
119
- * service so the config is validated, persisted, and editable from the Web
120
- * settings page; resolution order is schema defaults → composition `base`
121
- * → user layer (`~/.dsh/settings.yaml`).
153
+ * Schemastery schema mirroring `DEFAULTS`. Exported as the plugin's `Config`,
154
+ * which is what the Cordis loader validates the composition row against and
155
+ * what the modern host settings service projects into its live form
156
+ * (`volatileForm` keeps only `.volatile()` fields, so the settings page can
157
+ * never advertise a key it cannot honor).
158
+ *
159
+ * `.volatile()` marks a field as LIVE: the loader commits a settings write
160
+ * into the running fiber's references and emits `loader/volatile-update`
161
+ * instead of restarting the plugin (see `apply`). Ordinary (non-volatile)
162
+ * fields — `statusPath` / `iconPathPrefix` — are baked into the route table
163
+ * and the injected script at registration time, so a document change to them
164
+ * could never take effect; keeping them non-volatile removes them from the
165
+ * live form while still validating the composition entry.
122
166
  */
123
167
  const STATE_CONFIG_SCHEMA = z.object({
124
168
  effect: z.union(EFFECT_NAMES).default("static"),
@@ -129,24 +173,24 @@ const STATE_CONFIG_SCHEMA = z.object({
129
173
  });
130
174
 
131
175
  const CONFIG_SCHEMA = z.object({
132
- askingHoldMs: z.number().min(0).default(DEFAULTS.askingHoldMs),
133
- doneHoldMs: z.number().min(0).default(DEFAULTS.doneHoldMs),
176
+ askingHoldMs: LIVE(z.number().min(0).default(DEFAULTS.askingHoldMs)),
177
+ doneHoldMs: LIVE(z.number().min(0).default(DEFAULTS.doneHoldMs)),
134
178
  // Default icon color (`#rgb` / `#rrggbb`). Deliberately WITHOUT a schema
135
179
  // default: an absent value keeps the idle state's own colors[0], exactly as
136
180
  // before this key existed. `resolveConfig` folds a valid value into
137
181
  // `states.idle.colors[0]` and drops an invalid one with a warning.
138
- defaultColor: z.string(),
139
- // `statusPath` / `iconPathPrefix` are deliberately NOT part of this schema.
140
- // They are baked at registration time (the route table and the injected
141
- // script's URLs are built in apply()), so a settings-document change to them
142
- // could never take effect: the browser would poll the new path while the
143
- // server still served the old one — a dead favicon. Keeping them out of the
144
- // schema means the settings surface cannot advertise a key it cannot honor;
145
- // they stay composition-entry only (see the README config table).
146
- iconsDir: z.string(),
182
+ defaultColor: LIVE(z.string()),
183
+ iconsDir: LIVE(z.string()),
147
184
  // dict (not a four-key object): the user layer may override only some
148
- // states; the plugin merges the resolved value over DEFAULTS anyway.
149
- states: z.dict(STATE_CONFIG_SCHEMA).default(DEFAULTS.states),
185
+ // states; the plugin merges the resolved value over DEFAULTS anyway. The
186
+ // whole dict is one volatile field, so the custom card can stage and write
187
+ // it atomically through `scope.mutate([{ op: 'set', path: ['states'], … }])`.
188
+ states: LIVE(z.dict(STATE_CONFIG_SCHEMA).default(DEFAULTS.states)),
189
+ // Registration-time keys: route table + injected script URLs are built in
190
+ // apply(), so they are composition-entry only (non-volatile ⇒ absent from
191
+ // the live settings form). See the README config table.
192
+ statusPath: z.string().default(DEFAULTS.statusPath),
193
+ iconPathPrefix: z.string().default(DEFAULTS.iconPathPrefix),
150
194
  });
151
195
 
152
196
  /** Browser script injected into every served index.html response. */
@@ -695,34 +739,49 @@ function colorWarnings(states) {
695
739
  }
696
740
 
697
741
  /**
698
- * Cordis plugin entry. Returns the standard `{ apply, inject, config }`
699
- * shape so the host composition can mount it once at startup.
742
+ * Cordis plugin entry. Returns the standard `{ apply, inject, Config }`
743
+ * shape so the host composition can mount it once at startup. `Config` (the
744
+ * schemastery schema) is what the loader validates the row against and what
745
+ * the host settings service projects into its live form — a plugin without it
746
+ * has no server-declared configuration surface at all. One bundle serves both
747
+ * host lines: the legacy `settings.installSection` path is feature-detected at
748
+ * runtime (see `apply`).
700
749
  */
701
750
  export default {
702
751
  name: "dsh-web-icon-indicator",
703
752
  inject: ["webServer", "timer", "agents", "fs"],
704
- config: {
705
- askingHoldMs: DEFAULTS.askingHoldMs,
706
- doneHoldMs: DEFAULTS.doneHoldMs,
707
- statusPath: DEFAULTS.statusPath,
708
- iconPathPrefix: DEFAULTS.iconPathPrefix,
709
- iconsDir: undefined,
710
- defaultColor: undefined,
711
- states: DEFAULTS.states,
712
- },
713
- /** Settings namespace + validation schema (exported for reuse/tooling). */
753
+ Config: CONFIG_SCHEMA,
754
+ /** Settings namespace (the profile entry id on ≥0.1.7) + schema, for tooling. */
714
755
  SETTINGS_NAMESPACE,
756
+ /** Settings namespace used on the legacy (≤0.1.6-alpha.1) settings service. */
757
+ LEGACY_SETTINGS_NAMESPACE,
715
758
  CONFIG_SCHEMA,
716
759
  apply(ctx, config) {
717
- // Cordis passes the resolved composition config as the second argument
718
- // (`apply(ctx, config)`). `config` is the row's own `config` from the
719
- // profile composition (merged over the schema only when a `Config` schema
720
- // is declared); this plugin merges its `DEFAULTS` itself via `resolveConfig`,
721
- // so an absent row config simply yields the defaults. Fall back to the old
722
- // `ctx.get("config")` read for compatibility (it is undefined on the host
723
- // but kept for the zero-dep test harness and older hosts).
724
- const entry = config ?? ctx.get("config") ?? {};
725
- let source = () => entry;
760
+ // Cordis passes the schema-resolved config as the second argument
761
+ // (`apply(ctx, config)`). Every `.volatile()` field arrives as a live
762
+ // reference (`config.states.get()`), and the loader mutates those
763
+ // references in place when a settings write lands — that is what
764
+ // `loader/volatile-update` below reacts to. Unwrap the references once per
765
+ // read so the rest of this file keeps working on plain values. The
766
+ // `ctx.get("config")` fallback keeps the zero-dependency test harness (and
767
+ // any host that hands the row config through the service container) working.
768
+ const readRef = (value) =>
769
+ value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
770
+ const readConfig = (raw) => {
771
+ const out = {};
772
+ for (const [key, value] of Object.entries(raw || {})) {
773
+ const plain = readRef(value);
774
+ if (plain !== undefined) out[key] = plain;
775
+ }
776
+ return out;
777
+ };
778
+ // Legacy host line (DSH ≤ 0.1.6-alpha.1): `settings.installSection` owns the
779
+ // namespace lifecycle and hands this plugin a live source through
780
+ // `setSource`. Until it does, the composition config passed to `apply` (or
781
+ // the zero-dep test harness's `ctx.get("config")`) is the source.
782
+ let legacySource = null;
783
+ const source = () =>
784
+ readConfig(legacySource ? legacySource() : config ?? ctx.get("config") ?? {});
726
785
  // Merge each configured state over its default: { effect, colors, speed }.
727
786
  // Idempotent on schema-resolved values, so it doubles as the fallback
728
787
  // normalization when the profile composes no settings service.
@@ -806,54 +865,72 @@ export default {
806
865
  }
807
866
  } catch (e) {}
808
867
  };
809
- let cfg = resolveConfig(entry);
868
+ let cfg = resolveConfig(source());
810
869
  logWarnings(cfg.warnings);
811
870
  // The injected script bakes config at injection time; rebuild it whenever
812
- // the settings section changes so the NEXT page load picks the new values
813
- // up (reload the tab — same contract as before this registration).
871
+ // the live config changes so the NEXT page load picks the new values up
872
+ // (reload the tab — the running tab is already carried by the status poll's
873
+ // `states` echo).
814
874
  const buildScript = (c) => INJECTED_SCRIPT
815
875
  .replace("__STATUS_PATH__", c.statusPath)
816
876
  .replace("__BASE_PATH__", c.iconPathPrefix + "/base.svg")
817
877
  .replace("__CFG__", JSON.stringify({ states: c.states }));
818
878
  let script = buildScript(cfg);
819
879
  let iconsDir = resolveIconsDir(cfg.iconsDir);
820
- // Register the settings namespace with the host `settings` service. Since
821
- // DSH 0.1.2 the standalone `installSettingsSection` export is gone — the
822
- // host exposes a `settings` service whose `installSection(owner, ns,
823
- // schema, entry, hooks)` carries the same attach/detach lifecycle (base
824
- // layer + `setSource` sink + `onChange` notification). The host's own
825
- // plugins all use `ctx.inject(["settings"], …)` so the section attaches as
826
- // soon as the provider is available — a plain `ctx.get("settings")` can run
827
- // before the provider is ready and silently skip the registration. When no
828
- // settings provider is composed the callback never fires and the plugin
829
- // keeps working on its composition entry + DEFAULTS only.
830
- ctx.inject(["settings"], (settingsCtx) => {
831
- settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, CONFIG_SCHEMA, entry, {
832
- setSource: (next) => { source = next; },
833
- onChange: () => {
834
- // Settings service mounted, or the section changed: re-resolve, then
835
- // mutate cfg in place so the state-machine closures (asking/done hold)
836
- // and the injected script see the new values immediately.
837
- // `statusPath` / `iconPathPrefix` are NOT taken from the settings
838
- // source: they are baked into the route table and the injected script
839
- // at registration time, so honoring a settings edit here would break
840
- // the live poll (see CONFIG_SCHEMA).
841
- const next = resolveConfig(source());
842
- cfg.askingHoldMs = next.askingHoldMs;
843
- cfg.doneHoldMs = next.doneHoldMs;
844
- if (next.iconsDir !== undefined) cfg.iconsDir = next.iconsDir;
845
- cfg.states = next.states;
846
- // `defaultColor` / `warnings` ride along with `states`: the color is
847
- // already folded into `states.idle.colors[0]`, so the browser needs no
848
- // new key — they are kept for observability (status echo + log).
849
- cfg.defaultColor = next.defaultColor;
850
- cfg.warnings = next.warnings;
851
- iconsDir = resolveIconsDir(cfg.iconsDir);
852
- script = buildScript(cfg);
853
- logWarnings(cfg.warnings);
854
- },
880
+ // Live settings, modern line (DSH ≥ 0.1.7). The host owns the whole
881
+ // configuration lifecycle: the loader validates the composition row against
882
+ // `Config`, the `settings` service (`SettingsForms`) derives the live form
883
+ // from the schema's `.volatile()` fields and writes them into the profile
884
+ // patch, and a volatile-only write is committed into the running fiber's
885
+ // references with a `loader/volatile-update` event instead of a restart.
886
+ // This listener rebuilds the cached `cfg` and the injected script from the
887
+ // same references; the legacy line below drives it through `onChange`
888
+ // instead. `statusPath` / `iconPathPrefix` are NOT volatile: they are baked
889
+ // into the route table and the injected script at registration time, so a
890
+ // document change to them re-applies the whole plugin instead (they stay
891
+ // outside the live form — see CONFIG_SCHEMA).
892
+ const refreshFromConfig = () => {
893
+ const next = resolveConfig(source());
894
+ cfg.askingHoldMs = next.askingHoldMs;
895
+ cfg.doneHoldMs = next.doneHoldMs;
896
+ if (next.iconsDir !== undefined) cfg.iconsDir = next.iconsDir;
897
+ cfg.states = next.states;
898
+ // `defaultColor` / `warnings` ride along with `states`: the color is
899
+ // already folded into `states.idle.colors[0]`, so the browser needs no
900
+ // new key — they are kept for observability (status echo + log).
901
+ cfg.defaultColor = next.defaultColor;
902
+ cfg.warnings = next.warnings;
903
+ iconsDir = resolveIconsDir(cfg.iconsDir);
904
+ script = buildScript(cfg);
905
+ logWarnings(cfg.warnings);
906
+ };
907
+ // A plain `ctx.on` listener is enough: the loader emits this event on the
908
+ // owning fiber's context only, so a volatile write to another plugin's
909
+ // config never reaches here. Guard for the zero-dep test harness, whose
910
+ // fake ctx carries no event bus.
911
+ if (typeof ctx.on === "function") ctx.on("loader/volatile-update", refreshFromConfig);
912
+ // Legacy host line (DSH ≤ 0.1.6-alpha.1): the `settings` service exposes
913
+ // `installSection`, and the plugin must register its namespace itself —
914
+ // that service method was removed in 0.1.7, where the exported `Config`
915
+ // schema plus `loader/volatile-update` carry the same lifecycle. Feature-
916
+ // detect instead of switching on a version: whichever service the running
917
+ // host provides decides the path, so one bundle serves both lines.
918
+ // The base layer is a RESOLVED PLAIN COPY (`resolveConfig(source())`), not
919
+ // the raw `apply` config: the legacy loader deep-freezes the config it
920
+ // resolved against the exported `Config` schema, and the legacy settings
921
+ // service resolves the schema over the base IN PLACE (its `mergeLayers`
922
+ // returns the base unchanged when no user section exists) — handing it a
923
+ // frozen object throws "Cannot assign to read only property".
924
+ if (typeof ctx.inject === "function") {
925
+ ctx.inject(["settings"], (settingsCtx) => {
926
+ const settings = settingsCtx.settings;
927
+ if (!settings || typeof settings.installSection !== "function") return;
928
+ settings.installSection(ctx, LEGACY_SETTINGS_NAMESPACE, CONFIG_SCHEMA, resolveConfig(source()), {
929
+ setSource: (next) => { legacySource = next; },
930
+ onChange: refreshFromConfig,
931
+ });
855
932
  });
856
- });
933
+ }
857
934
  const webServer = ctx.webServer;
858
935
  const agents = ctx.agents;
859
936
  const fs = ctx.fs;
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Browser half of dsh-web-icon-indicator (dsh.client bundle).
3
3
  *
4
- * Registers one card into the shared plugin-configuration surface
5
- * (`settings.plugin.item` slot, keyed by the `web-icon-indicator` settings
6
- * namespace). The card is inert unless the host serves the namespace, which
7
- * the dispatching tab (`dsh-client-ui-settings-plugins`) checks for us.
4
+ * Registers one configuration page and serves both host generations from one
5
+ * bundle: `plugins.row.config` (keyed `dsh-web-icon-indicator#dsh-web-icon-indicator`)
6
+ * bound through the modern `configForms` service on DSH ≥ 0.1.7, and
7
+ * `settings.plugin.item` (keyed by the legacy `web-icon-indicator` namespace)
8
+ * bound through the legacy `settingsScope` service on DSH ≤ 0.1.6-alpha.1.
9
+ * Neither settings provider appears in `inject`: both are read weakly at render
10
+ * time, so the fiber activates on either line.
8
11
  */
9
12
 
10
- /** Cordis services this browser plugin injects. */
11
- export declare const inject: readonly ["slots", "settingsScope", "locale"];
13
+ /**
14
+ * Cordis services this browser plugin injects. Only the two services every
15
+ * host generation provides; the settings providers are optional weak reads.
16
+ */
17
+ export declare const inject: readonly ["slots", "locale"];
12
18
 
13
- /** Mount the settings card for the `web-icon-indicator` namespace. */
19
+ /** Mount the settings page on whichever host generation is running. */
14
20
  export declare function apply(ctx: unknown): void;
@@ -163,17 +163,26 @@ export interface DshWebIconColorWarning {
163
163
  }
164
164
 
165
165
  /**
166
- * Cordis plugin entry: `{ name, inject, config, apply, SETTINGS_NAMESPACE,
167
- * CONFIG_SCHEMA }`. Mount once per profile through the bundle patch
168
- * (`cordis.patch.yml`), never as a session-scoped agent preset.
166
+ * Cordis plugin entry: `{ name, inject, Config, apply, SETTINGS_NAMESPACE,
167
+ * LEGACY_SETTINGS_NAMESPACE, CONFIG_SCHEMA }`. Mount once per profile through
168
+ * the bundle patch (`cordis.patch.yml`), never as a session-scoped agent
169
+ * preset.
170
+ *
171
+ * `Config` is the schemastery schema the loader validates the profile row
172
+ * against and the host settings service projects into its live form
173
+ * (`.volatile()` fields only); this entry has no `config` object. The same
174
+ * bundle serves the legacy host line through a feature-detected
175
+ * `settings.installSection` registration.
169
176
  */
170
177
  declare const plugin: {
171
178
  name: "dsh-web-icon-indicator";
172
179
  inject: readonly ["webServer", "timer", "agents", "fs"];
173
- config: DshWebIconIndicatorConfig;
180
+ Config: unknown;
174
181
  apply(ctx: unknown, config?: unknown): void;
175
- /** Settings namespace carrying the config (`web-icon-indicator`). */
182
+ /** Settings namespace on DSH ≥ 0.1.7 (= the profile entry id). */
176
183
  SETTINGS_NAMESPACE: string;
184
+ /** Settings namespace on the legacy settings service (DSH ≤ 0.1.6-alpha.1). */
185
+ LEGACY_SETTINGS_NAMESPACE: string;
177
186
  /** Schemastery schema validating the config surface. */
178
187
  CONFIG_SCHEMA: unknown;
179
188
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-web-icon-indicator",
3
3
  "description": "Browser tab favicon reflects the DSH session state: idle / running / asking / done, recolored and animated from one base SVG in the browser (fully configurable colors + effects). · 浏览器标签页 favicon 实时反映 DSH 会话状态:待机 / 运行中 / 提问 / 完成,由单个 base.svg 在浏览器中动态上色与动画(颜色与特效完全可配置)。",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "dsh": ">=0.1.2-rc.1"
@@ -27,7 +27,8 @@
27
27
  "@deepseek-ai/dsh-client-locale",
28
28
  "@deepseek-ai/dsh-client-ui-renderer",
29
29
  "@deepseek-ai/dsh-client-ui-settings",
30
- "@deepseek-ai/dsh-client-ui-settings-plugins"
30
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
31
+ "@deepseek-ai/dsh-client-ui-plugin-manager"
31
32
  ]
32
33
  }
33
34
  },