@mars-sea/dsh-commandcode-provider 0.10.4 → 0.10.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.10.5] - 2026-09-11
8
+
9
+ ### Added
10
+ - **The terminal settings page lists every model as a checkbox.** The TUI's model allowlist was a text field expecting comma-separated catalog ids typed from memory; it now renders the whole catalog under plan-tier headings (Go → GOAT → Pro → Provider), free models first, each row carrying its plan/deal/context summary as the footer hint. Everything reads as checked until something is excluded, matching the adapter's "an unset allowlist shows every model" rule, and a model this build does not recognize (retired upstream, or added after this release) still gets a row of its own instead of becoming impossible to switch from the terminal.
11
+ - **`Config.modelVisibility`, a per-model override layer beside `visibleModels`.** The TUI cannot add an id to the array it edits, so each checkbox records `modelVisibility.<id>` (true listed / false hidden) instead; an id absent from the map keeps following `visibleModels`, and the subscription-tier filter still applies on top. A hand-edited document carrying a non-boolean entry falls back to the array rather than hiding the model.
12
+
13
+ ### Fixed
14
+ - **The terminal model checkboxes now save the model you toggled.** dsh-TUI keys a staged edit by the field's PATH, so the whole list of checkboxes sharing one `visibleModels` path shared one draft: each of them parsed that same draft on save, every write op addressed the same path, and only the last field's op survived — silently replacing the allowlist with what the last catalog model's row evaluated to instead of the row the user had switched. Each checkbox now addresses a path of its own, and a toggle writes only when it disagrees with the inherited state, so switching a model back leaves no residue in the document.
15
+
16
+ ### Changed
17
+ - **Per-release compatibility now covers dsh `0.1.5-rc.2`.** `dsh.compatibility.dshReleases` declares it `compatible` alongside `0.1.2-rc.1`, `0.1.3-alpha.1`, `0.1.3-alpha.2`, `0.1.5-alpha.1` and `0.1.5-rc.1`, so the DSH STORE can list the plugin for the current Harness release. Verified on a dsh-TUI profile running that engine: the `/settings` → Command Code page, the model checkbox list, and the picker's catalog all behave as they do on 0.1.5-rc.1. An older release keeps its own record rather than being replaced, so a user who has not moved to the newest engine still sees the listing.
18
+
19
+ ### Documentation
20
+ - **The install and update instructions now say which profile receives the plugin and how to escape pnpm's release-age gate** (both READMEs). A `dsh-tui` profile owns its own plugin list, so the web install does not reach the terminal: the terminal UI needs its own `dsh plugin --profile dsh-tui add`, and it now shows an exact version because a bare package name — or `@latest` — is silently resolved to the previous release for the first 24 hours after a publish (pnpm 11's `minimumReleaseAge`) and the install still exits successfully, which is how a fresh terminal install can end up with no Command Code settings page and no `commandcode` models at all.
21
+
7
22
  ## [0.10.4] - 2026-09-11
8
23
 
9
24
  ### Added
package/README.md CHANGED
@@ -47,7 +47,15 @@ Pick the release line that matches your DeepSeek Harness version:
47
47
  dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.9.1
48
48
  ```
49
49
 
50
- > The `latest` tag now always points at the current 0.1.2-line plugin release, so a plain `@latest` install gets the newest stable release for dsh 0.1.2+. Users on the old 0.5.0-era Harness line must pin `@0.9.1` explicitly.
50
+ > The `latest` tag points at the current 0.1.2-line plugin release. Users on the old 0.5.0-era Harness line must pin `@0.9.1` explicitly.
51
+
52
+ **pnpm 11 holds back new releases.** Its `minimumReleaseAge` defaults to 1440 minutes, so a version published less than a day ago is skipped and `@latest` resolves to the *previous* release — silently, with a success exit code. To install a release from the last 24 hours, name it exactly:
53
+
54
+ ```sh
55
+ dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.10.5
56
+ ```
57
+
58
+ The same applies to every profile you install into, including the terminal UI below.
51
59
 
52
60
  Fresh pnpm 10 marketplace generations are supported directly. Do not add a separate `@deepseek-ai/dsh-invariants` dependency; the plugin declares it as a Host peer so the active dsh profile remains the owner of Harness packages.
53
61
 
@@ -60,6 +68,14 @@ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@latest #
60
68
  dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 # older dsh (0.5.0 line, unmaintained)
61
69
  ```
