@mars-sea/dsh-commandcode-provider 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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.6.1] - 2026-08-22
8
+
9
+ ### Added
10
+
11
+ - **New model: Ox Alpha (`stealth/ox-alpha`, command-code@1.31.0).** A free 1M-context reasoning model with vision input, included on every plan: it appears in the picker under the Go tier with `FREE` · `Image` · `1M` markers, is whitelisted as vision-capable and auto-thinking (it reasons automatically with no selectable effort levels), and carries a `FREE` deal while the stealth preview lasts.
12
+ - **The settings page now shows the plugin version** as a muted footer line ("Command Code Provider v0.6.1"), read from package.json at build time so it always matches the published release.
13
+ - **Settings-page input polish:** a brief "Saved ✓" confirmation flashes after each accepted save (failures already showed an error); the two millisecond timeout fields now validate their range while typing with specific messages ("must be at least 1" / "above the allowed maximum") instead of a generic save-time failure; and the API-key fields (default + every account) gained a Show/Hide toggle so a pasted key can be spot-checked.
14
+
15
+ ### Changed
16
+
17
+ - **Retries are now persistent (opencode-style) instead of two attempts.** Transient failures — rate limits, server errors, timeouts, transport drops, empty responses — retry up to 1000 times at the agent-step boundary, with waits doubling from 500 ms and capping at 15 minutes (±10% jitter). Permanent failures (an invalid key, unsupported content, plan rejections) still fail fast on the first attempt. Waits are smart: a 429's `Retry-After` header and the rotation pool's earliest known window-reset time are honored verbatim up to the 15-minute cap, so when every account's usage window is exhausted the session sleeps through the window and recovers in place instead of dying after two quick tries.
18
+ - **Synced with the official command-code@1.31.0 CLI** (upstream moved 1.28.4 → 1.29.0 → 1.30.0 → 1.30.1 → 1.31.0; the intermediate releases shipped BYOK provider support, model-traffic routing, and CLI UI fixes — none touch the Provider API). `COMMAND_CODE_CLI_VERSION` is now `1.31.0`. The only snapshot change across those releases is the new Ox Alpha model above; wire protocol, endpoints, effort map, plan maps, deals, and peak pricing are all unchanged.
19
+ - **Verified compatibility with DeepSeek Harness 0.1.0-rc.7 / 0.1.0-rc.8 / 0.1.1-rc.1**, including rc.8's reworked client boot protocol: the client bundle now declares its module-graph dependency (`dsh.client.external`), so client-module load order no longer relies on the platform seed table. The supported peer range widened to `^0.1.0-rc.6 || ^0.1.1-rc.1`, so 0.1.1 hosts install without unmet-peer-dependency warnings.
20
+
21
+ ## [0.6.0] - 2026-08-19
22
+
23
+ ### Changed
24
+
25
+ - **Synced with the official command-code@1.28.4 CLI** (upstream moved 1.28.1 → 1.28.4: 1.28.2 raised the Qwen 3.8 27B usage allowance on GOAT and Pro, 1.28.3 fixed a CLI UI overlay issue; 1.28.4 is not on the changelog page yet). `COMMAND_CODE_CLI_VERSION` is now `1.28.4`. No snapshot or wire changes: the 1.28.4 bundle's effort map, plan maps, ZA model table, endpoints, request fields, and stream event types are all identical to 1.28.1, and the live catalog still lists the same 56 models.
26
+ - **The settings page's account-usage card is now a carousel.** With several accounts configured, a tab strip (account label + status dot) switches between per-account reports instead of stacking them, so the page no longer grows with each account; the selector defaults to the currently serving account. Each account's report carries its own **remove** button (staged like every other edit, persisted on save) — offered only for accounts the settings document actually owns (the default account and composition-only literal-key accounts are not removable from the page), and the per-row remove buttons in the account-management card below were removed (unsaved additions keep theirs, since they never appear in the Host-side usage report). Accounts staged for removal disappear from the carousel immediately and stay hidden across the post-save refresh.
27
+
7
28
  ## [0.5.0] - 2026-08-19
8
29
 
