dsh-ds-balance 1.1.0 → 2.0.0-alpha.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/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
  > [!NOTE]
28
28
  > **余额读自 DeepSeek 官方 `GET /user/balance`**,不是估算。凭据只经 DSH 的凭据通道解析:API Key 不会出现在设置文件里,也不会返回给界面。
29
29
 
30
- 余额要有地方看,但不该占地方。左边栏底部一个常驻的状态环,点开是三段金额与数据新鲜度;要改配置,进设置页那一张卡片。
30
+ 余额要有地方看,但不该占地方。左边栏底部一个常驻的状态环,点开是三段金额与数据新鲜度;要改配置,进侧边栏**插件(Plugins)**页里本插件的详情页。
31
31
 
32
32
  <p align="center">
33
33
  <img src="assets/sidebar-popover.png" alt="左边栏底部的「DeepSeek 余额」条目与展开的余额浮层" width="360">
@@ -39,25 +39,25 @@
39
39
 
40
40
  | 界面 | 一句话 | 你会用它来 |
41
41
  |---|---|---|
42
- | 侧栏圆环 | 常驻左边栏底部的状态环 + 名称,**环里填了多少 = 余额离该币种预警线还有多远** | 扫一眼就知道还剩多少、离预警线多远 |
43
- | 余额浮层 | 点条目展开:总额、赠送 / 充值拆分、数据新鲜度、手动刷新(带冷却) | 核对具体数字,以及数据是几分钟前的 |
44
- | 设置卡片 | 连接 / 展示 / 阈值 / 刷新四组,各自可折叠,默认四组全收起 | 换端点、换币种、调预警线、调节奏 |
42
+ | 侧栏圆环 | 常驻左边栏底部的状态环 + 名称,**展开态悬浮条目就报出余额金额**,**环里填了多少 = 余额离该币种预警线还有多远** | 扫一眼就知道还剩多少、离预警线多远 |
43
+ | 余额浮层 | 点条目展开:总额、赠送 / 充值拆分、数据新鲜度、手动刷新(带冷却);**右上角图标一键跳到插件页** | 核对具体数字,以及数据是几分钟前的 |
44
+ | 设置卡片 | 连接 / 展示 / 阈值 / 刷新四组,**默认四组全收起**,点组头逐组展开 | 换端点、换币种、调预警线、调节奏 |
45
45
 
46
46
  三种界面的分工是死的:**圆环回答「大概还剩多少」,浮层回答「具体是多少」,卡片回答「怎么算」。**
47
47
 
48
48
  <p align="center">
49
- <img src="assets/settings-card.png" alt="插件配置页中的「DeepSeek 余额」卡片" width="360">
49
+ <img src="assets/settings-card.png" alt="插件页里的「DeepSeek 余额」卡片" width="360">
50
50
  <br>
51
- <em>在 <strong>设置 → 插件 → 插件配置</strong> 中与其他插件并排;四组默认全收起,卡片一打开只占四行折叠头。</em>
51
+ <em>卡片就是这四组:默认四组全收起,点组头逐组展开;组里有填错的项时那一组会自己展开。</em>
52
52
  </p>
53
53
 
54
54
  ## 能力
55
55
 
56
56
  - 左边栏底部常驻一个状态环加名称,点开看明细;收起与展开是同一个环,位置不变。
57
57
  - 余额按设定的周期自动刷新,界面读的是缓存 —— 开着界面不会反复去打上游接口。
