@fonlan/dsh-model-router 0.3.6 → 0.3.7

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
@@ -311,10 +311,36 @@ function setOrder(config, modelId, order, catalogProviderIds) {
311
311
  changed: true
312
312
  };
313
313
  }
314
+ /**
315
+ * Dereference one config field as the loader may hand it over.
316
+ *
317
+ * dsh >= 0.1.7 owns plugin configuration as entry config, and every field the
318
+ * `Config` schema marks `.volatile()` arrives as a stable REFERENCE
319
+ * (`{ get() }`, schemastery's `Volatile<T>`) instead of a plain value — a
320
+ * reference serializes to `{}`. Reading such a field raw silently yields
321
+ * `models: {}`, `modelSort: 'custom'`, an empty `modelOrder` and no
322
+ * `recentlyUsed`: the stored document is dropped, and the next persist
323
+ * overwrites it with catalog defaults. Plain values pass through unchanged, so
324
+ * this is safe for the write paths and for callers that already hold a
325
+ * resolved document.
326
+ */
327
+ function derefField(value) {
328
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
329
+ return value;
330
+ }
331
+ /** Dereference every own field of a config object (per-field volatile refs). */
332
+ function derefConfig(value) {
333
+ const root = derefField(value);
334
+ if (root === null || typeof root !== "object" || Array.isArray(root)) return root;
335
+ const out = {};
336
+ for (const [key, field] of Object.entries(root)) out[key] = derefField(field);
337
+ return out;
338
+ }
314
339
  /** Shape-validate an unknown value as a RouterConfigShape (lenient). */
315
340
  function normalizeConfig(value) {
316
341
  const models = {};
317
- if (typeof value !== "object" || value === null) return {
342
+ const source = derefConfig(value);
343
+ if (typeof source !== "object" || source === null) return {
318
344
  models,
319
345
  showQuickSwitch: true,
320
346
  ignoreModelIdPrefix: true,
@@ -322,7 +348,7 @@ function normalizeConfig(value) {
322
348
  modelOrder: [],
323
349
  recentlyUsed: {}
324
350
  };
325
- const root = value;
351
+ const root = source;
326
352
  const showQuickSwitch = root.showQuickSwitch !== false;
327
353
  const ignoreModelIdPrefix = root.ignoreModelIdPrefix !== false;
328
354
  const modelSort = root.modelSort === "name" || root.modelSort === "recent" ? root.modelSort : "custom";
@@ -589,25 +615,17 @@ const RouterConfigSchema = z.object({
589
615
  models: z.dict(z.object({
590
616
  order: z.array(z.string()),
591
617
  active: z.string()
592
- })),
593
- showQuickSwitch: z.boolean(),
594
- ignoreModelIdPrefix: z.boolean(),
618
+ })).volatile(),
619
+ showQuickSwitch: z.boolean().volatile(),
620
+ ignoreModelIdPrefix: z.boolean().volatile(),
595
621
  modelSort: z.union([
596
622
  z.const("custom"),
597
623
  z.const("name"),
598
624
  z.const("recent")
599
- ]),
600
- modelOrder: z.array(z.string()),
601
- recentlyUsed: z.dict(z.number())
625
+ ]).volatile(),
626
+ modelOrder: z.array(z.string()).volatile(),
627
+ recentlyUsed: z.dict(z.number()).volatile()
602
628
  });