9
30
  ### Added
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
11
  [![npm](https://img.shields.io/badge/npm-@mars--sea%2Fdsh--commandcode--provider-blue.svg)](https://www.npmjs.com/package/@mars-sea/dsh-commandcode-provider)
12
12
 
13
- Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) LLM provider plugin for **Command Code**, ported from [pi-commandcode-provider](https://github.com/patlux/pi-commandcode-provider) (MIT). It registers a `commandcode` provider whose requests are translated to Command Code's Provider API (`POST /alpha/generate`, reverse-engineered by the pi plugin, `command-code@1.28.1`).
13
+ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) LLM provider plugin for **Command Code**, ported from [pi-commandcode-provider](https://github.com/patlux/pi-commandcode-provider) (MIT). It registers a `commandcode` provider whose requests are translated to Command Code's Provider API (`POST /alpha/generate`, reverse-engineered by the pi plugin, `command-code@1.28.4`).
14
14
 
15
15
  > This is a community integration. You need your own Command Code account and API key or subscription, and Command Code's terms apply. This project is not affiliated with Command Code, Inc.
16
16
 
@@ -22,7 +22,7 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
22
22
  - **API key resolution order**: `config.apiKey` → credential ref `apiKeyEnv` (default `COMMANDCODE_API_KEY`) → launch environment → the official CLI auth file (`~/.commandcode/auth.json`, from `command-code login`).
23
23
  - **Model-picker annotations**: every model shows the **minimum plan** that includes it (`KNOWN_PLANS`), an **active deal** or `FREE` badge (`KNOWN_DEALS`, expiry-aware so lapsed discounts hide themselves), the **current peak/off-peak state** (`Peak`/`Half`) for time-of-day-priced models, an **`Image`** marker for Vision models, and the **context window** (`1M` / `256K` / `262K`) — e.g. *"Go · 50% off · Image · 1M"*, *"Go · Half · 1M"*. The list is **sorted by plan tier** (Go → GOAT → Pro → Provider/Max), so the models your plan can use lead the picker.
24
24
  - **Plan-aware picker filtering**: the picker **hides models above your subscription tier** outright (resolved live from your account's billing state). It fails open — an unreachable billing endpoint, an unknown plan, or a positive on-demand credit balance (which the official CLI treats as unlocking every model) all keep the full catalog visible — and the server stays the final gate. Set **Hide out-of-plan models** off on the settings page (or `filterModelsByPlan: false` in the `llm-commandcode` settings section) to always list every model.
25
- - **Reasoning-effort support** for models the official catalog marks as such (`KNOWN_EFFORTS`, matching `command-code@1.28.1`); reasoning models without effort levels still think automatically, exactly like the official CLI.
25
+ - **Reasoning-effort support** for models the official catalog marks as such (`KNOWN_EFFORTS`, matching `command-code@1.28.4`); reasoning models without effort levels still think automatically, exactly like the official CLI.
26
26
  - **Image input for Vision-capable models** (sent in the official wire format via the dsh attachment service); text-only models refuse images loudly (`UNSUPPORTED_CONTENT`) rather than dropping them.
27
27
 
28
28
  <img src="assets/screenshots/model-picker.png" alt="Model picker with plan, deal, image and context annotations" width="250">
package/README.zh-CN.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
11
  [![npm](https://img.shields.io/badge/npm-@mars--sea%2Fdsh--commandcode--provider-blue.svg)](https://www.npmjs.com/package/@mars-sea/dsh-commandcode-provider)
12
12
 
13
- 非官方 [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) 的 LLM provider 插件,用于 **Command Code**,移植自 [pi-commandcode-provider](https://github.com/patlux/pi-commandcode-provider)(MIT 协议)。它注册了一个 `commandcode` provider,将请求转换为 Command Code 的 Provider API(`POST /alpha/generate`,由 pi 插件逆向工程,对应 `command-code@1.28.1`)。
13
+ 非官方 [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) 的 LLM provider 插件,用于 **Command Code**,移植自 [pi-commandcode-provider](https://github.com/patlux/pi-commandcode-provider)(MIT 协议)。它注册了一个 `commandcode` provider,将请求转换为 Command Code 的 Provider API(`POST /alpha/generate`,由 pi 插件逆向工程,对应 `command-code@1.28.4`)。
14
14
 
15
15
  > 这是一个社区集成。你需要自己的 Command Code 账号、API key 或订阅,并遵守 Command Code 的服务条款。本项目与 Command Code, Inc. 无关。
16
16
 
@@ -22,7 +22,7 @@
22
22
  - **API key 解析顺序**:`config.apiKey` → 凭据引用 `apiKeyEnv`(默认 `COMMANDCODE_API_KEY`)→ 启动环境变量 → 官方 CLI 认证文件(`~/.commandcode/auth.json`,由 `command-code login` 写入)。
23
23
  - **模型选择器标注**:每个模型显示包含它的**最低套餐**(`KNOWN_PLANS`)、**活动折扣**或 `FREE` 徽章(`KNOWN_DEALS`,到期自动隐藏已失效折扣)、峰谷定价模型的**当前时段状态**(`Peak`/`Half`,按当前 UTC 小时动态判断)、Vision 模型的 **`Image`** 标记,以及**上下文长度**(`1M` / `256K` / `262K`)——例如 *"Go · 50% off · Image · 1M"*、*"Go · Half · 1M"*。列表**按套餐排序**(Go → GOAT → Pro → Provider/Max),你当前套餐能用的模型总是排在最前。
24
24
  - **按套餐过滤选择器**:选择器会**直接隐藏超出你订阅套餐的模型**(根据账户账单状态实时判断)。全程失败开放——账单接口不可用、套餐未知,或账户持有按需余额(官方 CLI 视为解锁全部模型)时都显示完整目录——服务端仍是最终闸门。在设置页关闭「隐藏套餐外模型」开关(或在 `llm-commandcode` 设置段中设 `filterModelsByPlan: false`)可始终列出全部模型。
25
- - **推理强度(reasoning-effort)支持**:针对官方目录标为推理模型的模型(`KNOWN_EFFORTS`,与 `command-code@1.28.1` 一致);没有可选档位但仍支持思考的模型会自动思考,与官方 CLI 行为完全一致。
25
+ - **推理强度(reasoning-effort)支持**:针对官方目录标为推理模型的模型(`KNOWN_EFFORTS`,与 `command-code@1.28.4` 一致);没有可选档位但仍支持思考的模型会自动思考,与官方 CLI 行为完全一致。
26
26
  - **支持视觉模型的图片输入**(通过 dsh 附件服务、以官方 wire 格式发送);纯文本模型会明确拒绝图片(`UNSUPPORTED_CONTENT`)而非静默丢弃。
27
27
 
28
28
  <img src="assets/screenshots/model-picker.png" alt="带套餐、折扣、图片与上下文标注的模型选择器" width="250">
package/lib/client.js CHANGED
@@ -59,8 +59,15 @@ window.__ModuleLoader__.load({
59
59
  }
60
60
  };
61
61
  }
62
- /** A whole-number field; an empty draft clears it, anything non-numeric blocks save. */
63
- function numberField$1(field) {
62
+ /**
63
+ * A numeric field; an empty draft clears it, anything non-numeric blocks
64
+ * save, and an optional inclusive `bounds` range rejects out-of-range values
65
+ * with a specific reason (the Host schema would reject them at save time with
66
+ * only a generic failure — catching it here names the problem while typing).
67
+ * Decimals pass: the Host schema is `z.number()` too, and a fractional
68
+ * millisecond value is harmless even if pointless.
69
+ */
70
+ function numberField$1(field, bounds) {
64
71
  return {
65
72
  field,
66
73
  format: (value) => typeof value === "number" ? String(value) : "",
@@ -68,10 +75,22 @@ window.__ModuleLoader__.load({
68
75
  const trimmed = text.trim();
69
76
  if (trimmed === "") return { kind: "clear" };
70
77
  const parsed = Number(trimmed);
71
- return Number.isFinite(parsed) ? {
78
+ if (!Number.isFinite(parsed)) return {
79
+ kind: "invalid",
80
+ reason: "format"
81
+ };
82
+ if (bounds?.min !== void 0 && parsed < bounds.min) return {
83
+ kind: "invalid",
84
+ reason: "tooSmall"
85
+ };
86
+ if (bounds?.max !== void 0 && parsed > bounds.max) return {
87
+ kind: "invalid",
88
+ reason: "tooLarge"
89
+ };
90
+ return {
72
91
  kind: "set",
73
92
  value: parsed
74
- } : { kind: "invalid" };
93
+ };
75
94
  }
76
95
  };
77
96
  }
@@ -95,16 +114,26 @@ window.__ModuleLoader__.load({
95
114
  kind: "set",
96
115
  value: false
97
116
  };
98
- return { kind: "invalid" };
117
+ return {
118
+ kind: "invalid",
119
+ reason: "format"
120
+ };
99
121
  }
100
122
  };
101
123
  }
124
+ const MAX_TIMEOUT_MS = 2147483647;
102
125
  /** The fields this page edits inside the `llm-commandcode` namespace. */
103
126
  const SECTION_FIELDS = [
104
127
  textField("apiBase"),
105
128
  textField("workingDir"),
106
- numberField$1("requestTimeoutMs"),
107
- numberField$1("streamIdleTimeoutMs"),
129
+ numberField$1("requestTimeoutMs", {
130
+ min: 1,
131
+ max: MAX_TIMEOUT_MS
132
+ }),
133
+ numberField$1("streamIdleTimeoutMs", {
134
+ min: 1,
135
+ max: MAX_TIMEOUT_MS
136
+ }),
108
137
  booleanField$1("filterModelsByPlan"),
109
138
  textField("activeAccount")
110
139
  ];
@@ -136,6 +165,7 @@ window.__ModuleLoader__.load({
136
165
  keyDrafts = /* @__PURE__ */ new Map();
137
166
  saving = false;
138
167
  failed = false;
168
+ savedCount = 0;
139
169
  /**
140
170
  * @param scope - bound scope for the `llm-commandcode` namespace.
141
171
  * @param api - credentials wire face.
@@ -206,7 +236,8 @@ window.__ModuleLoader__.load({
206
236
  text: this.staged.get("apiKey")?.text ?? "",
207
237
  clear: false,
208
238
  overridden: false,
209
- invalid: false
239
+ invalid: false,
240
+ invalidReason: void 0
210
241
  },
211
242
  apiBase: this.field("apiBase"),
212
243
  workingDir: this.field("workingDir"),
@@ -216,10 +247,12 @@ window.__ModuleLoader__.load({
216
247
  filterModelsByPlan: this.field("filterModelsByPlan"),
217
248
  activeAccount: this.field("activeAccount"),
218
249
  accounts,
250
+ accountsRemoving: [...this.removedRefs],
219
251
  dirty: plan.length > 0 || this.accountsDirty(),
220
252
  invalid: plan.some((item) => item.run === void 0),
221
253
  saving: this.saving,
222
- failed: this.failed
254
+ failed: this.failed,
255
+ savedCount: this.savedCount
223
256
  };
224
257
  }
225
258
  /** Stage a new extra account (saved on the next `save()`). */
@@ -321,6 +354,7 @@ window.__ModuleLoader__.load({
321
354
  this.saving = false;
322
355
  this.failed = !landed;
323
356
  if (landed) {
357
+ this.savedCount += 1;
324
358
  this.staged.clear();
325
359
  this.clearAccountStaging();
326
360
  } else this.reconcileAccountStaging();
@@ -355,14 +389,16 @@ window.__ModuleLoader__.load({
355
389
  text: spec.format(this.sectionValue(field)),
356
390
  clear: false,
357
391
  overridden: this.stored(field),
358
- invalid: false
392
+ invalid: false,
393
+ invalidReason: void 0
359
394
  };
360
395
  const parsed = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
361
396
  return {
362
397
  text: staged.text,
363
398
  clear: staged.clear,
364
399
  overridden: parsed.kind === "set",
365
- invalid: parsed.kind === "invalid"
400
+ invalid: parsed.kind === "invalid",
401
+ invalidReason: parsed.kind === "invalid" ? parsed.reason : void 0
366
402
  };
367
403
  }
368
404
  sectionValue(field) {
@@ -808,6 +844,21 @@ window.__ModuleLoader__.load({
808
844
  }]
809
845
  };
810
846
  //#endregion
847
+ //#region src/client/version.ts
848
+ /**
849
+ * The plugin's own version, read from package.json at build time.
850
+ *
851
+ * The client bundle inlines the JSON import (rolldown resolves it during the
852
+ * tsdown build; node tests read it through tsx), so the rendered value always
853
+ * matches the published package version with no second constant to keep in
854
+ * sync. Rendered as a muted footer line on the settings page so a user can
855
+ * report the exact build they run.
856
+ *
857
+ * @module dsh-commandcode-provider/client/version
858
+ */
859
+ /** The published package version (e.g. `'0.6.0'`). */
860
+ const PLUGIN_VERSION = "0.6.1";
861
+ //#endregion
811
862
  //#region src/client/section.tsx
812
863
  /**
813
864
  * React component for the "Command Code" settings page (browser half).
@@ -860,11 +911,17 @@ window.__ModuleLoader__.load({
860
911
  }),
861
912
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
862
913
  className: state.invalid ? "cc-invalid" : "cc-hint",
863
- children: state.invalid ? t("invalidNumber") : hint
914
+ children: state.invalid ? invalidCopy(state.invalidReason, t) : hint
864
915
  })
865
916
  ]
866
917
  });
867
918
  }
919
+ /** The per-field error copy for a staged draft's failure reason. */
920
+ function invalidCopy(reason, t) {
921
+ if (reason === "tooSmall") return t("numberTooSmall");
922
+ if (reason === "tooLarge") return t("numberTooLarge");
923
+ return t("invalidNumber");
924
+ }
868
925
  /**
869
926
  * One boolean field row rendered as a toggle. The staged text is `'true'` /
870
927
  * `'false'` / `''` (unset → `defaultChecked`); toggling stages the string the
@@ -910,8 +967,13 @@ window.__ModuleLoader__.load({
910
967
  })]
911
968
  });
912
969
  }
913
- /** The API-key control: write-only, reports configured state, never echoes the key. */
914
- function SecretKeyField({ label, hint, state, disabled, configured, configuredLabel, unconfiguredLabel, onEdit }) {
970
+ /**
971
+ * The API-key control: write-only, reports configured state, never echoes the
972
+ * key. The input is masked by default with a Show/Hide toggle so a pasted key
973
+ * can be spot-checked without leaving the field.
974
+ */
975
+ function SecretKeyField({ label, hint, state, disabled, configured, configuredLabel, unconfiguredLabel, showLabel, hideLabel, onEdit }) {
976
+ const [visible, setVisible] = (0, react.useState)(false);
915
977
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
916
978
  className: "cc-field",
917
979
  children: [
@@ -921,19 +983,26 @@ window.__ModuleLoader__.load({
921
983
  className: "cc-label",
922
984
  htmlFor: "cc-api-key",
923
985
  children: label
924
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
986
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
925
987
  className: "cc-badges",
926
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
988
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
927
989
  className: configured ? "cc-badge" : "cc-badgeMuted",
928
990
  children: configured ? configuredLabel : unconfiguredLabel
929
- })
991
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
992
+ type: "button",
993
+ className: "cc-reset",
994
+ disabled,
995
+ onClick: () => setVisible((value) => !value),
996
+ children: visible ? hideLabel : showLabel
997
+ })]
930
998
  })]
931
999
  }),
932
1000
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
933
1001
  id: "cc-api-key",
934
1002
  className: "cc-input",
935
- type: "password",
1003
+ type: visible ? "text" : "password",
936
1004
  autoComplete: "off",
1005
+ spellCheck: false,
937
1006
  value: state.text,
938
1007
  disabled,
939
1008
  onChange: (event) => onEdit(event.target.value)
@@ -1039,7 +1108,7 @@ window.__ModuleLoader__.load({
1039
1108
  * One pool account's facts (identity, totals, credits, window limits)
1040
1109
  * rendered inside the account-usage card.
1041
1110
  */
1042
- function AccountReport({ entry, t }) {
1111
+ function AccountReport({ entry, t, onRemove }) {
1043
1112
  const report = entry.report;
1044
1113
  const account = report.account;
1045
1114
  const accountName = account === void 0 ? "" : account.userName || account.name;
@@ -1072,6 +1141,13 @@ window.__ModuleLoader__.load({
1072
1141
  planStatus !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1073
1142
  className: "cc-usagePlanStatus",
1074
1143
  children: planStatus
1144
+ }) : null,
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
1146
+ onRemove !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1147
+ type: "button",
1148
+ className: "cc-usageRefresh",
1149
+ onClick: onRemove,
1150
+ children: t("accountRemove")
1075
1151
  }) : null
1076
1152
  ]
1077
1153
  }),
@@ -1161,13 +1237,21 @@ window.__ModuleLoader__.load({
1161
1237
  ]
1162
1238
  });
1163
1239
  }
1240
+ /** The status dot on an account tab: cooling/invalid warn, everything else ok. */
1241
+ function AccountTabDot({ entry }) {
1242
+ const cls = entry.mark === "invalid-credential" ? "cc-tabDot cc-tabDotError" : entry.mark !== "" || entry.cooldownUntil > 0 ? "cc-tabDot cc-tabDotWarn" : "cc-tabDot cc-tabDotOk";
1243
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cls });
1244
+ }
1164
1245
  /**
1165
1246
  * The account-usage card: the `/commandcode` dashboard's facts rendered as
1166
- * a native settings card one section per pool account. Data arrives
1167
- * through the `commandcode/report` Remote; the API keys never leave the
1168
- * Host.
1247
+ * a native settings card. With several accounts the card is a carousel — a
1248
+ * tab strip (label + status dot) switches between accounts so the page stays
1249
+ * short; each account's report carries its own remove affordance (the
1250
+ * default account is not removable). Accounts staged for removal in the
1251
+ * management card are hidden here immediately. Data arrives through the
1252
+ * `commandcode/report` Remote; the API keys never leave the Host.
1169
1253
  */
