dsh-model-visibility 0.2.0 → 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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Lzh3070
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lzh3070
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # Layer applied when a profile lists this bundle: mount the host half.
2
- # The client half is served automatically by the client module system
3
- # (the package declares dsh.client and exports ./client).
4
- - insert:
5
- - id: model-visibility
6
- name: dsh-model-visibility
1
+ # Layer applied when a profile lists this bundle: mount the host half.
2
+ # The client half is served automatically by the client module system
3
+ # (the package declares dsh.client and exports ./client).
4
+ - insert:
5
+ - id: model-visibility
6
+ name: dsh-model-visibility
package/lib/client.js CHANGED
@@ -531,6 +531,145 @@ const en = {
531
531
  providerRow: "{visible} / {total} visible in this provider"
532
532
  };
533
533
 
534
+ //#endregion
535
+ //#region src/client/settings-nav-icon.ts
536
+ /**
537
+ * The eye glyph in the settings navigation.
538
+ *
539
+ * The settings shell picks nav glyphs from a closed list of section ids
540
+ * (`account`, `models`, `agent-presets`, `plugins`, `archived-sessions`) and
541
+ * falls back to its own gear for every other id; `settings.section` projects
542
+ * only `id` / `order` / `label`, so a registrant has no icon to pass — the
543
+ * slot contract in `@deepseek-ai/dsh-client-ui-settings` and the runtime
544
+ * slot inventory both list exactly those options. Every third-party section
545
+ * therefore wears the gear, this one included.
546
+ *
547
+ * So this card claims its own row once the dialog is mounted and swaps the
548
+ * fallback gear for an eye — the same glyph the feature is about. The
549
+ * approach follows the precedent set by `dshmarket` (and credited there to
550
+ * `dsh-better-sidebar` / `dsh-skill-mcp-panel`).
551
+ *
552
+ * Scope, deliberately narrow:
553
+ *
554
+ * - only the row whose visible text equals this plugin's own localized
555
+ * section label is marked; no shell structure is touched;
556
+ * - the marker and the injected stylesheet belong to a `ctx.effect`, so they
557
+ * are removed with the fiber;
558
+ * - a locale switch re-claims the row through the MutationObserver, so the
559
+ * label and the glyph never disagree.
560
+ *
561
+ * Delete this module (and its call in index.ts) the day `settings.section`
562
+ * grows an `icon` field.
563
+ */
564
+ /** Marks the one nav row this plugin owns. */
565
+ const NAV_ICON_MARKER = "data-dsh-model-visibility-nav-icon";
566
+ /**
567
+ * The nav rows of the settings dialog. The shell renders each
568
+ * `settings.section` entry as a `<button>` inside the panel's `<nav>`
569
+ * (SettingsPanel in dsh-client-ui-settings-general).
570
+ */
571
+ const NAV_ROW_SELECTOR = "[role=\"dialog\"] nav button";
572
+ /**
573
+ * Glyph box in px. The shell renders every nav icon at this size and ships no
574
+ * media query at all, so there is deliberately no narrow variant here.
575
+ */
576
+ const NAV_ICON_SIZE = 16;
577
+ /**
578
+ * The eye as standalone SVG for a CSS `mask-image`.
579
+ *
580
+ * Painted pure black on purpose: a mask reads alpha only, and the visible
581
+ * colour comes from the element's `background-color: currentColor`, which
582
+ * keeps the glyph in step with the shell's own icons on hover/active state.
583
+ * The shape is the classic outline eye (almond contour + iris) on a 24 grid;
584
+ * `mask-size` scales it down to the shell's 16px box.
585
+ */
586
+ function eyeMaskSvg() {
587
+ return "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>";
588
+ }
589
+ /** The mask URL for the glyph (encoded at runtime, never hand-escaped). */
590
+ function eyeMaskUrl(svg = eyeMaskSvg()) {
591
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
592
+ }
593
+ /**
594
+ * Whether a nav row is this plugin's own.
595
+ *
596
+ * Pure, and the only decision this feature makes: the row whose visible text
597
+ * is the section label the shell is currently projecting. An empty label
598
+ * matches nothing — a locale that has not resolved yet must not mark the
599
+ * whole nav.
600
+ */
601
+ function isOwnNavRow(rowText, wantedLabel) {
602
+ const wanted = String(wantedLabel ?? "").trim();
603
+ if (wanted.length === 0) return false;
604
+ return String(rowText ?? "").trim() === wanted;
605
+ }
606
+ /** Stylesheet for the marked row: hide the shell's gear, draw the eye. */
607
+ function navIconCss(maskUrl) {
608
+ return [
609
+ `[${NAV_ICON_MARKER}] > svg { display: none; }`,
610
+ `[${NAV_ICON_MARKER}]::before {`,
611
+ ` content: '';`,
612
+ ` flex: none;`,
613
+ ` width: ${NAV_ICON_SIZE}px;`,
614
+ ` height: ${NAV_ICON_SIZE}px;`,
615
+ ` background-color: currentColor;`,
616
+ ` -webkit-mask-image: url("${maskUrl}");`,
617
+ ` mask-image: url("${maskUrl}");`,
618
+ ` -webkit-mask-repeat: no-repeat;`,
619
+ ` mask-repeat: no-repeat;`,
620
+ ` -webkit-mask-position: center;`,
621
+ ` mask-position: center;`,
622
+ ` -webkit-mask-size: ${NAV_ICON_SIZE}px ${NAV_ICON_SIZE}px;`,
623
+ ` mask-size: ${NAV_ICON_SIZE}px ${NAV_ICON_SIZE}px;`,
624
+ `}`
625
+ ].join("\n");
626
+ }
627
+ /**
628
+ * Install the nav glyph.
629
+ *
630
+ * @param ctx - client context, for effect ownership.
631
+ * @param resolveLabel - this plugin's current section label (the same thunk
632
+ * the `settings.section` registration passes), re-read on every sync so a
633
+ * locale switch is picked up without re-registering.
634
+ */
635
+ function installSettingsNavIcon(ctx, resolveLabel) {
636
+ if (typeof document === "undefined") return;
637
+ ctx.effect(() => {
638
+ const tag = document.createElement("style");
639
+ tag.dataset.plugin = "dsh-model-visibility";
640
+ tag.dataset.pluginCss = "dsh-model-visibility/settings-nav-icon";
641
+ tag.textContent = navIconCss(eyeMaskUrl());
642
+ document.head.appendChild(tag);
643
+ let disposed = false;
644
+ let scheduled = false;
645
+ const sync = () => {
646
+ scheduled = false;
647
+ if (disposed) return;
648
+ const wanted = resolveLabel();
649
+ for (const row of document.querySelectorAll(NAV_ROW_SELECTOR)) if (isOwnNavRow(row.textContent, wanted)) row.setAttribute(NAV_ICON_MARKER, "");
650
+ else row.removeAttribute(NAV_ICON_MARKER);
651
+ };
652
+ const schedule = () => {
653
+ if (scheduled || disposed) return;
654
+ scheduled = true;
655
+ queueMicrotask(sync);
656
+ };
657
+ sync();
658
+ const observer = new MutationObserver(schedule);
659
+ observer.observe(document.body, {
660
+ childList: true,
661
+ subtree: true,
662
+ characterData: true
663
+ });
664
+ return () => {
665
+ disposed = true;
666
+ observer.disconnect();
667
+ for (const row of document.querySelectorAll(`[${NAV_ICON_MARKER}]`)) row.removeAttribute(NAV_ICON_MARKER);
668
+ tag.remove();
669
+ };
670
+ }, "model-visibility: settings nav icon");
671
+ }
672
+
534
673
  //#endregion
535
674
  //#region src/client/index.ts
536
675
  const name = "dsh-model-visibility";
@@ -569,6 +708,7 @@ function apply(ctx) {
569
708
  };
570
709
  }, "model-visibility: catalog invalidation");
571
710
  const t = ctx.locale.bind(LOCALE_NS);