62
70
 
71
+ Each profile updates separately — the terminal UI owns its own plugin list (see below):
72
+
73
+ ```sh
74
+ dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.10.5
75
+ ```
76
+
77
+ To move to a version published less than 24 hours ago, name it exactly as in Install above; pnpm 11's age gate resolves `@latest` to the previous release instead.
78
+
63
79
  Then restart the web app.
64
80
 
65
81
  ## Getting an API key
@@ -81,12 +97,14 @@ After restart, enter your API key in **Settings → Command Code** and save; **S
81
97
 
82
98
  ## Terminal UI (dsh-TUI)
83
99
 
84
- The plugin also works under a terminal front door — no separate install and no extra configuration:
100
+ The plugin also works under a terminal front door. **Each dsh profile owns its own plugin list**, so the web install above does not reach the terminal — add the plugin to the `dsh-tui` profile as well:
85
101
 
86
102
  ```sh
87
- dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
103
+ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.10.5
88
104
  ```
89
105
 
106
+ Pin the exact version here. For the first 24 hours after a release, a bare package name (or `@latest`) is silently resolved to the previous one: the install succeeds, but the profile gets the older build — which is how a fresh terminal install ends up with no **`/settings` → Command Code** page and no `commandcode` models at all.
107
+
90
108
  Then pick the provider in the model selector, or name it directly:
91
109
 
92
110
  ```text
@@ -95,7 +113,9 @@ Then pick the provider in the model selector, or name it directly:
95
113
 
96
114
  `/model` lists every registered provider, with Command Code's live catalog and its plan/deal/context annotations. The `/commandcode` usage dashboard works there too.
97
115
 
98
- **Entering the key.** The TUI has no web Models page, so the plugin declares its own page in the TUI settings screen — **`/settings` → Command Code** — with the API key, the API base, the out-of-plan model filter, the model allowlist, the active account, and the command language. The key field is write-only: it shows whether a key is configured and writes what you type to the credential store, never to a settings document. The same page is the only place a TUI-only user needs to visit.
116
+ **Entering the key.** The TUI has no web Models page, so the plugin declares its own page in the TUI settings screen — **`/settings` → Command Code** — with the API key, the API base, the out-of-plan model filter, the active account, and the command language. The key field is write-only: it shows whether a key is configured and writes what you type to the credential store, never to a settings document. The same page is the only place a TUI-only user needs to visit.
117
+
118
+ **Picking models.** That page lists the whole catalog as checkboxes, grouped by plan tier (Go → GOAT → Pro → Provider) with the free models first, so nothing has to be typed from memory. Everything starts checked, because an unset allowlist means "show every model" — uncheck the ones you do not want in the picker. The choice is stored as per-model overrides next to the `visibleModels` list the web page edits, so both surfaces can be used interchangeably and a plain `visibleModels` written by hand still works.
99
119
 
100
120
  Alternatively, set the key outside the TUI — any of these work, in this order of precedence:
101
121
 
package/README.zh-CN.md CHANGED
@@ -45,7 +45,15 @@
45
45
  dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.9.1
46
46
  ```
47
47
 