1170
- function UsageCard({ t, usage, apiKeyConfigured, onRefresh }) {
1254
+ function UsageCard({ t, usage, apiKeyConfigured, removingIds, removableIds, canManage, onRefresh, onRemoveAccount }) {
1171
1255
  (0, react.useEffect)(() => {
1172
1256
  if (apiKeyConfigured && usage.status === "idle") onRefresh();
1173
1257
  }, [
@@ -1177,6 +1261,25 @@ window.__ModuleLoader__.load({
1177
1261
  ]);
1178
1262
  const loading = usage.status === "loading";
1179
1263
  const report = usage.report;
1264
+ const [locallyRemoved, setLocallyRemoved] = (0, react.useState)([]);
1265
+ (0, react.useEffect)(() => {
1266
+ setLocallyRemoved([]);
1267
+ }, [usage.fetchedAt]);
1268
+ const hidden = /* @__PURE__ */ new Set([...removingIds, ...locallyRemoved]);
1269
+ const seenIds = /* @__PURE__ */ new Set();
1270
+ const entries = (report?.accounts ?? []).filter((entry) => {
1271
+ if (hidden.has(entry.id)) return false;
1272
+ if (seenIds.has(entry.id)) return false;
1273
+ seenIds.add(entry.id);
1274
+ return true;
1275
+ });
1276
+ const [selectedId, setSelectedId] = (0, react.useState)(void 0);
1277
+ const selected = entries.find((entry) => entry.id === selectedId) ?? entries.find((entry) => entry.active) ?? entries[0];
1278
+ const removeSelected = canManage && selected !== void 0 && removableIds.includes(selected.id) ? () => {
1279
+ const id = selected.id;
1280
+ setLocallyRemoved((prev) => [...prev, id]);
1281
+ onRemoveAccount(id);
1282
+ } : void 0;
1180
1283
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1181
1284
  className: "cc-usageCard",
1182
1285
  "aria-label": t("usageTitle"),
@@ -1211,10 +1314,24 @@ window.__ModuleLoader__.load({
1211
1314
  role: "status",
1212
1315
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("usageError"), usage.error !== void 0 && usage.error !== "" ? ` — ${usage.error}` : ""] })
1213
1316
  }) : null,
1214
- report?.accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountReport, {
1215
- entry,
1216
- t
1217
- }, entry.id)),
1317
+ entries.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1318
+ className: "cc-tabs",
1319
+ role: "tablist",
1320
+ "aria-label": t("accountsTitle"),
1321
+ children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1322
+ type: "button",
1323
+ role: "tab",
1324
+ "aria-selected": selected?.id === entry.id,
1325
+ className: selected?.id === entry.id ? "cc-tab cc-tabActive" : "cc-tab",
1326
+ onClick: () => setSelectedId(entry.id),
1327
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTabDot, { entry }), entry.label]
1328
+ }, entry.id))
1329
+ }) : null,
1330
+ selected !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountReport, {
1331
+ entry: selected,
1332
+ t,
1333
+ onRemove: removeSelected
1334
+ }, selected.id) : null,
1218
1335
  report !== void 0 && usage.fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1219
