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

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,28 @@ 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.4] - 2026-09-11
8
+
9
+ ### Added
10
+ - **A Command Code page in the dsh-TUI settings screen** ([#28](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/28)). The terminal front door now lists Command Code under `/settings`, 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 is a write-only credential field: the literal goes to the credential store and never into a settings document, and the field shows only whether one is configured. Until now a TUI-only user had no way to enter a key at all — dsh-TUI's own `/provider` wizard manages its `llm-pi-ai` routes exclusively, and the web Models page was the only surface that could write it. The page appears only on a profile that mounts dsh-TUI; everywhere else the plugin stays a plain LLM-provider bundle. Every model list and account choice reads live settings, so a change lands on the next request with no restart.
11
+
12
+ ### Changed
13
+ - **Per-release compatibility now covers dsh `0.1.5-rc.1`.** `dsh.compatibility.dshReleases` declares it `compatible` alongside `0.1.2-rc.1`, `0.1.3-alpha.1`, `0.1.3-alpha.2` and `0.1.5-alpha.1`, so the DSH STORE can list the plugin for the current Harness release. Verified on both surfaces: the web profile runs the plugin on this engine (settings page, Models-page card, chat and the `/commandcode` dashboard), and a dsh-TUI 0.10.0 profile selects and serves Command Code there. 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.
14
+
15
+ ### Security
16
+ - **A literal API key in the composition config no longer rides a settings response.** The `apiKey` fields (top-level and per extra account) are now declared `role('secret')`, so the harness strips them from every settings descriptor it serves. The settings page was already writing keys through the credentials seam — which is what keeps them out of a settings document — but a key written by hand into `cordis.patch.yml` or `settings.yaml` was returned to the browser verbatim, including on a remote-Host setup where that means another machine.
17
+ - **A web page can no longer cancel a login in progress.** The loopback callback's denial branch now checks the login's state token before it ends the attempt, matching the official CLI. A denial is terminal, and the browser sends a `text/plain` POST as a CORS simple request regardless of the origin allowlist, so any open page could previously kill the attempt (and show "authorization denied") by blindly posting `{"error":"access_denied"}` to the loopback port. A denial that carries the real token still ends the attempt, as the Studio's own flow does.
18
+
19
+ ### Fixed
20
+ - **Cancelling a login now really cancels it.** The key delivered by the callback is validated against `/alpha/whoami` before it is stored, and cancelling during that check was ignored: the credential was written anyway and the state flipped back to success while the page (which had already stopped polling) kept showing "cancelled". The completion now re-checks that it still owns the attempt — before writing the key and before publishing any status — so a cancel or a newly started attempt wins.
21
+ - **"Sign in" after a completed callback starts a fresh attempt.** Once the callback is consumed the loopback server is closed, but the status still read `waiting`, so the next sign-in handed back the previous attempt's URL — a link to a dead port. A `waiting` status whose server is gone is now retired instead of reused.
22
+ - **Saving the settings page no longer deletes account entries it cannot show.** The accounts list is written whole, and the page rebuilt it from the rows it displays — so a composition-config entry carrying a literal `apiKey` (which has no row) was silently deleted by any save, and the literal key of the entries it could show was stripped. The write now starts from the stored list and only rewrites the rows the page manages, so unknown entries and literal keys survive.
23
+ - **A failed save no longer throws away what you typed.** Two staged edits were cleared even when the failing write never ran: an account's label reverted to the generated name (and was then persisted that way on the retry), and a routing-rule edit was silently reverted with the page left clean — no dirty marker and no way to retry. Both are now kept until the stored section proves the write landed. Rule drafts are still dropped once the rules write itself lands, because the positional row ids shift and a kept draft would be applied to the wrong row.
24
+ - **The "clean up stale models" button no longer empties the allowlist while the catalog is missing.** Before the first catalog fetch lands — and after a failure — the catalog is empty, so every selected model looked retired and one click cleared the whole list. The cleanup, and its hint, are now shown only when a catalog is actually in hand; the explicit "show all" action remains available.
25
+ - **Whitespace around a stored API key no longer breaks multi-account rotation.** The account pool marked a rejected key exactly as it was stored, while the request path trims it (a key from the credentials seam, a `.env` line, or a shell export picks up surrounding whitespace), so a 429/401 mark landed on a string nothing looked up again: the same account was offered again, the account card showed no mark, and the usage endpoints failed while chat kept working. Keys are now normalized once, where a slot's key enters the pool.
26
+ - **Images returned by a tool are no longer invisible to Vision models** ([#30](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/30)). When the agent read a local image with `read_image` (an absolute path in the prompt) instead of receiving it as an attachment or pasted image, the request replayed only the tool's metadata text — the nested `image` block inside the tool result was flattened away, so the model answered that it could see no pixels. Both transports now carry the image's bytes in a user message right after the tool message, introduced by an `Attached image(s) from tool result:` note: the image stays outside the tool result, where neither `/alpha/generate`'s text-only tool output nor Chat Completions' `role: 'tool'` content could hold it. A tool result that repeats one attachment sends it once, an image-only result no longer produces an empty tool message, and text-only tool results are byte-for-byte unchanged.
27
+ - **Test coverage for the above**, plus the previously unpinned paths they exposed: account writes preserving composition entries, label/rule drafts across every failure order, the login callback's authentication and attempt ownership, credential normalization in the pool, and the catalog-readiness gate (each verified by reverting its fix and watching the new test fail).
28
+
7
29
  ## [0.10.3] - 2026-09-10
8
30
 
9
31
  ### Changed
package/README.md CHANGED
@@ -18,6 +18,7 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
18
18
 
19
19
  - **Plugin bundle** — install into any dsh profile with `dsh plugin add`; registers a `commandcode` provider route with a live model catalog.
20
20
  - **Dedicated settings page** — API key, connection options, a live account-usage card, and a "Hide out-of-plan models" toggle.
21
+ - **Works in the terminal too** — the same install serves a [dsh-TUI](#terminal-ui-dsh-tui) profile, with its own **`/settings` → Command Code** page for the API key and the model controls.
21
22
  - **Models-page key card** — the **Settings → Models → Command Code** card carries the key status, a paste field, and the sign-in button inline.
22
23
  - **In-browser sign-in for keys** — start the official authorization flow (the same one `cmd login` runs) from the settings page; the approved key lands in the local credential service automatically. Manual paste remains the fallback.
23
24
  - **Multi-account rotation** — when one account hits its usage limit, requests switch to the next account automatically. See [Account rotation](#account-rotation).
@@ -78,6 +79,47 @@ Or skip the CLI: click **Sign in to Command Code** under **Settings → Command
78
79
 
79
80
  After restart, enter your API key in **Settings → Command Code** and save; **Settings → Models** shows a **Command Code** card, and the model picker lists the live catalog under **commandcode**. Send a message with a model your plan includes.
80
81
 
82
+ ## Terminal UI (dsh-TUI)
83
+
84
+ The plugin also works under a terminal front door — no separate install and no extra configuration:
85
+
86
+ ```sh
87
+ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
88
+ ```
89
+
90
+ Then pick the provider in the model selector, or name it directly:
91
+
92
+ ```text
93
+ /model commandcode/deepseek/deepseek-v4.1-flash
94
+ ```
95
+
96
+ `/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
+
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.
99
+
100
+ Alternatively, set the key outside the TUI — any of these work, in this order of precedence:
101
+
102
+ ```sh
103
+ export COMMANDCODE_API_KEY="user_..." # launching environment
104
+ cmd login # writes ~/.commandcode/auth.json
105
+ ```
106
+
107
+ **Making Command Code the default.** dsh-TUI pins its own agent route, and its `agent-default-model` setting does not override it. To start every session on Command Code, override the `agent-loop` row in your profile patch (`$DSH_HOME/profiles/dsh-tui/cordis.patch.yml`):
108
+
109
+ ```yaml
110
+ - id: agent-loop
111
+ inject: [tuiStartup]
112
+ config:
113
+ agents:
114
+ - id: main
115
+ provider: commandcode
116
+ model: deepseek/deepseek-v4.1-flash
117
+ reasoningEffort: max
118
+ cwd: !!js process.cwd()
119
+ ```
120
+
121
+ **Engine version.** The plugin needs a dsh engine that exports `ToolCallId` from `@deepseek-ai/dsh-llm` — **dsh 0.1.2-alpha.3 or later**. That includes the engine dsh-TUI recommends (0.1.2-rc.1) and every later release, but *not* the oldest engines its peer range nominally allows: on dsh 0.1.0-rc.6 or 0.1.1-rc.2 the plugin's module import fails and the TUI will not start. Upgrade the engine, or use the web profile.
122
+
81
123
  ## Usage dashboard
82
124
 
83
125
  The plugin registers a `/commandcode` slash command showing per-account usage:
package/README.zh-CN.md CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  - **插件包**:一条 `dsh plugin add` 命令安装到任意 dsh 配置,注册 `commandcode` provider 路由,带实时模型目录。
20
20
  - **专属设置页**:API key 输入、连接参数、实时「账户用量」卡片和「隐藏套餐外模型」开关。
21
+ - **终端界面同样可用**:同一次安装即可服务 [dsh-TUI](#终端界面dsh-tui) 配置,终端里有自己的 **`/settings` → Command Code** 页面来填 key 和调模型。
21
22
  - **Models 页快捷卡片**:**设置 → Models → Command Code** 卡片内直接显示 key 状态、粘贴输入框和登录按钮。
22
23
  - **浏览器内登录获取 key**:设置页一键发起官方授权(与 `cmd login` 同一流程),完成后密钥自动写入本机凭据服务,无需手动创建或粘贴;不可用时随时退回手动粘贴。
23
24
  - **多账户轮换**:一个账户用量打满后,请求自动切换到下一个账户。详见[多账户轮换](#多账户轮换)。
@@ -76,6 +77,47 @@ cmd login # macOS/Linux;Windows 原生版:cmdc login
76
77
 
77
78
  重启后,在 **设置 → Command Code** 填入 API key 并保存;**设置 → Models** 出现 **Command Code** 卡片,模型选择器在 **commandcode** 下列出实时目录。选择套餐内包含的模型发送消息即可。
78
79
 
80
+ ## 终端界面(dsh-TUI)
81
+
82
+ 插件同样支持终端前端,无需单独安装、无需额外配置:
83
+
84
+ ```sh
85
+ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
86
+ ```
87
+
88
+ 之后在模型选择器里选,或者直接指定:
89
+
90
+ ```text
91
+ /model commandcode/deepseek/deepseek-v4.1-flash
92
+ ```
93
+
94
+ `/model` 会列出所有已注册的 provider,Command Code 的实时目录连同套餐/优惠/上下文标注一起出现,`/commandcode` 用量面板在终端里同样可用。
95
+
96
+ **填写 API key。** 终端没有网页版 Models 页面,所以插件会在终端设置页里声明自己的页面 —— **`/settings` → Command Code** —— 包含 API key、API 地址、隐藏套餐外模型、模型白名单、当前账号和命令语言。key 字段是只写的:它只显示"是否已配置",输入的内容写进凭据库,不会写进任何 settings 文档。只用终端的用户只需要访问这一个页面。
97
+
98
+ 也可以在终端外配置 key,以下三种方式按优先级生效:
99
+
100
+ ```sh
101
+ export COMMANDCODE_API_KEY="user_..." # 启动环境变量
102
+ cmd login # 写入 ~/.commandcode/auth.json
103
+ ```
104
+
105
+ **把 Command Code 设为默认模型。** dsh-TUI 写死了自己的 agent 路由,它的 `agent-default-model` 设置不会覆盖它。要让每个会话默认走 Command Code,请在自己的 profile patch(`$DSH_HOME/profiles/dsh-tui/cordis.patch.yml`)里覆盖 `agent-loop` 行:
106
+
107
+ ```yaml
108
+ - id: agent-loop
109
+ inject: [tuiStartup]
110
+ config:
111
+ agents:
112
+ - id: main
113
+ provider: commandcode
114
+ model: deepseek/deepseek-v4.1-flash
115
+ reasoningEffort: max
116
+ cwd: !!js process.cwd()
117
+ ```
118
+
119
+ **引擎版本要求。** 插件需要导出了 `ToolCallId` 的 dsh 引擎,即 **dsh 0.1.2-alpha.3 或更高**。这覆盖了 dsh-TUI 推荐的引擎(0.1.2-rc.1)以及之后的所有版本,但不包含它 peer 范围里名义上允许的最老引擎:在 dsh 0.1.0-rc.6 / 0.1.1-rc.2 上,插件的模块导入会失败,TUI 无法启动。请升级引擎,或改用 web profile。
120
+
79
121
  ## 用量面板
80
122
 
81
123
  插件注册了 `/commandcode` 斜杠命令,显示各账户的用量状态:
package/lib/client.js CHANGED
@@ -312,6 +312,14 @@ window.__ModuleLoader__.load({
312
312
  return b.every((id) => set.has(id));
313
313
  }
314
314
  /**
315
+ * Order-sensitive content fingerprint of the stored routing rules. Stored rule
316
+ * ids are positional (`rule-<index>`), so equality of this fingerprint across a
317
+ * save is exactly the statement "no rules write landed and no row shifted".
318
+ */
319
+ function ruleFingerprint(rules) {
320
+ return JSON.stringify(rules.map((rule) => [rule.models, rule.account]));
321
+ }
322
+ /**
315
323
  * Controller bridging the `llm-commandcode` scope and the credentials domain
316
324
  * onto the page. Public API mirrors the harness's CardForm actions, so the
317
325
  * component stays thin.
@@ -351,6 +359,12 @@ window.__ModuleLoader__.load({
351
359
  * would delete the wrong row).
352
360
  */
353
361
  removedRuleIds = /* @__PURE__ */ new Map();
362
+ /**
363
+ * Fingerprint of the stored routing rules when the last `save()` started;
364
+ * `undefined` outside a save. Lets reconcile distinguish a landed rules
365
+ * write from a save that failed before reaching it.
366
+ */
367
+ rulesBeforeSave = void 0;
354
368
  /** Staged visible-model allowlist (undefined = no draft). */
355
369
  visibleModelsDraft = void 0;
356
370
  /** The catalog the model editors offer (Host-side). */
@@ -646,6 +660,7 @@ window.__ModuleLoader__.load({
646
660
  }
647
661
  this.saving = true;
648
662
  this.failed = false;
663
+ this.rulesBeforeSave = ruleFingerprint(this.storedRules());
649
664
  this.publish();
650
665
  let landed = true;
651
666
  for (const run of [
@@ -682,10 +697,10 @@ window.__ModuleLoader__.load({
682
697
  }
683
698
  /**
684
699
  * Drop account staging the stored section already reflects: additions whose
685
- * ref is now stored, removals whose ref is gone, and label drafts matching
686
- * the stored label. Key drafts are kept — a landed key write is idempotent
687
- * on retry, and the draft carries the user's intent when it was the
688
- * accounts write that failed.
700
+ * ref is now stored, removals whose ref is gone, and label drafts that the
701
+ * stored label proves landed. Key drafts are kept — a landed key write is
702
+ * idempotent on retry, and the draft carries the user's intent when it was
703
+ * the accounts write that failed.
689
704
  */
690
705
  reconcileAccountStaging() {
691
706
  const stored = new Set(this.storedExtras().map((extra) => extra.ref));
@@ -693,7 +708,7 @@ window.__ModuleLoader__.load({
693
708
  for (const ref of [...this.removedRefs]) if (!stored.has(ref)) this.removedRefs.delete(ref);
694
709
  for (const [ref, text] of [...this.labelDrafts]) {
695
710
  const storedLabel = this.storedExtras().find((extra) => extra.ref === ref)?.label;
696
- if (storedLabel === void 0 || storedLabel === text.trim()) this.labelDrafts.delete(ref);
711
+ if (storedLabel !== void 0 && storedLabel === text.trim()) this.labelDrafts.delete(ref);
697
712
  }
698
713
  for (const ref of [...this.keyClears]) if (this.credentialStates.get(ref)?.configured !== true) this.keyClears.delete(ref);
699
714
  }
@@ -861,14 +876,25 @@ window.__ModuleLoader__.load({
861
876
  this.catalogFailed = true;
862
877
  }).then(() => this.publish());
863
878
  }
864
- /** The stored extra accounts from the settings section (`accounts`). */
865
- storedExtras() {
879
+ /** The raw `accounts` array of the stored section, verbatim. */
880
+ rawStoredAccounts() {
866
881
  const raw = this.scope.getSnapshot().value?.accounts;
867
882
  if (!Array.isArray(raw)) return [];
883
+ return raw.filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
884
+ }
885
+ /**
886
+ * The stored extra accounts from the settings section (`accounts`): the rows
887
+ * this page can address, i.e. the ones carrying a credential reference.
888
+ *
889
+ * Entries the page cannot name are deliberately NOT listed here but are also
890
+ * never dropped — `writeAccounts()` rebuilds the stored list from
891
+ * {@link rawStoredAccounts} and only rewrites the reference-carrying entries
892
+ * it manages (see the note there), so a literal-key entry stays in the
893
+ * document and simply has no row.
894
+ */
895
+ storedExtras() {
868
896
  const out = [];
869
- for (const entry of raw) {
870
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
871
- const record = entry;
897
+ for (const record of this.rawStoredAccounts()) {
872
898
  const ref = record.apiKeyEnv;
873
899
  if (typeof ref !== "string" || ref === "") continue;
874
900
  const label = record.label;
@@ -945,20 +971,54 @@ window.__ModuleLoader__.load({
945
971
  runs.push(() => this.writeAccounts());
946
972
  return runs;
947
973
  }
948
- /** Persist the staged accounts list into the settings section. */
974
+ /**
975
+ * Persist the staged accounts list into the settings section.
976
+ *
977
+ * The stored list is the base — NOT a list rebuilt from this page's rows.
978
+ * A composition-config entry may carry a literal `apiKey` (or a shape this
979
+ * page does not know), and the settings layer replaces the whole array, so a
980
+ * rebuilt list would silently delete every entry the page cannot name along
981
+ * with the literal keys of the entries it can. Entries are therefore carried
982
+ * over verbatim and only the reference-carrying rows are rewritten (label
983
+ * draft applied, staged removals dropped, staged additions appended).
984
+ */
949
985
  async writeAccounts() {
950
- const base = [...this.storedExtras().filter((extra) => !this.removedRefs.has(extra.ref)), ...this.addedAccounts];
951
- const seen = /* @__PURE__ */ new Set();
952
- const list = base.filter((extra) => !seen.has(extra.ref) && (seen.add(extra.ref), true)).map((extra) => {
953
- const draft = this.labelDrafts.get(extra.ref)?.trim();
954
- return {
955
- label: draft !== void 0 && draft !== "" ? draft : extra.label,
956
- apiKeyEnv: extra.ref
957
- };
958
- });
986
+ const removed = this.removedRefs;
987
+ const written = /* @__PURE__ */ new Set();
988
+ const list = [];
989
+ for (const entry of this.rawStoredAccounts()) {
990
+ const ref = entry.apiKeyEnv;
991
+ if (typeof ref !== "string" || ref === "") {
992
+ list.push({ ...entry });
993
+ continue;
994
+ }
995
+ if (removed.has(ref) || written.has(ref)) continue;
996
+ written.add(ref);
997
+ list.push(this.accountEntry(ref, entry));
998
+ }
999
+ for (const extra of this.addedAccounts) {
1000
+ if (written.has(extra.ref)) continue;
1001
+ written.add(extra.ref);
1002
+ list.push(this.accountEntry(extra.ref, extra));
1003
+ }
959
1004
  await this.scope.set("accounts", list);
960
- const after = this.storedExtras();
961
- return after.length === list.length && list.every((item, index) => after[index]?.ref === item.apiKeyEnv);
1005
+ const after = this.rawStoredAccounts();
1006
+ return after.length === list.length && list.every((item, index) => after[index]?.apiKeyEnv === item.apiKeyEnv);
1007
+ }
1008
+ /**
1009
+ * One written account entry: the stored/added facts plus the label draft.
1010
+ * `fallback` contributes the non-managed fields (a stored entry's literal
1011
+ * `apiKey`, or any future field) so a rewrite never strips them.
1012
+ */
1013
+ accountEntry(ref, fallback) {
1014
+ const draft = this.labelDrafts.get(ref)?.trim();
1015
+ const base = "ref" in fallback ? { label: fallback.label } : { ...fallback };
1016
+ const storedLabel = base.label;
1017
+ return {
1018
+ ...base,
1019
+ label: draft !== void 0 && draft !== "" ? draft : typeof storedLabel === "string" && storedLabel !== "" ? storedLabel : ref,
1020
+ apiKeyEnv: ref
1021
+ };
962
1022
  }
963
1023
  /** The stored routing rules from the settings section (`modelAccountRules`). */
964
1024
  storedRules() {
@@ -1034,6 +1094,7 @@ window.__ModuleLoader__.load({
1034
1094
  const stored = this.storedRules();
1035
1095
  this.addedRules = this.addedRules.filter((added) => !stored.some((rule) => sameModels(added.models, rule.models) && added.account === rule.account));
1036
1096
  for (const [id, snapshot] of [...this.removedRuleIds]) if (!stored.some((rule) => sameModels(snapshot.models, rule.models) && snapshot.account === rule.account)) this.removedRuleIds.delete(id);
1097
+ if (this.rulesBeforeSave !== void 0 && ruleFingerprint(stored) === this.rulesBeforeSave) return;
1037
1098
  this.ruleDrafts.clear();
1038
1099
  }
1039
1100
  /** The stored visible-model allowlist (`visibleModels`); empty = show all. */
@@ -1966,9 +2027,31 @@ window.__ModuleLoader__.load({
1966
2027
  function toggleModelSelection(selected, modelId) {
1967
2028
  return selected.includes(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId];
1968
2029
  }
2030
+ /**
2031
+ * Whether the catalog is trustworthy enough to call an unlisted selection
2032
+ * "retired". FALSE while the first fetch is still in flight and after a
2033
+ * failure, because the catalog is empty then and every selected id would look
2034
+ * stale — which turns the one-click stale cleanup into a button that silently
2035
+ * empties the allowlist. A successfully loaded but empty catalog is treated as
2036
+ * untrustworthy too: an empty list is far more likely a Host problem than every
2037
+ * model being retired at once, and the explicit "show all" entry covers the
2038
+ * user who really wants to clear the list.
2039
+ */
2040
+ function catalogIsReady(readiness) {
2041
+ return readiness.catalogIds.length > 0 && !readiness.catalogFailed;
2042
+ }
2043
+ /**
2044
+ * Selected ids the loaded catalog no longer carries, in selection order.
2045
+ * Callers gate user-visible "stale" affordances on {@link catalogIsReady} —
2046
+ * the list itself is informational.
2047
+ */
2048
+ function staleModelIds(selected, readiness) {
2049
+ const catalogIds = new Set(readiness.catalogIds);
2050
+ return selected.filter((id) => !catalogIds.has(id));
2051
+ }
1969
2052
  //#endregion
1970
2053
  //#region package.json
1971
- var version = "0.10.3";
2054
+ var version = "0.10.4";
1972
2055
  var repository = {
1973
2056
  "type": "git",
1974
2057
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -3190,8 +3273,12 @@ window.__ModuleLoader__.load({
3190
3273
  if (key === "ruleModelCount") return t("visibleModelsCount", params);
3191
3274
  return t(key, params);
3192
3275
  };
3193
- const catalogIds = new Set(state.catalogModels.map((model) => model.id));
3194
- const staleIds = state.visibleModels.filter((id) => !catalogIds.has(id));
3276
+ const readiness = {
3277
+ catalogIds: state.catalogModels.map((model) => model.id),
3278
+ catalogFailed: state.catalogFailed
3279
+ };
3280
+ const staleIds = staleModelIds(state.visibleModels, readiness);
3281
+ const catalogReady = catalogIsReady(readiness);
3195
3282
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3196
3283
  className: "cc-card",
3197
3284
  "aria-label": t("visibleModelsTitle"),
@@ -3205,11 +3292,11 @@ window.__ModuleLoader__.load({
3205
3292
  children: t("visibleModelsTitle")
3206
3293
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3207
3294
  className: "cc-badges",
3208
- children: [staleIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3295
+ children: [catalogReady && staleIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3209
3296
  type: "button",
3210
3297
  className: "cc-reset",
3211
3298
  disabled,
3212
- onClick: () => onSelect(state.visibleModels.filter((id) => catalogIds.has(id))),
3299
+ onClick: () => onSelect(state.visibleModels.filter((id) => !staleIds.includes(id))),
3213
3300
  children: t("visibleModelsCleanStale", { count: staleIds.length })
3214
3301
  }) : null, count > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3215
3302
  type: "button",
@@ -3228,7 +3315,7 @@ window.__ModuleLoader__.load({
3228
3315
  className: "cc-invalid",
3229
3316
  children: t("rulesCatalogFailed")
3230
3317
  }) : null,
3231
- staleIds.length > 0 && !state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3318
+ staleIds.length > 0 && catalogReady ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3232
3319
  className: "cc-hint",
3233
3320
  children: t("visibleModelsStaleHint", { count: staleIds.length })
3234
3321
  }) : null,