48
- > `latest` tag 现在始终指向当前 0.1.2 线的插件版本,普通 `@latest` 安装即可获得 dsh 0.1.2+ 的最新稳定版。旧 0.5.0 时代的 Harness 用户必须显式钉住 `@0.9.1`。
48
+ > `latest` tag 指向当前 0.1.2 线的插件版本。旧 0.5.0 时代的 Harness 用户必须显式钉住 `@0.9.1`。
49
+
50
+ **pnpm 11 会拦下刚发布的新版本。** 它的 `minimumReleaseAge` 默认为 1440 分钟,发布不足一天的版本会被跳过,`@latest` 解析到**上一个**版本 —— 而且是静默的,命令照样以成功退出。想装 24 小时内发布的版本,必须写精确版本号:
51
+
52
+ ```sh
53
+ dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.10.5
54
+ ```
55
+
56
+ 这一点对每个 profile 都成立,包括下面的终端界面。
49
57
 
50
58
  插件可直接在 pnpm 10 的全新插件市场 generation 中安装。不要另行添加 `@deepseek-ai/dsh-invariants` dependency;插件已将其声明为 Host peer,Harness 包仍由当前 dsh profile 统一管理。
51
59
 
@@ -58,6 +66,14 @@ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@latest #
58
66
  dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 # 更早的 dsh(0.5.0 线,不再维护)
59
67
  ```
60
68
 
69
+ 每个 profile 各自更新 —— 终端界面有独立的插件列表(见下文):
70
+
71
+ ```sh
72
+ dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.10.5
73
+ ```
74
+
75
+ 要更新到发布不足 24 小时的版本,和上面的安装一样写精确版本号;pnpm 11 的年龄门禁会把 `@latest` 解析成上一个版本。
76
+
61
77
  然后重启 Web 应用。
62
78
 
63
79
  ## 获取 API key
@@ -79,12 +95,14 @@ cmd login # macOS/Linux;Windows 原生版:cmdc login
79
95
 
80
96
  ## 终端界面(dsh-TUI)
81
97
 
82
- 插件同样支持终端前端,无需单独安装、无需额外配置:
98
+ 插件同样支持终端前端。**每个 dsh profile 有独立的插件列表**,所以上面那条 Web 安装命令不会装到终端里 —— 还要把插件装进 `dsh-tui` profile:
83
99
 
84
100
  ```sh
85
- dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
101
+ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.10.5
86
102
  ```
87
103
 
104
+ 这里请写精确版本号。新版本发布后的 24 小时内,只写包名(或 `@latest`)会被静默解析到上一个版本 —— 安装命令照样成功,但 profile 里拿到的是旧版本,结果就是全新的终端安装里既没有 **`/settings` → Command Code** 页面,也看不到任何 `commandcode` 模型。
105
+
88
106
  之后在模型选择器里选,或者直接指定:
89
107
 
90
108
  ```text
@@ -93,7 +111,9 @@ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
93
111
 
94
112
  `/model` 会列出所有已注册的 provider,Command Code 的实时目录连同套餐/优惠/上下文标注一起出现,`/commandcode` 用量面板在终端里同样可用。
95
113
 
96
- **填写 API key。** 终端没有网页版 Models 页面,所以插件会在终端设置页里声明自己的页面 —— **`/settings` → Command Code** —— 包含 API key、API 地址、隐藏套餐外模型、模型白名单、当前账号和命令语言。key 字段是只写的:它只显示"是否已配置",输入的内容写进凭据库,不会写进任何 settings 文档。只用终端的用户只需要访问这一个页面。
114
+ **填写 API key。** 终端没有网页版 Models 页面,所以插件会在终端设置页里声明自己的页面 —— **`/settings` → Command Code** —— 包含 API key、API 地址、隐藏套餐外模型、当前账号和命令语言。key 字段是只写的:它只显示"是否已配置",输入的内容写进凭据库,不会写进任何 settings 文档。只用终端的用户只需要访问这一个页面。
115
+
116
+ **选择模型。** 同一页面把整个模型目录列成勾选框,按套餐档位分组(Go → GOAT → Pro → Provider),免费模型排在最前,不需要手打任何模型 id。默认全部勾选——未设置白名单就等于"显示全部模型";把不想在选择器里看到的取消勾选即可。选择会以「单模型覆盖」的形式保存在网页端编辑的 `visibleModels` 列表旁边,两个界面可以混用,手写的 `visibleModels` 也照常生效。
97
117
 