58
- - 浮层给出总额、赠送 / 充值拆分、数据是多久之前的,以及一个带冷却的手动刷新。
59
- - 多币种:显示哪个币种由账户决定;设置里选的币种账户里没有时,浮层会说明并给一键改用。
60
- - 设置卡片分四组、各自可折叠,默认全收起;组里有填错的项时,那一组会自己展开。
58
+ - 浮层给出总额、赠送 / 充值拆分、数据是多久之前的,以及一个带冷却的手动刷新;标题那行「鲸鱼图标 + DeepSeek 余额」本身是外链,点开是新标签页里的[官网用量页](https://platform.deepseek.com/usage)。
59
+ - 多币种:显示哪个币种由账户决定;设置里选的币种账户里没有时,浮层会说明并给「改用实际币种」——**点它直接写进设置**,设置页那一格跟着变;宿主不可写时按钮置灰。
60
+ - 设置卡片分四组、**默认全收起**、点组头展开;组里有填错的项时,那一组会自己展开。
61
61
  - 凭据默认继承官方模型页配好的那一份,不必重填;徽标只报「已配置密钥。/ 未配置密钥。」(官方卡片同款),
62
62
  只读时不给编辑,并说明原因。
63
63
  - 颜色只表达状态(正常 / 偏低 / 告急),与金额大小无关;读法与理由见「[圆环怎么读](#圆环怎么读)」。
@@ -75,7 +75,7 @@
75
75
  npm install -g @deepseek-ai/dsh@alpha # 本插件承诺支持的线
76
76
  ```
77
77
 
78
- 兼容性不是推断出来的:每周由 [compat.yml](.github/workflows/compat.yml) 在 `alpha` 与 `next` 两条线上换包实跑一遍现有测试。当前结论与红了怎么办见 [兼容性](docs/PUBLISHING.md#兼容性)。
78
+ 兼容性不是推断出来的:每周由 [compat.yml](.github/workflows/compat.yml) 在 `alpha` 与 `next` 两条线上换包实跑一遍现有测试,另有一步单独判声明面罩不罩得住;红了会开或更新一条固定标题的跟踪 issue。当前结论与红了怎么办见 [兼容性](docs/PUBLISHING.md#兼容性),宿主版本的分水岭见[版本兼容](#版本兼容)。
79
79
 
80
80
  ### 从 npm 安装
81
81
 
@@ -104,9 +104,22 @@ dsh plugin --profile web add "$PWD"
104
104
 
105
105
  仓库带有 GitHub topic [`dsh-plugin`](https://github.com/topics/dsh-plugin),插件市场据此自动发现插件。
106
106
 
107
+ ## 版本兼容
108
+
109
+ - 配置界面注册在宿主的 `plugins.bundle.config` 槽,**该槽由 DSH 0.1.6-alpha.2 引入**(下限的真源是 [package.json](package.json) 的 `engines.dsh`,现状现查 `node scripts/compat-swap.mjs check`)。
110
+ - 更早的宿主上圆环与浮层照常工作,只是**插件页里不会出现配置区**(不报错)—— 这就是分水岭。插件不查宿主版本号,只探测这一格在不在:探测不到时**浮层里会多一行英文 `[WARN]`**,说明配置界面为什么不可用、该往哪儿升级。
111
+ - 需要配置界面,就把宿主升到 `engines.dsh` 声明的那一版或更高:`npm install -g @deepseek-ai/dsh@alpha`。
112
+ - 声明是**窄**的:下限是引入该槽的那一版,上限排掉下一个可能不兼容的大版本 —— 不是「以后都兼容」,也不把更早的版本算进来。可它罩不住新的预发布段,所以宿主推新的 alpha 之后仍可能过期;每周的 `declaration` 巡检专门盯这件事,红了怎么办见[兼容性](docs/PUBLISHING.md#兼容性)。
113
+
107
114
  ## 配置
108
115
 
109
- 打开 **设置 → 插件 → 插件配置 → DeepSeek 余额**,四组各自可折叠:
116
+ 打开侧边栏 **插件(Plugins)** →「已安装(Installed)」组,点进 **dsh-ds-balance** 的详情页;配置表单直接铺在那一页上,四组**默认全收起**,点组头逐组展开:
117
+
118
+ <p align="center">
119
+ <img src="assets/settings-cards-position.png" alt="Plugins 页列表里「DeepSeek 余额」的位置" width="480">
120
+ <br>
121
+ <em>Plugins 页列表里的位置:<code>dsh-ds-balance</code> 与其他已安装插件并排;点进它的详情页才是上面那张配置表单。</em>
122
+ </p>
110
123
 
111
124
  - **连接**:API 地址与凭据,两项都默认留空 —— 地址留空即用 DeepSeek 官方端点,凭据继承官方模型页那一份、只读不可改;
112
125
  二级「自定义设置」里只放凭据引用名。
package/README_en.md CHANGED
@@ -27,7 +27,7 @@
27
27
  > [!NOTE]
28
28
  > **The balance is read from the official `GET /user/balance`** — not estimated. The credential is resolved only through DSH's credential channel: the API key never lands in a settings file and is never returned to the UI.
29
29
 
30
- The balance needs somewhere to live that does not take up room. A permanent status ring at the bottom of the sidebar; click it for the three amounts and the freshness of the data. Anything you want to configure lives on the settings card.
30
+ The balance needs somewhere to live that does not take up room. A permanent status ring at the bottom of the sidebar; click it for the three amounts and the freshness of the data. Anything you want to configure lives on the plugin's details page under **Plugins → Installed**.
31
31
 
32
32
  <p align="center">
33
33
  <img src="assets/sidebar-popover_en.png" alt="The DeepSeek balance entry at the bottom of the sidebar, with its popover open" width="360">
@@ -39,25 +39,25 @@ The balance needs somewhere to live that does not take up room. A permanent stat
39
39
 
40
40
  | Surface | One line | Use it for |
41
41
  |---|---|---|
42
- | Sidebar ring | A status ring plus label at the bottom of the sidebar; **how full it is = how far the balance is from that currency's warning line** | Seeing at a glance how much is left and how close it is to the warning line |
43
- | Balance popover | Click the entry: total, granted / topped-up split, data freshness, manual refresh (with cooldown) | Checking the exact figures, and how many minutes old they are |
44
- | Settings card | Connection / Display / Thresholds / Refresh, each collapsible, all collapsed by default | Changing the endpoint, the currency, the warning lines, the cadence |
42
+ | Sidebar ring | A status ring plus label at the bottom of the sidebar; **in the expanded state, hovering the entry reports the balance amount**, and **how full it is = how far the balance is from that currency's warning line** | Seeing at a glance how much is left and how close it is to the warning line |
43
+ | Balance popover | Click the entry: total, granted / topped-up split, data freshness, manual refresh (with cooldown); **an icon at its top right jumps straight to the Plugins page** | Checking the exact figures, and how many minutes old they are |
44
+ | Settings card | Connection / Display / Thresholds / Refresh, **all collapsed by default**, expand a group from its header | Changing the endpoint, the currency, the warning lines, the cadence |
45
45
 
46
46
  The division of labour is fixed: **the ring answers "roughly how much is left", the popover answers "exactly how much", and the card answers "how is that computed".**
47
47
 
48
48
  <p align="center">
49
- <img src="assets/settings-card_en.png" alt="The DeepSeek balance card in the plugin settings" width="360">
49
+ <img src="assets/settings-card_en.png" alt="The DeepSeek balance card on the Plugins page" width="360">
50
50
  <br>
51
- <em>Sits alongside other plugins in <strong>Settings → Plugins → Plugin configuration</strong>; all four groups are collapsed by default, so the card opens as four header rows.</em>
51
+ <em>The card is those four groups: all collapsed by default, expanded one header at a time; a group holding a bad value opens itself.</em>
52
52
  </p>
53
53
 
54
54
  ## Capabilities
55
55
 
56
56
  - A permanent status ring plus label at the bottom of the sidebar; click it for the breakdown. The collapsed and expanded states share the same ring, in the same place.
57
57
  - The balance refreshes on its own schedule and the UI reads a cache — leaving the interface open does not hammer the upstream.
58
- - The popover shows the total, the granted / topped-up split, how old the data is, and a manual refresh with a cooldown.
59
- - Multiple currencies: the account decides which currency is shown; when the one chosen in settings is absent, the popover explains and offers a one-click switch.
60
- - The settings card has four collapsible groups, all collapsed by default; a group holding a bad value opens itself.
58
+ - The popover shows the total, the granted / topped-up split, how old the data is, and a manual refresh with a cooldown; its title line (whale icon + "DeepSeek balance") is itself a link that opens the [official usage page](https://platform.deepseek.com/usage) in a new tab.
59
+ - Multiple currencies: the account decides which currency is shown; when the one chosen in settings is absent, the popover explains and offers "switch to the shown currency" — **that writes straight into settings**, and the settings field follows; the button is disabled when the Host is not writable.
60
+ - The settings card has four groups, **all collapsed by default**, expanded one header at a time; a group holding a bad value opens itself.
61
61
  - The credential is inherited from the official model settings by default, so there is nothing to re-enter; where it comes from is on a badge
62
62
  (a key is configured / no key is configured, exactly the official card's wording), and a read-only field explains itself instead of offering an edit.
63
63
  - Colour carries state only (normal / low / critical), never an amount; see "[Reading the ring](#reading-the-ring)".
@@ -75,7 +75,7 @@ Install the host by **naming the version line explicitly**: the `latest` tag of
75
75
  npm install -g @deepseek-ai/dsh@alpha # the line this plugin promises to support
76
76
  ```
77
77
 
78
- Compatibility is measured, not inferred: every week [compat.yml](.github/workflows/compat.yml) swaps packages onto the `alpha` and `next` lines and reruns the existing tests. The current verdict, and what to do when it goes red, are in [Compatibility](docs/PUBLISHING.md#兼容性).
78
+ Compatibility is measured, not inferred: every week [compat.yml](.github/workflows/compat.yml) swaps packages onto the `alpha` and `next` lines and reruns the existing tests, and one separate job judges whether the declared ranges still cover the line; a red patrol opens or updates a tracking issue with a fixed title. The current verdict, and what to do when it goes red, are in [Compatibility](docs/PUBLISHING.md#兼容性); the Host-version watershed is in [Version compatibility](#version-compatibility).
79
79
 
80
80
  ### From npm
81
81
 
@@ -104,9 +104,22 @@ Same as the npm route: it takes effect after a restart.
104
104
 
105
105
  The repository carries the GitHub topic [`dsh-plugin`](https://github.com/topics/dsh-plugin), which is how the plugin marketplace discovers plugins.
106
106
 
107
+ ## Version compatibility
108
+
109
+ - The configuration UI registers into the Host's `plugins.bundle.config` slot, which **arrives with DSH 0.1.6-alpha.2** (the floor's single source is `engines.dsh` in [package.json](package.json); check the current lines with `node scripts/compat-swap.mjs check`).
110
+ - On an earlier Host the ring and the popover keep working, but the configuration area never appears on the Plugins page (with no error) — that is the watershed. The plugin never reads a Host version: it probes whether that slot exists, and when it does not, **the popover carries one extra English `[WARN]` line** saying why the configuration page is unavailable and where to upgrade.
111
+ - To get the configuration page, upgrade the Host to the version `engines.dsh` declares or higher: `npm install -g @deepseek-ai/dsh@alpha`.
112
+ - The declaration is **narrow**: the floor is the version that introduced the slot and the ceiling excludes the next possibly-incompatible version — not "everything in the future counts", and nothing earlier is claimed either. It still cannot cover a new prerelease segment, so a new Host alpha can outdate it; the weekly `declaration` patrol watches exactly that, and what to do when it goes red is in [Compatibility](docs/PUBLISHING.md#兼容性).
113
+
107
114
  ## Configuration
108
115
 
109
- Open **Settings → Plugins → Plugin configuration → DeepSeek balance**. Four groups, each collapsible:
116
+ Open **Plugins → Installed** and step into the **dsh-ds-balance** details page. The form runs straight down that page with four groups **all collapsed by default** — expand them from their headers:
117
+
118
+ <p align="center">
119
+ <img src="assets/settings-cards-position_en.png" alt="Where the DeepSeek balance entry sits in the Plugins list" width="480">
120
+ <br>
121
+ <em>Where it sits: the Plugins page's list view, with <code>dsh-ds-balance</code> alongside the other installed plugins; the details page behind it carries the form shown above.</em>
122
+ </p>
110
123
 
111
124
  - **Connection**: the API base URL and the credential, both blank by default — a blank URL means the official DeepSeek endpoint, and the credential is inherited from the official model page and is read-only.
112
125
  The nested "Customised settings" holds only the credential reference name.
package/lib/client.js CHANGED
@@ -34,24 +34,6 @@ var import_react6 = require("react");
34
34
 
35
35
  // src/client/settings/BalanceSettingsCard.tsx
36
36
  var import_react4 = require("react");
37
-
38
- // node_modules/clsx/dist/clsx.mjs
39
- function r(e) {
40
- var t, f, n = "";
41
- if ("string" == typeof e || "number" == typeof e) n += e;
42
- else if ("object" == typeof e) if (Array.isArray(e)) {
43
- var o = e.length;
44
- for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
45
- } else for (f in e) e[f] && (n && (n += " "), n += f);
46
- return n;
47
- }
48
- function clsx() {
49
- for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
50
- return n;
51
- }
52
- var clsx_default = clsx;
53
-
54
- // src/client/settings/BalanceSettingsCard.tsx
55
37
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
56
38
 
57
39
  // src/client/locales.ts
@@ -72,15 +54,13 @@ var zh = {
72
54
  "popover.cooldown": "{value} \u79D2\u540E\u53EF\u518D\u6B21\u5237\u65B0",
73
55
  "popover.mismatch": "\u672A\u627E\u5230 {wanted} \u4F59\u989D\uFF0C\u5F53\u524D\u663E\u793A {shown}",
74
56
  "popover.action.useShown": "\u6539\u7528 {shown}",
75
- "popover.action.openSettings": "\u53BB\u8BBE\u7F6E",
57
+ "popover.action.openPlugins": "\u6253\u5F00\u63D2\u4EF6\u9875",
76
58
  "state.unavailable": "\u8D26\u6237\u4E0D\u53EF\u7528",
77
59
  "state.stale": "\u6570\u636E\u5DF2\u8FC7\u671F",
78
60
  "state.error": "\u8BFB\u53D6\u5931\u8D25",
79
61
  "state.empty": "\u5C1A\u672A\u914D\u7F6E",
80
62
  "state.noKey": "\u5C1A\u672A\u586B\u5199 API Key",
81
63
  "state.noBalance": "\u6682\u65E0\u4F59\u989D",
82
- "settings.title": "DeepSeek \u4F59\u989D",
83
- "settings.description": "\u914D\u7F6E\u8D26\u6237\u8FDE\u63A5\u3001\u5237\u65B0\u8282\u594F\u4E0E\u9884\u8B66\u9608\u503C\u3002",
84
64
  "settings.group.connection": "\u8FDE\u63A5",
85
65
  "settings.group.refresh": "\u5237\u65B0",
86
66
  "settings.group.display": "\u5C55\u793A",
@@ -121,7 +101,6 @@ var zh = {
121
101
  "settings.test.fail": "\u8FDE\u63A5\u5931\u8D25\uFF1A{message}",
122
102
  "settings.save": "\u4FDD\u5B58",
123
103
  "settings.saving": "\u4FDD\u5B58\u4E2D\u2026",
124
- "settings.discard": "\u653E\u5F03\u4FEE\u6539",
125
104
  "settings.failed": "\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002",
126
105
  "settings.readOnly": "\u5F53\u524D\u914D\u7F6E\u53EA\u8BFB\uFF0C\u65E0\u6CD5\u4FDD\u5B58\u4FEE\u6539\u3002",
127
106
  "settings.unsaved": "\u672A\u4FDD\u5B58",
@@ -152,15 +131,13 @@ var en = {
152
131
  "popover.cooldown": "Refresh available in {value}s",
153
132
  "popover.mismatch": "No {wanted} balance found; showing {shown}",
154
133
  "popover.action.useShown": "Use {shown}",
155
- "popover.action.openSettings": "Open settings",
134
+ "popover.action.openPlugins": "Open the Plugins page",
156
135
  "state.unavailable": "Account unavailable",
157
136
  "state.stale": "Data is stale",
158
137
  "state.error": "Failed to load",
159
138
  "state.empty": "Not configured",
160
139
  "state.noKey": "API key missing",
161
140
  "state.noBalance": "No balance",
162
- "settings.title": "DeepSeek balance",
163
- "settings.description": "Connection, refresh cadence, and warning thresholds.",
164
141
  "settings.group.connection": "Connection",
165
142
  "settings.group.refresh": "Refresh",
166
143
  "settings.group.display": "Display",
@@ -197,7 +174,6 @@ var en = {
197
174
  "settings.test.fail": "Connection failed: {message}",
198
175
  "settings.save": "Save",
199
176
  "settings.saving": "Saving\u2026",
200
- "settings.discard": "Discard",
201
177
  "settings.failed": "Saving failed. Try again.",
202
178
  "settings.readOnly": "This configuration is read-only.",
203
179
  "settings.unsaved": "Unsaved",
@@ -218,6 +194,24 @@ function interpolate(template, params) {
218
194
 
219
195
  // src/client/settings/fields.tsx
220
196
  var import_react = require("react");
197
+
198
+ // node_modules/clsx/dist/clsx.mjs
199
+ function r(e) {
200
+ var t, f, n = "";
201
+ if ("string" == typeof e || "number" == typeof e) n += e;
202
+ else if ("object" == typeof e) if (Array.isArray(e)) {
203
+ var o = e.length;
204
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
205
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
206
+ return n;
207
+ }
208
+ function clsx() {
209
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
210
+ return n;
211
+ }
212
+ var clsx_default = clsx;
213
+
214
+ // src/client/settings/fields.tsx
221
215
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
222
216
 
223
217
  // src/client/settings/fields.module.css
@@ -533,6 +527,15 @@ function landedWrite(snapshot, field, write) {
533
527
  if (write.kind === "clear") return !Object.hasOwn(snapshot.user, field);
534
528
  return Object.hasOwn(snapshot.user, field) && snapshot.user[field] === write.value;
535
529
  }
530
+ async function writeFieldValue(scope, field, value) {
531
+ await settle(scope.set(field, value));
532
+ const snapshot = scope.getSnapshot();
533
+ return landedWrite(
534
+ { value: asRecord(snapshot.value), user: asRecord(snapshot.user), writable: snapshot.writable },
535
+ field,
536
+ { kind: "set", value }
537
+ );
538
+ }
536
539
  function planWrites(snapshot, staged) {
537
540
  const plan = [];
538
541
  for (const [field, edit] of staged) {
@@ -891,22 +894,11 @@ function useCredentialState(ref) {
891
894
 
892
895
  // src/client/settings/BalanceSettingsCard.module.css
893
896
  var BalanceSettingsCard_default = {
894
- card: "BalanceSettingsCard_card",
895
- cardOpen: "BalanceSettingsCard_cardOpen",
896
- header: "BalanceSettingsCard_header",
897
- headText: "BalanceSettingsCard_headText",
898
- name: "BalanceSettingsCard_name",
899
- description: "BalanceSettingsCard_description",
900
- chevronWrap: "BalanceSettingsCard_chevronWrap",
901
- chevron: "BalanceSettingsCard_chevron",
902
- chevronOpen: "BalanceSettingsCard_chevronOpen",
903
- pending: "BalanceSettingsCard_pending",
904
- body: "BalanceSettingsCard_body",
897
+ form: "BalanceSettingsCard_form",
905
898
  readOnly: "BalanceSettingsCard_readOnly",
906
899
  footer: "BalanceSettingsCard_footer",
907
900
  failed: "BalanceSettingsCard_failed",
908
901
  notice: "BalanceSettingsCard_notice",
909
- discard: "BalanceSettingsCard_discard",
910
902
  save: "BalanceSettingsCard_save"
911
903
  };
912
904
 
@@ -953,24 +945,13 @@ function isFilled(value) {
953
945
  }
954
946
  function BalanceSettingsCard({ t, scope }) {
955
947
  const form = useConfigForm(scope);
956
- const [open, setOpen] = (0, import_react4.useState)(false);
957
948
  const [groupOpen, setGroupOpen] = (0, import_react4.useState)(DEFAULT_GROUP_OPEN);
958
- const saveStarted = (0, import_react4.useRef)(false);
959
949
  const toggleGroup = (key) => {
960
950
  setGroupOpen((current) => ({ ...current, [key]: !current[key] }));
961
951
  };
962
952
  const groupOpenNow = (key) => groupOpen[key] || GROUP_FIELDS[key].some((name) => form.field(name).invalid) || key === "thresholds" && THRESHOLD_PAIRS.some((pair) => !form.thresholdPairOk(pair.currency));
963
953
  const { writable, dirty, invalid, saving, failed } = form.state;
964
954
  const disabled = !writable || saving;
965
- (0, import_react4.useEffect)(() => {
966
- if (saving) {
967
- saveStarted.current = true;
968
- return;
969
- }
970
- if (!saveStarted.current) return;
971
- saveStarted.current = false;
972
- if (!dirty && !failed) setOpen(false);
973
- }, [dirty, failed, saving]);
974
955
  const credentialStatus = (name) => {
975
956
  const state = form.field(name);
976
957
  if (state.stored) return { label: t("settings.overridden"), tone: "neutral" };
@@ -1105,27 +1086,9 @@ function BalanceSettingsCard({ t, scope }) {
1105
1086
  ] });
1106
1087
  const testResult = form.test.ok === null ? null : form.test.ok ? { ok: true, text: t("settings.test.ok") } : { ok: false, text: interpolate(t("settings.test.fail"), { message: form.test.message }) };
1107
1088
  const footerNote = failed ? { text: t("settings.failed"), className: BalanceSettingsCard_default.failed } : invalid ? { text: t("settings.invalid"), className: BalanceSettingsCard_default.notice } : null;
1108
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: clsx_default(BalanceSettingsCard_default.card, open && BalanceSettingsCard_default.cardOpen), children: [
1109
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1110
- "button",
1111
- {
1112
- type: "button",
1113
- className: BalanceSettingsCard_default.header,
1114
- "aria-expanded": open,
1115
- onClick: () => {
1116
- setOpen(!open);
1117
- },
1118
- children: [
1119
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: BalanceSettingsCard_default.headText, children: [
1120
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: BalanceSettingsCard_default.name, children: t("settings.title") }),
1121
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: BalanceSettingsCard_default.description, children: t("settings.description") })
1122
- ] }),
1123
- dirty ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tag, { tone: "neutral", className: BalanceSettingsCard_default.pending, children: t("settings.unsaved") }) : null,
1124
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: BalanceSettingsCard_default.chevronWrap, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.IconChevronDownOutline14, { className: clsx_default(BalanceSettingsCard_default.chevron, open && BalanceSettingsCard_default.chevronOpen) }) })
1125
- ]
1126
- }
1127
- ),
1128
- open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: BalanceSettingsCard_default.body, children: [
1089
+ return (
1090
+ // 页面把这一格渲染在自己的 <section> 里:无外框的一列控件,所以根节点是 div、不是 li。
1091
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: BalanceSettingsCard_default.form, children: [
1129
1092
  writable ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: BalanceSettingsCard_default.readOnly, role: "status", children: t("settings.readOnly") }),
1130
1093
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1131
1094
  FieldGroup,
@@ -1224,16 +1187,6 @@ function BalanceSettingsCard({ t, scope }) {
1224
1187
  ),
1225
1188
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: BalanceSettingsCard_default.footer, children: [
1226
1189
  footerNote === null ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: footerNote.className, role: "status", children: footerNote.text }),
1227
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1228
- "button",
1229
- {
1230
- type: "button",
1231
- className: BalanceSettingsCard_default.discard,
1232
- disabled: !dirty || saving,
1233
- onClick: form.discard,
1234
- children: t("settings.discard")
1235
- }
1236
- ),
1237
1190
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1238
1191
  "button",
1239
1192
  {
@@ -1247,8 +1200,8 @@ function BalanceSettingsCard({ t, scope }) {
1247
1200
  }
1248
1201
  )
1249
1202
  ] })