1336
  className: "cc-usageMeta",
1220
1337
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
@@ -1229,9 +1346,16 @@ window.__ModuleLoader__.load({
1229
1346
  ]
1230
1347
  });
1231
1348
  }
1232
- /** One extra account row: label, key, configured badge, remove affordance. */
1349
+ /**
1350
+ * One extra account row: label, key, configured badge. Saved accounts are
1351
+ * removed from the usage card above; a NOT-YET-SAVED addition never appears
1352
+ * there (the usage report is Host-side), so it keeps its own remove button —
1353
+ * otherwise the only way to undo a mistaken Add would be discarding every
1354
+ * other staged edit.
1355
+ */
1233
1356
  function AccountRow({ account, disabled, t, onLabel, onKey, onRemove }) {
1234
1357
  const locked = !account.writable;
1358
+ const [keyVisible, setKeyVisible] = (0, react.useState)(false);
1235
1359
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1236
1360
  className: "cc-field",
1237
1361
  children: [
@@ -1253,12 +1377,19 @@ window.__ModuleLoader__.load({
1253
1377
  children: account.configured ? t("apiKeySet") : t("apiKeyUnset")
1254
1378
  }),
1255
1379
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1380
+ type: "button",
1381
+ className: "cc-reset",
1382
+ disabled,
1383
+ onClick: () => setKeyVisible((value) => !value),
1384
+ children: keyVisible ? t("hide") : t("show")
1385
+ }),
1386
+ account.added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1256
1387
  type: "button",