711
+ installSettingsNavIcon(ctx, () => t("nav"));
572
712
  const injected = () => ({
573
713
  controller,
574
714
  t,
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["EMPTY: ModelVisibilityState","form: ConfigForm<VisibilitySection>","catalogReader: ModelCatalogReader","name","error: unknown","current: HiddenModel[]","provider: string","model: string","name: string","hidden: boolean","next: HiddenModel[]","id: string","groups: ProviderGroupRow[]","row: ModelRow","created: ProviderGroupRow","props: ModelVisibilityCardProps","_selector: (s: ModelVisibilityState) => unknown","snapshot: ModelVisibilityState","props: { on: boolean; disabled: boolean; label: string; onChange: (next: boolean) => void }","zh: Record<ModelVisibilityLocaleKey, string>","en: Record<ModelVisibilityLocaleKey, string>","ctx: Context","catalog: ModelCatalogReader"],"sources":["../src/namespace.ts","../src/client/controller.ts","../src/client/ModelVisibilityCard.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\r\n * Shared settings namespace of the model-visibility plugin. Lives in its own\r\n * module so the browser half can name the namespace without value-importing\r\n * the Host half (which would drag the settings schema and its Node-side deps\r\n * into the client bundle).\r\n */\r\nimport type { HiddenModel } from './index.ts'\r\n\r\nexport const MODEL_VISIBILITY_NS = 'model-visibility'\r\n\r\n/**\r\n * The settings section the browser card edits: the JSON projection of the\r\n * Host `Config` section as the Settings describe mirror serves it (volatile\r\n * references are unwrapped into plain data at the form boundary).\r\n */\r\nexport interface VisibilitySection {\r\n /** Models excluded from the model-selector catalog; absent means all visible. */\r\n hidden?: HiddenModel[]\r\n}\r\n","/**\r\n * The model-visibility card's controller: React-free state over two sources —\r\n * the (already filtered) Host model catalog and the `model-visibility`\r\n * settings form. The rendered list is their union: catalog rows are visible\r\n * models; hidden entries restate the models the filter removed, labeled by\r\n * the name snapshot each entry carries. Beyond the reference shape, the\r\n * controller exposes provider-level batch writes and a whole-list write so\r\n * the card can offer search + bulk actions.\r\n */\r\nimport { createSnapshotStore } from '@deepseek-ai/dsh-client-store'\r\nimport type { SnapshotStore } from '@deepseek-ai/dsh-client-store'\r\nimport type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type { ModelProviderGroup } from '@deepseek-ai/dsh-api-remotes/client'\r\nimport type { HiddenModel } from '../index.ts'\r\nimport type { VisibilitySection } from '../namespace.ts'\r\n\r\n/** One toggle row. */\r\nexport interface ModelRow {\r\n id: string\r\n name: string\r\n hidden: boolean\r\n}\r\n\r\n/** One provider group of rows. */\r\nexport interface ProviderGroupRow {\r\n id: string\r\n name: string\r\n models: ModelRow[]\r\n}\r\n\r\n/** What the card renders. */\r\nexport interface ModelVisibilityState {\r\n phase: 'loading' | 'ready' | 'error' | 'unavailable'\r\n error?: string\r\n /** Whether the Host settings document accepts writes. */\r\n writable: boolean\r\n groups: ProviderGroupRow[]\r\n /** Union count and how many of them remain visible. */\r\n total: number\r\n visible: number\r\n}\r\n\r\n/** Injected catalog reader: decouples the controller from the Remote wire shape. */\r\nexport interface ModelCatalogReader {\r\n read(): Promise<readonly ModelProviderGroup[]>\r\n}\r\n\r\n/** The face the slot registration injects into the card component. */\r\nexport interface ModelVisibilityFace {\r\n hooks: {\r\n /** Card snapshot bound by the renderer as useModelVisibility. */\r\n modelVisibility: SnapshotStore<ModelVisibilityState>\r\n }\r\n /** Flip one model's visibility. */\r\n toggle: (provider: string, model: string, name: string, hidden: boolean) => void\r\n /** Hide or show every model of one provider. */\r\n toggleProvider: (provider: string, hidden: boolean) => void\r\n /** Clear the hidden list entirely (everything visible again). */\r\n resetAll: () => void\r\n /** Re-read the catalog after a failure. */\r\n reload: () => void\r\n}\r\n\r\nconst EMPTY: ModelVisibilityState = { phase: 'loading', writable: false, groups: [], total: 0, visible: 0 }\r\n\r\nexport class ModelVisibilityCardController {\r\n /** Exposed: the section contribution passes the store through its face as `hooks.snapshot`. */\r\n readonly store = createSnapshotStore<ModelVisibilityState>(EMPTY)\r\n private catalog: ModelProviderGroup[] | undefined\r\n private loadError: string | undefined\r\n private generation = 0\r\n private readonly unsubscribe: () => void\r\n\r\n constructor(\r\n private readonly form: ConfigForm<VisibilitySection>,\r\n private readonly catalogReader: ModelCatalogReader,\r\n ) {\r\n this.unsubscribe = form.subscribe(() => { this.recompute() })\r\n void this.load()\r\n }\r\n\r\n /** The registration-side inject face. */\r\n face(): ModelVisibilityFace {\r\n return {\r\n hooks: { modelVisibility: this.store },\r\n toggle: (provider, model, name, hidden) => { void this.toggle(provider, model, name, hidden) },\r\n toggleProvider: (provider, hidden) => { void this.toggleProvider(provider, hidden) },\r\n resetAll: () => { void this.resetAll() },\r\n reload: () => { void this.load() },\r\n }\r\n }\r\n\r\n /** Re-read the catalog; an older response never overwrites a newer one. */\r\n async load(): Promise<void> {\r\n const generation = ++this.generation\r\n if (this.catalog === undefined) this.store.update((draft) => { draft.phase = 'loading' })\r\n try {\r\n const groups = await this.catalogReader.read()\r\n if (generation !== this.generation) return\r\n this.catalog = groups.map(group => ({\r\n id: group.id,\r\n name: group.name,\r\n models: group.models.map(model => ({ id: model.id, name: model.name })),\r\n }))\r\n this.loadError = undefined\r\n } catch (error: unknown) {\r\n if (generation !== this.generation) return\r\n this.loadError = error instanceof Error ? error.message : String(error)\r\n }\r\n this.recompute()\r\n }\r\n\r\n /** Stop following the settings form (plugin unload). */\r\n dispose(): void {\r\n this.unsubscribe()\r\n }\r\n\r\n /** Compute the next hidden list from the current one plus one model's flip. */\r\n private nextHidden(current: HiddenModel[], provider: string, model: string, name: string, hidden: boolean): HiddenModel[] {\r\n const rest = current.filter(entry => !(entry.provider === provider && entry.model === model))\r\n return hidden ? [...rest, { provider, model, name }] : rest\r\n }\r\n\r\n /** Write the next hidden list, then re-pull the catalog the filter changed. */\r\n private async write(next: HiddenModel[]): Promise<void> {\r\n try {\r\n await this.form.set('hidden', next)\r\n } catch {\r\n // A rejected latest write makes the form reload Host state; the\r\n // recompute below re-renders whatever the mirror now reports.\r\n }\r\n await this.load()\r\n }\r\n\r\n /** Flip one model's visibility (public API — the section card calls it directly). */\r\n async toggle(provider: string, model: string, name: string, hidden: boolean): Promise<void> {\r\n const current = this.form.getSnapshot().value?.hidden ?? []\r\n await this.write(this.nextHidden(current, provider, model, name, hidden))\r\n }\r\n\r\n /**\r\n * Batch flip for one provider. Models are taken from the union of the\r\n * (filtered) catalog rows and existing hidden entries so a provider with\r\n * every model already hidden still flips back fully.\r\n */\r\n /** Batch flip for one provider (public API). */\r\n async toggleProvider(provider: string, hidden: boolean): Promise<void> {\r\n const current = this.form.getSnapshot().value?.hidden ?? []\r\n const providerRows = this.stateRows().find(group => group.id === provider)\r\n const ids = new Set<string>([\r\n ...providerRows ? providerRows.models.map(model => model.id) : [],\r\n ...current.filter(entry => entry.provider === provider).map(entry => entry.model),\r\n ])\r\n if (ids.size === 0) return\r\n let next = current.filter(entry => entry.provider !== provider)\r\n if (hidden) {\r\n const nameOf = (id: string): string => providerRows?.models.find(model => model.id === id)?.name ?? id\r\n next = [...next, ...[...ids].map(id => ({ provider, model: id, name: nameOf(id) }))]\r\n }\r\n await this.write(next)\r\n }\r\n\r\n /** Clear the hidden list entirely (public API). */\r\n async resetAll(): Promise<void> {\r\n await this.write([])\r\n }\r\n\r\n /** Current union rows (catalog ∪ hidden) without touching the store. */\r\n private stateRows(): ProviderGroupRow[] {\r\n const snapshot = this.form.getSnapshot()\r\n const hidden = snapshot.value?.hidden ?? []\r\n if (this.catalog === undefined) return []\r\n const hiddenKeys = new Set(hidden.map(entry => `${entry.provider}\\u0000${entry.model}`))\r\n const groups: ProviderGroupRow[] = this.catalog.map(group => ({\r\n id: group.id,\r\n name: group.name,\r\n models: group.models.map(model => ({\r\n id: model.id,\r\n name: model.name,\r\n hidden: hiddenKeys.has(`${group.id}\\u0000${model.id}`),\r\n })),\r\n }))\r\n const groupsById = new Map(groups.map(group => [group.id, group]))\r\n for (const entry of hidden) {\r\n const group = groupsById.get(entry.provider)\r\n if (group?.models.some(model => model.id === entry.model)) continue\r\n const row: ModelRow = { id: entry.model, name: entry.name ?? entry.model, hidden: true }\r\n if (group !== undefined) {\r\n group.models.push(row)\r\n } else {\r\n const created: ProviderGroupRow = { id: entry.provider, name: entry.provider, models: [row] }\r\n groupsById.set(entry.provider, created)\r\n groups.push(created)\r\n }\r\n }\r\n return groups\r\n }\r\n\r\n /** Union the filtered catalog with the hidden entries into render rows. */\r\n private recompute(): void {\r\n const snapshot = this.form.getSnapshot()\r\n if (snapshot.status === 'unavailable') {\r\n // The namespace left the describe mirror (Host half unloading), or this\r\n // connection keeps settings process-local (memory mode never answers).\r\n this.store.set({ ...EMPTY, phase: 'unavailable', writable: snapshot.writable })\r\n return\r\n }\r\n if (this.catalog === undefined && this.loadError !== undefined) {\r\n this.store.set({ ...EMPTY, phase: 'error', error: this.loadError, writable: snapshot.writable })\r\n return\r\n }\r\n if (this.catalog === undefined || snapshot.status === 'loading') {\r\n this.store.set({ ...EMPTY, writable: snapshot.writable })\r\n return\r\n }\r\n const groups = this.stateRows()\r\n const total = groups.reduce((count, group) => count + group.models.length, 0)\r\n const visible = groups.reduce(\r\n (count, group) => count + group.models.filter(model => !model.hidden).length, 0)\r\n this.store.set({\r\n phase: 'ready',\r\n writable: snapshot.writable,\r\n groups,\r\n total,\r\n visible,\r\n })\r\n }\r\n}\r\n","/**\r\n * The model-visibility settings section, styled to mirror the official\r\n * settings pages: big page title + muted intro, a controls row, then one\r\n * bordered rounded card per provider (like the provider cards on the\r\n * 模型 page) with pill action buttons and token-style switches.\r\n *\r\n * Consumes the `settings.section` contribution contract exactly like the\r\n * official ModelsSection: props = spread inject face, where `hooks.snapshot`\r\n * arrives as the `useSnapshot` selector hook; all three seats are undefined\r\n * while the shell has not injected yet (render null then).\r\n */\r\nimport { useMemo, useState } from 'react'\r\nimport type { CSSProperties } from 'react'\r\nimport type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-store'\r\nimport type { ModelVisibilityCardController, ModelVisibilityState } from './controller.ts'\r\nimport type {} from './locales.ts'\r\n\r\n/** Composed props: spread face fields + the `useSnapshot` hook from hooks.snapshot. */\r\nexport interface ModelVisibilityCardProps {\r\n controller: ModelVisibilityCardController | undefined\r\n useSnapshot: SnapshotSelectorHook<ModelVisibilityState> | undefined\r\n t: TSeat | undefined\r\n}\r\n\r\n/** Localized text seat bound to this card's namespace. */\r\ntype TSeat = (key: string, params?: Record<string, unknown>) => string\r\n\r\nconst STYLE_ID = 'dsh-model-visibility-styles'\r\n\r\nconst STYLES = `\r\n.dsh-mvw { display:flex; flex-direction:column; gap:16px; }\r\n.dsh-mvw .mvw-title { font-size:20px; font-weight:700; letter-spacing:.01em; }\r\n.dsh-mvw .mvw-sub { font-size:13px; color:inherit; opacity:.62; margin-top:6px; max-width:64ch; line-height:1.55; }\r\n.dsh-mvw .mvw-controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:4px; }\r\n.dsh-mvw .mvw-search {\r\n flex:1 1 220px; min-width:160px; height:38px; padding:0 14px;\r\n font-size:13px; border-radius:10px;\r\n border:1px solid rgba(128,128,128,.38);\r\n background:transparent; color:inherit; outline:none;\r\n}\r\n.dsh-mvw .mvw-search:focus { border-color: rgba(76,139,245,.85); }\r\n.dsh-mvw .mvw-pill {\r\n height:36px; padding:0 16px; font-size:13px; cursor:pointer;\r\n border-radius:999px; border:1px solid rgba(128,128,128,.38);\r\n background:transparent; color:inherit;\r\n}\r\n.dsh-mvw .mvw-pill:hover:not(:disabled) { background:rgba(128,128,128,.08); }\r\n.dsh-mvw .mvw-pill:disabled { opacity:.45; cursor:not-allowed; }\r\n.dsh-mvw .mvw-count { font-size:12.5px; opacity:.62; margin-left:auto; }\r\n.dsh-mvw .mvw-card {\r\n border:1px solid rgba(128,128,128,.30); border-radius:16px;\r\n padding:6px 18px 2px;\r\n}\r\n.dsh-mvw .mvw-prov-head {\r\n display:flex; align-items:center; justify-content:space-between; gap:10px;\r\n padding:12px 0 10px;\r\n}\r\n.dsh-mvw .mvw-prov-name { font-size:15px; font-weight:650; }\r\n.dsh-mvw .mvw-prov-count { font-size:12px; opacity:.55; margin-left:10px; font-weight:400; }\r\n.dsh-mvw .mvw-prov-btns { display:flex; gap:8px; }\r\n.dsh-mvw .mvw-mini {\r\n height:30px; padding:0 13px; font-size:12.5px; cursor:pointer;\r\n border-radius:999px; border:1px solid rgba(128,128,128,.38);\r\n background:transparent; color:inherit;\r\n}\r\n.dsh-mvw .mvw-mini:hover:not(:disabled) { background:rgba(128,128,128,.08); }\r\n.dsh-mvw .mvw-mini:disabled { opacity:.4; cursor:not-allowed; }\r\n.dsh-mvw .mvw-row {\r\n display:flex; align-items:center; justify-content:space-between; gap:12px;\r\n min-height:46px; border-top:1px solid rgba(128,128,128,.16);\r\n}\r\n.dsh-mvw .mvw-row:first-of-type { border-top:none; }\r\n.dsh-mvw .mvw-name {\r\n font-size:14px;\r\n min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\r\n}\r\n.dsh-mvw .mvw-id {\r\n opacity:.5; margin-left:8px; padding-right:6px; font-size:12px;\r\n}\r\n.dsh-mvw .mvw-note { font-size:12.5px; opacity:.66; }\r\n.dsh-mvw .mvw-warn { font-size:12.5px; color:#c77700; }\r\n.dsh-mvw .mvw-seats-off { font-size:12.5px; opacity:.66; }\r\n`\r\n\r\nfunction ensureStyles(): void {\r\n if (typeof document === 'undefined') return\r\n if (document.getElementById(STYLE_ID)) return\r\n const el = document.createElement('style')\r\n el.id = STYLE_ID\r\n el.textContent = STYLES\r\n document.head.appendChild(el)\r\n}\r\n\r\nexport function ModelVisibilityCard(props: ModelVisibilityCardProps) {\r\n const { controller, useSnapshot, t } = props\r\n\r\n // Hooks stay unconditional regardless of injection timing.\r\n const state = (useSnapshot ?? ((_selector: (s: ModelVisibilityState) => unknown) => undefined))(\r\n (snapshot: ModelVisibilityState) => snapshot,\r\n ) as ModelVisibilityState | undefined\r\n const [query, setQuery] = useState('')\r\n\r\n // Styles must exist before first paint of this subtree; guarded, so idempotent.\r\n useMemo(() => { ensureStyles() }, [])\r\n\r\n const filteredGroups = useMemo(() => {\r\n const q = query.trim().toLowerCase()\r\n if (q === '') return state?.groups ?? []\r\n return (state?.groups ?? [])\r\n .map(group => ({\r\n ...group,\r\n models: group.models.filter(model =>\r\n model.id.toLowerCase().includes(q) || model.name.toLowerCase().includes(q) || group.id.toLowerCase().includes(q)),\r\n }))\r\n .filter(group => group.models.length > 0)\r\n }, [state, query])\r\n\r\n if (controller === undefined || useSnapshot === undefined || t === undefined) return null\r\n if (state === undefined) {\r\n return (\r\n <section className=\"dsh-mvw\">\r\n <div className=\"mvw-note\">{t('loading')}</div>\r\n </section>\r\n )\r\n }\r\n\r\n return (\r\n <section className=\"dsh-mvw\">\r\n <div>\r\n <div className=\"mvw-title\">{t('title')}</div>\r\n <p className=\"mvw-sub\">{t('intro')}</p>\r\n </div>\r\n\r\n <div className=\"mvw-controls\">\r\n <input\r\n type=\"search\"\r\n className=\"mvw-search\"\r\n value={query}\r\n placeholder={t('searchPlaceholder')}\r\n onChange={event => { setQuery(event.target.value) }}\r\n />\r\n <button\r\n type=\"button\"\r\n className=\"mvw-pill\"\r\n disabled={!state.writable || state.total === state.visible}\r\n onClick={() => { controller.resetAll() }}\r\n >\r\n {t('showAllProviders')}\r\n </button>\r\n <span className=\"mvw-count\">{t('visibleCount', { visible: state.visible, total: state.total })}</span>\r\n </div>\r\n\r\n {state.phase === 'error' && (\r\n <div className=\"mvw-warn\">\r\n {t('loadFailed', { message: state.error ?? '' })}{' '}\r\n <button type=\"button\" className=\"mvw-mini\" onClick={() => { controller.load() }}>{t('retry')}</button>\r\n </div>\r\n )}\r\n\r\n {state.phase === 'unavailable' && <div className=\"mvw-note\">{t('unavailable')}</div>}\r\n {state.phase === 'ready' && !state.writable && <div className=\"mvw-note\">{t('readOnly')}</div>}\r\n {state.phase === 'ready' && state.groups.length === 0 && <div className=\"mvw-note\">{t('empty')}</div>}\r\n {state.phase === 'ready' && state.total > 0 && state.visible === 0 && (\r\n <div className=\"mvw-warn\">{t('allHiddenWarning')}</div>\r\n )}\r\n {state.phase === 'ready' && filteredGroups.length === 0 && state.groups.length > 0 && (\r\n <div className=\"mvw-note\">{t('noMatch')}</div>\r\n )}\r\n\r\n {state.phase === 'ready' && filteredGroups.map(group => {\r\n const groupVisible = group.models.filter(model => !model.hidden).length\r\n return (\r\n <div key={group.id} className=\"mvw-card\">\r\n <div className=\"mvw-prov-head\">\r\n <div>\r\n <span className=\"mvw-prov-name\">{group.name}</span>\r\n <span className=\"mvw-prov-count\">\r\n {t('providerRow', { visible: groupVisible, total: group.models.length })}\r\n </span>\r\n </div>\r\n <div className=\"mvw-prov-btns\">\r\n <button\r\n type=\"button\"\r\n className=\"mvw-mini\"\r\n disabled={!state.writable || groupVisible === 0}\r\n onClick={() => { controller.toggleProvider(group.id, true) }}\r\n >\r\n {t('hideAll')}\r\n </button>\r\n <button\r\n type=\"button\"\r\n className=\"mvw-mini\"\r\n disabled={!state.writable || groupVisible === group.models.length}\r\n onClick={() => { controller.toggleProvider(group.id, false) }}\r\n >\r\n {t('showAll')}\r\n </button>\r\n </div>\r\n </div>\r\n <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>\r\n {group.models.map(model => (\r\n <li key={model.id} className=\"mvw-row\">\r\n <span className=\"mvw-name\">\r\n {model.name}\r\n {model.name !== model.id && <span className=\"mvw-id\">{model.id}</span>}\r\n </span>\r\n <Switch\r\n on={!model.hidden}\r\n disabled={!state.writable}\r\n label={model.name}\r\n onChange={visible => { controller.toggle(group.id, model.id, model.name, !visible) }}\r\n />\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n )\r\n })}\r\n </section>\r\n )\r\n}\r\n\r\nfunction Switch(props: { on: boolean; disabled: boolean; label: string; onChange: (next: boolean) => void }) {\r\n return (\r\n <button\r\n type=\"button\"\r\n role=\"switch\"\r\n aria-checked={props.on}\r\n aria-label={props.label}\r\n disabled={props.disabled}\r\n onClick={() => { props.onChange(!props.on) }}\r\n style={{\r\n width: 40,\r\n height: 22,\r\n borderRadius: 11,\r\n border: 'none',\r\n padding: 2,\r\n cursor: props.disabled ? 'not-allowed' : 'pointer',\r\n background: props.on ? '#4c8bf5' : 'rgba(128,128,128,.45)',\r\n opacity: props.disabled ? 0.5 : 1,\r\n display: 'inline-flex',\r\n justifyContent: props.on ? 'flex-end' : 'flex-start',\r\n transition: 'background .15s',\r\n flex: 'none',\r\n }}\r\n >\r\n <span style={{ width: 18, height: 18, borderRadius: '50%', background: '#fff', display: 'block' }} />\r\n </button>\r\n )\r\n}\r\n","/**\r\n * Locale dictionaries for the model-visibility settings card.\r\n * The namespace is merged into the slot system's LocaleNamespaceMap so the\r\n * register site can declare `locale:` and receive the typed `t` seat.\r\n */\r\n\r\n/** Dictionary keys of this card's copy. */\r\nexport type ModelVisibilityLocaleKey =\r\n | 'nav'\r\n | 'title'\r\n | 'intro'\r\n | 'loading'\r\n | 'loadFailed'\r\n | 'retry'\r\n | 'unavailable'\r\n | 'empty'\r\n | 'allHiddenWarning'\r\n | 'readOnly'\r\n | 'visibleCount'\r\n | 'searchPlaceholder'\r\n | 'noMatch'\r\n | 'hideAll'\r\n | 'showAll'\r\n | 'showAllProviders'\r\n | 'providerRow'\r\n\r\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\r\n interface LocaleNamespaceMap {\r\n /** The model-visibility card's copy. */\r\n 'settings.modelVisibility': ModelVisibilityLocaleKey\r\n }\r\n}\r\n\r\nexport const LOCALE_NS = 'settings.modelVisibility' as const\r\n\r\nexport const zh: Record<ModelVisibilityLocaleKey, string> = {\r\n nav: '模型可见性',\r\n title: '模型可见性',\r\n intro: '控制各模型是否出现在对话界面的模型选择菜单中。隐藏一个正在使用的模型不会中断该会话,也不会改动 Provider 配置。',\r\n loading: '正在读取模型目录…',\r\n loadFailed: '模型目录读取失败:{message}',\r\n retry: '重试',\r\n unavailable: '此连接不共享宿主设置,模型可见性不可用。',\r\n empty: '没有任何 Provider 提供模型。',\r\n allHiddenWarning: '所有模型都已隐藏,模型选择菜单将是空的。',\r\n readOnly: '设置文档不可写,开关已禁用。',\r\n visibleCount: '{visible} / {total} 可见',\r\n searchPlaceholder: '搜索模型…',\r\n noMatch: '没有匹配的模型。',\r\n hideAll: '全部隐藏',\r\n showAll: '全部显示',\r\n showAllProviders: '恢复全部',\r\n providerRow: '该渠道 {visible} / {total} 可见',\r\n}\r\n\r\nexport const en: Record<ModelVisibilityLocaleKey, string> = {\r\n nav: 'Model visibility',\r\n title: 'Model visibility',\r\n intro: 'Control which models appear in the conversation model selector. Hiding a model a session already uses never interrupts that session or touches your provider config.',\r\n loading: 'Loading the model catalog…',\r\n loadFailed: 'Failed to load the model catalog: {message}',\r\n retry: 'Retry',\r\n unavailable: 'Model visibility is unavailable: this connection does not share the Host settings.',\r\n empty: 'No provider advertises any model.',\r\n allHiddenWarning: 'Every model is hidden; the model selector will be empty.',\r\n readOnly: 'The settings document is read-only; toggles are disabled.',\r\n visibleCount: '{visible} / {total} visible',\r\n searchPlaceholder: 'Search models…',\r\n noMatch: 'No model matches.',\r\n hideAll: 'Hide all',\r\n showAll: 'Show all',\r\n showAllProviders: 'Reset all',\r\n providerRow: '{visible} / {total} visible in this provider',\r\n}\r\n","/**\r\n * dsh-model-visibility — browser half.\r\n *\r\n * Registers the model-visibility card as a top-level settings section\r\n * (`settings.section`) and its locale dictionaries. Cross-plugin\r\n * collaboration goes through cordis services only: the slot key's\r\n * declaration, the settings scope, and the wire handle arrive as type-only\r\n * merges, never value imports (the loader bundle-purity rule).\r\n *\r\n * 0.2.0: the `settingsScope` service is gone; the card's durable state rides\r\n * `ctx.configForms` (the shared describe mirror over the Host form\r\n * projection), and the Remote namespace carries formal types, so the wire\r\n * shape is no longer restated locally.\r\n */\r\nimport type { Context } from '@deepseek-ai/cordis'\r\n// Type-only merges: ctx.configForms (ui-settings), ctx.locale (locale),\r\n// ctx.slots (ui-renderer), and the `settings.section` slot declaration.\r\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\r\n// Type-only: ctx.remote with the formal `session.modelCatalog()` signature.\r\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\r\nimport { MODEL_VISIBILITY_NS, type VisibilitySection } from '../namespace.ts'\r\nimport { ModelVisibilityCardController, type ModelCatalogReader } from './controller.ts'\r\nimport { ModelVisibilityCard } from './ModelVisibilityCard.tsx'\r\nimport { LOCALE_NS, en, zh } from './locales.ts'\r\n\r\nexport const name = 'dsh-model-visibility'\r\n// `remote.session` carries the Host model catalog; `configForms` is the\r\n// durable hidden-list form; `slots`/`locale` are the contribution surfaces.\r\nexport const inject = ['slots', 'locale', 'configForms', 'remote', 'remote.session'] as const\r\n\r\nexport function apply(ctx: Context): void {\r\n ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'model-visibility: copy dictionaries')\r\n\r\n // The shared form over this plugin's own profile entry (id from\r\n // cordis.patch.yml); writes serialize through the Host settings document.\r\n const form = ctx.configForms.get<VisibilitySection>(MODEL_VISIBILITY_NS)\r\n const catalog: ModelCatalogReader = {\r\n async read() {\r\n const response = await ctx.remote.session.modelCatalog()\r\n if (!response.ok) {\r\n throw new Error(`${response.error.code}: ${response.error.message}`)\r\n }\r\n return response.value.groups\r\n },\r\n }\r\n const controller = new ModelVisibilityCardController(form, catalog)\r\n ctx.effect(() => () => { controller.dispose() }, 'model-visibility: card controller')\r\n\r\n // Keep the catalog fresh on pushed invalidations (the same channels the\r\n // official Models page listens to): provider-topology changes, and settings\r\n // writes — a provider's model-catalog edit (renames included) commits\r\n // volatile-only, which emits NO adapter event, so the settings document\r\n // channel is the only signal. Our own hidden-list write already re-reads\r\n // the catalog, so it is filtered out to avoid a redundant fetch.\r\n ctx.effect(() => {\r\n const offAdapters = ctx.remote.$on('llm/adapters-updated', () => { void controller.load() })\r\n const offSettings = ctx.remote.$on('settings/document-updated', (ns) => {\r\n if (ns !== MODEL_VISIBILITY_NS) void controller.load()\r\n })\r\n return () => {\r\n offAdapters()\r\n offSettings()\r\n }\r\n }, 'model-visibility: catalog invalidation')\r\n\r\n // The section card consumes the locale seat through the inject face\r\n // (mirroring the official Models page, whose face carries `t`).\r\n const t = ctx.locale.bind(LOCALE_NS)\r\n\r\n // Register as a top-level settings SECTION (nav entry 「模型可见性」, ordered\r\n // right after the official 模型 page at order=10) instead of a tab inside\r\n // the Plugins section — the card is a whole-page surface (search + batch\r\n // controls read better with room to breathe). The label thunk re-binds the\r\n // locale each call so a host language switch shows fresh text.\r\n //\r\n // Face contract for `settings.section` contributions (ui-slots): every\r\n // field of the injected object spreads onto the component as props, and\r\n // each `hooks` entry becomes a `use<Name>` snapshot-selector hook. The\r\n // component destructures { controller, useSnapshot, t } exactly like the\r\n // official ModelsSection does.\r\n const injected = () => ({\r\n controller,\r\n t,\r\n hooks: { snapshot: controller.store },\r\n })\r\n // Register the section only while the Host serves this plugin's namespace:\r\n // a deployment without the Host half shows no trace of the page instead of\r\n // a card stuck on its loading row.\r\n ctx.effect(\r\n () => ctx.configForms.whileServed([MODEL_VISIBILITY_NS], () =>\r\n ctx.slots.inject('settings.section', () => ctx.slots.register({\r\n name: 'settings.section',\r\n id: 'model-visibility',\r\n order: 11,\r\n label: () => t('nav'),\r\n inject: injected,\r\n } as never, ModelVisibilityCard as never))),\r\n 'model-visibility: settings section',\r\n )\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["EMPTY: ModelVisibilityState","form: ConfigForm<VisibilitySection>","catalogReader: ModelCatalogReader","name","error: unknown","current: HiddenModel[]","provider: string","model: string","name: string","hidden: boolean","next: HiddenModel[]","id: string","groups: ProviderGroupRow[]","row: ModelRow","created: ProviderGroupRow","props: ModelVisibilityCardProps","_selector: (s: ModelVisibilityState) => unknown","snapshot: ModelVisibilityState","props: { on: boolean; disabled: boolean; label: string; onChange: (next: boolean) => void }","zh: Record<ModelVisibilityLocaleKey, string>","en: Record<ModelVisibilityLocaleKey, string>","svg: string","rowText: string | null | undefined","wantedLabel: string | null | undefined","maskUrl: string","ctx: NavIconContext","resolveLabel: () => string","ctx: Context","catalog: ModelCatalogReader"],"sources":["../src/namespace.ts","../src/client/controller.ts","../src/client/ModelVisibilityCard.tsx","../src/client/locales.ts","../src/client/settings-nav-icon.ts","../src/client/index.ts"],"sourcesContent":["/**\r\n * Shared settings namespace of the model-visibility plugin. Lives in its own\r\n * module so the browser half can name the namespace without value-importing\r\n * the Host half (which would drag the settings schema and its Node-side deps\r\n * into the client bundle).\r\n */\r\nimport type { HiddenModel } from './index.ts'\r\n\r\nexport const MODEL_VISIBILITY_NS = 'model-visibility'\r\n\r\n/**\r\n * The settings section the browser card edits: the JSON projection of the\r\n * Host `Config` section as the Settings describe mirror serves it (volatile\r\n * references are unwrapped into plain data at the form boundary).\r\n */\r\nexport interface VisibilitySection {\r\n /** Models excluded from the model-selector catalog; absent means all visible. */\r\n hidden?: HiddenModel[]\r\n}\r\n","/**\r\n * The model-visibility card's controller: React-free state over two sources —\r\n * the (already filtered) Host model catalog and the `model-visibility`\r\n * settings form. The rendered list is their union: catalog rows are visible\r\n * models; hidden entries restate the models the filter removed, labeled by\r\n * the name snapshot each entry carries. Beyond the reference shape, the\r\n * controller exposes provider-level batch writes and a whole-list write so\r\n * the card can offer search + bulk actions.\r\n */\r\nimport { createSnapshotStore } from '@deepseek-ai/dsh-client-store'\r\nimport type { SnapshotStore } from '@deepseek-ai/dsh-client-store'\r\nimport type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type { ModelProviderGroup } from '@deepseek-ai/dsh-api-remotes/client'\r\nimport type { HiddenModel } from '../index.ts'\r\nimport type { VisibilitySection } from '../namespace.ts'\r\n\r\n/** One toggle row. */\r\nexport interface ModelRow {\r\n id: string\r\n name: string\r\n hidden: boolean\r\n}\r\n\r\n/** One provider group of rows. */\r\nexport interface ProviderGroupRow {\r\n id: string\r\n name: string\r\n models: ModelRow[]\r\n}\r\n\r\n/** What the card renders. */\r\nexport interface ModelVisibilityState {\r\n phase: 'loading' | 'ready' | 'error' | 'unavailable'\r\n error?: string\r\n /** Whether the Host settings document accepts writes. */\r\n writable: boolean\r\n groups: ProviderGroupRow[]\r\n /** Union count and how many of them remain visible. */\r\n total: number\r\n visible: number\r\n}\r\n\r\n/** Injected catalog reader: decouples the controller from the Remote wire shape. */\r\nexport interface ModelCatalogReader {\r\n read(): Promise<readonly ModelProviderGroup[]>\r\n}\r\n\r\n/** The face the slot registration injects into the card component. */\r\nexport interface ModelVisibilityFace {\r\n hooks: {\r\n /** Card snapshot bound by the renderer as useModelVisibility. */\r\n modelVisibility: SnapshotStore<ModelVisibilityState>\r\n }\r\n /** Flip one model's visibility. */\r\n toggle: (provider: string, model: string, name: string, hidden: boolean) => void\r\n /** Hide or show every model of one provider. */\r\n toggleProvider: (provider: string, hidden: boolean) => void\r\n /** Clear the hidden list entirely (everything visible again). */\r\n resetAll: () => void\r\n /** Re-read the catalog after a failure. */\r\n reload: () => void\r\n}\r\n\r\nconst EMPTY: ModelVisibilityState = { phase: 'loading', writable: false, groups: [], total: 0, visible: 0 }\r\n\r\nexport class ModelVisibilityCardController {\r\n /** Exposed: the section contribution passes the store through its face as `hooks.snapshot`. */\r\n readonly store = createSnapshotStore<ModelVisibilityState>(EMPTY)\r\n private catalog: ModelProviderGroup[] | undefined\r\n private loadError: string | undefined\r\n private generation = 0\r\n private readonly unsubscribe: () => void\r\n\r\n constructor(\r\n private readonly form: ConfigForm<VisibilitySection>,\r\n private readonly catalogReader: ModelCatalogReader,\r\n ) {\r\n this.unsubscribe = form.subscribe(() => { this.recompute() })\r\n void this.load()\r\n }\r\n\r\n /** The registration-side inject face. */\r\n face(): ModelVisibilityFace {\r\n return {\r\n hooks: { modelVisibility: this.store },\r\n toggle: (provider, model, name, hidden) => { void this.toggle(provider, model, name, hidden) },\r\n toggleProvider: (provider, hidden) => { void this.toggleProvider(provider, hidden) },\r\n resetAll: () => { void this.resetAll() },\r\n reload: () => { void this.load() },\r\n }\r\n }\r\n\r\n /** Re-read the catalog; an older response never overwrites a newer one. */\r\n async load(): Promise<void> {\r\n const generation = ++this.generation\r\n if (this.catalog === undefined) this.store.update((draft) => { draft.phase = 'loading' })\r\n try {\r\n const groups = await this.catalogReader.read()\r\n if (generation !== this.generation) return\r\n this.catalog = groups.map(group => ({\r\n id: group.id,\r\n name: group.name,\r\n models: group.models.map(model => ({ id: model.id, name: model.name })),\r\n }))\r\n this.loadError = undefined\r\n } catch (error: unknown) {\r\n if (generation !== this.generation) return\r\n this.loadError = error instanceof Error ? error.message : String(error)\r\n }\r\n this.recompute()\r\n }\r\n\r\n /** Stop following the settings form (plugin unload). */\r\n dispose(): void {\r\n this.unsubscribe()\r\n }\r\n\r\n /** Compute the next hidden list from the current one plus one model's flip. */\r\n private nextHidden(current: HiddenModel[], provider: string, model: string, name: string, hidden: boolean): HiddenModel[] {\r\n const rest = current.filter(entry => !(entry.provider === provider && entry.model === model))\r\n return hidden ? [...rest, { provider, model, name }] : rest\r\n }\r\n\r\n /** Write the next hidden list, then re-pull the catalog the filter changed. */\r\n private async write(next: HiddenModel[]): Promise<void> {\r\n try {\r\n await this.form.set('hidden', next)\r\n } catch {\r\n // A rejected latest write makes the form reload Host state; the\r\n // recompute below re-renders whatever the mirror now reports.\r\n }\r\n await this.load()\r\n }\r\n\r\n /** Flip one model's visibility (public API — the section card calls it directly). */\r\n async toggle(provider: string, model: string, name: string, hidden: boolean): Promise<void> {\r\n const current = this.form.getSnapshot().value?.hidden ?? []\r\n await this.write(this.nextHidden(current, provider, model, name, hidden))\r\n }\r\n\r\n /**\r\n * Batch flip for one provider. Models are taken from the union of the\r\n * (filtered) catalog rows and existing hidden entries so a provider with\r\n * every model already hidden still flips back fully.\r\n */\r\n /** Batch flip for one provider (public API). */\r\n async toggleProvider(provider: string, hidden: boolean): Promise<void> {\r\n const current = this.form.getSnapshot().value?.hidden ?? []\r\n const providerRows = this.stateRows().find(group => group.id === provider)\r\n const ids = new Set<string>([\r\n ...providerRows ? providerRows.models.map(model => model.id) : [],\r\n ...current.filter(entry => entry.provider === provider).map(entry => entry.model),\r\n ])\r\n if (ids.size === 0) return\r\n let next = current.filter(entry => entry.provider !== provider)\r\n if (hidden) {\r\n const nameOf = (id: string): string => providerRows?.models.find(model => model.id === id)?.name ?? id\r\n next = [...next, ...[...ids].map(id => ({ provider, model: id, name: nameOf(id) }))]\r\n }\r\n await this.write(next)\r\n }\r\n\r\n /** Clear the hidden list entirely (public API). */\r\n async resetAll(): Promise<void> {\r\n await this.write([])\r\n }\r\n\r\n /** Current union rows (catalog ∪ hidden) without touching the store. */\r\n private stateRows(): ProviderGroupRow[] {\r\n const snapshot = this.form.getSnapshot()\r\n const hidden = snapshot.value?.hidden ?? []\r\n if (this.catalog === undefined) return []\r\n const hiddenKeys = new Set(hidden.map(entry => `${entry.provider}\\u0000${entry.model}`))\r\n const groups: ProviderGroupRow[] = this.catalog.map(group => ({\r\n id: group.id,\r\n name: group.name,\r\n models: group.models.map(model => ({\r\n id: model.id,\r\n name: model.name,\r\n hidden: hiddenKeys.has(`${group.id}\\u0000${model.id}`),\r\n })),\r\n }))\r\n const groupsById = new Map(groups.map(group => [group.id, group]))\r\n for (const entry of hidden) {\r\n const group = groupsById.get(entry.provider)\r\n if (group?.models.some(model => model.id === entry.model)) continue\r\n const row: ModelRow = { id: entry.model, name: entry.name ?? entry.model, hidden: true }\r\n if (group !== undefined) {\r\n group.models.push(row)\r\n } else {\r\n const created: ProviderGroupRow = { id: entry.provider, name: entry.provider, models: [row] }\r\n groupsById.set(entry.provider, created)\r\n groups.push(created)\r\n }\r\n }\r\n return groups\r\n }\r\n\r\n /** Union the filtered catalog with the hidden entries into render rows. */\r\n private recompute(): void {\r\n const snapshot = this.form.getSnapshot()\r\n if (snapshot.status === 'unavailable') {\r\n // The namespace left the describe mirror (Host half unloading), or this\r\n // connection keeps settings process-local (memory mode never answers).\r\n this.store.set({ ...EMPTY, phase: 'unavailable', writable: snapshot.writable })\r\n return\r\n }\r\n if (this.catalog === undefined && this.loadError !== undefined) {\r\n this.store.set({ ...EMPTY, phase: 'error', error: this.loadError, writable: snapshot.writable })\r\n return\r\n }\r\n if (this.catalog === undefined || snapshot.status === 'loading') {\r\n this.store.set({ ...EMPTY, writable: snapshot.writable })\r\n return\r\n }\r\n const groups = this.stateRows()\r\n const total = groups.reduce((count, group) => count + group.models.length, 0)\r\n const visible = groups.reduce(\r\n (count, group) => count + group.models.filter(model => !model.hidden).length, 0)\r\n this.store.set({\r\n phase: 'ready',\r\n writable: snapshot.writable,\r\n groups,\r\n total,\r\n visible,\r\n })\r\n }\r\n}\r\n","/**\r\n * The model-visibility settings section, styled to mirror the official\r\n * settings pages: big page title + muted intro, a controls row, then one\r\n * bordered rounded card per provider (like the provider cards on the\r\n * 模型 page) with pill action buttons and token-style switches.\r\n *\r\n * Consumes the `settings.section` contribution contract exactly like the\r\n * official ModelsSection: props = spread inject face, where `hooks.snapshot`\r\n * arrives as the `useSnapshot` selector hook; all three seats are undefined\r\n * while the shell has not injected yet (render null then).\r\n */\r\nimport { useMemo, useState } from 'react'\r\nimport type { CSSProperties } from 'react'\r\nimport type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-store'\r\nimport type { ModelVisibilityCardController, ModelVisibilityState } from './controller.ts'\r\nimport type {} from './locales.ts'\r\n\r\n/** Composed props: spread face fields + the `useSnapshot` hook from hooks.snapshot. */\r\nexport interface ModelVisibilityCardProps {\r\n controller: ModelVisibilityCardController | undefined\r\n useSnapshot: SnapshotSelectorHook<ModelVisibilityState> | undefined\r\n t: TSeat | undefined\r\n}\r\n\r\n/** Localized text seat bound to this card's namespace. */\r\ntype TSeat = (key: string, params?: Record<string, unknown>) => string\r\n\r\nconst STYLE_ID = 'dsh-model-visibility-styles'\r\n\r\nconst STYLES = `\r\n.dsh-mvw { display:flex; flex-direction:column; gap:16px; }\r\n.dsh-mvw .mvw-title { font-size:20px; font-weight:700; letter-spacing:.01em; }\r\n.dsh-mvw .mvw-sub { font-size:13px; color:inherit; opacity:.62; margin-top:6px; max-width:64ch; line-height:1.55; }\r\n.dsh-mvw .mvw-controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:4px; }\r\n.dsh-mvw .mvw-search {\r\n flex:1 1 220px; min-width:160px; height:38px; padding:0 14px;\r\n font-size:13px; border-radius:10px;\r\n border:1px solid rgba(128,128,128,.38);\r\n background:transparent; color:inherit; outline:none;\r\n}\r\n.dsh-mvw .mvw-search:focus { border-color: rgba(76,139,245,.85); }\r\n.dsh-mvw .mvw-pill {\r\n height:36px; padding:0 16px; font-size:13px; cursor:pointer;\r\n border-radius:999px; border:1px solid rgba(128,128,128,.38);\r\n background:transparent; color:inherit;\r\n}\r\n.dsh-mvw .mvw-pill:hover:not(:disabled) { background:rgba(128,128,128,.08); }\r\n.dsh-mvw .mvw-pill:disabled { opacity:.45; cursor:not-allowed; }\r\n.dsh-mvw .mvw-count { font-size:12.5px; opacity:.62; margin-left:auto; }\r\n.dsh-mvw .mvw-card {\r\n border:1px solid rgba(128,128,128,.30); border-radius:16px;\r\n padding:6px 18px 2px;\r\n}\r\n.dsh-mvw .mvw-prov-head {\r\n display:flex; align-items:center; justify-content:space-between; gap:10px;\r\n padding:12px 0 10px;\r\n}\r\n.dsh-mvw .mvw-prov-name { font-size:15px; font-weight:650; }\r\n.dsh-mvw .mvw-prov-count { font-size:12px; opacity:.55; margin-left:10px; font-weight:400; }\r\n.dsh-mvw .mvw-prov-btns { display:flex; gap:8px; }\r\n.dsh-mvw .mvw-mini {\r\n height:30px; padding:0 13px; font-size:12.5px; cursor:pointer;\r\n border-radius:999px; border:1px solid rgba(128,128,128,.38);\r\n background:transparent; color:inherit;\r\n}\r\n.dsh-mvw .mvw-mini:hover:not(:disabled) { background:rgba(128,128,128,.08); }\r\n.dsh-mvw .mvw-mini:disabled { opacity:.4; cursor:not-allowed; }\r\n.dsh-mvw .mvw-row {\r\n display:flex; align-items:center; justify-content:space-between; gap:12px;\r\n min-height:46px; border-top:1px solid rgba(128,128,128,.16);\r\n}\r\n.dsh-mvw .mvw-row:first-of-type { border-top:none; }\r\n.dsh-mvw .mvw-name {\r\n font-size:14px;\r\n min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\r\n}\r\n.dsh-mvw .mvw-id {\r\n opacity:.5; margin-left:8px; padding-right:6px; font-size:12px;\r\n}\r\n.dsh-mvw .mvw-note { font-size:12.5px; opacity:.66; }\r\n.dsh-mvw .mvw-warn { font-size:12.5px; color:#c77700; }\r\n.dsh-mvw .mvw-seats-off { font-size:12.5px; opacity:.66; }\r\n`\r\n\r\nfunction ensureStyles(): void {\r\n if (typeof document === 'undefined') return\r\n if (document.getElementById(STYLE_ID)) return\r\n const el = document.createElement('style')\r\n el.id = STYLE_ID\r\n el.textContent = STYLES\r\n document.head.appendChild(el)\r\n}\r\n\r\nexport function ModelVisibilityCard(props: ModelVisibilityCardProps) {\r\n const { controller, useSnapshot, t } = props\r\n\r\n // Hooks stay unconditional regardless of injection timing.\r\n const state = (useSnapshot ?? ((_selector: (s: ModelVisibilityState) => unknown) => undefined))(\r\n (snapshot: ModelVisibilityState) => snapshot,\r\n ) as ModelVisibilityState | undefined\r\n const [query, setQuery] = useState('')\r\n\r\n // Styles must exist before first paint of this subtree; guarded, so idempotent.\r\n useMemo(() => { ensureStyles() }, [])\r\n\r\n const filteredGroups = useMemo(() => {\r\n const q = query.trim().toLowerCase()\r\n if (q === '') return state?.groups ?? []\r\n return (state?.groups ?? [])\r\n .map(group => ({\r\n ...group,\r\n models: group.models.filter(model =>\r\n model.id.toLowerCase().includes(q) || model.name.toLowerCase().includes(q) || group.id.toLowerCase().includes(q)),\r\n }))\r\n .filter(group => group.models.length > 0)\r\n }, [state, query])\r\n\r\n if (controller === undefined || useSnapshot === undefined || t === undefined) return null\r\n if (state === undefined) {\r\n return (\r\n <section className=\"dsh-mvw\">\r\n <div className=\"mvw-note\">{t('loading')}</div>\r\n </section>\r\n )\r\n }\r\n\r\n return (\r\n <section className=\"dsh-mvw\">\r\n <div>\r\n <div className=\"mvw-title\">{t('title')}</div>\r\n <p className=\"mvw-sub\">{t('intro')}</p>\r\n </div>\r\n\r\n <div className=\"mvw-controls\">\r\n <input\r\n type=\"search\"\r\n className=\"mvw-search\"\r\n value={query}\r\n placeholder={t('searchPlaceholder')}\r\n onChange={event => { setQuery(event.target.value) }}\r\n />\r\n <button\r\n type=\"button\"\r\n className=\"mvw-pill\"\r\n disabled={!state.writable || state.total === state.visible}\r\n onClick={() => { controller.resetAll() }}\r\n >\r\n {t('showAllProviders')}\r\n </button>\r\n <span className=\"mvw-count\">{t('visibleCount', { visible: state.visible, total: state.total })}</span>\r\n </div>\r\n\r\n {state.phase === 'error' && (\r\n <div className=\"mvw-warn\">\r\n {t('loadFailed', { message: state.error ?? '' })}{' '}\r\n <button type=\"button\" className=\"mvw-mini\" onClick={() => { controller.load() }}>{t('retry')}</button>\r\n </div>\r\n )}\r\n\r\n {state.phase === 'unavailable' && <div className=\"mvw-note\">{t('unavailable')}</div>}\r\n {state.phase === 'ready' && !state.writable && <div className=\"mvw-note\">{t('readOnly')}</div>}\r\n {state.phase === 'ready' && state.groups.length === 0 && <div className=\"mvw-note\">{t('empty')}</div>}\r\n {state.phase === 'ready' && state.total > 0 && state.visible === 0 && (\r\n <div className=\"mvw-warn\">{t('allHiddenWarning')}</div>\r\n )}\r\n {state.phase === 'ready' && filteredGroups.length === 0 && state.groups.length > 0 && (\r\n <div className=\"mvw-note\">{t('noMatch')}</div>\r\n )}\r\n\r\n {state.phase === 'ready' && filteredGroups.map(group => {\r\n const groupVisible = group.models.filter(model => !model.hidden).length\r\n return (\r\n <div key={group.id} className=\"mvw-card\">\r\n <div className=\"mvw-prov-head\">\r\n <div>\r\n <span className=\"mvw-prov-name\">{group.name}</span>\r\n <span className=\"mvw-prov-count\">\r\n {t('providerRow', { visible: groupVisible, total: group.models.length })}\r\n </span>\r\n </div>\r\n <div className=\"mvw-prov-btns\">\r\n <button\r\n type=\"button\"\r\n className=\"mvw-mini\"\r\n disabled={!state.writable || groupVisible === 0}\r\n onClick={() => { controller.toggleProvider(group.id, true) }}\r\n >\r\n {t('hideAll')}\r\n </button>\r\n <button\r\n type=\"button\"\r\n className=\"mvw-mini\"\r\n disabled={!state.writable || groupVisible === group.models.length}\r\n onClick={() => { controller.toggleProvider(group.id, false) }}\r\n >\r\n {t('showAll')}\r\n </button>\r\n </div>\r\n </div>\r\n <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>\r\n {group.models.map(model => (\r\n <li key={model.id} className=\"mvw-row\">\r\n <span className=\"mvw-name\">\r\n {model.name}\r\n {model.name !== model.id && <span className=\"mvw-id\">{model.id}</span>}\r\n </span>\r\n <Switch\r\n on={!model.hidden}\r\n disabled={!state.writable}\r\n label={model.name}\r\n onChange={visible => { controller.toggle(group.id, model.id, model.name, !visible) }}\r\n />\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n )\r\n })}\r\n </section>\r\n )\r\n}\r\n\r\nfunction Switch(props: { on: boolean; disabled: boolean; label: string; onChange: (next: boolean) => void }) {\r\n return (\r\n <button\r\n type=\"button\"\r\n role=\"switch\"\r\n aria-checked={props.on}\r\n aria-label={props.label}\r\n disabled={props.disabled}\r\n onClick={() => { props.onChange(!props.on) }}\r\n style={{\r\n width: 40,\r\n height: 22,\r\n borderRadius: 11,\r\n border: 'none',\r\n padding: 2,\r\n cursor: props.disabled ? 'not-allowed' : 'pointer',\r\n background: props.on ? '#4c8bf5' : 'rgba(128,128,128,.45)',\r\n opacity: props.disabled ? 0.5 : 1,\r\n display: 'inline-flex',\r\n justifyContent: props.on ? 'flex-end' : 'flex-start',\r\n transition: 'background .15s',\r\n flex: 'none',\r\n }}\r\n >\r\n <span style={{ width: 18, height: 18, borderRadius: '50%', background: '#fff', display: 'block' }} />\r\n </button>\r\n )\r\n}\r\n","/**\r\n * Locale dictionaries for the model-visibility settings card.\r\n * The namespace is merged into the slot system's LocaleNamespaceMap so the\r\n * register site can declare `locale:` and receive the typed `t` seat.\r\n */\r\n\r\n/** Dictionary keys of this card's copy. */\r\nexport type ModelVisibilityLocaleKey =\r\n | 'nav'\r\n | 'title'\r\n | 'intro'\r\n | 'loading'\r\n | 'loadFailed'\r\n | 'retry'\r\n | 'unavailable'\r\n | 'empty'\r\n | 'allHiddenWarning'\r\n | 'readOnly'\r\n | 'visibleCount'\r\n | 'searchPlaceholder'\r\n | 'noMatch'\r\n | 'hideAll'\r\n | 'showAll'\r\n | 'showAllProviders'\r\n | 'providerRow'\r\n\r\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\r\n interface LocaleNamespaceMap {\r\n /** The model-visibility card's copy. */\r\n 'settings.modelVisibility': ModelVisibilityLocaleKey\r\n }\r\n}\r\n\r\nexport const LOCALE_NS = 'settings.modelVisibility' as const\r\n\r\nexport const zh: Record<ModelVisibilityLocaleKey, string> = {\r\n nav: '模型可见性',\r\n title: '模型可见性',\r\n intro: '控制各模型是否出现在对话界面的模型选择菜单中。隐藏一个正在使用的模型不会中断该会话,也不会改动 Provider 配置。',\r\n loading: '正在读取模型目录…',\r\n loadFailed: '模型目录读取失败:{message}',\r\n retry: '重试',\r\n unavailable: '此连接不共享宿主设置,模型可见性不可用。',\r\n empty: '没有任何 Provider 提供模型。',\r\n allHiddenWarning: '所有模型都已隐藏,模型选择菜单将是空的。',\r\n readOnly: '设置文档不可写,开关已禁用。',\r\n visibleCount: '{visible} / {total} 可见',\r\n searchPlaceholder: '搜索模型…',\r\n noMatch: '没有匹配的模型。',\r\n hideAll: '全部隐藏',\r\n showAll: '全部显示',\r\n showAllProviders: '恢复全部',\r\n providerRow: '该渠道 {visible} / {total} 可见',\r\n}\r\n\r\nexport const en: Record<ModelVisibilityLocaleKey, string> = {\r\n nav: 'Model visibility',\r\n title: 'Model visibility',\r\n intro: 'Control which models appear in the conversation model selector. Hiding a model a session already uses never interrupts that session or touches your provider config.',\r\n loading: 'Loading the model catalog…',\r\n loadFailed: 'Failed to load the model catalog: {message}',\r\n retry: 'Retry',\r\n unavailable: 'Model visibility is unavailable: this connection does not share the Host settings.',\r\n empty: 'No provider advertises any model.',\r\n allHiddenWarning: 'Every model is hidden; the model selector will be empty.',\r\n readOnly: 'The settings document is read-only; toggles are disabled.',\r\n visibleCount: '{visible} / {total} visible',\r\n searchPlaceholder: 'Search models…',\r\n noMatch: 'No model matches.',\r\n hideAll: 'Hide all',\r\n showAll: 'Show all',\r\n showAllProviders: 'Reset all',\r\n providerRow: '{visible} / {total} visible in this provider',\r\n}\r\n","/**\n * The eye glyph in the settings navigation.\n *\n * The settings shell picks nav glyphs from a closed list of section ids\n * (`account`, `models`, `agent-presets`, `plugins`, `archived-sessions`) and\n * falls back to its own gear for every other id; `settings.section` projects\n * only `id` / `order` / `label`, so a registrant has no icon to pass — the\n * slot contract in `@deepseek-ai/dsh-client-ui-settings` and the runtime\n * slot inventory both list exactly those options. Every third-party section\n * therefore wears the gear, this one included.\n *\n * So this card claims its own row once the dialog is mounted and swaps the\n * fallback gear for an eye — the same glyph the feature is about. The\n * approach follows the precedent set by `dshmarket` (and credited there to\n * `dsh-better-sidebar` / `dsh-skill-mcp-panel`).\n *\n * Scope, deliberately narrow:\n *\n * - only the row whose visible text equals this plugin's own localized\n * section label is marked; no shell structure is touched;\n * - the marker and the injected stylesheet belong to a `ctx.effect`, so they\n * are removed with the fiber;\n * - a locale switch re-claims the row through the MutationObserver, so the\n * label and the glyph never disagree.\n *\n * Delete this module (and its call in index.ts) the day `settings.section`\n * grows an `icon` field.\n */\n\n/** Marks the one nav row this plugin owns. */\nexport const NAV_ICON_MARKER = 'data-dsh-model-visibility-nav-icon'\n\n/**\n * The nav rows of the settings dialog. The shell renders each\n * `settings.section` entry as a `<button>` inside the panel's `<nav>`\n * (SettingsPanel in dsh-client-ui-settings-general).\n */\nexport const NAV_ROW_SELECTOR = '[role=\"dialog\"] nav button'\n\n/**\n * Glyph box in px. The shell renders every nav icon at this size and ships no\n * media query at all, so there is deliberately no narrow variant here.\n */\nexport const NAV_ICON_SIZE = 16\n\n/**\n * The eye as standalone SVG for a CSS `mask-image`.\n *\n * Painted pure black on purpose: a mask reads alpha only, and the visible\n * colour comes from the element's `background-color: currentColor`, which\n * keeps the glyph in step with the shell's own icons on hover/active state.\n * The shape is the classic outline eye (almond contour + iris) on a 24 grid;\n * `mask-size` scales it down to the shell's 16px box.\n */\nexport function eyeMaskSvg(): string {\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\"'\n + ' stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">'\n + '<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0'\n + ' 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/>'\n + '<circle cx=\"12\" cy=\"12\" r=\"3\"/>'\n + '</svg>'\n}\n\n/** The mask URL for the glyph (encoded at runtime, never hand-escaped). */\nexport function eyeMaskUrl(svg: string = eyeMaskSvg()): string {\n return `data:image/svg+xml,${encodeURIComponent(svg)}`\n}\n\n/**\n * Whether a nav row is this plugin's own.\n *\n * Pure, and the only decision this feature makes: the row whose visible text\n * is the section label the shell is currently projecting. An empty label\n * matches nothing — a locale that has not resolved yet must not mark the\n * whole nav.\n */\nexport function isOwnNavRow(rowText: string | null | undefined, wantedLabel: string | null | undefined): boolean {\n const wanted = String(wantedLabel ?? '').trim()\n if (wanted.length === 0) return false\n return String(rowText ?? '').trim() === wanted\n}\n\n/** Stylesheet for the marked row: hide the shell's gear, draw the eye. */\nexport function navIconCss(maskUrl: string): string {\n return [\n `[${NAV_ICON_MARKER}] > svg { display: none; }`,\n `[${NAV_ICON_MARKER}]::before {`,\n ` content: '';`,\n ` flex: none;`,\n ` width: ${NAV_ICON_SIZE}px;`,\n ` height: ${NAV_ICON_SIZE}px;`,\n ` background-color: currentColor;`,\n ` -webkit-mask-image: url(\"${maskUrl}\");`,\n ` mask-image: url(\"${maskUrl}\");`,\n ` -webkit-mask-repeat: no-repeat;`,\n ` mask-repeat: no-repeat;`,\n ` -webkit-mask-position: center;`,\n ` mask-position: center;`,\n ` -webkit-mask-size: ${NAV_ICON_SIZE}px ${NAV_ICON_SIZE}px;`,\n ` mask-size: ${NAV_ICON_SIZE}px ${NAV_ICON_SIZE}px;`,\n `}`,\n ].join('\\n')\n}\n\n/** The slice of the client context this feature needs. */\nexport interface NavIconContext {\n effect(callback: () => unknown, label?: string): void\n}\n\n/**\n * Install the nav glyph.\n *\n * @param ctx - client context, for effect ownership.\n * @param resolveLabel - this plugin's current section label (the same thunk\n * the `settings.section` registration passes), re-read on every sync so a\n * locale switch is picked up without re-registering.\n */\nexport function installSettingsNavIcon(ctx: NavIconContext, resolveLabel: () => string): void {\n if (typeof document === 'undefined') return\n\n ctx.effect(() => {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-model-visibility'\n tag.dataset.pluginCss = 'dsh-model-visibility/settings-nav-icon'\n tag.textContent = navIconCss(eyeMaskUrl())\n document.head.appendChild(tag)\n\n let disposed = false\n let scheduled = false\n\n const sync = () => {\n scheduled = false\n if (disposed) return\n const wanted = resolveLabel()\n for (const row of document.querySelectorAll(NAV_ROW_SELECTOR)) {\n if (isOwnNavRow(row.textContent, wanted)) row.setAttribute(NAV_ICON_MARKER, '')\n else row.removeAttribute(NAV_ICON_MARKER)\n }\n }\n\n // Coalesce a burst of DOM mutations into one sync, and land it before the\n // next paint so the row never shows the gear first.\n const schedule = () => {\n if (scheduled || disposed) return\n scheduled = true\n queueMicrotask(sync)\n }\n\n sync()\n const observer = new MutationObserver(schedule)\n observer.observe(document.body, { childList: true, subtree: true, characterData: true })\n\n return () => {\n disposed = true\n observer.disconnect()\n for (const row of document.querySelectorAll(`[${NAV_ICON_MARKER}]`)) row.removeAttribute(NAV_ICON_MARKER)\n tag.remove()\n }\n }, 'model-visibility: settings nav icon')\n}\n","/**\r\n * dsh-model-visibility — browser half.\r\n *\r\n * Registers the model-visibility card as a top-level settings section\r\n * (`settings.section`) and its locale dictionaries. Cross-plugin\r\n * collaboration goes through cordis services only: the slot key's\r\n * declaration, the settings scope, and the wire handle arrive as type-only\r\n * merges, never value imports (the loader bundle-purity rule).\r\n *\r\n * 0.2.0: the `settingsScope` service is gone; the card's durable state rides\r\n * `ctx.configForms` (the shared describe mirror over the Host form\r\n * projection), and the Remote namespace carries formal types, so the wire\r\n * shape is no longer restated locally.\r\n */\r\nimport type { Context } from '@deepseek-ai/cordis'\r\n// Type-only merges: ctx.configForms (ui-settings), ctx.locale (locale),\r\n// ctx.slots (ui-renderer), and the `settings.section` slot declaration.\r\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\r\n// Type-only: ctx.remote with the formal `session.modelCatalog()` signature.\r\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\r\nimport { MODEL_VISIBILITY_NS, type VisibilitySection } from '../namespace.ts'\r\nimport { ModelVisibilityCardController, type ModelCatalogReader } from './controller.ts'\nimport { ModelVisibilityCard } from './ModelVisibilityCard.tsx'\nimport { LOCALE_NS, en, zh } from './locales.ts'\nimport { installSettingsNavIcon } from './settings-nav-icon.ts'\n\r\nexport const name = 'dsh-model-visibility'\r\n// `remote.session` carries the Host model catalog; `configForms` is the\r\n// durable hidden-list form; `slots`/`locale` are the contribution surfaces.\r\nexport const inject = ['slots', 'locale', 'configForms', 'remote', 'remote.session'] as const\r\n\r\nexport function apply(ctx: Context): void {\r\n ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'model-visibility: copy dictionaries')\r\n\r\n // The shared form over this plugin's own profile entry (id from\r\n // cordis.patch.yml); writes serialize through the Host settings document.\r\n const form = ctx.configForms.get<VisibilitySection>(MODEL_VISIBILITY_NS)\r\n const catalog: ModelCatalogReader = {\r\n async read() {\r\n const response = await ctx.remote.session.modelCatalog()\r\n if (!response.ok) {\r\n throw new Error(`${response.error.code}: ${response.error.message}`)\r\n }\r\n return response.value.groups\r\n },\r\n }\r\n const controller = new ModelVisibilityCardController(form, catalog)\r\n ctx.effect(() => () => { controller.dispose() }, 'model-visibility: card controller')\r\n\r\n // Keep the catalog fresh on pushed invalidations (the same channels the\r\n // official Models page listens to): provider-topology changes, and settings\r\n // writes — a provider's model-catalog edit (renames included) commits\r\n // volatile-only, which emits NO adapter event, so the settings document\r\n // channel is the only signal. Our own hidden-list write already re-reads\r\n // the catalog, so it is filtered out to avoid a redundant fetch.\r\n ctx.effect(() => {\r\n const offAdapters = ctx.remote.$on('llm/adapters-updated', () => { void controller.load() })\r\n const offSettings = ctx.remote.$on('settings/document-updated', (ns) => {\r\n if (ns !== MODEL_VISIBILITY_NS) void controller.load()\r\n })\r\n return () => {\r\n offAdapters()\r\n offSettings()\r\n }\r\n }, 'model-visibility: catalog invalidation')\r\n\r\n // The section card consumes the locale seat through the inject face\n // (mirroring the official Models page, whose face carries `t`).\n const t = ctx.locale.bind(LOCALE_NS)\n\n // The section's nav glyph. The shell picks nav icons from its own built-in\n // list of section ids and falls back to the settings gear, and a\n // `settings.section` registration has no icon to pass — so the card claims\n // its own row and swaps the gear for an eye. Same label thunk as the\n // registration below, so the row is re-claimed when the locale changes.\n installSettingsNavIcon(ctx, () => t('nav'))\n\n // Register as a top-level settings SECTION (nav entry 「模型可见性」, ordered\n // right after the official 模型 page at order=10) instead of a tab inside\n // the Plugins section — the card is a whole-page surface (search + batch\r\n // controls read better with room to breathe). The label thunk re-binds the\r\n // locale each call so a host language switch shows fresh text.\r\n //\r\n // Face contract for `settings.section` contributions (ui-slots): every\r\n // field of the injected object spreads onto the component as props, and\r\n // each `hooks` entry becomes a `use<Name>` snapshot-selector hook. The\r\n // component destructures { controller, useSnapshot, t } exactly like the\r\n // official ModelsSection does.\r\n const injected = () => ({\r\n controller,\r\n t,\r\n hooks: { snapshot: controller.store },\r\n })\r\n // Register the section only while the Host serves this plugin's namespace:\r\n // a deployment without the Host half shows no trace of the page instead of\r\n // a card stuck on its loading row.\r\n ctx.effect(\r\n () => ctx.configForms.whileServed([MODEL_VISIBILITY_NS], () =>\r\n ctx.slots.inject('settings.section', () => ctx.slots.register({\r\n name: 'settings.section',\r\n id: 'model-visibility',\r\n order: 11,\r\n label: () => t('nav'),\r\n inject: injected,\r\n } as never, ModelVisibilityCard as never))),\r\n 'model-visibility: settings section',\r\n )\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The eye glyph in the settings navigation.
3
+ *
4
+ * The settings shell picks nav glyphs from a closed list of section ids
5
+ * (`account`, `models`, `agent-presets`, `plugins`, `archived-sessions`) and
6
+ * falls back to its own gear for every other id; `settings.section` projects
7
+ * only `id` / `order` / `label`, so a registrant has no icon to pass — the
8
+ * slot contract in `@deepseek-ai/dsh-client-ui-settings` and the runtime
9
+ * slot inventory both list exactly those options. Every third-party section
10
+ * therefore wears the gear, this one included.
11
+ *
12
+ * So this card claims its own row once the dialog is mounted and swaps the
13
+ * fallback gear for an eye — the same glyph the feature is about. The
14
+ * approach follows the precedent set by `dshmarket` (and credited there to
15
+ * `dsh-better-sidebar` / `dsh-skill-mcp-panel`).
16
+ *
17
+ * Scope, deliberately narrow:
18
+ *
19
+ * - only the row whose visible text equals this plugin's own localized
20
+ * section label is marked; no shell structure is touched;
21
+ * - the marker and the injected stylesheet belong to a `ctx.effect`, so they
22
+ * are removed with the fiber;
23
+ * - a locale switch re-claims the row through the MutationObserver, so the
24
+ * label and the glyph never disagree.
25
+ *
26
+ * Delete this module (and its call in index.ts) the day `settings.section`
27
+ * grows an `icon` field.
28
+ */
29
+ /** Marks the one nav row this plugin owns. */
30
+ export declare const NAV_ICON_MARKER = "data-dsh-model-visibility-nav-icon";
31
+ /**
32
+ * The nav rows of the settings dialog. The shell renders each
33
+ * `settings.section` entry as a `<button>` inside the panel's `<nav>`
34
+ * (SettingsPanel in dsh-client-ui-settings-general).
35
+ */
36
+ export declare const NAV_ROW_SELECTOR = "[role=\"dialog\"] nav button";
37
+ /**
38
+ * Glyph box in px. The shell renders every nav icon at this size and ships no
39
+ * media query at all, so there is deliberately no narrow variant here.
40
+ */
41
+ export declare const NAV_ICON_SIZE = 16;
42
+ /**
43
+ * The eye as standalone SVG for a CSS `mask-image`.
44
+ *
45
+ * Painted pure black on purpose: a mask reads alpha only, and the visible
46
+ * colour comes from the element's `background-color: currentColor`, which
47
+ * keeps the glyph in step with the shell's own icons on hover/active state.
48
+ * The shape is the classic outline eye (almond contour + iris) on a 24 grid;
49
+ * `mask-size` scales it down to the shell's 16px box.
50
+ */
51
+ export declare function eyeMaskSvg(): string;
52
+ /** The mask URL for the glyph (encoded at runtime, never hand-escaped). */
53
+ export declare function eyeMaskUrl(svg?: string): string;
54
+ /**
55
+ * Whether a nav row is this plugin's own.
56
+ *
57
+ * Pure, and the only decision this feature makes: the row whose visible text
58
+ * is the section label the shell is currently projecting. An empty label
59
+ * matches nothing — a locale that has not resolved yet must not mark the
60
+ * whole nav.
61
+ */
62
+ export declare function isOwnNavRow(rowText: string | null | undefined, wantedLabel: string | null | undefined): boolean;
63
+ /** Stylesheet for the marked row: hide the shell's gear, draw the eye. */
64
+ export declare function navIconCss(maskUrl: string): string;
65
+ /** The slice of the client context this feature needs. */
66
+ export interface NavIconContext {
67
+ effect(callback: () => unknown, label?: string): void;
68
+ }
69
+ /**
70
+ * Install the nav glyph.
71
+ *
72
+ * @param ctx - client context, for effect ownership.
73
+ * @param resolveLabel - this plugin's current section label (the same thunk
74
+ * the `settings.section` registration passes), re-read on every sync so a
75
+ * locale switch is picked up without re-registering.
76
+ */
77
+ export declare function installSettingsNavIcon(ctx: NavIconContext, resolveLabel: () => string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-visibility",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "DeepSeek Harness plugin: control which models appear in the model selector — per-model switches, provider-level batch toggles, and search. Hiding a model never touches your provider config or running sessions.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",