1250
- ] }) : null
1251
- ] });
1203
+ ] })
1204
+ );
1252
1205
  }
1253
1206
 
1254
1207
  // src/client/sidebar/SidebarBalance.tsx
@@ -1516,6 +1469,53 @@ function currentBalance() {
1516
1469
  return scenarios[currentScenario()];
1517
1470
  }
1518
1471
 
1472
+ // src/client/config-slot.ts
1473
+ var CONFIG_SLOT = "plugins.bundle.config";
1474
+ var CONFIG_SLOT_TIMEOUT_MS = 15e3;
1475
+ var CONFIG_SLOT_WARNING = '[WARN] This dsh host does not declare the plugins.bundle.config slot, so this plugin cannot show its configuration page here. The balance ring and its popover keep working. To get the configuration page, upgrade dsh to the version this plugin declares in engines.dsh of package.json -- see the "Version compatibility" section in the README.';
1476
+ function createConfigSlotProbe(options = {}) {
1477
+ const timeoutMs = options.timeoutMs ?? CONFIG_SLOT_TIMEOUT_MS;
1478
+ const schedule = options.schedule ?? ((fire, ms) => {
1479
+ const id = setTimeout(fire, ms);
1480
+ return () => {
1481
+ clearTimeout(id);
1482
+ };
1483
+ });
1484
+ let state = "pending";
1485
+ let disposed = false;
1486
+ let cancel = () => {
1487
+ };
1488
+ const listeners2 = /* @__PURE__ */ new Set();
1489
+ const publish = (next) => {
1490
+ if (disposed || next === state) return;
1491
+ state = next;
1492
+ for (const listener of [...listeners2]) listener();
1493
+ };
1494
+ cancel = schedule(() => {
1495
+ publish("missing");
1496
+ }, timeoutMs);
1497
+ return {
1498
+ getSnapshot: () => state,
1499
+ subscribe: (listener) => {
1500
+ if (disposed) return () => {
1501
+ };
1502
+ listeners2.add(listener);
1503
+ return () => {
1504
+ listeners2.delete(listener);
1505
+ };
1506
+ },
1507
+ markDeclared: () => {
1508
+ cancel();
1509
+ publish("available");
1510
+ },
1511
+ dispose: () => {
1512
+ disposed = true;
1513
+ cancel();
1514
+ listeners2.clear();
1515
+ }
1516
+ };
1517
+ }
1518
+
1519
1519
  // src/client/sidebar/BalancePopover.tsx