1257
1388
  className: "cc-reset",
1258
1389
  disabled,
1259
1390
  onClick: onRemove,
1260
1391
  children: t("accountRemove")
1261
- })
1392
+ }) : null
1262
1393
  ]
1263
1394
  })]
1264
1395
  }),
@@ -1273,8 +1404,9 @@ window.__ModuleLoader__.load({
1273
1404
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1274
1405
  id: `cc-account-key-${account.id}`,
1275
1406
  className: "cc-input",
1276
- type: "password",
1407
+ type: keyVisible ? "text" : "password",
1277
1408
  autoComplete: "off",
1409
+ spellCheck: false,
1278
1410
  placeholder: t("accountKey"),
1279
1411
  value: account.keyText,
1280
1412
  disabled: disabled || locked,
@@ -1287,7 +1419,7 @@ window.__ModuleLoader__.load({
1287
1419
  ]
1288
1420
  });
1289
1421
  }
1290
- /** The multi-account card: extra accounts in rotation order + add button. */
1422
+ /** The multi-account card: the active-account selector + extra accounts in rotation order + add button. */
1291
1423
  function AccountsCard({ t, state, disabled, onAdd, onRemove, onLabel, onKey, onActive, onActiveReset }) {
1292
1424
  const active = state.activeAccount;
1293
1425
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1377,6 +1509,21 @@ window.__ModuleLoader__.load({
1377
1509
  ]
1378
1510
  });