98
118
  也可以在终端外配置 key,以下三种方式按优先级生效:
99
119
 
package/lib/client.js CHANGED
@@ -2051,7 +2051,7 @@ window.__ModuleLoader__.load({
2051
2051
  }
2052
2052
  //#endregion
2053
2053
  //#region package.json
2054
- var version = "0.10.4";
2054
+ var version = "0.10.5";
2055
2055
  var repository = {
2056
2056
  "type": "git",
2057
2057
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
package/lib/index.d.ts CHANGED
@@ -52,6 +52,16 @@ interface CommandCodeConnectionOptions {
52
52
  * catalog regardless so the page can always offer every model.
53
53
  */
54
54
  visibleModels?: string[] | undefined;
55
+ /**
56
+ * Per-model visibility overrides, keyed by catalog id. Written by the
57
+ * terminal settings page, whose checkbox list gives every model its own
58
+ * boolean field: a field writes one value at one path, so membership in
59
+ * {@link visibleModels} cannot be expressed from there, while a flag can.
60
+ * An id present here decides that model on its own (true = listed, false =
61
+ * hidden); an id absent here follows {@link visibleModels} exactly as it did
62
+ * before, so composition configs and the web page are unaffected.
63
+ */
64
+ modelVisibility?: Readonly<Record<string, boolean>> | undefined;
55
65
  /**
56
66
  * Optional protocol hint. `'auto'` (default) uses billing/cache plus
57
67
  * Provider API fallback; `'cli'` forces `/alpha/generate`; `'openai'`
@@ -1277,6 +1287,17 @@ interface TuiSettingsSectionsService {
1277
1287
  declare const ACTIVE_ACCOUNT_AUTO = "auto";
1278
1288
  /** The selector value meaning "no language override — follow the shell locale". */
1279
1289
  declare const LANG_AUTO = "auto";
1290
+ /** One catalog model offered as a checkbox. */
1291
+ interface TuiModelChoice {
1292
+ /** Catalog model id, e.g. `deepseek/deepseek-v4-pro`. */
1293
+ readonly id: string;
1294
+ /** Minimum plan tier key from `KNOWN_PLANS`. */
1295
+ readonly tier: string;
1296
+ /** Whether the model is currently free, so it leads its group. */
1297
+ readonly free: boolean;
1298
+ /** Footer hint for the focused row: plan tier · deal · peak · `Image`. */
1299
+ readonly hint: string;
1300
+ }
1280
1301
  /** Everything the section needs from the plugin entry. */
1281
1302
  interface CommandCodeTuiSettingsDeps {
1282
1303
  /** The plugin's settings namespace (`llm-commandcode`). */
@@ -1298,6 +1319,21 @@ interface CommandCodeTuiSettingsDeps {
1298
1319
  id: string;
1299
1320
  label: string;
1300
1321
  }[];
1322
+ /**
1323
+ * The stored model allowlist, read LIVE at save time. A checkbox judges its
1324
+ * inherited state against this, so it must be read when the write runs, not
1325
+ * captured when the section was registered. An empty list means "every model
1326
+ * is visible" (the adapter's rule).
1327
+ */
1328
+ visibleModels: () => readonly string[];
1329
+ /**
1330
+ * The per-model override map the checkboxes write, read per registration so
1331
+ * ids this build's catalog does not know still get a row of their own. Same
1332
+ * live-read rule as {@link visibleModels}.
1333
+ */
1334
+ modelVisibility?: () => Readonly<Record<string, boolean>> | undefined;
1335
+ /** The models to offer as checkboxes; defaults to the static snapshot. */
1336
+ modelChoices?: () => readonly TuiModelChoice[];
1301
1337
  }
1302
1338
  /**
1303
1339
  * Build the section descriptor. Pure, so tests can pin the exact fields
@@ -1308,9 +1344,9 @@ interface CommandCodeTuiSettingsDeps {
1308
1344
  * cannot express "unset" — cycling only ever lands on a declared option, so a
1309
1345
  * `select` would strand the user on a pinned value with no way back to
1310
1346
  * automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
1311
- * unset state reachable. The two booleans format their EFFECTIVE default
1312
- * (`filterModelsByPlan` unset means true at the adapter), so a fresh install
1313
- * reads true instead of the screen's "(empty)".
1347
+ * unset state reachable. `filterModelsByPlan` formats its EFFECTIVE default
1348
+ * (unset means true at the adapter), so a fresh install reads true instead of
1349
+ * the screen's "(empty)".
1314
1350
  */
1315
1351
  declare function buildCommandCodeTuiSection(deps: CommandCodeTuiSettingsDeps): TuiSettingsSection;
1316
1352
  /**
@@ -1367,6 +1403,15 @@ interface Config {
1367
1403
  * filter card; applies after the subscription-tier filter.
1368
1404
  */
1369
1405
  visibleModels?: string[];
1406
+ /**
1407
+ * Per-model visibility overrides from the terminal settings page's checkbox
1408
+ * list, keyed by catalog id (`true` = listed, `false` = hidden). An id here
1409
+ * decides that model on its own; an id absent here follows `visibleModels`.
1410
+ * dsh-TUI keys a staged edit by the field's path, so the checkboxes need one
1411
+ * path per model — a map — because a boolean field cannot express "this id
1412
+ * is a member of the array".
1413
+ */
1414
+ modelVisibility?: Record<string, boolean>;
1370
1415
  /**
1371
1416
  * Extra accounts for multi-account rotation. The top-level
1372
1417
  * `apiKey`/`apiKeyEnv` (plus the CLI auth file) always form the first
package/lib/index.js CHANGED
@@ -1875,7 +1875,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
1875
1875
  const access = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadBillingAccess();
1876
1876
  const visible = this.deps.options().visibleModels;
1877
1877
  const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
1878
- return catalog.filter((model) => modelVisibleInPlan(model.id, access)).filter((model) => allow === void 0 || allow.has(model.id)).map(toInfo).sort(compareByPlan);
1878
+ const overrides = this.deps.options().modelVisibility;
1879
+ return catalog.filter((model) => modelVisibleInPlan(model.id, access)).filter((model) => {
1880
+ const override = overrides?.[model.id];
1881
+ if (typeof override === "boolean") return override;
1882
+ return allow === void 0 || allow.has(model.id);
1883
+ }).map(toInfo).sort(compareByPlan);
1879
1884
  }
1880
1885
  async resolveModel(provider, model, signal) {
1881
1886
  const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
@@ -3786,6 +3791,57 @@ var CommandCodeSearchProvider = class {
3786
3791
  const ACTIVE_ACCOUNT_AUTO = "auto";
3787
3792
  /** The selector value meaning "no language override — follow the shell locale". */
3788
3793
  const LANG_AUTO = "auto";
3794
+ /** Tier display names, mirroring the web dropdown's headings (`model-select.ts`). */
3795
+ const TIER_TITLES = {
3796
+ go: "Go",
3797
+ goat: "GOAT",
3798
+ pro: "Pro",
3799
+ provider: "Provider",
3800
+ max: "Max"
3801
+ };
3802
+ /** Tiers in picker order; a model outside this set joins the "Other" group. */
3803
+ const TIER_ORDER = [
3804
+ "go",
3805
+ "goat",
3806
+ "pro",
3807
+ "provider",
3808
+ "max"
3809
+ ];
3810
+ /** The group holding models this build's catalog does not know. */
3811
+ const OTHER_GROUP_ID = "models-other";
3812
+ /**
3813
+ * Every model this build knows, in checkbox order: plan tier (Go first), then
3814
+ * free before paid inside a tier, then by id.
3815
+ *
3816
+ * The list is the static capability snapshot rather than a live catalog read
3817
+ * on purpose — a settings page must draw synchronously, and the snapshot is
3818
+ * synced from the same upstream table the picker's tier headings come from.
3819
+ * A model added upstream after this build still reaches the user: an empty
3820
+ * allowlist shows everything, and a model named in the allowlist but absent
3821
+ * here is rendered by the "Other" group instead of disappearing.
3822
+ */
3823
+ function commandCodeTuiModelChoices() {
3824
+ return Object.keys(KNOWN_PLANS).map((id) => ({
3825
+ id,
3826
+ tier: KNOWN_PLANS[id] ?? "",
3827
+ free: isFreeModel(id),
3828
+ hint: capabilityDescription(id)
3829
+ })).sort((a, b) => {
3830
+ const tierDelta = tierRank(a.tier) - tierRank(b.tier);
3831
+ if (tierDelta !== 0) return tierDelta;
3832
+ if (a.free !== b.free) return a.free ? -1 : 1;
3833
+ return a.id.localeCompare(b.id);
3834
+ });
3835
+ }
3836
+ /** Sort rank of a tier key; unknown tiers trail every known one. */
3837
+ function tierRank(tier) {
3838
+ const rank = TIER_ORDER.indexOf(tier);
3839
+ return rank === -1 ? TIER_ORDER.length : rank;
3840
+ }
3841
+ /** The stored allowlist as a clean id list (non-strings and blanks dropped). */
3842
+ function storedIds(value) {
3843
+ return Array.isArray(value) ? value.filter((id) => typeof id === "string" && id !== "") : [];
3844
+ }
3789
3845
  /**
3790
3846
  * Build the section descriptor. Pure, so tests can pin the exact fields
3791
3847
  * without a dsh-TUI host.
@@ -3795,13 +3851,65 @@ const LANG_AUTO = "auto";
3795
3851
  * cannot express "unset" — cycling only ever lands on a declared option, so a
3796
3852
  * `select` would strand the user on a pinned value with no way back to
3797
3853
  * automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
3798
- * unset state reachable. The two booleans format their EFFECTIVE default
3799
- * (`filterModelsByPlan` unset means true at the adapter), so a fresh install
3800
- * reads true instead of the screen's "(empty)".
3854
+ * unset state reachable. `filterModelsByPlan` formats its EFFECTIVE default
3855
+ * (unset means true at the adapter), so a fresh install reads true instead of
3856
+ * the screen's "(empty)".
3801
3857
  */
3802
3858
  function buildCommandCodeTuiSection(deps) {
3803
3859
  const ref = deps.apiKeyRef();
3804
3860
  const slots = deps.accountSlots();
3861
+ const choices = (deps.modelChoices ?? commandCodeTuiModelChoices)();
3862
+ const catalogIds = choices.map((choice) => choice.id);
3863
+ const known = new Set(catalogIds);
3864
+ const stored = storedIds(deps.visibleModels());
3865
+ const overrides = deps.modelVisibility?.() ?? {};
3866
+ const extras = [.../* @__PURE__ */ new Set([...stored, ...Object.keys(overrides)])].filter((id) => !known.has(id));
3867
+ const tierGroups = TIER_ORDER.filter((tier) => choices.some((choice) => choice.tier === tier)).map((tier) => ({
3868
+ id: `models-${tier}`,
3869
+ title: `${TIER_TITLES[tier] ?? tier} models`,
3870
+ descriptions: { zh: `${TIER_TITLES[tier] ?? tier} 模型` }
3871
+ }));
3872
+ const unranked = choices.filter((choice) => tierRank(choice.tier) === TIER_ORDER.length);
3873
+ const otherGroup = unranked.length > 0 || extras.length > 0 ? [{
3874
+ id: OTHER_GROUP_ID,
3875
+ title: "Other models",
3876
+ descriptions: { zh: "其他模型" }
3877
+ }] : [];
3878
+ /**
3879
+ * One checkbox: a boolean at a path of its OWN (`modelVisibility.<id>`).
3880
+ *
3881
+ * The path must be unique per model. dsh-TUI keys a staged draft by the
3882
+ * field's path (`fieldKey`), so N checkboxes sharing `visibleModels` share
3883
+ * ONE draft: every one of them then parses that same draft on save, all N
3884
+ * write ops address the same path, and only the LAST field's op survives —
3885
+ * which silently rewrote the allowlist from the last catalog model instead
3886
+ * of the one that was toggled. A per-model key is what makes a checkbox
3887
+ * express one model's state.
3888
+ *
3889
+ * An override equal to what the array already says is written as a CLEAR, so
3890
+ * toggling a model back to its inherited state leaves no residue and the
3891
+ * document stays minimal.
3892
+ */
3893
+ const modelField = (id, hint, group) => ({
3894
+ path: ["modelVisibility", id],
3895
+ group,
3896
+ kind: "boolean",
3897
+ label: id,
3898
+ ...hint === "" ? {} : { hint },
3899
+ format: (value) => {
3900
+ if (typeof value === "boolean") return String(value);
3901
+ const listed = storedIds(deps.visibleModels());
3902
+ return String(listed.length === 0 || listed.includes(id));
3903
+ },
3904
+ parse: (text) => {
3905
+ const on = text.trim() === "true";
3906
+ const listed = storedIds(deps.visibleModels());
3907
+ return on === (listed.length === 0 || listed.includes(id)) ? { kind: "clear" } : {
3908
+ kind: "set",
3909
+ value: on
3910
+ };
3911
+ }
3912
+ });
3805
3913
  return {
3806
3914
  ns: deps.ns,
3807
3915
  title: deps.title ?? "Command Code",
@@ -3817,6 +3925,8 @@ function buildCommandCodeTuiSection(deps) {
3817
3925
  title: "Models",
3818
3926
  descriptions: { zh: "模型" }
3819
3927
  },
3928
+ ...tierGroups,
3929
+ ...otherGroup,
3820
3930
  {
3821
3931
  id: "advanced",
3822
3932
  title: "Advanced",
@@ -3865,23 +3975,9 @@ function buildCommandCodeTuiSection(deps) {
3865
3975
  value: text.trim() === "true"
3866
3976
  })
3867
3977
  },
3868
- {
3869
- path: ["visibleModels"],
3870
- group: "models",
3871
- kind: "text",
3872
- label: "Visible models",
3873
- descriptions: { zh: "显示的模型" },
3874
- hint: "Comma-separated catalog ids; empty shows every model.",
3875
- hintDescriptions: { zh: "以逗号分隔的模型 id;留空表示显示全部模型。" },
3876
- format: (value) => (Array.isArray(value) ? value.filter((id) => typeof id === "string" && id !== "") : []).join(", "),
3877
- parse: (text) => {
3878
- const ids = text.split(",").map((id) => id.trim()).filter((id) => id !== "");
3879
- return ids.length === 0 ? { kind: "clear" } : {
3880
- kind: "set",
3881
- value: ids
3882
- };
3883
- }
3884
- },
3978
+ ...choices.filter((choice) => tierRank(choice.tier) !== TIER_ORDER.length).map((choice) => modelField(choice.id, choice.hint, `models-${choice.tier}`)),
3979
+ ...unranked.map((choice) => modelField(choice.id, choice.hint, OTHER_GROUP_ID)),
3980
+ ...extras.map((id) => modelField(id, capabilityDescription(id), OTHER_GROUP_ID)),
3885
3981
  {
3886
3982
  path: ["activeAccount"],
3887
3983
  group: "advanced",
@@ -3968,15 +4064,18 @@ function tuiSettingsService(ctx) {
3968
4064
  }
3969
4065
  /**
3970
4066
  * Identity of everything in the section that can change at runtime: the
3971
- * credential reference behind the API-key field and the account slots behind
3972
- * the active-account selector. Re-registration is skipped while this matches,
3973
- * so ordinary settings writes never churn the screen's section list.
4067
+ * credential reference behind the API-key field, the account slots behind the
4068
+ * active-account selector, and the stored-but-unknown allowlist entries that
4069
+ * get a checkbox of their own. Re-registration is skipped while this matches,
4070
+ * so ordinary settings writes never churn the screen's section list — the
4071
+ * checkboxes themselves read their state live and need no re-declaration.
3974
4072
  */
3975
4073
  function sectionSignature(section) {
3976
4074
  const active = section.fields.find((field) => field.path.join(".") === "activeAccount");
3977
4075
  return JSON.stringify({
3978
4076
  secret: section.fields.find((field) => field.secret !== void 0)?.secret?.ref ?? "",
3979
- options: active?.options?.map((option) => option.value) ?? []
4077
+ options: active?.options?.map((option) => option.value) ?? [],
4078
+ other: section.fields.filter((field) => field.group === OTHER_GROUP_ID).map((field) => field.label)
3980
4079
  });
3981
4080
  }
3982
4081
  /**
@@ -4065,6 +4164,15 @@ const Config = z.object({
4065
4164
  streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
4066
4165
  filterModelsByPlan: z.boolean(),
4067
4166
  visibleModels: z.array(z.string()),
4167
+ /**
4168
+ * Per-model visibility overrides for the terminal settings page's checkbox
4169
+ * list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
4170
+ * PATH, so two checkboxes sharing one path would overwrite each other's
4171
+ * draft and the section's last model would decide every write; a map gives
4172
+ * each checkbox a path of its own. An id listed here wins over
4173
+ * {@link visibleModels}; ids absent here keep following it.
4174
+ */
4175
+ modelVisibility: z.dict(z.boolean()),
4068
4176
  webSearch: z.boolean().default(true),
4069
4177
  accounts: z.array(z.object({
4070
4178
  label: z.string(),
@@ -4094,9 +4202,22 @@ function resolveAdapterOptions(config) {
4094
4202
  requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
4095
4203
  streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
4096
4204
  filterModelsByPlan: config.filterModelsByPlan ?? true,
4097
- visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0
4205
+ visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
4206
+ modelVisibility: readModelVisibility(config.modelVisibility)
4098
4207
  };
4099
4208
  }
4209
+ /**
4210
+ * Per-model visibility overrides, cleaned for the adapter. Programmatic
4211
+ * construction may bypass Schemastery normalization, so a non-object or a
4212
+ * non-boolean entry is dropped here instead of reaching the picker filter.
4213
+ * @param raw - The `modelVisibility` value from any config source.
4214
+ * @returns A frozen id → boolean map, or undefined when nothing is set.
4215
+ */
4216
+ function readModelVisibility(raw) {
4217
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
4218
+ const entries = Object.entries(raw).filter((entry) => entry[0] !== "" && typeof entry[1] === "boolean");
4219
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
4220
+ }
4100
4221
  function apply(ctx, config) {
4101
4222
  let current = () => config;
4102
4223
  let lastRaw;
@@ -4268,7 +4389,9 @@ function apply(ctx, config) {
4268
4389
  accountSlots: () => slots().map((slot) => ({
4269
4390
  id: slot.id,
4270
4391
  label: slot.label
4271
- }))
4392
+ })),
4393
+ visibleModels: () => options().visibleModels ?? [],
4394
+ modelVisibility: () => options().modelVisibility
4272
4395
  });
4273
4396
  tuiCtx.effect(() => () => {
4274
4397
  refreshTuiSettings = void 0;