1520
1520
  var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
1521
1521
 
@@ -1524,6 +1524,7 @@ var BalancePopover_default = {
1524
1524
  panel: "BalancePopover_panel",
1525
1525
  title: "BalancePopover_title",
1526
1526
  titleLabel: "BalancePopover_titleLabel",
1527
+ iconButton: "BalancePopover_iconButton",
1527
1528
  titleRule: "BalancePopover_titleRule",
1528
1529
  details: "BalancePopover_details",
1529
1530
  notice: "BalancePopover_notice",
@@ -1567,11 +1568,13 @@ function BalancePopover(props) {
1567
1568
  ageMs,
1568
1569
  refreshing,
1569
1570
  cooldownSeconds,
1571
+ configSlotWarning,
1570
1572
  panelRef,
1571
1573
  style,
1572
1574
  onRefresh,
1573
1575
  onUseShown,
1574
- onOpenSettings
1576
+ useShownDisabled,
1577
+ onOpenPlugins
1575
1578
  } = props;
1576
1579
  const shown = selection.shown;
1577
1580
  const totalText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.total, shown.currency);
@@ -1595,10 +1598,31 @@ function BalancePopover(props) {
1595
1598
  role: "dialog",
1596
1599
  "aria-label": t("popover.title"),
1597
1600
  children: [
1598
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: BalancePopover_default.title, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: BalancePopover_default.titleLabel, children: [
1599
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.FishLogo, { size: 14 }),
1600
- t("popover.title")
1601
- ] }) }),
1601
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.title, children: [
1602
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1603
+ "a",
1604
+ {
1605
+ className: BalancePopover_default.titleLabel,
1606
+ href: "https://platform.deepseek.com/usage",
1607
+ target: "_blank",
1608
+ rel: "noopener noreferrer",
1609
+ children: [
1610
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.FishLogo, { size: 14 }),
1611
+ t("popover.title")
1612
+ ]
1613
+ }
1614
+ ),
1615
+ onOpenPlugins === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label: t("popover.action.openPlugins"), side: "top", delayMs: 500, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1616
+ "button",
1617
+ {
1618
+ type: "button",
1619
+ className: BalancePopover_default.iconButton,
1620
+ "aria-label": t("popover.action.openPlugins"),
1621
+ onClick: onOpenPlugins,
1622
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.IconPluginPinwheelOutline16, { size: 16 })
1623
+ }
1624
+ ) })
1625
+ ] }),
1602
1626
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: BalancePopover_default.titleRule, "aria-hidden": true }),
1603
1627
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("dl", { className: BalancePopover_default.details, children: [
1604
1628
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: t("popover.total") }),
@@ -1610,11 +1634,9 @@ function BalancePopover(props) {
1610
1634
  ] }),
1611
1635
  shown !== null && !selection.matchesPreference ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.notice, children: [
1612
1636
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: BalancePopover_default.noticeText, children: interpolate(t("popover.mismatch"), { wanted: displayCurrency, shown: shown.currency }) }),
1613
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.noticeActions, children: [
1614
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Button, { size: "sm", variant: "outline", onClick: onUseShown, children: interpolate(t("popover.action.useShown"), { shown: shown.currency }) }),
1615
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Button, { size: "sm", variant: "outline", onClick: onOpenSettings, children: t("popover.action.openSettings") })
1616
- ] })
1637
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: BalancePopover_default.noticeActions, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives3.Button, { size: "sm", variant: "outline", disabled: useShownDisabled === true, onClick: onUseShown, children: interpolate(t("popover.action.useShown"), { shown: shown.currency }) }) })
1617
1638
  ] }) : null,
1639
+ configSlotWarning === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: BalancePopover_default.notice, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: BalancePopover_default.noticeText, children: configSlotWarning }) }),
1618
1640
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: BalancePopover_default.footer, children: [
1619
1641
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: BalancePopover_default.updated, children: updatedText }),
1620
1642
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: BalancePopover_default.status, role: "status", children: statusText }),
@@ -1724,6 +1746,30 @@ var PANEL_MARGIN = 12;
1724
1746
  var MEASURE_STYLE = { visibility: "hidden", left: 0, top: 0 };
1725
1747
  var MS_PER_SECOND = 1e3;
1726
1748
  var TICK_MS = MS_PER_SECOND;
1749
+ function useConfigSlotState(probe) {
1750
+ const [state, setState] = (0, import_react5.useState)(() => probe.getSnapshot());
1751
+ (0, import_react5.useEffect)(() => {
1752
+ setState(probe.getSnapshot());
1753
+ return probe.subscribe(() => {
1754
+ setState(probe.getSnapshot());
1755
+ });
1756
+ }, [probe]);
1757
+ return state;
1758
+ }
1759
+ function usePluginsNavigation(navigation) {
1760
+ const [openPlugins, setOpenPlugins] = (0, import_react5.useState)(() => navigation?.getSnapshot());
1761
+ (0, import_react5.useEffect)(() => {
1762
+ if (navigation === void 0) {
1763
+ setOpenPlugins(void 0);
1764
+ return;
1765
+ }
1766
+ setOpenPlugins(() => navigation.getSnapshot());
1767
+ return navigation.subscribe(() => {
1768
+ setOpenPlugins(() => navigation.getSnapshot());
1769
+ });
1770
+ }, [navigation]);
1771
+ return openPlugins;
1772
+ }
1727
1773
  function stateLabelKey(response, selection) {
1728
1774
  if (response.state === "error") return response.error?.code === "NO_KEY" ? "state.noKey" : "state.error";
1729
1775
  if (response.state === "empty") return response.error?.code === "NO_KEY" ? "state.noKey" : "state.empty";
@@ -1736,7 +1782,14 @@ function ringSpecFor(severity) {
1736
1782
  const spec = ringSpecOf(severity);
1737
1783
  return { state: spec.state === "ongoing" ? "idle" : spec.state, marker: spec.marker };
1738
1784
  }
1739
- function SidebarBalance({ wide, t, config }) {
1785
+ function SidebarBalance({
1786
+ wide,
1787
+ t,
1788
+ config,
1789
+ configSlotProbe,
1790
+ onSelectCurrency,
1791
+ pluginsNavigation
1792
+ }) {
1740
1793
  const [view, setView] = (0, import_react5.useState)(() => {
1741
1794
  const response2 = resolveScenario() === null ? pendingView() : currentBalance();
1742
1795
  return { response: response2, seenAt: Date.now(), seenAgeMs: response2.ageMs };
@@ -1744,7 +1797,6 @@ function SidebarBalance({ wide, t, config }) {
1744
1797
  const response = view.response;
1745
1798
  const [mock, setMock] = (0, import_react5.useState)(() => resolveScenario() !== null);
1746
1799
  const loadedRef = (0, import_react5.useRef)(false);
1747
- const [localCurrency, setLocalCurrency] = (0, import_react5.useState)(null);
1748
1800
  const [open, setOpen] = (0, import_react5.useState)(false);
1749
1801
  const [refreshing, setRefreshing] = (0, import_react5.useState)(false);
1750
1802
  const [cooldownUntil, setCooldownUntil] = (0, import_react5.useState)(0);
@@ -1763,7 +1815,7 @@ function SidebarBalance({ wide, t, config }) {
1763
1815
  const markFresh = (0, import_react5.useCallback)(() => {
1764
1816
  setView((current) => ({ ...current, seenAt: Date.now(), seenAgeMs: 0 }));
1765
1817
  }, []);
1766
- const preference = localCurrency ?? config.displayCurrency;
1818
+ const preference = config.displayCurrency;
1767
1819
  const selection = (0, import_react5.useMemo)(() => selectionOf(response, preference), [response, preference]);
1768
1820
  (0, import_react5.useEffect)(() => subscribeScenario((key) => {
1769
1821
  const active = resolveScenario() !== null;
@@ -1868,10 +1920,14 @@ function SidebarBalance({ wide, t, config }) {
1868
1920
  const handleUseShown = (0, import_react5.useCallback)(() => {
1869
1921
  const current = selection.shown;
1870
1922
  if (current === null) return;
1871
- setLocalCurrency(current.currency);
1872
- }, [selection]);
1873
- const handleOpenSettings = (0, import_react5.useCallback)(() => {
1874
- }, []);
1923
+ void onSelectCurrency(current.currency);
1924
+ }, [selection, onSelectCurrency]);
1925
+ const openPlugins = usePluginsNavigation(pluginsNavigation);
1926
+ const handleOpenPlugins = (0, import_react5.useCallback)(() => {
1927
+ closeNow();
1928
+ openPlugins?.();
1929
+ }, [closeNow, openPlugins]);
1930
+ const configSlotState = useConfigSlotState(configSlotProbe);
1875
1931
  const shown = selection.shown;
1876
1932
  const ring = ringSpecFor(response.severity);
1877
1933
  const ringRatio = ringRatioOf(
@@ -1898,6 +1954,22 @@ function SidebarBalance({ wide, t, config }) {
1898
1954
  const cooldownSeconds = Math.max(0, Math.ceil((cooldownUntil - now) / MS_PER_SECOND));
1899
1955
  const ageMs = currentAgeMs(view.seenAt, view.seenAgeMs, now);
1900
1956
  const ringTitle = !wide && stateText !== null ? `${t("sidebar.aria.ring")} ${stateText}` : void 0;
1957
+ const hoverLabel = shown === null ? stateText : formatMoney(shown.total, shown.currency);
1958
+ const trigger = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1959
+ "button",
1960
+ {
1961
+ type: "button",
1962
+ className: SidebarBalance_default.trigger,
1963
+ "aria-label": ariaLabel,
1964
+ "aria-expanded": open,
1965
+ onClick: toggleOpen,
1966
+ children: wide ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1967
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.icon, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PercentRing, { state: ring.state, marker: ring.marker, ratio: ringRatio, size: 16 }) }),
1968
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.label, children: t("sidebar.label") }),
1969
+ markerLabel === null ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.Tooltip, { label: markerHint, side: "bottom", delayMs: 500, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.marker, role: "img", "aria-label": markerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.IconWarningOutline16, { size: 12 }) }) })
1970
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PercentRing, { state: ring.state, marker: ring.marker, ratio: ringRatio, size: 18, title: ringTitle })
1971
+ }
1972
+ );
1901
1973
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1902
1974
  "div",