1379
1511
  }
1512
+ /**
1513
+ * Show the "Saved ✓" affordance for a short window after each accepted save.
1514
+ * The controller only counts saves (`savedCount`); the flash timing lives
1515
+ * here so the state machine stays timer-free.
1516
+ */
1517
+ function useSavedFlash(tick) {
1518
+ const [visible, setVisible] = (0, react.useState)(false);
1519
+ (0, react.useEffect)(() => {
1520
+ if (tick === 0) return;
1521
+ setVisible(true);
1522
+ const timer = setTimeout(() => setVisible(false), 2500);
1523
+ return () => clearTimeout(timer);
1524
+ }, [tick]);
1525
+ return visible;
1526
+ }
1380
1527
  /** The settings page body: connection facts for the Command Code provider. */
1381
1528
  function CommandCodeSettingsPage(props) {
1382
1529
  const { t } = props;
@@ -1384,6 +1531,7 @@ window.__ModuleLoader__.load({
1384
1531
  const usage = props.useCommandCodeUsage((snapshot) => snapshot);
1385
1532
  const disabled = !state.writable;
1386
1533
  const keyLocked = !state.apiKeyWritable;
1534
+ const savedVisible = useSavedFlash(state.savedCount);
1387
1535
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1388
1536
  className: "cc-section",
1389
1537
  "aria-label": t("title"),
@@ -1405,7 +1553,11 @@ window.__ModuleLoader__.load({
1405
1553
  t,
1406
1554
  usage,
1407
1555
  apiKeyConfigured: state.anyAccountConfigured,
1408
- onRefresh: props.refreshUsage
1556
+ removingIds: state.accountsRemoving,
1557
+ removableIds: state.accounts.map((account) => account.id),
1558
+ canManage: state.writable,
1559
+ onRefresh: props.refreshUsage,
1560
+ onRemoveAccount: props.removeAccount
1409
1561
  }),
1410
1562
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsCard, {
1411
1563
  t,
@@ -1429,6 +1581,8 @@ window.__ModuleLoader__.load({
1429
1581
  configured: state.apiKeyConfigured,
1430
1582
  configuredLabel: t("apiKeySet"),
1431
1583
  unconfiguredLabel: t("apiKeyUnset"),
1584
+ showLabel: t("show"),
1585
+ hideLabel: t("hide"),
1432
1586
  onEdit: (text) => props.edit("apiKey", text)
1433
1587
  }),
1434
1588
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
@@ -1495,6 +1649,11 @@ window.__ModuleLoader__.load({
1495
1649
  role: "status",
1496
1650
  children: t("saveFailed")
1497
1651
  }) : null,
1652
+ savedVisible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1653
+ className: "cc-saved",
1654
+ role: "status",
1655
+ children: t("saved")
1656
+ }) : null,
1498
1657
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1499
1658
  variant: "ghost",
1500
1659
  size: "sm",
@@ -1510,6 +1669,10 @@ window.__ModuleLoader__.load({
1510
1669
  children: t(state.saving ? "saving" : "save")
1511
1670
  })
1512
1671
  ]
1672
+ }),
1673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1674
+ className: "cc-version",
1675
+ children: ["Command Code Provider v", PLUGIN_VERSION]
1513
1676
  })