603
- const EMPTY_CONFIG = {
604
- models: {},
605
- showQuickSwitch: true,
606
- ignoreModelIdPrefix: true,
607
- modelSort: "custom",
608
- modelOrder: [],
609
- recentlyUsed: {}
610
- };
611
629
  var ModelRouterService = class {
612
630
  ctx;
613
631
  cached = {
@@ -619,22 +637,30 @@ var ModelRouterService = class {
619
637
  };
620
638
  inflight = null;
621
639
  cachedAt = 0;
622
- scope;
640
+ /** Optional host settings service; absent in profiles without one (no persistence). */
641
+ settings;
642
+ /** Live config document (seeded from the entry config, updated on write). */
643
+ configDoc;
623
644
  registration;
624
645
  /** How long a built catalog is considered fresh before being rebuilt. */
625
646
  catalogTtlMs = 3e4;
626
- /** Debounced recent-use persistence (see noteModelUsed / flushUsage). */
627
- usageFlushMs = 1500;
647
+ /**
648
+ * Debounced recent-use persistence (see noteModelUsed / flushUsage). Kept
649
+ * long on purpose: every persist writes the entry config, which restarts
650
+ * this plugin's entry, so usage bookkeeping must not stream live traffic.
651
+ */
652
+ usageFlushMs = 6e4;
628
653
  usageDebounce = null;
629
654
  pendingUsage = {};
630
655
  usageFlushRunning = false;
631
- constructor(ctx) {
656
+ constructor(ctx, config) {
632
657
  this.ctx = ctx;
658
+ this.configDoc = normalizeConfig(config);
633
659
  }
634
- /** Register settings + adapter and wire topology listeners. */
660
+ /** Register the settings seam + adapter and wire topology listeners. */
635
661
  start() {
636
662
  this.ctx.inject(["settings"], (sctx) => {
637
- this.scope = sctx.settings.register(NS, RouterConfigSchema);
663
+ this.settings = sctx.settings;
638
664
  });
639
665
  const adapter = new ModelRouterAdapter(this);
640
666
  this.registration = this.ctx.llm.registerAdapter([ROUTER_PROVIDER_ID], adapter);
@@ -648,7 +674,7 @@ var ModelRouterService = class {
648
674
  clearTimeout(this.usageDebounce);
649
675
  this.usageDebounce = null;
650
676
  }
651
- if (Object.keys(this.pendingUsage).length > 0) this.flushUsage();
677
+ this.pendingUsage = {};
652
678
  this.registration?.();
653
679
  this.registration = void 0;
654
680
  }
@@ -659,12 +685,7 @@ var ModelRouterService = class {
659
685
  };
660
686
  }
661
687
  config() {
662
- if (this.scope === void 0) return EMPTY_CONFIG;
663
- try {
664
- return normalizeConfig(this.scope.get());
665
- } catch {
666
- return EMPTY_CONFIG;
667
- }
688
+ return this.configDoc;
668
689
  }
669
690
  providerNames() {
670
691
  return this.cached.providerNames;
@@ -679,7 +700,7 @@ var ModelRouterService = class {
679
700
  * write.
680
701
  */
681
702
  noteModelUsed(modelId) {
682
- if (this.scope === void 0) return;
703
+ if (this.settings === void 0) return;
683
704
  this.pendingUsage[modelId] = Date.now();
684
705
  if (this.usageDebounce !== null) return;
685
706
  this.usageDebounce = setTimeout(() => {
@@ -694,9 +715,9 @@ var ModelRouterService = class {
694
715
  const pending = this.pendingUsage;
695
716
  this.pendingUsage = {};
696
717
  try {
697
- if (this.scope === void 0 || Object.keys(pending).length === 0) return;
718
+ if (this.settings === void 0 || Object.keys(pending).length === 0) return;
698
719
  const current = this.config();
699
- await this.scope.replace({
720
+ await this.writeConfig({
700
721
  ...current,
701
722
  recentlyUsed: {
702
723
  ...current.recentlyUsed,
@@ -743,12 +764,12 @@ var ModelRouterService = class {
743
764
  /** Refresh (forced), then persist any config drift (migration, new/vanished models). */
744
765
  async refreshAndReconcile() {
745
766
  const catalog = await this.refreshCatalog(true);
746
- if (this.scope === void 0) return;
767
+ if (this.settings === void 0) return;
747
768
  const current = this.config();
748
769
  const { models, modelOrder, changed } = reconcileConfig(current, catalog.models);
749
770
  if (!changed) return;
750
771
  try {
751
- await this.scope.replace({
772
+ await this.writeConfig({
752
773
  ...current,
753
774
  models,
754
775
  ...modelOrder === void 0 ? {} : { modelOrder }
@@ -770,6 +791,7 @@ var ModelRouterService = class {
770
791
  const names = catalog.providerNames;
771
792
  const creds = catalog.credentialConfigured;
772
793
  return {
794
+ writable: this.settings !== void 0,
773
795
  showQuickSwitch: config.showQuickSwitch,
774
796
  ignoreModelIdPrefix: config.ignoreModelIdPrefix,
775
797
  modelSort: config.modelSort,
@@ -818,10 +840,9 @@ var ModelRouterService = class {
818
840
  }
819
841
  /** Toggle the composer quick route-switcher button (global, persisted). */
820
842
  async setShowQuickSwitch(value) {
821
- if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
822
843
  const current = this.config();
823
844
  if (current.showQuickSwitch === value) return;
824
- await this.scope.replace({
845
+ await this.writeConfig({
825
846
  ...current,
826
847
  showQuickSwitch: value
827
848
  });
@@ -833,10 +854,9 @@ var ModelRouterService = class {
833
854
  * turning on; split models re-initialize when turning off).
834
855
  */
835
856
  async setIgnoreModelIdPrefix(value) {
836
- if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
837
857
  const current = this.config();
838
858
  if (current.ignoreModelIdPrefix === value) return;
839
- await this.scope.replace({
859
+ await this.writeConfig({
840
860
  ...current,
841
861
  ignoreModelIdPrefix: value
842
862
  });
@@ -844,10 +864,9 @@ var ModelRouterService = class {
844
864
  }
845
865
  /** Switch the model-list display order mode (global, persisted). */
846
866
  async setModelSort(mode) {
847
- if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
848
867
  const current = this.config();
849
868
  if (current.modelSort === mode) return;
850
- await this.scope.replace({
869
+ await this.writeConfig({
851
870
  ...current,
852
871
  modelSort: mode
853
872
  });
@@ -858,22 +877,26 @@ var ModelRouterService = class {
858
877
  * always covers the live catalog.
859
878
  */
860
879
  async setModelOrder(order) {
861
- if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
862
880
  await this.refreshCatalog();
863
881
  const ids = new Set(this.cached.models.map((model) => model.id));
864
882
  const clean = order.filter((id) => ids.has(id));
865
883
  for (const model of this.cached.models) if (!clean.includes(model.id)) clean.push(model.id);
866
884
  const current = this.config();
867
885
  if (clean.length === current.modelOrder.length && clean.every((id, at) => id === current.modelOrder[at])) return;
868
- await this.scope.replace({
886
+ await this.writeConfig({
869
887
  ...current,
870
888
  modelOrder: clean
871
889
  });
872
890
  }
891
+ /** Write the document through the host settings service (entry config). */
892
+ async writeConfig(next) {
893
+ if (this.settings === void 0) throw new Error("model-router: settings service is not available in this profile");
894
+ await this.settings.replace(NS, next, void 0);
895
+ this.configDoc = normalizeConfig(next);
896
+ }
873
897
  async persist(models) {
874
- if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
875
898
  const current = this.config();
876
- await this.scope.replace({
899
+ await this.writeConfig({
877
900
  ...current,
878
901
  models
879
902
  });
@@ -1023,9 +1046,9 @@ function requireString(value, name) {
1023
1046
  //#region src/index.ts
1024
1047
  const name = "@fonlan/dsh-model-router";
1025
1048
  const inject = ["llm"];
1026
- const Config = z.object({});
1027
- function apply(ctx) {
1028
- const service = new ModelRouterService(ctx);
1049
+ const Config = RouterConfigSchema;
1050
+ function apply(ctx, config) {
1051
+ const service = new ModelRouterService(ctx, config);
1029
1052
  ctx.effect(() => {
1030
1053
  service.start();
1031
1054
  return () => service.stop();
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Registers one `model-router` LLM adapter that aggregates every configured
5
5
  * provider and model (strict model-id merge), routes each request to the
6
- * model's currently active provider by in-process delegation, persists the
7
- * per-model provider order/active config in the `model-router` settings
8
- * namespace, and serves the fenced JSON API the web settings page calls.
6
+ * model's currently active provider by in-process delegation, keeps the
7
+ * per-model provider order/active config in the entry config of this bundle
8
+ * (dsh >= 0.1.7: the `Config` schema below is the settings document), and
9
+ * serves the fenced JSON API the web settings page calls.
9
10
  */
10
11
  import type { Context } from '@deepseek-ai/cordis';
11
- import z from '@deepseek-ai/schemastery';
12
+ import type { RouterConfigShape } from './shared/config.js';
12
13
  export declare const name = "@fonlan/dsh-model-router";
13
14
  export declare const inject: string[];
14
- export declare const Config: z<Schemastery.ObjectS<{}>, Schemastery.ObjectT<{}>>;
15
- export declare function apply(ctx: Context): void;
15
+ export declare const Config: import("@deepseek-ai/schemastery").default<RouterConfigShape>;
16
+ export declare function apply(ctx: Context, config: RouterConfigShape): void;
@@ -9,10 +9,24 @@ import { type ModelSortMode, type RouterConfigShape } from '../shared/config.js'
9
9
  import { type RouterCatalog } from './catalog.js';
10
10
  import { type RouterFacts } from './adapter.js';
11
11
  export declare const NS = "model-router";
12
+ /**
13
+ * The slice of the host settings service this plugin writes through. dsh >=
14
+ * 0.1.7 owns plugin configuration as entry config (the plugin's `Config`
15
+ * schema), so the router reads the document it was applied with and persists
16
+ * updates via `settings.replace`, which writes the profile patch.
17
+ */
18
+ export interface SettingsReplaceFace {
19
+ replace(ns: string, section: object, expectedRevision?: number): Promise<void>;
20
+ }
12
21
  /** Settings document schema: one entry per model id. */
13
22
  export declare const RouterConfigSchema: z<RouterConfigShape>;
14
23
  /** The settings-page state view served by the API. */
15
24
  export interface ModelRouterState {
25
+ /**
26
+ * Whether this deployment can persist configuration. False (a profile
27
+ * without the host settings service) makes the page render read-only.
28
+ */
29
+ writable: boolean;
16
30
  /** Whether provider switching is shown inside the composer model picker menu. */
17
31
  showQuickSwitch: boolean;
18
32
  /** Whether model ids are matched with their leading vendor/ prefix ignored. */
@@ -46,17 +60,24 @@ export declare class ModelRouterService implements RouterFacts {
46
60
  private cached;
47
61
  private inflight;
48
62
  private cachedAt;
49
- private scope;
63
+ /** Optional host settings service; absent in profiles without one (no persistence). */
64
+ private settings;
65
+ /** Live config document (seeded from the entry config, updated on write). */
66
+ private configDoc;
50
67
  private registration;
51
68
  /** How long a built catalog is considered fresh before being rebuilt. */
52
69
  private readonly catalogTtlMs;
53
- /** Debounced recent-use persistence (see noteModelUsed / flushUsage). */
70
+ /**
71
+ * Debounced recent-use persistence (see noteModelUsed / flushUsage). Kept
72
+ * long on purpose: every persist writes the entry config, which restarts
73
+ * this plugin's entry, so usage bookkeeping must not stream live traffic.
74
+ */
54
75
  private readonly usageFlushMs;
55
76
  private usageDebounce;
56
77
  private pendingUsage;
57
78
  private usageFlushRunning;
58
- constructor(ctx: Context);
59
- /** Register settings + adapter and wire topology listeners. */
79
+ constructor(ctx: Context, config: RouterConfigShape);
80
+ /** Register the settings seam + adapter and wire topology listeners. */
60
81
  start(): void;
61
82
  stop(): void;
62
83
  catalog(): {
@@ -115,5 +136,7 @@ export declare class ModelRouterService implements RouterFacts {
115
136
  * always covers the live catalog.
116
137
  */
117
138
  setModelOrder(order: string[]): Promise<void>;
139
+ /** Write the document through the host settings service (entry config). */
140
+ private writeConfig;
118
141
  private persist;
119
142
  }
@@ -198,5 +198,7 @@ export declare function setOrder(config: RouterConfigShape, modelId: string, ord
198
198
  models: Record<string, RouterModelConfig>;
199
199
  changed: boolean;
200
200
  };
201
+ /** Dereference every own field of a config object (per-field volatile refs). */
202
+ export declare function derefConfig(value: unknown): unknown;
201
203
  /** Shape-validate an unknown value as a RouterConfigShape (lenient). */
202
204
  export declare function normalizeConfig(value: unknown): RouterConfigShape;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-model-router",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "DSH plugin: a virtual model-router provider that aggregates every configured provider and model, routes each request to the model's currently active provider, and ships a settings page for per-model provider ordering and switching.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -63,15 +63,15 @@
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@deepseek-ai/cordis": "^4.0.1",
66
- "@deepseek-ai/schemastery": "^3.18.2",
67
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
68
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
69
- "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.2",
70
66
  "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
71
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
72
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
73
67
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
74
68
  "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.2-alpha.2",
69
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
70
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
71
+ "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.2",
72
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
73
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
74
+ "@deepseek-ai/schemastery": "^3.18.2",
75
75
  "react": "^18.2.0",
76
76
  "react-dom": "^18.2.0"
77
77
  },
@@ -92,7 +92,7 @@
92
92
  "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
93
93
  "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
94
94
  "@deepseek-ai/dsh-timeout": "0.1.2-alpha.2",
95
- "@deepseek-ai/schemastery": "^3.18.2",
95
+ "@deepseek-ai/schemastery": "^3.18.3",
96
96
  "@types/node": "^22.0.0",
97
97
  "@types/react": "~18.3.1",
98
98
  "@types/react-dom": "~18.3.0",
package/src/client/api.ts CHANGED
@@ -16,6 +16,11 @@ export interface RouterModelView {
16
16
  }
17
17
 
18
18
  export interface ModelRouterState {
19
+ /**
20
+ * Whether this deployment can persist router configuration (the host half
21
+ * found the settings service). False renders the page read-only.
22
+ */
23
+ writable: boolean
19
24
  /** Whether provider switching is shown inside the composer model picker menu. */
20
25
  showQuickSwitch: boolean
21
26
  /** Whether model ids are matched with their leading vendor/ prefix ignored. */
@@ -0,0 +1,97 @@
1
+ import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
2
+
3
+ /** Props every shell icon accepts: square edge in px, color rides currentColor. */
4
+ export interface IconProps {
5
+ size?: number
6
+ className?: string
7
+ }
8
+
9
+ /** One shell icon component. */
10
+ export type IconComponent = (props: IconProps) => JSX.Element
11
+
12
+ /** The runtime export table, read by capability instead of by one hard-coded name. */
13
+ const table = primitives as unknown as Record<string, IconComponent | undefined>
14
+
15
+ /** First name the runtime actually provides (a missing export is `undefined`). */
16
+ function firstDefined(...names: string[]): IconComponent | undefined {
17
+ for (const name of names) {
18
+ let candidate: IconComponent | undefined
19
+ try {
20
+ candidate = table[name]
21
+ } catch {
22
+ // Some module namespaces reject unknown names instead of yielding
23
+ // `undefined`; a rename must never throw on its way to the fallback.
24
+ continue
25
+ }
26
+ if (typeof candidate === 'function') return candidate
27
+ }
28
+ return undefined
29
+ }
30
+
31
+ /** One-path stroked glyph fallback (16×16 viewBox — the 0.1.7 artwork). */
32
+ function glyph(path: string, defaultSize: number): IconComponent {
33
+ return function Glyph({ size = defaultSize, className }: IconProps): JSX.Element {
34
+ return (
35
+ <svg
36
+ width={size}
37
+ height={size}
38
+ className={className}
39
+ viewBox="0 0 16 16"
40
+ fill="none"
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ aria-hidden="true"
43
+ strokeWidth={1}
44
+ >
45
+ <path d={path} stroke="currentColor" />
46
+ </svg>
47
+ )
48
+ }
49
+ }
50
+
51
+ /** Warning circle fallback (the 0.1.7 artwork: ring, stem, dot). */
52
+ function WarningFallback({ size = 16, className }: IconProps): JSX.Element {
53
+ return (
54
+ <svg
55
+ width={size}
56
+ height={size}
57
+ className={className}
58
+ viewBox="0 0 16 16"
59
+ fill="none"
60
+ xmlns="http://www.w3.org/2000/svg"
61
+ aria-hidden="true"
62
+ strokeWidth={1}
63
+ >
64
+ <path d="M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z" stroke="currentColor" />
65
+ <path d="M8 4.29199V9.79199" stroke="currentColor" />
66
+ <path d="M8 10.708V11.708" stroke="currentColor" />
67
+ </svg>
68
+ )
69
+ }
70
+
71
+ /**
72
+ * Shell icons used by the model seat.
73
+ *
74
+ * DSH 0.1.7 renamed every size-suffixed icon export (`IconCheckOutline16`,
75
+ * `IconChevronDownOutline14`, …) into stroke-weight variants (`…OutlineRegular`
76
+ * 1px / `…OutlineMedium` 1.3px). A stale named import resolves to `undefined`
77
+ * and React then throws "Element type is invalid" while rendering the slot
78
+ * entry — the whole seat is dropped from the conversation input instead of
79
+ * degrading (this is exactly what the 0.1.7 upgrade did to this plugin).
80
+ * Resolve the names at runtime, newest first, so the next rename lands on
81
+ * another shipped variant or on the inlined artwork below.
82
+ */
83
+ export const CheckIcon: IconComponent =
84
+ firstDefined('IconCheckOutlineRegular', 'IconCheckOutlineMedium', 'IconCheckOutline16')
85
+ ?? glyph('M2.25 8.5L5.49732 11.7473C5.90519 12.1552 6.57263 12.1344 6.95426 11.7018L13.75 4', 16)
86
+
87
+ export const ChevronDownIcon: IconComponent =
88
+ firstDefined('IconChevronDownOutlineRegular', 'IconChevronDownOutlineMedium', 'IconChevronDownOutline14')
89
+ ?? glyph('M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6', 14)
90
+
91
+ export const ChevronRightIcon: IconComponent =
92
+ firstDefined('IconChevronRightOutlineRegular', 'IconChevronRightOutlineMedium', 'IconChevronRightOutline14')
93
+ ?? glyph('M6 12L9.29289 8.70711C9.68342 8.31658 9.68342 7.68342 9.29289 7.29289L6 4', 14)
94
+
95
+ export const WarningIcon: IconComponent =
96
+ firstDefined('IconWarningOutlineRegular', 'IconWarningOutlineMedium', 'IconWarningOutline16')
97
+ ?? WarningFallback
@@ -15,27 +15,22 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
15
15
  import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
16
16
  import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'
17
17
  import { LOCALE_NS, zh, en } from './locales'
18
- import { makeSettingsSection, type SettingsScopeFace } from './settings-section'
18
+ import { makeSettingsSection } from './settings-section'
19
19
  import { registerModelRouterSeat } from './model-seat'
20
20
 
21
- /** The settings namespace this page edits (must match the host half). */
22
- const ROUTER_NS = 'model-router'
23
-
24
21
  /** The settings sidebar entry this page owns (must stay stable). */
25
22
  const SECTION_ID = 'model-router'
26
23
 
27
24
  /** Slots face: the settings.section list slot is declared at runtime by the
28
25
  * settings shell (ui-settings-general), which external plugins do not depend
29
26
  * on; the register call below is typed through this local face (erased at
30
- * build time). Its inject face binds the model-router settings scope, which
31
- * reaches the component as the scope prop. */
27
+ * build time). */
32
28
  interface SectionEntry {
33
29
  name: string
34
30
  id?: string
35
31
  order?: number
36
32
  label?: () => string
37
33
  locale?: string
38
- inject?: () => unknown
39
34
  }
40
35
 
41
36
  interface Slots {
@@ -47,7 +42,7 @@ interface Slots {
47
42
  * `remote.session` is included like the host ui-model-selection declares it:
48
43
  * the seat's `directoryFor` depends on the namespace, which mounts
49
44
  * asynchronously at startup. */
50
- export const inject = ['slots', 'locale', 'modelDirectories', 'sessions', 'settingsScope', 'connection', 'remote', 'remote.session']
45
+ export const inject = ['slots', 'locale', 'modelDirectories', 'sessions', 'connection', 'remote', 'remote.session']
51
46
 
52
47
  /** Client plugin body. */
53
48
  export function apply(ctx: ClientContext): void {
@@ -60,24 +55,24 @@ export function apply(ctx: ClientContext): void {
60
55
  const SettingsSection = makeSettingsSection(ctx)
61
56
 
62
57
  // The page registers on the settings.section list slot under a stable id, so
63
- // it appears in the settings sidebar's left navigation. The bound
64
- // model-router settings scope (the same namespace the host half registers)
65
- // travels through the inject face and reaches the component as scope.
66
- const services = ctx as unknown as {
67
- slots: Slots
68
- settingsScope: { bind(spec: { namespace: string }): SettingsScopeFace }
69
- }
70
- const scope = services.settingsScope.bind({ namespace: ROUTER_NS })
71
-
72
- services.slots.inject('settings.section', () =>
73
- services.slots.register(
58
+ // it appears in the settings sidebar's left navigation.
59
+ //
60
+ // dsh >= 0.1.7 dropped the client-side `settingsScope` service (plugin
61
+ // configuration became loader-entry config, projected by the host), and the
62
+ // old conditional registration therefore never fired: the section silently
63
+ // disappeared from Settings. Register unconditionally — the page reads and
64
+ // writes through this plugin's own fenced API (see ./settings-section), so
65
+ // it needs no settings service at all. Never gate a slot registration on a
66
+ // service that may not exist: the failure mode is an invisible page.
67
+ const slots = (ctx as unknown as { slots: Slots }).slots
68
+ slots.inject('settings.section', () =>
69
+ slots.register(
74
70
  {
75
71
  name: 'settings.section',
76
72
  id: SECTION_ID,
77
73
  order: 300,
78
74
  label: () => t('settingsTitle'),
79
75
  locale: LOCALE_NS,
80
- inject: () => ({ scope }),
81
76
  },
82
77
  SettingsSection,
83
78
  ),
@@ -15,15 +15,10 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types'
15
15
  type ClientContext = Context
16
16
  import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'
17
17
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
18
- import {
19
- IconCheckOutline16,
20
- IconChevronDownOutline14,
21
- IconChevronRightOutline14,
22
- IconWarningOutline16,
23
- Toast,
24
- } from '@deepseek-ai/dsh-client-ui-primitives'
18
+ import { Toast } from '@deepseek-ai/dsh-client-ui-primitives'
25
19
  import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
26
20
  import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'
21
+ import { CheckIcon, ChevronDownIcon, ChevronRightIcon, WarningIcon } from './icons'
27
22
  import { api, type ModelRouterState, type RouterModelView } from './api'
28
23
  import { LOCALE_NS } from './locales'
29
24
  import './model-seat.css'
@@ -392,7 +387,7 @@ export function ModelRouterSeat({
392
387
  <span className="mr-seat-triggerEffort">{effortLabel}</span>
393
388
  )}
394
389
  <span className={`mr-seat-chevron${open ? ' mr-seat-chevronOpen' : ''}`} aria-hidden>
395
- <IconChevronDownOutline14 />
390
+ <ChevronDownIcon />
396
391
  </span>
397
392
  </button>
398
393
  {open && (
@@ -415,7 +410,7 @@ export function ModelRouterSeat({
415
410
  <span className="mr-seat-cellLabel">{t('menuModel')}</span>
416
411
  <span className="mr-seat-cellValue">{modelLabel}</span>
417
412
  <span className="mr-seat-cellChevron" aria-hidden>
418
- <IconChevronRightOutline14 />
413
+ <ChevronRightIcon />
419
414
  </span>
420
415
  </button>
421
416
  {providerRowVisible && (
@@ -429,7 +424,7 @@ export function ModelRouterSeat({
429
424
  <span className="mr-seat-cellLabel">{t('menuProvider')}</span>
430
425
  <span className="mr-seat-cellValue">{activeProviderName}</span>
431
426
  <span className="mr-seat-cellChevron" aria-hidden>
432
- <IconChevronRightOutline14 />
427
+ <ChevronRightIcon />
433
428
  </span>
434
429
  </button>
435
430
  )}
@@ -444,7 +439,7 @@ export function ModelRouterSeat({
444
439
  <span className="mr-seat-cellLabel">{t('menuEffort')}</span>
445
440
  <span className="mr-seat-cellValue">{effortLabel}</span>
446
441
  <span className="mr-seat-cellChevron" aria-hidden>
447
- <IconChevronRightOutline14 />
442
+ <ChevronRightIcon />
448
443
  </span>
449
444
  </button>
450
445
  )}
@@ -505,7 +500,7 @@ export function ModelRouterSeat({
505
500
  )}
506
501
  </span>
507
502
  <span className="mr-seat-check" aria-hidden>
508
- {selected ? <IconCheckOutline16 /> : null}
503
+ {selected ? <CheckIcon /> : null}
509
504
  </span>
510
505
  </button>
511
506
  )
@@ -562,7 +557,7 @@ export function ModelRouterSeat({
562
557
  )}
563
558
  </span>
564
559
  <span className="mr-seat-check" aria-hidden>
565
- {selected ? <IconCheckOutline16 /> : null}
560
+ {selected ? <CheckIcon /> : null}
566
561
  </span>
567
562
  </button>
568
563
  )
@@ -603,7 +598,7 @@ export function ModelRouterSeat({
603
598
  )}
604
599
  </span>
605
600
  <span className="mr-seat-check" aria-hidden>
606
- {effectiveEffort === level.effort ? <IconCheckOutline16 /> : null}
601
+ {effectiveEffort === level.effort ? <CheckIcon /> : null}
607
602
  </span>
608
603
  </button>
609
604
  ))}
@@ -615,7 +610,7 @@ export function ModelRouterSeat({
615
610
  <Toast
616
611
  key={toast.seq}
617
612
  text={toast.text}
618
- icon={<IconWarningOutline16 />}
613
+ icon={<WarningIcon />}
619
614
  anchor={rootRef.current?.closest('[data-composer-card]') ?? null}
620
615
  onDone={() => setToast(null)}
621
616
  />