1903
1975
  {
@@ -1906,21 +1978,7 @@ function SidebarBalance({ wide, t, config }) {
1906
1978
  "data-mode": wide ? "wide" : "rail",
1907
1979
  tabIndex: -1,
1908
1980
  children: [
1909
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1910
- "button",
1911
- {
1912
- type: "button",
1913
- className: SidebarBalance_default.trigger,
1914
- "aria-label": ariaLabel,
1915
- "aria-expanded": open,
1916
- onClick: toggleOpen,
1917
- children: wide ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1918
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.icon, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PercentRing, { state: ring.state, marker: ring.marker, ratio: ringRatio, size: 16 }) }),
1919
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.label, children: t("sidebar.label") }),
1920
- markerLabel === null ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.Tooltip, { label: markerHint, side: "bottom", delayMs: 500, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: SidebarBalance_default.marker, role: "img", "aria-label": markerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.IconWarningOutline16, { size: 12 }) }) })
1921
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PercentRing, { state: ring.state, marker: ring.marker, ratio: ringRatio, size: 18, title: ringTitle })
1922
- }
1923
- ),
1981
+ wide && hoverLabel !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives4.Tooltip, { label: hoverLabel, side: "right", delayMs: 500, disabled: open, children: trigger }) : trigger,
1924
1982
  open ? (0, import_react_dom.createPortal)(
1925
1983
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1926
1984
  BalancePopover,
@@ -1931,11 +1989,13 @@ function SidebarBalance({ wide, t, config }) {
1931
1989
  ageMs,
1932
1990
  refreshing,
1933
1991
  cooldownSeconds,
1992
+ configSlotWarning: configSlotState === "missing" ? CONFIG_SLOT_WARNING : null,
1934
1993
  panelRef,
1935
1994
  style: pos ?? MEASURE_STYLE,
1936
1995
  onRefresh: handleRefresh,
1937
1996
  onUseShown: handleUseShown,
1938
- onOpenSettings: handleOpenSettings
1997
+ useShownDisabled: !config.writable,
1998
+ onOpenPlugins: openPlugins === void 0 ? void 0 : handleOpenPlugins
1939
1999
  }
1940
2000
  ),
1941
2001
  document.body
@@ -1987,6 +2047,16 @@ function useScopeValue(scope) {
1987
2047
  }, [scope]);
1988
2048
  return value;
1989
2049
  }
2050
+ function useScopeWritable(scope) {
2051
+ const [writable, setWritable] = (0, import_react6.useState)(() => scope.getSnapshot().writable);
2052
+ (0, import_react6.useEffect)(() => {
2053
+ setWritable(scope.getSnapshot().writable);
2054
+ return scope.subscribe(() => {
2055
+ setWritable(scope.getSnapshot().writable);
2056
+ });
2057
+ }, [scope]);
2058
+ return writable;
2059
+ }
1990
2060
  function readDisplayCurrency(value) {
1991
2061
  const raw = value.displayCurrency;
1992
2062
  return typeof raw === "string" && raw.length > 0 ? raw : DEFAULT_CONFIG.displayCurrency;
@@ -2010,37 +2080,102 @@ function readSignature(value) {
2010
2080
  }
2011
2081
  function SidebarSeatComponent(props) {
2012
2082
  const value = useScopeValue(props.scope);
2083
+ const writable = useScopeWritable(props.scope);
2084
+ const selectCurrency = (0, import_react6.useCallback)(
2085
+ (currency) => writeFieldValue(props.scope, "displayCurrency", currency),
2086
+ [props.scope]
2087
+ );
2013
2088
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2014
2089
  SidebarBalance,
2015
2090
  {
2016
2091
  wide: props.seat.wide,
2017
2092
  t: props.seat.t,
2093
+ configSlotProbe: props.configSlotProbe,
2094
+ pluginsNavigation: props.pluginsNavigation,
2095
+ onSelectCurrency: selectCurrency,
2018
2096
  config: {
2019
2097
  displayCurrency: readDisplayCurrency(value),
2020
2098
  manualRefreshCooldownSeconds: readCooldown(value),
2021
2099
  clientPollSeconds: readPollSeconds(value),
2022
2100
  configSignature: readSignature(value),
2023
- warnThresholdOf: (currency) => readWarnThreshold(value, currency)
2101
+ warnThresholdOf: (currency) => readWarnThreshold(value, currency),
2102
+ writable
2024
2103
  }
2025
2104
  }
2026
2105
  );
2027
2106
  }
2028
2107
  function SettingsSeatComponent(props) {
2029
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BalanceSettingsCard, { t: props.seat.t, scope: props.scope });
2108
+ switch (props.seat.view) {
2109
+ case "page":
2110
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BalanceSettingsCard, { t: props.seat.t, scope: props.scope });
2111
+ case "summary":
2112
+ return null;
2113
+ }
2114
+ }
2115
+ var PLUGINS_PANEL_ID = "plugins";
2116
+ function createPluginsNavigation() {
2117
+ let open;
2118
+ const listeners2 = /* @__PURE__ */ new Set();
2119
+ const publish = (next) => {
2120
+ if (next === open) return;
2121
+ open = next;
2122
+ for (const listener of [...listeners2]) listener();
2123
+ };
2124
+ return {
2125
+ getSnapshot: () => open,
2126
+ subscribe: (listener) => {
2127
+ listeners2.add(listener);
2128
+ return () => {
2129
+ listeners2.delete(listener);
2130
+ };
2131
+ },
2132
+ attach: (callback) => {
2133
+ publish(callback);
2134
+ return () => {
2135
+ if (open === callback) publish(void 0);
2136
+ };
2137
+ }
2138
+ };
2030
2139
  }
2031
2140
  function apply(ctx) {
2032
2141
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ds-balance: dictionaries");
2033
2142
  const scope = adaptScope(ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE }));
2034
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register(
2035
- { name: "settings.plugin.item", key: SETTINGS_NAMESPACE, locale: NS },
2036
- (seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SettingsSeatComponent, { seat, scope })
2037
- ));
2143
+ const configSlotProbe = createConfigSlotProbe();
2144
+ ctx.effect(() => () => configSlotProbe.dispose(), "ds-balance: config slot probe");
2145
+ const pluginsNavigation = createPluginsNavigation();
2146
+ ctx.inject(["layout"], (layoutCtx) => {
2147
+ const face = layoutCtx.layout;
2148
+ if (face === void 0 || typeof face.selectPanel !== "function") return;
2149
+ const selectPanel = face.selectPanel.bind(face);
2150
+ layoutCtx.effect(() => pluginsNavigation.attach(() => {
2151
+ try {
2152
+ selectPanel(PLUGINS_PANEL_ID);
2153
+ } catch (error) {
2154
+ console.warn("[WARN] ds-balance: cannot switch to the plugins panel", error);
2155
+ }
2156
+ }), "ds-balance: plugins panel navigation");
2157
+ });
2158
+ ctx.slots.inject(CONFIG_SLOT, () => {
2159
+ configSlotProbe.markDeclared();
2160
+ return ctx.slots.register(
2161
+ { name: CONFIG_SLOT, key: "dsh-ds-balance", locale: NS },
2162
+ (seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SettingsSeatComponent, { seat, scope })
2163
+ );
2164
+ });
2038
2165
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
2039
2166
  { name: "sidebar.footer.action", id: SETTINGS_NAMESPACE, order: 0, locale: NS },
2040
- (seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SidebarSeatComponent, { seat, scope })
2167
+ (seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2168
+ SidebarSeatComponent,
2169
+ {
2170
+ seat,
2171
+ scope,
2172
+ configSlotProbe,
2173
+ pluginsNavigation
2174
+ }
2175
+ )
2041
2176
  ));
2042
2177
  }
