@mars-sea/dsh-commandcode-provider 0.11.1 → 0.11.2

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
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.2] - 2026-09-15
10
+
11
+ ### Added
12
+ - **A pre-release engine smoke test** (`npm run test:engine`, [#43](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/43)). It installs the newest release `package.json` declares compatible, stages this checkout's published surface in a scratch tree whose peers ARE that engine's, and imports the bundle there — the only check that can see a bundle which cannot link against the engine it will run on, because a `link:`-installed profile resolves the plugin's peers from the plugin's own `node_modules` instead. It also audits every static named import, the client bundle's `require()` calls, and the request-image policy the engine exposes (including that `LlmError` carries the offload count back).
13
+ - **Session images are now priced the way the upstream that serves them charges for them.** The plugin declares request-image pricing to the harness's token meter, so an image occurrence is charged its family's published visual-token count — Anthropic's one-token-per-750-pixels rule, OpenAI's 512-px tiles, Gemini's 768-px crops, DeepSeek's token grid — read off the model's catalog id, with the most expensive of those rules as the conservative estimate for the families whose accounting we cannot tell apart (Qwen, MiniMax, xAI, moonshot and friends). Without it the meter fell back to a structural estimate that charged a handful of tokens for a full screenshot, so an image-heavy session under-reported the context it was carrying and could walk into its own context window. Offloaded images and text-only routes are priced as the placeholder text the model actually receives. These are context estimates, not invoices: a completed request's own usage stays authoritative.
14
+
15
+
16
+ ### Changed
17
+ - **The declared Harness range now admits every release the manifest supports.** A caret on a prerelease (`^0.1.2-rc.1`) can only ever resolve to `0.1.2-rc.1`, because semver admits a prerelease only inside the same `major.minor.patch` tuple — so a fresh generation installed a second, older copy of the Harness beside the running engine instead of pairing with it. The peer range is now an exact-version disjunction covering `0.1.2-rc.1` through `0.1.6-alpha.1`, repeated verbatim in `dsh.compatibility.dsh` and `engines.dsh`, and `dsh.compatibility.dshReleases` records `0.1.6-alpha.1` as compatible so the catalog keeps listing the plugin for the current release.
18
+ - **Request images now travel as request versions instead of the stored originals.** An attachment is kept normalized at the size admission accepted; every provider request now asks the attachment service for the version encoded to this route's target — a 1568-px long edge (Anthropic's ceiling, inside the high-detail band of the OpenAI and Gemini tile schemes) and at most 1 MiB per image — and that version is cached by the service and reused across occurrences. Measured on a 3600x2400 screenshot: the harness-normalized original was 2508x1672 at 2.4 MiB, and what reached the wire was 1568x1045 at 0.7 MiB, a 3.4x cut in the base64 that every subsequent request in the session re-sends — which is what pushes a long vision session toward the gateway's undocumented ~50 MB body cap. The request-image budget still accounts the stored size, so the safety margin it computes is unchanged (and conservative).
19
+ - **The plans & quota panel now follows the harness language.** Its copy was English on every harness by construction; both the sidebar card and the dashboard now read in the active language (`panel.commandcode` is a registered locale namespace binding the two panel slots' `t` seat, and every label, state word and the card's tooltip is composed through it). Switching the harness language re-renders the panel immediately. The composer session-cost estimate remains English by construction.
20
+
21
+
22
+ ### Fixed
23
+ - **The plugin loads on dsh `0.1.6-alpha.1` again** ([#43](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/43)). dsh-llm 0.1.6 removed `offloadRequestImagesWithPolicy`, which the adapter imported by name, so the built bundle failed at ESM link time and the host half never instantiated: no `commandcode` route, no settings page, no panel — and 0.1.6 reports an optional plugin's failure far more quietly than 0.1.5 did. The request-image policy is now reached through a namespace import and selected at runtime, so one build serves both engine generations. On 0.1.2–0.1.5 nothing changes: the adapter still projects its own history under the same 32 MiB / 60-image budget. On 0.1.6 the offload set belongs to the session surface: the adapter renders the surface's durable `offloaded` marks as placeholder text, and an over-budget history fails with `IMAGE_OFFLOAD_REQUIRED` + the count to offload, which the default `dsh-compaction-image-offload` plugin records as one `image/offload` event before retrying. An omission therefore survives session restore and fork, and the token meter stops counting evicted images as context. A 413 asks for the stricter rung's shortfall through the same channel; when nothing is left to offload, the existing bilingual request-size diagnosis stands.
24
+ - **The plans & quota dashboard can be closed again** ([#41](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/41)). Opening it from the sidebar card replaces the conversation in the center column, and there was no way back: the card only re-selected the same panel, so the session stayed unreachable until the whole page was reloaded. The dashboard header now carries a **×** button (its tooltip and accessible name say "Back to the conversation") that returns to the conversation (`ctx.layout.selectPanel(null)`) without changing the current session; on a layout whose panel selection predates that `null` form it falls back to ui-conversation's reserved `main` seat.
25
+
26
+ ### Removed
27
+ - **The client-side image-gate error rewrite, which could never fire.** The plugin used to wrap the legacy `connection.api.sessions` face so a `model-unavailable` "does not accept image input" rejection came back friendlier. That face is gone (model selection moved to the remote in 0.1.2), and the rejection it looked for is not the one this harness raises: the real guard runs on the SEND path as `session/attachment-invalid` with `reason: MODEL_DOES_NOT_SUPPORT_IMAGES`, and the conversation UI already renders it in the active language ("当前模型不支持图片,请切换支持图片的模型" / "The current model does not support images; switch to a model that does"). So the wrapper, its tests and its two dictionary strings are gone rather than retargeted — a second copy of a message the harness already localizes would only be able to drift from it.
28
+
9
29
  ## [0.11.1] - 2026-09-15
10
30
 
11
31
  ### Changed
package/README.md CHANGED
@@ -27,7 +27,7 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
27
27
  - **Plan-aware picker** — models above your subscription tier are hidden by default (toggleable); an optional **Model allowlist** keeps only your favorites in the picker.
28
28
  - **Reasoning-effort support** — models with selectable reasoning effort levels expose them in the picker.
29
29
  - **Image input** — Vision-capable models accept images.
30
- - **Plans & quota panel** — an optional Command Code card at the bottom of the sidebar (directly above Settings) shows the serving account's plan and its 5-hour and weekly windows; clicking it opens a dashboard with the billing period, both windows as progress bars with reset times, monthly credit consumption, and the purchased/free balances. **Off by default** — turn on *"Show the quota card in the sidebar"* under **Settings → Command Code → Advanced** to show it, and the same switch hides it again (a hidden card draws nothing at all and runs no background usage poll); the change lands as soon as you save. Needs dsh 0.1.5 (rc.1) or newer, and reads in English on every harness language.
30
+ - **Plans & quota panel** — an optional Command Code card at the bottom of the sidebar (directly above Settings) shows the serving account's plan and its 5-hour and weekly windows; clicking it opens a dashboard with the billing period, both windows as progress bars with reset times, monthly credit consumption, and the purchased/free balances. The dashboard's **×** button hands the center column back to your conversation (the current session is untouched). **Off by default** — turn on *"Show the quota card in the sidebar"* under **Settings → Command Code → Advanced** to show it, and the same switch hides it again (a hidden card draws nothing at all and runs no background usage poll); the change lands as soon as you save. The panel follows your harness language (中文 / English) and needs dsh 0.1.5 (rc.1) or newer.
31
31
  - **Session cost estimate** — published-rate estimates (`≈`) beside the composer token counter and in its usage dialog, using each request's model, request time and context tier from durable session history. Model switches and viewing the session later do not reprice earlier requests. Mixed-provider or unpriced usage shows a labeled subtotal (`≥`); missing history or wholly unpriceable usage stays hidden. These are estimates from the installed price snapshot, not provider invoices. Also English on every harness language.
32
32
  - **Web search** — the dsh `web_search` tool is backed by the Command Code Provider API (`/alpha/web-search`) with the same key/endpoint as chat, so no separate search key or base URL is needed. See [Web search](#web-search).
33
33
 
package/README.zh-CN.md CHANGED
@@ -27,7 +27,7 @@
27
27
  - **按套餐过滤**:默认隐藏超出订阅套餐的模型,可一键关闭;「模型白名单」可进一步只保留常用模型。
28
28
  - **推理强度支持**:支持推理强度的模型可在选择器中选择档位。
29
29
  - **图片输入**:Vision 模型支持发送图片。
30
- - **套餐与配额面板**:可选的 Command Code 卡片位于侧边栏底部(Settings 正上方),显示当前服务账号的套餐与 5 小时 / 每周两个配额窗口;点击后在中间栏打开面板,包含计费周期、两个窗口的进度条与重置时间、月度额度消耗,以及已购买 / 赠送余额。**默认关闭**——在 **设置 → Command Code → 高级设置** 中打开「在侧边栏显示额度卡片」即可显示,同一开关也能随时隐藏(隐藏时左侧不渲染任何内容,也不会为其后台刷新用量)。需要 dsh 0.1.5(rc.1)或更高版本;无论 Harness 语言如何,该面板固定为英文。
30
+ - **套餐与配额面板**:可选的 Command Code 卡片位于侧边栏底部(Settings 正上方),显示当前服务账号的套餐与 5 小时 / 每周两个配额窗口;点击后在中间栏打开面板,包含计费周期、两个窗口的进度条与重置时间、月度额度消耗,以及已购买 / 赠送余额。面板右上角的 **×** 按钮可随时把中间栏交还给会话(不会切换当前会话)。**默认关闭**——在 **设置 → Command Code → 高级设置** 中打开「在侧边栏显示额度卡片」即可显示,同一开关也能随时隐藏(隐藏时左侧不渲染任何内容,也不会为其后台刷新用量)。面板文案跟随 Harness 显示语言(中文 / English),需要 dsh 0.1.5(rc.1)或更高版本。
31
31
  - **会话费用估算**:在输入框下方的 token 计数旁及用量对话框中显示估算金额(`≈`),根据持久化历史中每次请求的模型、请求时间和上下文阶梯分别计价。切换模型或稍后查看不会重新定价之前的请求。混合供应商或缺少费率时显示已定价部分的小计(`≥`);缺少历史事实或完全无法定价时不显示金额。结果基于插件内的价格快照,不等同于供应商账单。同样固定为英文。
32
32
  - **联网搜索**:dsh 的 `web_search` 工具由 Command Code Provider API(`/alpha/web-search`)承载,复用聊天同一个 key 与端点,无需单独配置搜索 key 或地址。详见[联网搜索](#联网搜索)。
33
33
 
package/lib/client.js CHANGED
@@ -36,150 +36,6 @@ window.__ModuleLoader__.load({
36
36
  };
37
37
  }
38
38
  //#endregion
39
- //#region src/command-locales.ts
40
- const commandcodeCommand = {
41
- zh: {
42
- title: "📊 Command Code 用量{account}",
43
- accountTitle: "📊 {label}{badges}",
44
- accountSeparator: "────────────────────",
45
- activeBadge: " ✅ 当前使用",
46
- invalidCredentialBadge: " ⛔ 密钥无效",
47
- cooldownBadge: " ⏳ 限额冷却中,重置 {when}",
48
- rateLimitBadge: " ⏳ 已达限额(等待窗口探测)",
49
- unconfigured: " (未配置 API 密钥)",
50
- blockedInvalidKey: "⛔ API 密钥无效或已过期 — 服务端拒绝了全部请求(401),请检查该账户的密钥配置",
51
- blockedServiceUnavailable: "⚠️ Command Code 服务暂时不可用(5xx),稍后重试",
52
- blockedNetwork: "⚠️ 无法连接 Command Code 服务 — 请检查网络或 API 地址",
53
- planLine: " 📦 套餐 {name}{status}{period}",
54
- planPeriodSuffix: " · 账期截止 {date}",
55
- usageHeader: "── 请求 ──────────────────────────────",
56
- requestsLine: " 💬 请求 {n} 次 / 失败 {f} 成功率 {r}%",
57
- costLine: " 💰 花费 {money} ({credits} credits)",
58
- tokensLine: " 🔤 Token {in} 入 / {out} 出",
59
- creditsHeader: "── 信用 ──────────────────────────────",
60
- monthlyLine: " 💳 月额度 {monthly} (已购 {purchased} / 赠送 {free})",
61
- barLine: " └ {bar} {pct}%",
62
- windowsHeader: "── 窗口用量 ──────────────────────────",
63
- fiveHourLine: " ⏱ 5 小时 {used} / {cap}{warn}",
64
- weeklyLine: " 📅 每周 {used} / {cap}{warn}",
65
- windowBarLine: " └ {bar} 重置 {when}",
66
- exceededWarning: " ⚠️ 超限!",
67
- resetSuffix: "重置 {when}",
68
- partialFailures: "⚠️ 部分端点失败: {list}",
69
- noData: "(无数据 — 请检查 API 密钥)",
70
- errorText: "获取 Command Code 用量失败:{message}",
71
- imageGate: "当前会话已包含图片,而模型 {model} 不支持图片输入;请选择支持图片的模型,或先移除会话中的图片。"
72
- },
73
- en: {
74
- title: "📊 Command Code usage{account}",
75
- accountTitle: "📊 {label}{badges}",
76
- accountSeparator: "────────────────────",
77
- activeBadge: " ✅ active",
78
- invalidCredentialBadge: " ⛔ invalid key",
79
- cooldownBadge: " ⏳ cooling down, resets {when}",
80
- rateLimitBadge: " ⏳ rate-limited (waiting for window probe)",
81
- unconfigured: " (no API key configured)",
82
- blockedInvalidKey: "⛔ API key invalid or expired — the server rejected every request (401); check the key configured for this account",
83
- blockedServiceUnavailable: "⚠️ Command Code service temporarily unavailable (5xx); try again later",
84
- blockedNetwork: "⚠️ could not reach the Command Code service — check your network or the API base setting",
85
- planLine: " 📦 Plan {name}{status}{period}",
86
- planPeriodSuffix: " · period ends {date}",
87
- usageHeader: "── Requests ──────────────────────────",
88
- requestsLine: " 💬 Requests {n} / failed {f} success rate {r}%",
89
- costLine: " 💰 Spend {money} ({credits} credits)",
90
- tokensLine: " 🔤 Tokens {in} in / {out} out",
91
- creditsHeader: "── Credits ───────────────────────────",
92
- monthlyLine: " 💳 Monthly {monthly} (purchased {purchased} / free {free})",
93
- barLine: " └ {bar} {pct}%",
94
- windowsHeader: "── Window usage ──────────────────────",
95
- fiveHourLine: " ⏱ 5-hour {used} / {cap}{warn}",
96
- weeklyLine: " 📅 Weekly {used} / {cap}{warn}",
97
- windowBarLine: " └ {bar} resets {when}",
98
- exceededWarning: " ⚠️ exceeded!",
99
- resetSuffix: "resets {when}",
100
- partialFailures: "⚠️ some endpoints failed: {list}",
101
- noData: "(no data — check your API key)",
102
- errorText: "Could not fetch Command Code usage: {message}",
103
- imageGate: "This session already contains images, and model {model} does not accept image input; please select an image-capable model, or remove the images from the session first."
104
- }
105
- };
106
- //#endregion
107
- //#region src/client/sessions.ts
108
- /**
109
- * Friendly-error wrapper for the harness's image-session gate.
110
- *
111
- * The host rejects switching to a text-only model while the session already
112
- * contains images with a `model-unavailable` error
113
- * (`dsh-host-apiproxy`'s `session.selectModel` handler). That rejection is
114
- * intentional and cannot be relaxed from the plugin side — the adapter's
115
- * `inputModalities` is exactly what makes the guard work. What we CAN do is
116
- * make the error message friendlier: wrap the shared
117
- * `connection.api.sessions.selectModel` face so a `model-unavailable`
118
- * rejection shows a clear, actionable hint (with the requested model name)
119
- * instead of the raw English harness message.
120
- *
121
- * The wrapper is deliberately narrow: only the `model-unavailable` code is
122
- * rewritten, only when the message matches the image-session gate, and only
123
- * the message text changes — the error code and details pass through
124
- * untouched so any caller that switches on `error.code` keeps working.
125
- *
126
- * The wire types are spelled structurally here (not imported from
127
- * `@deepseek-ai/dsh-host-apiproxy`) so this client bundle does not drag an
128
- * extra peer dependency into the package; the shapes are stable and the
129
- * client build inlines them anyway.
130
- *
131
- * The wrapper takes a `getLocale` thunk because it is reached from a
132
- * non-React path that has no `t` in scope; the supplied thunk reads the
133
- * active locale at call time (typically `() => ctx.locale.getLocale().active`
134
- * in the client entry), and the message template lives in the shared
135
- * `commandcodeCommand` dictionary used by the Host-side `/commandcode`
136
- * command — the same bilingual surface serves both.
137
- *
138
- * This module is deliberately free of React and other client-platform
139
- * imports so the node test runner can exercise it directly.
140
- */
141
- /** Whether a selectModel rejection is the harness's image-session gate. */
142
- function isImageSessionRejection(result) {
143
- return !result.result.ok && result.result.error.code === "model-unavailable" && result.result.error.message.includes("does not accept image input");
144
- }
145
- /** Wrap the shared sessions API so selectModel failures read friendlier. */
146
- function withFriendlyImageError(sessions, getLocale) {
147
- const selectModel = sessions.selectModel.bind(sessions);
148
- return {
149
- ...sessions,
150
- selectModel: async (payload, signal) => {
151
- const result = await selectModel(payload, signal);
152
- if (!isImageSessionRejection(result)) return result;
153
- const model = result.result.error.details?.model ?? payload.model;
154
- const template = commandcodeCommand[getLocale()].imageGate ?? commandcodeCommand.en.imageGate;
155
- return {
156
- ...result,
157
- result: {
158
- ...result.result,
159
- error: {
160
- ...result.result.error,
161
- message: template.replace("{model}", model)
162
- }
163
- }
164
- };
165
- }
166
- };
167
- }
168
- /**
169
- * Install the friendly-error wrapper when a legacy sessions face is present.
170
- *
171
- * @returns whether the wrapper was installed. The rewrite is only UX polish;
172
- * a 0.1.2 connection has no `api.sessions`, and its absence must never block
173
- * the plugin from mounting its settings, credential, or usage surfaces.
174
- */
175
- function installFriendlyImageError(connection, getLocale) {
176
- const api = connection.api;
177
- const sessions = api?.sessions;
178
- if (api === void 0 || sessions === void 0 || typeof sessions.selectModel !== "function") return false;
179
- api.sessions = withFriendlyImageError(sessions, getLocale);
180
- return true;
181
- }
182
- //#endregion
183
39
  //#region src/client/settings.ts
184
40
  /**
185
41
  * Browser controller for the "Command Code" settings page.
@@ -2294,7 +2150,7 @@ window.__ModuleLoader__.load({
2294
2150
  }
2295
2151
  //#endregion
2296
2152
  //#region package.json
2297
- var version = "0.11.1";
2153
+ var version = "0.11.2";
2298
2154
  var repository = {
2299
2155
  "type": "git",
2300
2156
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -3981,12 +3837,68 @@ window.__ModuleLoader__.load({
3981
3837
  }
3982
3838
  //#endregion
3983
3839
  //#region src/client/panel-copy.ts
3984
- /** The panel's literal string table. */
3985
- const PANEL_COPY = {
3840
+ /** The locale namespace both panel slots bind their `t` seat to. */
3841
+ const PANEL_LOCALE_NS = "panel.commandcode";
3842
+ /** The panel's Simplified Chinese string table (the key-set source of truth). */
3843
+ const PANEL_COPY_ZH = {
3844
+ nav: "Command Code",
3845
+ subtitle: "套餐、额度与配额窗口",
3846
+ refresh: "刷新",
3847
+ refreshing: "刷新中…",
3848
+ close: "关闭",
3849
+ closeHint: "返回会话",
3850
+ loading: "正在获取账户用量…",
3851
+ noKey: "尚未配置 API 密钥",
3852
+ noKeyHint: "在 设置 → Command Code 中粘贴密钥或登录后,点击刷新。",
3853
+ plan: "套餐",
3854
+ credits: "额度",
3855
+ limits: "配额窗口",
3856
+ usage: "用量",
3857
+ monthly: "月额度",
3858
+ monthlyLimit: "月额度上限",
3859
+ monthlyUsed: "本月已用",
3860
+ remaining: "剩余",
3861
+ purchased: "已购",
3862
+ free: "赠送",
3863
+ fiveHour: "5 小时窗口",
3864
+ weekly: "每周窗口",
3865
+ fiveHourShort: "5 小时",
3866
+ weeklyShort: "每周",
3867
+ windowUnlimited: "不限",
3868
+ exceeded: "已超限",
3869
+ exhausted: "已用尽",
3870
+ resets: "重置于",
3871
+ requests: "请求",
3872
+ failed: "失败",
3873
+ successRate: "成功率",
3874
+ spend: "花费",
3875
+ tokens: "Token",
3876
+ tokensIn: "入",
3877
+ tokensOut: "出",
3878
+ periodEnds: "账期截止",
3879
+ updated: "更新于",
3880
+ partial: "部分端点数据不可用",
3881
+ active: "当前使用",
3882
+ coolingDown: "限额冷却中",
3883
+ invalidKey: "密钥无效",
3884
+ unconfigured: "未配置",
3885
+ unavailable: "无数据",
3886
+ errorInvalidKey: "API 密钥无效或已过期",
3887
+ errorInvalidKeyHint: "服务端拒绝了全部请求(401)。请检查该账户的密钥,或到 commandcode.ai 控制台重新生成。",
3888
+ errorServiceUnavailable: "Command Code 服务暂时不可用",
3889
+ errorServiceUnavailableHint: "服务端返回错误(5xx),请稍后点击刷新重试。",
3890
+ errorNetwork: "无法连接 Command Code 服务",
3891
+ errorNetworkHint: "所有请求都没有到达服务端。请检查网络连接或 API 地址设置。",
3892
+ errorGeneric: "用量获取失败"
3893
+ };
3894
+ /** The panel's English string table (must mirror {@link PANEL_COPY_ZH}). */
3895
+ const PANEL_COPY_EN = {
3986
3896
  nav: "Command Code",
3987
3897
  subtitle: "Plans, credits and quota windows",
3988
3898
  refresh: "Refresh",
3989
3899
  refreshing: "Refreshing…",
3900
+ close: "Close",
3901
+ closeHint: "Back to the conversation",
3990
3902
  loading: "Loading account usage…",
3991
3903
  noKey: "No API key configured",
3992
3904
  noKeyHint: "Paste a key — or sign in — under Settings → Command Code, then refresh.",
@@ -4031,9 +3943,15 @@ window.__ModuleLoader__.load({
4031
3943
  errorNetworkHint: "No request reached the server. Check your network connection or the API base setting.",
4032
3944
  errorGeneric: "Could not fetch account usage"
4033
3945
  };
4034
- /** Look up one panel string. The fallback key keeps a bad call visible, never blank. */
4035
- function panelText(key) {
4036
- return PANEL_COPY[key] ?? key;
3946
+ /** Every panel key, in the English table's declaration order. */
3947
+ const PANEL_KEYS = Object.keys(PANEL_COPY_EN);
3948
+ /**
3949
+ * The English translator. Used as the fallback when no locale seat is
3950
+ * available (a defensive path: every supported engine supplies one) and as the
3951
+ * default in tests that only assert the projection's figures.
3952
+ */
3953
+ function panelTextEN(key) {
3954
+ return PANEL_COPY_EN[key] ?? key;
4037
3955
  }
4038
3956
  //#endregion
4039
3957
  //#region src/client/panel.ts
@@ -4131,7 +4049,7 @@ window.__ModuleLoader__.load({
4131
4049
  };
4132
4050
  }
4133
4051
  /** Build one account's view. */
4134
- function accountView(entry) {
4052
+ function accountView(entry, t) {
4135
4053
  const { report } = entry;
4136
4054
  const account = report.account;
4137
4055
  const plan = report.plan;
@@ -4142,7 +4060,7 @@ window.__ModuleLoader__.load({
4142
4060
  stats.push({
4143
4061
  label: "requests",
4144
4062
  value: String(usage.completedCount),
4145
- sub: `${usage.failedCount} ${panelText("failed")}`
4063
+ sub: `${usage.failedCount} ${t("failed")}`
4146
4064
  });
4147
4065
  stats.push({
4148
4066
  label: "successRate",
@@ -4157,7 +4075,7 @@ window.__ModuleLoader__.load({
4157
4075
  stats.push({
4158
4076
  label: "tokens",
4159
4077
  value: formatTokensCompact(usage.totalTokensIn + usage.totalTokensOut),
4160
- sub: `${formatTokensCompact(usage.totalTokensIn)} ${panelText("tokensIn")} / ${formatTokensCompact(usage.totalTokensOut)} ${panelText("tokensOut")}`
4078
+ sub: `${formatTokensCompact(usage.totalTokensIn)} ${t("tokensIn")} / ${formatTokensCompact(usage.totalTokensOut)} ${t("tokensOut")}`
4161
4079
  });
4162
4080
  }
4163
4081
  const windows = [];
@@ -4208,15 +4126,14 @@ window.__ModuleLoader__.load({
4208
4126
  };
4209
4127
  }
4210
4128
  /**
4211
- * Every panel string, resolved once per projection. One object with all keys
4212
- * (rather than per-field lookups in the components) keeps the copy table and
4213
- * the render sites in lockstep: a key cannot be read from `text` unless
4214
- * {@link PANEL_COPY} declares it.
4129
+ * Every panel string, resolved once per projection through the translator.
4130
+ * One object with all keys (rather than per-field lookups in the components)
4131
+ * keeps the copy table and the render sites in lockstep: a key cannot be read
4132
+ * from `text` unless {@link PANEL_KEYS} declares it.
4215
4133
  */
4216
- function panelStrings() {
4217
- const keys = Object.keys(PANEL_COPY);
4134
+ function panelStrings(t) {
4218
4135
  const out = {};
4219
- for (const key of keys) out[key] = panelText(key);
4136
+ for (const key of PANEL_KEYS) out[key] = t(key);
4220
4137
  return out;
4221
4138
  }
4222
4139
  /**
@@ -4228,6 +4145,7 @@ window.__ModuleLoader__.load({
4228
4145
  */
4229
4146
  function buildPanelView(input) {
4230
4147
  const { usage } = input;
4148
+ const t = input.t ?? panelTextEN;
4231
4149
  const hidden = new Set(input.removingIds ?? []);
4232
4150
  const seen = /* @__PURE__ */ new Set();
4233
4151
  const entries = (usage.report?.accounts ?? []).filter((entry) => {
@@ -4236,7 +4154,7 @@ window.__ModuleLoader__.load({
4236
4154
  seen.add(entry.id);
4237
4155
  return true;
4238
4156
  });
4239
- const accounts = entries.map(accountView);
4157
+ const accounts = entries.map((entry) => accountView(entry, t));
4240
4158
  const selectedEntry = entries.find((entry) => entry.active) ?? entries[0];
4241
4159
  const selectedView = accounts.find((view) => view.id === selectedEntry?.id);
4242
4160
  const footerBars = [];
@@ -4255,18 +4173,18 @@ window.__ModuleLoader__.load({
4255
4173
  const totalCost = selectedEntry?.report.usage?.totalCost;
4256
4174
  const cost = totalCost === void 0 ? "" : money(totalCost);
4257
4175
  let status = "";
4258
- if (entries.length > 0 && !entries.some((entry) => entry.configured)) status = panelText("unconfigured");
4259
- else if (selectedView?.mark !== void 0 && selectedView.mark !== "active") status = panelText(selectedView.mark);
4260
- else if (selectedEntry === void 0) status = panelText("unavailable");
4261
- const planName = selectedView !== void 0 && selectedView.planName !== "" ? selectedView.planName : panelText("nav");
4262
- const titleParts = planName === panelText("nav") ? [planName] : [panelText("nav"), planName];
4176
+ if (entries.length > 0 && !entries.some((entry) => entry.configured)) status = t("unconfigured");
4177
+ else if (selectedView?.mark !== void 0 && selectedView.mark !== "active") status = t(selectedView.mark);
4178
+ else if (selectedEntry === void 0) status = t("unavailable");
4179
+ const planName = selectedView !== void 0 && selectedView.planName !== "" ? selectedView.planName : t("nav");
4180
+ const titleParts = planName === t("nav") ? [planName] : [t("nav"), planName];
4263
4181
  for (const bar of footerBars) {
4264
4182
  const figures = bar.detail === "" ? "" : ` ${bar.detail}`;
4265
- titleParts.push(`${panelText(bar.label)}${figures} (${bar.percent})`);
4183
+ titleParts.push(`${t(bar.label)}${figures} (${bar.percent})`);
4266
4184
  }
4267
- if (cost !== "") titleParts.push(`${panelText("spend")} ${cost}`);
4185
+ if (cost !== "") titleParts.push(`${t("spend")} ${cost}`);
4268
4186
  return {
4269
- text: panelStrings(),
4187
+ text: panelStrings(t),
4270
4188
  planName,
4271
4189
  status,
4272
4190
  footerBars,
@@ -4352,11 +4270,11 @@ window.__ModuleLoader__.load({
4352
4270
  * the sidebar footer card and the dashboard it opens in the center column.
4353
4271
  *
4354
4272
  * Both render one {@link PanelView} projected by `./panel.ts` — no fact is
4355
- * derived here. Strings arrive as keys into `view.text`, and every one of them
4356
- * is English, because the view is built from `./panel-copy.ts` rather than the
4357
- * harness `ctx.locale` namespace (which follows the user's language and would
4358
- * render this panel in Chinese on a Chinese harness — the thing this surface
4359
- * exists to avoid).
4273
+ * derived here. Strings arrive already localized in `view.text`: the slots
4274
+ * declare the `panel.commandcode` locale namespace, so the renderer hands this
4275
+ * component a `t` seat that is passed into `buildPanelView` — the panel follows
4276
+ * the harness's active language (a language switch mints a new `t`, so the
4277
+ * surfaces re-render without an extra channel).
4360
4278
  *
4361
4279
  * The footer card is the panel's home: the sidebar shell renders it in the foot
4362
4280
  * area directly above the Settings seat, so each quota window's own spend and
@@ -4378,7 +4296,8 @@ window.__ModuleLoader__.load({
4378
4296
  return buildPanelView({
4379
4297
  usage,
4380
4298
  apiKeyConfigured: settings.anyAccountConfigured,
4381
- removingIds: settings.accountsRemoving
4299
+ removingIds: settings.accountsRemoving,
4300
+ t: props.t ?? panelTextEN
4382
4301
  });
4383
4302
  }
4384
4303
  /**
@@ -4704,6 +4623,18 @@ window.__ModuleLoader__.load({
4704
4623
  disabled: view.loading,
4705
4624
  onClick: () => props.refresh(),
4706
4625
  children: view.loading ? text("refreshing") : text("refresh")
4626
+ }),
4627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4628
+ variant: "ghost",
4629
+ size: "sm",
4630
+ className: "ccp-close",
4631
+ "aria-label": text("close"),
4632
+ title: text("closeHint"),
4633
+ onClick: () => props.close(),
4634
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4635
+ "aria-hidden": "true",
4636
+ children: "×"
4637
+ })
4707
4638
  })
4708
4639
  ]
4709
4640
  }),
@@ -5738,6 +5669,11 @@ window.__ModuleLoader__.load({
5738
5669
  .ccp-subtitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
5739
5670
  .ccp-spacer{flex:1}
5740
5671
  .ccp-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;font-variant-numeric:tabular-nums}
5672
+ /* The dashboard's exit: an icon-sized glyph button. The primitive's own
5673
+ geometry is kept (size="sm"); only the glyph is enlarged and optically
5674
+ centred, so it reads as a dismiss control rather than a text button. */
5675
+ .ccp-close{min-width:28px;justify-content:center;padding-left:0;padding-right:0}
5676
+ .ccp-close span{font-size:16px;line-height:1}
5741
5677
  .ccp-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
5742
5678
 
5743
5679
  /* Notices: the no-key guidance, a blocked report, and a stale-data error. */
@@ -6231,6 +6167,15 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6231
6167
  */
6232
6168
  const PANEL_ID = "commandcode-panel";
6233
6169
  /**
6170
+ * ui-conversation's reserved `main` key, used only as the exit fallback for a
6171
+ * layout whose `selectPanel` predates the `null` "show the Conversation"
6172
+ * selection (see `close` in {@link applyClientSurfaces}). Declared locally for
6173
+ * the same reason as {@link PANEL_ID}: ui-conversation is not a dependency of
6174
+ * this bundle, and the key is a published contract of the layout (its README
6175
+ * names `conversation` as reserved for the Conversation).
6176
+ */
6177
+ const CONVERSATION_PANEL_ID = "conversation";
6178
+ /**
6234
6179
  * The composer figure's entry id in `conversation.composer.dock`. Its own id,
6235
6180
  * not the shipped `stats` cell's: reusing `stats` would REPLACE the tokens /
6236
6181
  * cache-hit / throughput readout rather than inject into it, and that readout
@@ -6245,12 +6190,15 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6245
6190
  */
6246
6191
  function apply(ctx) {
6247
6192
  injectPageCss();
6248
- const connection = ctx.get("connection");
6249
- if (connection !== void 0) installFriendlyImageError(connection, () => ctx.locale.getLocale().active === "zh" ? "zh" : "en");
6250
6193
  ctx.effect(() => ctx.locale.register("settings.commandcode", {
6251
6194
  zh,
6252
6195
  en
6253
6196
  }), "dsh-commandcode-provider: page copy");
6197
+ ctx.effect(() => ctx.locale.register(PANEL_LOCALE_NS, {
6198
+ zh: PANEL_COPY_ZH,
6199
+ en: PANEL_COPY_EN
6200
+ }), "dsh-commandcode-provider: panel copy");
6201
+ const connection = ctx.get("connection");
6254
6202
  const legacyApi = adaptLegacyCredentials(connection?.api?.credentials);
6255
6203
  if (legacyApi !== void 0) {
6256
6204
  applyClientSurfaces(ctx, legacyApi, connection?.hostDescription);
@@ -6463,6 +6411,19 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6463
6411
  open: () => {
6464
6412
  const layout = ctx.get("layout");
6465
6413
  if (typeof layout?.selectPanel === "function") layout.selectPanel(PANEL_ID);
6414
+ },
6415
+ close: () => {
6416
+ const layout = ctx.get("layout");
6417
+ if (typeof layout?.selectPanel !== "function") return;
6418
+ try {
6419
+ layout.selectPanel(null);
6420
+ } catch {
6421
+ try {
6422
+ layout.selectPanel(CONVERSATION_PANEL_ID);
6423
+ } catch (error) {
6424
+ console.error("[dsh-commandcode-provider] could not close the plans & quota panel:", error);
6425
+ }
6426
+ }
6466
6427
  }
6467
6428
  });
6468
6429
  ctx.effect(() => injectPanelCss(), "dsh-commandcode-provider: panel styles");
@@ -6470,6 +6431,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6470
6431
  ctx.slots.inject("main", () => ctx.slots.register({
6471
6432
  name: "main",
6472
6433
  key: PANEL_ID,
6434
+ locale: PANEL_LOCALE_NS,
6473
6435
  inject: panelFace
6474
6436
  }, CommandCodePanel));
6475
6437
  } catch (error) {
@@ -6482,6 +6444,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6482
6444
  name: "sidebar.footer.action",
6483
6445
  id: PANEL_ID,
6484
6446
  order: 1,
6447
+ locale: PANEL_LOCALE_NS,
6485
6448
  inject: panelFace
6486
6449
  }, CommandCodeFooterEntry));
6487
6450
  } catch (error) {
@@ -6509,8 +6472,6 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6509
6472
  //#endregion
6510
6473
  exports.apply = apply;
6511
6474
  exports.inject = inject;
6512
- exports.isImageSessionRejection = isImageSessionRejection;
6513
- exports.withFriendlyImageError = withFriendlyImageError;
6514
6475
  return module.exports;
6515
6476
  }
6516
6477
  });