1514
1677
  ]
1515
1678
  });
@@ -1549,13 +1712,18 @@ window.__ModuleLoader__.load({
1549
1712
  overridden: "已覆盖",
1550
1713
  reset: "重置",
1551
1714
  invalidNumber: "无效数字",
1715
+ numberTooSmall: "不能小于 1(毫秒)",
1716
+ numberTooLarge: "超出允许上限(2147483647 毫秒)",
1552
1717
  readOnly: "当前配置为只读。",
1553
1718
  unsaved: "未保存",
1554
1719
  save: "保存",
1555
1720
  saving: "保存中",
1721
+ saved: "已保存 ✓",
1556
1722
  saveFailed: "保存失败,请重试。",
1557
1723
  discard: "放弃",
1558
1724
  cancel: "取消",
1725
+ show: "显示",
1726
+ hide: "隐藏",
1559
1727
  usageTitle: "账户用量",
1560
1728
  usageRefresh: "刷新",
1561
1729
  usageRefreshing: "刷新中…",
@@ -1617,13 +1785,18 @@ window.__ModuleLoader__.load({
1617
1785
  overridden: "Overridden",
1618
1786
  reset: "Reset",
1619
1787
  invalidNumber: "Invalid number",
1788
+ numberTooSmall: "Must be at least 1 (ms)",
1789
+ numberTooLarge: "Above the allowed maximum (2147483647 ms)",
1620
1790
  readOnly: "Settings are read-only.",
1621
1791
  unsaved: "Unsaved",
1622
1792
  save: "Save",
1623
1793
  saving: "Saving",
1794
+ saved: "Saved ✓",
1624
1795
  saveFailed: "Save failed, please retry.",
1625
1796
  discard: "Discard",
1626
1797
  cancel: "Cancel",
1798
+ show: "Show",
1799
+ hide: "Hide",
1627
1800
  usageTitle: "Account usage",
1628
1801
  usageRefresh: "Refresh",
1629
1802
  usageRefreshing: "Refreshing…",
@@ -1713,11 +1886,19 @@ window.__ModuleLoader__.load({
1713
1886
  .cc-usageBarFillWarn{background:var(--dsw-alias-label-error)}
1714
1887
  .cc-usageWindowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
1715
1888
  .cc-usageMeta{align-items:center;gap:8px;display:flex}
1716
- .cc-accountReport{border-top:1px solid var(--dsw-alias-border-l2);padding-top:12px;flex-direction:column;gap:12px;display:flex}
1717
- .cc-accountReport:first-of-type{border-top:none;padding-top:0}
1889
+ .cc-accountReport{flex-direction:column;gap:12px;display:flex}
1890
+ .cc-tabs{flex-wrap:wrap;gap:6px;display:flex}
1891
+ .cc-tab{align-items:center;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex;gap:6px}
1892
+ .cc-tab:hover:not(.cc-tabActive){color:var(--dsw-alias-label-primary)}
1893
+ .cc-tabActive{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary)}
1894
+ .cc-tabDotOk{background:var(--dsw-alias-brand-primary);border-radius:50%;width:6px;height:6px}
1895
+ .cc-tabDotWarn{background:#d97706;border-radius:50%;width:6px;height:6px}
1896
+ .cc-tabDotError{background:var(--dsw-alias-label-error);border-radius:50%;width:6px;height:6px}
1718
1897
  .cc-usageMetaSpacer{flex:1}
1719
1898
  .cc-usageUpdated{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
1720
1899
  .cc-usagePartial{color:var(--dsw-alias-label-error);margin:0;font-size:11px;line-height:1.5}
1900
+ .cc-version{margin:4px 0 0;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
1901
+ .cc-saved{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary));margin:0;font-size:12px;font-weight:500;line-height:1.5}
1721
1902
  `;
1722
1903
  /** Inject the page stylesheet once (idempotent per tag). */
1723
1904
  function injectPageCss() {