2043
- var __dsbCss = "/* src/client/settings/fields.module.css */\n.fields_group {\n padding: 12px 0 0;\n}\n.fields_groupLast {\n padding-bottom: 12px;\n}\n.fields_groupNote {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_fields {\n display: flex;\n flex-direction: column;\n}\n.fields_fields > * + * {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.fields_details {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-top: 12px;\n padding-top: 12px;\n}\n.fields_detailsSummary {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n list-style: none;\n}\n.fields_detailsSummary::-webkit-details-marker {\n display: none;\n}\n.fields_detailsSummary::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor;\n transform: translate(-0.5px, -1px) rotate(-45deg);\n transition: transform 120ms ease;\n}\n.fields_details[open] > .fields_detailsSummary::before {\n transform: translate(-0.5px, -1px) rotate(45deg);\n}\n.fields_detailsBody {\n padding-top: 10px;\n}\n.fields_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.fields_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n}\n.fields_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n.fields_rowTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_rowTitle {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_badges {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.fields_reset {\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.fields_reset:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reset:disabled {\n cursor: default;\n}\n.fields_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n opacity: 0.6;\n cursor: default;\n}\ninput.fields_inputStatic,\ninput.fields_inputStatic:hover,\ninput.fields_inputStatic:focus,\ninput.fields_inputStatic:focus-visible,\ninput.fields_inputStatic:active {\n border-color: var(--dsw-alias-border-l4);\n background: var(--dsw-alias-bg-layer-3);\n outline: none;\n box-shadow: none;\n cursor: default;\n pointer-events: none;\n}\n.fields_inputInvalid {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_inputInvalid:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputInvalid:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_inputWithAction {\n position: relative;\n display: flex;\n align-items: center;\n}\n.fields_inputWithAction .fields_input,\n.fields_inputWithAction .fields_inputInvalid {\n padding-right: 38px;\n}\n.fields_reveal {\n position: absolute;\n right: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.fields_reveal:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.fields_revealOn {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reveal:disabled {\n cursor: default;\n opacity: 0.4;\n}\n.fields_selectorAnchor {\n flex: none;\n}\n.fields_selector {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n flex: none;\n max-width: 100%;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n background: var(--dsw-alias-bg-module-platform);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n.fields_selector:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.fields_selector:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_selectorChevron {\n flex: none;\n}\n.fields_hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_pairNote {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n.fields_pairNote::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-radius: 999px;\n corner-shape: round;\n background: currentColor;\n}\n.fields_invalid {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.fields_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_actionButton {\n appearance: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.fields_actionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.fields_actionButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.fields_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.fields_resultOk {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary);\n}\n.fields_resultFail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* src/client/settings/BalanceSettingsCard.module.css */\n.BalanceSettingsCard_card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.BalanceSettingsCard_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.BalanceSettingsCard_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.BalanceSettingsCard_headText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.BalanceSettingsCard_name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.BalanceSettingsCard_description {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_chevronWrap {\n flex: none;\n display: inline-flex;\n align-items: center;\n}\n.BalanceSettingsCard_chevron {\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.BalanceSettingsCard_chevronOpen {\n transform: rotate(180deg);\n}\n.BalanceSettingsCard_pending {\n flex: none;\n}\n.BalanceSettingsCard_body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding-bottom: 0;\n}\n.BalanceSettingsCard_readOnly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalanceSettingsCard_failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.BalanceSettingsCard_notice {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_discard,\n.BalanceSettingsCard_save {\n appearance: none;\n border: 0.5px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.BalanceSettingsCard_discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.BalanceSettingsCard_discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.BalanceSettingsCard_discard:disabled,\n.BalanceSettingsCard_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalanceSettingsCard_discard:focus-visible,\n.BalanceSettingsCard_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n.BalancePopover_title {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n.BalancePopover_titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\n}\n.BalancePopover_titleRule {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalancePopover_details {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_details dt,\n.BalancePopover_details dd {\n min-width: 0;\n margin: 0;\n}\n.BalancePopover_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.BalancePopover_notice {\n margin-top: 12px;\n}\n.BalancePopover_noticeText {\n margin: 0 0 8px;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_noticeActions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.BalancePopover_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n}\n.BalancePopover_updated {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_status {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_refresh {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.BalancePopover_refresh:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_refresh:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.BalancePopover_refresh:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalancePopover_refresh[data-cooling] {\n color: var(--dsw-alias-label-caption);\n}\n.BalancePopover_refresh[data-refreshing] svg {\n animation: BalancePopover_balance-refresh-spin 900ms linear infinite;\n}\n@keyframes BalancePopover_balance-refresh-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .BalancePopover_refresh[data-refreshing] svg {\n animation-duration: 2400ms;\n }\n}\n\n/* src/client/sidebar/PercentRing.module.css */\n.PercentRing_ring {\n display: block;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.PercentRing_ring[data-state=done] {\n color: var(--dsw-alias-state-success-primary);\n}\n.PercentRing_ring[data-state=warning] {\n color: var(--dsw-alias-state-warn-primary);\n}\n.PercentRing_ring[data-state=error] {\n color: var(--dsw-alias-state-error-primary);\n}\n.PercentRing_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n}\n.PercentRing_cross {\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\n stroke-linecap: round;\n}\n\n/* src/client/sidebar/SidebarBalance.module.css */\n.SidebarBalance_root {\n position: relative;\n display: flex;\n min-width: 0;\n}\n.SidebarBalance_root[data-mode=wide] {\n flex: none;\n width: calc(100% + 4px);\n margin: 4px -2px 0;\n}\n.SidebarBalance_root[data-mode=rail] {\n flex: none;\n margin: 8px 0 0;\n}\n.SidebarBalance_trigger {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n overflow: hidden;\n}\n.SidebarBalance_trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.SidebarBalance_trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.SidebarBalance_root[data-mode=wide] .SidebarBalance_trigger {\n flex: none;\n gap: 8px;\n width: 100%;\n height: 42px;\n padding: 0 10px 0 8px;\n border-radius: 12px;\n}\n.SidebarBalance_root[data-mode=rail] .SidebarBalance_trigger {\n flex: none;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 50%;\n corner-shape: round;\n}\n.SidebarBalance_icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.SidebarBalance_label {\n flex: 0 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.SidebarBalance_marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
2178
+ var __dsbCss = "/* src/client/settings/fields.module.css */\n.fields_group {\n padding: 12px 0 0;\n}\n.fields_groupLast {\n padding-bottom: 12px;\n}\n.fields_groupNote {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_fields {\n display: flex;\n flex-direction: column;\n}\n.fields_fields > * + * {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.fields_details {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-top: 12px;\n padding-top: 12px;\n}\n.fields_detailsSummary {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n list-style: none;\n}\n.fields_detailsSummary::-webkit-details-marker {\n display: none;\n}\n.fields_detailsSummary::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor;\n transform: translate(-0.5px, -1px) rotate(-45deg);\n transition: transform 120ms ease;\n}\n.fields_details[open] > .fields_detailsSummary::before {\n transform: translate(-0.5px, -1px) rotate(45deg);\n}\n.fields_detailsBody {\n padding-top: 10px;\n}\n.fields_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.fields_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n}\n.fields_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n.fields_rowTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_rowTitle {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_badges {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.fields_reset {\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.fields_reset:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reset:disabled {\n cursor: default;\n}\n.fields_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n opacity: 0.6;\n cursor: default;\n}\ninput.fields_inputStatic,\ninput.fields_inputStatic:hover,\ninput.fields_inputStatic:focus,\ninput.fields_inputStatic:focus-visible,\ninput.fields_inputStatic:active {\n border-color: var(--dsw-alias-border-l4);\n background: var(--dsw-alias-bg-layer-3);\n outline: none;\n box-shadow: none;\n cursor: default;\n pointer-events: none;\n}\n.fields_inputInvalid {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_inputInvalid:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputInvalid:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_inputWithAction {\n position: relative;\n display: flex;\n align-items: center;\n}\n.fields_inputWithAction .fields_input,\n.fields_inputWithAction .fields_inputInvalid {\n padding-right: 38px;\n}\n.fields_reveal {\n position: absolute;\n right: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.fields_reveal:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.fields_revealOn {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reveal:disabled {\n cursor: default;\n opacity: 0.4;\n}\n.fields_selectorAnchor {\n flex: none;\n}\n.fields_selector {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n flex: none;\n max-width: 100%;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n background: var(--dsw-alias-bg-module-platform);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n.fields_selector:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.fields_selector:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_selectorChevron {\n flex: none;\n}\n.fields_hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_pairNote {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n.fields_pairNote::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-radius: 999px;\n corner-shape: round;\n background: currentColor;\n}\n.fields_invalid {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.fields_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_actionButton {\n appearance: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.fields_actionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.fields_actionButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.fields_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.fields_resultOk {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary);\n}\n.fields_resultFail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* src/client/settings/BalanceSettingsCard.module.css */\n.BalanceSettingsCard_form {\n display: flex;\n flex-direction: column;\n}\n.BalanceSettingsCard_readOnly {\n margin: 0 0 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 16px;\n}\n.BalanceSettingsCard_failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.BalanceSettingsCard_notice {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_save {\n appearance: none;\n border: 0.5px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.BalanceSettingsCard_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalanceSettingsCard_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n.BalancePopover_title {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n.BalancePopover_titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n cursor: pointer;\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.BalancePopover_titleLabel:hover,\n.BalancePopover_titleLabel:visited {\n color: inherit;\n}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\n}\n.BalancePopover_iconButton {\n position: relative;\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n corner-shape: round;\n padding: 0;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_iconButton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.BalancePopover_iconButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.BalancePopover_titleRule {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalancePopover_details {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_details dt,\n.BalancePopover_details dd {\n min-width: 0;\n margin: 0;\n}\n.BalancePopover_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.BalancePopover_notice {\n margin-top: 12px;\n}\n.BalancePopover_noticeText {\n margin: 0 0 8px;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_noticeActions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.BalancePopover_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n}\n.BalancePopover_updated {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_status {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_refresh {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.BalancePopover_refresh:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_refresh:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.BalancePopover_refresh:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalancePopover_refresh[data-cooling] {\n color: var(--dsw-alias-label-caption);\n}\n.BalancePopover_refresh[data-refreshing] svg {\n animation: BalancePopover_balance-refresh-spin 900ms linear infinite;\n}\n@keyframes BalancePopover_balance-refresh-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .BalancePopover_refresh[data-refreshing] svg {\n animation-duration: 2400ms;\n }\n}\n\n/* src/client/sidebar/PercentRing.module.css */\n.PercentRing_ring {\n display: block;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.PercentRing_ring[data-state=done] {\n color: var(--dsw-alias-state-success-primary);\n}\n.PercentRing_ring[data-state=warning] {\n color: var(--dsw-alias-state-warn-primary);\n}\n.PercentRing_ring[data-state=error] {\n color: var(--dsw-alias-state-error-primary);\n}\n.PercentRing_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n}\n.PercentRing_cross {\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\n stroke-linecap: round;\n}\n\n/* src/client/sidebar/SidebarBalance.module.css */\n.SidebarBalance_root {\n position: relative;\n display: flex;\n min-width: 0;\n}\n.SidebarBalance_root[data-mode=wide] {\n flex: none;\n width: calc(100% + 4px);\n margin: 4px -2px 0;\n}\n.SidebarBalance_root[data-mode=rail] {\n flex: none;\n margin: 8px 0 0;\n}\n.SidebarBalance_trigger {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n overflow: hidden;\n}\n.SidebarBalance_trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.SidebarBalance_trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.SidebarBalance_root[data-mode=wide] .SidebarBalance_trigger {\n flex: none;\n gap: 8px;\n width: 100%;\n height: 42px;\n padding: 0 10px 0 8px;\n border-radius: 12px;\n}\n.SidebarBalance_root[data-mode=rail] .SidebarBalance_trigger {\n flex: none;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 12px;\n}\n.SidebarBalance_icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.SidebarBalance_label {\n flex: 0 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.SidebarBalance_marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-tertiary);\n position: relative;\n top: 1.63px;\n}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
2044
2179
  var __dsbTagId = "dsh-ds-balance/client.css";
2045
2180
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + __dsbTagId + '"]') === null) {
2046
2181
  var __dsbTag = document.createElement('style');
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 配置槽的能力探测:**运行时不查版本号,只问「这个槽在不在」**。
3
+ *
4
+ * 为什么查能力而不是查版本:客户端半边拿不到宿主版本(没有 hostVersion 一类通道),
5
+ * 而「槽在不在」是当场可观测的事实。机制见 README 的「版本兼容」一节。
6
+ *
7
+ * 三态且**可逆**:
8
+ * - `pending` —— 还没等到上限,此时不下结论(新宿主上槽可能在启动后一小会儿才声明)。
9
+ * - `missing` —— 等满了还没等到,提示路径启用。
10
+ * - `available` —— 槽声明到了。若提示已经出现过,这一步会把提示撤掉(防宿主将来改成延迟声明)。
11
+ *
12
+ * 探测本身**不参与注册**:注册照旧交给 `ctx.slots.inject`,槽真的在就正常注册。
13
+ * 探测失败只影响提示,不影响圆环、浮层与后端。
14
+ * @module dsh-ds-balance/client/config-slot
15
+ */
16
+ /** 新宿主才声明的配置槽:配置卡片注册在它上面。 */
17
+ export declare const CONFIG_SLOT = "plugins.bundle.config";
18
+ /**
19
+ * 等槽声明的上限(毫秒)。
20
+ *
21
+ * 给宽是有意的:宿主半边与浏览器半边、以及各插件 `apply` 的次序都不由我们定,
22
+ * 慢机器上启动期本来就要几秒。宁可提示晚一点,也不要在新宿主上误报 ——
23
+ * 误报会让「缺槽」这个信号变得不可信。
24
+ */
25
+ export declare const CONFIG_SLOT_TIMEOUT_MS = 15000;
26
+ /**
27
+ * 缺槽时的提示。
28
+ *
29
+ * **英文、`[WARN]` 前缀、无 emoji** —— 兼容性硬约束规定提示文案一律英文,
30
+ * 所以它**有意不走** [locales.ts](locales.ts) 的双语词典:那里放的是产品文案,
31
+ * 中文用户也应该看到中文;而这一条是**跨版本诊断信息**,**只在探测到旧宿主(槽缺席)
32
+ * 时才会出现**,两种语言下一律照原文给。SPEC §5 把这记作一处有意的例外。
33
+ */
34
+ export declare const CONFIG_SLOT_WARNING: string;
35
+ /** 探测的三态。 */
36
+ export type ConfigSlotState = 'pending' | 'available' | 'missing';
37
+ /** 探测结果,供 UI 订阅。 */
38
+ export interface ConfigSlotProbe {
39
+ /** 当前态。 */
40
+ getSnapshot(): ConfigSlotState;
41
+ /**
42
+ * 订阅状态变化。
43
+ * @param listener - 变化回调。
44
+ * @returns 退订函数。
45
+ */
46
+ subscribe(listener: () => void): () => void;
47
+ /** 槽的声明到了。晚到的声明会把 `missing` 拨回 `available`,于是提示被撤回。 */
48
+ markDeclared(): void;
49
+ /** 释放计时器与订阅者。 */
50
+ dispose(): void;
51
+ }
52
+ /** 探测的可注入点。 */
53
+ export interface ConfigSlotProbeOptions {
54
+ /** 上限毫秒数,默认 {@link CONFIG_SLOT_TIMEOUT_MS}。 */
55
+ timeoutMs?: number;
56
+ /**
57
+ * 定时器。默认 `setTimeout`;测试用假时钟替掉它,不依赖真实时间。
58
+ * @param fire - 到点回调。
59
+ * @param ms - 毫秒数。
60
+ * @returns 取消函数。
61
+ */
62
+ schedule?: (fire: () => void, ms: number) => () => void;
63
+ }
64
+ /**
65
+ * 建一个探测。
66
+ * @param options - 可注入的上限与定时器。
67
+ * @returns 三态探测结果。
68
+ */
69
+ export declare function createConfigSlotProbe(options?: ConfigSlotProbeOptions): ConfigSlotProbe;
@@ -1,8 +1,12 @@
1
1
  /**
2
- * 浏览器半边入口:注册词典、设置卡片与左下角条目。
2
+ * 浏览器半边入口:注册词典、Plugins 页里的配置卡片与左下角条目。
3
3
  *
4
- * 这两半各占一个 slot,互不依赖;设置卡片的位置由宿主 `installSection` 与
5
- * 本文件注册的 `settings.plugin.item` 用同一个命名空间配对决定。
4
+ * 这两半各占一个 slot,互不依赖;配置卡片挂在 `plugins.bundle.config` 上,
5
+ * 宿主按**包名**取这一格,设置命名空间只用来绑作用域。
6
+ *
7
+ * 配置那一格另有一层**能力探测**(不查版本号):旧宿主根本没有这一格,
8
+ * 注册会一直等下去、也不报错。探测只决定要不要在浮层里提示,不参与注册 ——
9
+ * 槽真的在时,下面那次 `inject` 照旧正常注册。见 [config-slot.ts](config-slot.ts)。
6
10
  * @module dsh-ds-balance/client
7
11
  */
8
12
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
@@ -21,15 +21,13 @@ export declare const zh: {
21
21
  readonly 'popover.cooldown': "{value} 秒后可再次刷新";
22
22
  readonly 'popover.mismatch': "未找到 {wanted} 余额,当前显示 {shown}";
23
23
  readonly 'popover.action.useShown': "改用 {shown}";
24
- readonly 'popover.action.openSettings': "去设置";
24
+ readonly 'popover.action.openPlugins': "打开插件页";
25
25
  readonly 'state.unavailable': "账户不可用";
26
26
  readonly 'state.stale': "数据已过期";
27
27
  readonly 'state.error': "读取失败";
28
28
  readonly 'state.empty': "尚未配置";
29
29
  readonly 'state.noKey': "尚未填写 API Key";
30
30
  readonly 'state.noBalance': "暂无余额";
31
- readonly 'settings.title': "DeepSeek 余额";
32
- readonly 'settings.description': "配置账户连接、刷新节奏与预警阈值。";
33
31
  readonly 'settings.group.connection': "连接";
34
32
  readonly 'settings.group.refresh': "刷新";
35
33
  readonly 'settings.group.display': "展示";
@@ -66,7 +64,6 @@ export declare const zh: {
66
64
  readonly 'settings.test.fail': "连接失败:{message}";
67
65
  readonly 'settings.save': "保存";
68
66
  readonly 'settings.saving': "保存中…";
69
- readonly 'settings.discard': "放弃修改";
70
67
  readonly 'settings.failed': "保存失败,请重试。";
71
68
  readonly 'settings.readOnly': "当前配置只读,无法保存修改。";
72
69
  readonly 'settings.unsaved': "未保存";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 「DeepSeek 余额」在设置页里的配置卡片。
2
+ * 「DeepSeek 余额」在 Plugins 页里该 bundle 详情页上的配置卡片:标题由页面画,卡内只有控件与保存。
3
3
  * 只做配置:连接、展示、阈值、刷新四组;不展示任何额度信息,也不按阈值给任何东西上色。
4
4
  * 分组按使用频率排序:刷新三项有合理默认值,放最后。
5
5
  * @module dsh-ds-balance/client/settings/BalanceSettingsCard
@@ -186,6 +186,17 @@ export declare const TEST_LATENCY_MS = 800;
186
186
  * @returns 失败原因;可通过时为 null。
187
187
  */
188
188
  export declare function probeFailure(baseUrl: string, apiKey: string, apiKeyRef: string): string | null;
189
+ /**
190
+ * 写一个字段,并读回 user 层确认它落定。
191
+ *
192
+ * 设置卡片与侧栏条目共用这一条写路径:宿主拒绝写入时**不抛错**,只让快照保持不变,
193
+ * 所以成败只能从读回的快照判定 —— 判据与 `save()` 用的是同一个 `landedWrite`。
194
+ * @param scope - 设置作用域。
195
+ * @param field - 字段名。
196
+ * @param value - 要写进该字段的值。
197
+ * @returns 这次写入是否落进 user 层。
198
+ */
199
+ export declare function writeFieldValue(scope: SettingsScope, field: string, value: unknown): Promise<boolean>;
189
200
  /** 计划中的一次写入;write 为 undefined 表示草稿非法。 */
190
201
  interface PlannedWrite {
191
202
  readonly field: string;
@@ -25,6 +25,13 @@ export interface BalancePopoverProps {
25
25
  refreshing: boolean;
26
26
  /** 冷却剩余秒数;0 表示可以刷新。 */
27
27
  cooldownSeconds: number;
28
+ /**
29
+ * 「本宿主没有配置槽」的提示行;null 表示不提示。
30
+ *
31
+ * 文案由 [config-slot.ts](../config-slot.ts) 给:它是英文 `[WARN]` 诊断行,
32
+ * 按兼容性硬约束两种语言下都照原文给,所以刻意不进双语词典。
33
+ */
34
+ configSlotWarning: string | null;
28
35
  /** 面板自身的引用:useAnchoredPosition 用它量尺寸,外部点击判定用它算「内部」。 */
29
36
  panelRef: RefObject<HTMLElement>;
30
37
  /** 由 useAnchoredPosition 给出的 fixed 坐标;首帧是 MEASURE_STYLE(隐藏待测)。 */
@@ -33,8 +40,16 @@ export interface BalancePopoverProps {
33
40
  onRefresh: () => void;
34
41
  /** 「改用 X」回调。 */
35
42
  onUseShown: () => void;
36
- /** 「去设置」回调;本阶段只做占位。 */
37
- onOpenSettings: () => void;
43
+ /**
44
+ * 「改用 X」按钮是否置灰;缺省 false。
45
+ * 设置不可写时置灰 —— 不假装写入成功:点下去什么都不会发生,比留一个按得动却没反应的按钮诚实。
46
+ */
47
+ useShownDisabled?: boolean;
48
+ /**
49
+ * 「切到 Plugins 页」回调。宿主没提供 layout 服务时给 undefined ——
50
+ * 这时右上角那个图标按钮**不渲染**:不留按不动的死按钮。
51
+ */
52
+ onOpenPlugins?: () => void;
38
53
  }
39
54
  /**
40
55
  * 渲染余额浮层。
@@ -12,12 +12,47 @@
12
12
  * @module dsh-ds-balance/client/sidebar/SidebarBalance
13
13
  */
14
14
  import { type LocaleKey } from '../locales.ts';
15
+ import { type ConfigSlotProbe } from '../config-slot.ts';
16
+ /**
17
+ * 「切到 Plugins 页」这个**可选**入口。宿主没提供 layout 服务时整条链不存在,
18
+ * 浮层右上角的图标因此不渲染 —— 不留按不动的死按钮。
19
+ * 形态与 `configSlotProbe` 一样是个可订阅的小对象:服务可能晚到,图标要能自己出现、也能自己消失。
20
+ */
21
+ export interface PluginsNavigation {
22
+ /** 当前可用的导航回调;服务还没到位时是 undefined。 */
23
+ getSnapshot: () => (() => void) | undefined;
24
+ /**
25
+ * 订阅它的出现与消失。
26
+ * @param listener - 变化回调。
27
+ * @returns 退订函数。
28
+ */
29
+ subscribe: (listener: () => void) => () => void;
30
+ }
15
31
  /** 条目属性。 */
16
32
  export interface SidebarBalanceProps {
17
33
  /** 侧栏是否展开(false = 56px 轨道)。 */
18
34
  wide: boolean;
19
35
  /** 词典函数。 */
20
36
  t: (key: LocaleKey) => string;
37
+ /**
38
+ * 配置槽的探测结果。旧宿主上 `plugins.bundle.config` 不存在,
39
+ * 浮层据此给一条英文 `[WARN]` 提示;探测本身不影响余额与刷新。
40
+ */
41
+ configSlotProbe: ConfigSlotProbe;
42
+ /**
43
+ * 「切到 Plugins 页」的可选入口:有它才渲染浮层右上角那个图标按钮。
44
+ * 宿主缺 layout 服务时整条链不存在,图标就不出现。
45
+ */
46
+ pluginsNavigation?: PluginsNavigation;
47
+ /**
48
+ * 「改用 X」:把后端实际给的那个币种写进设置作用域的 `displayCurrency`。
49
+ *
50
+ * 写入是否落盘由父代理读回 user 层判定(宿主拒绝写入时不抛错)。
51
+ * 本组件不做乐观更新:界面只跟设置快照走,没落盘就什么都不变,浮层的提示留着让用户重试。
52
+ * @param currency - 后端实际给出的币种代码。
53
+ * @returns 这次写入是否落进 user 层。
54
+ */
55
+ onSelectCurrency: (currency: string) => Promise<boolean>;
21
56
  /** 本组件消费的配置切片。 */
22
57
  config: {
23
58
  displayCurrency: string;
@@ -34,6 +69,11 @@ export interface SidebarBalanceProps {
34
69
  * 没配的币种给 `undefined`,由 `ringRatioOf` 退回按 severity 定性。
35
70
  */
36
71
  warnThresholdOf: (currency: string) => number | undefined;
72
+ /**
73
+ * 设置存储是否接受写入。false 时写入入口(浮层的「改用 X」)disabled ——
74
+ * 点了必被宿主拒绝,不该让用户以为能改。
75
+ */
76
+ writable: boolean;
37
77
  };
38
78
  }
39
79
  /**
@@ -41,4 +81,4 @@ export interface SidebarBalanceProps {
41
81
  * @param props - 展开态、词典与配置切片。
42
82
  * @returns 条目元素。
43
83
  */
44
- export declare function SidebarBalance({ wide, t, config }: SidebarBalanceProps): JSX.Element | null;
84
+ export declare function SidebarBalance({ wide, t, config, configSlotProbe, onSelectCurrency, pluginsNavigation, }: SidebarBalanceProps): JSX.Element | null;
package/lib/version.d.ts CHANGED
@@ -8,5 +8,5 @@
8
8
  /** 线上 schema 版本。响应里逐条回传,消费方据此判断兼容性。 */
9
9
  export declare const SCHEMA_VERSION = 1;
10
10
  /** 插件版本。与 `package.json` 的 `version` 一致(有测试兜底)。 */
11
- export declare const PLUGIN_VERSION = "1.1.0";
11
+ export declare const PLUGIN_VERSION = "2.0.0-alpha.1";
12
12
  //# sourceMappingURL=version.d.ts.map
package/lib/version.js CHANGED
@@ -8,5 +8,5 @@
8
8
  /** 线上 schema 版本。响应里逐条回传,消费方据此判断兼容性。 */
9
9
  export const SCHEMA_VERSION = 1;
10
10
  /** 插件版本。与 `package.json` 的 `version` 一致(有测试兜底)。 */
11
- export const PLUGIN_VERSION = '1.1.0';
11
+ export const PLUGIN_VERSION = '2.0.0-alpha.1';
12
12
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-ds-balance",
3
- "version": "1.1.0",
4
- "description": "DSH plugin: shows the DeepSeek account balance at the bottom of the sidebar, with a configuration card in settings.",
3
+ "version": "2.0.0-alpha.1",
4
+ "description": "DSH plugin: shows the DeepSeek account balance at the bottom of the sidebar, with a configuration card on the Plugins page.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "deepseek-harness",
@@ -43,7 +43,7 @@
43
43
  "LICENSE"
44
44
  ],
45
45
  "engines": {
46
- "dsh": "^0.1.6-alpha.1",
46
+ "dsh": "^0.1.6-alpha.2",
47
47
  "node": ">=20"
48
48
  },
49
49
  "dsh": {
@@ -53,7 +53,7 @@
53
53
  "client": {
54
54
  "platform": "web",
55
55
  "inject": [
56
- "@deepseek-ai/dsh-client-ui-settings-plugins"
56
+ "@deepseek-ai/dsh-client-ui-plugin-manager"
57
57
  ]
58
58
  }
59
59
  },
@@ -63,15 +63,16 @@
63
63
  "test": "npm run build && vitest run",
64
64
  "test:watch": "vitest",
65
65
  "test:contract": "vitest run --config vitest.contract.config.ts",
66
- "check:release": "node scripts/check-release.mjs"
66
+ "check:release": "node scripts/check-release.mjs",
67
+ "check:declaration": "node scripts/check-declaration.mjs"
67
68
  },
68
69
  "peerDependencies": {
69
70
  "@deepseek-ai/cordis": "^4.0.2",
70
71
  "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
71
72
  "@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
72
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
73
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.6-alpha.2",
74
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.2",
73
75
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
74
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.6-alpha.1",
75
76
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
76
77
  "@deepseek-ai/dsh-credentials": "^0.1.6-alpha.1",
77
78
  "@deepseek-ai/dsh-home-paths": "^0.1.6-alpha.1",
@@ -85,10 +86,10 @@
85
86
  "@deepseek-ai/cordis": "^4.0.2",
86
87
  "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
87
88
  "@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
88
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
89
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.6-alpha.2",
90
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.2",
89
91
  "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1",
90
92
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
91
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.6-alpha.1",
92
93
  "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.6-alpha.1",
93
94
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
94
95
  "@deepseek-ai/dsh-credentials": "^0.1.6-alpha.1",