@mars-sea/dsh-commandcode-provider 0.2.1 → 0.2.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 +11 -0
- package/README.md +8 -5
- package/README.zh-CN.md +8 -5
- package/lib/client.js +666 -10
- package/lib/client.js.map +1 -1
- package/package.json +23 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.2] - 2026-08-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A dedicated "Command Code" settings page** (Settings → **Command Code**, a top-level nav entry at the same level as General / Models / Plugins) where you can configure the provider **entirely from the web UI**: an **API-key field** (write-only, stored through the dsh credentials service under the `COMMANDCODE_API_KEY` reference the plugin resolves, with a configured/unconfigured badge) plus the **API base URL**, **working directory**, and **request/stream timeouts** (written to the `llm-commandcode` settings namespace, effective on the next request — no restart). The browser half registers a `settings.section` entry (`id: commandcode`) from the bundle's client plugin; the controller is covered by new unit tests in `tests/settings.test.ts`.
|
|
12
|
+
- **The working-directory field is now genuinely optional in the settings page**: the page reads the Host process cwd (`host.describe().cwd`) and shows it as the field's placeholder, so a blank field visibly resolves to the process working directory — nothing to configure unless you want to pin a specific path. The page hints this in both languages.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The API key could not actually be configured from the Models page for this provider.** The Models page renders an unknown-adapter-family card for `commandcode` and disables its editor (submit is blocked when `layout === 'unknown'`), so the card's key field could not save. The new dedicated settings page is the working surface for the key (and the connection knobs); the Models card remains as a status/reflection of the provider.
|
|
17
|
+
|
|
7
18
|
## [0.2.1] - 2026-08-16
|
|
8
19
|
|
|
9
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -14,8 +14,9 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
|
|
|
14
14
|
|
|
15
15
|
- A **plugin bundle** installable into any dsh profile with `dsh plugin add` (npm package with a `dsh.bundle` layer).
|
|
16
16
|
- A **`commandcode` provider route** registered on the `llm` service, selectable in the model picker, with the **live model catalog** fetched from `GET {apiBase}/provider/v1/models` (cached at `~/.commandcode/models-cache.json`).
|
|
17
|
-
- A **
|
|
18
|
-
- **
|
|
17
|
+
- A **dedicated "Command Code" settings page** (Settings → **Command Code**, a top-level nav entry at the same level as General / Models / Plugins) with an **API-key field** plus the connection knobs (API base, working directory, request/stream timeouts). The key is stored through the dsh credentials service — the same seam the Models page uses — and connection fields land in the `llm-commandcode` settings section, so a change reaches the very next request with no restart.
|
|
18
|
+
- A **Models-page card** ("Command Code") that reflects the provider's presence (the Models page's generic card disables its editor for unknown adapter families, so configure the key on the dedicated page above).
|
|
19
|
+
- **API key resolution** in this order: `config.apiKey` → credential reference `apiKeyEnv` (the Command Code settings page writes it, default `COMMANDCODE_API_KEY`) → the launching environment → the official Command Code CLI auth file (`~/.commandcode/auth.json`, written by `command-code login`).
|
|
19
20
|
- **Reasoning-effort support** for the models Command Code's catalog marks as such (e.g. `claude-opus-5`, `gpt-5.5`, `deepseek/deepseek-v4-pro`, `google/gemini-3.7-flash`, …) via `KNOWN_EFFORTS`, matching the official command-code@1.26.0 bundled model table. Reasoning models without selectable effort levels (e.g. `MiniMaxAI/MiniMax-M3`, `moonshotai/Kimi-K3`, `moonshotai/Kimi-K2.5`) still think — Command Code drives their reasoning depth automatically, exactly like the official CLI.
|
|
20
21
|
- **Plan-tier annotation in the model picker**: every Command Code model is tagged with the minimum plan that includes it (`KNOWN_PLANS`, synced from the [official plan pages](https://commandcode.ai/docs/plans/go)) — **Go** (33 models), **GOAT** (+3), **Pro** (+14), or **Provider/Max** (+5: Claude Opus/Fable, Fugu Ultra). The picker's `description` leads with the plan label, e.g. *"Go · 50% off · Image · 1M"*, *"Pro · Image · 1M"*, so you know which plan a model needs before switching — no more 403 `MODEL_NOT_IN_PLAN` surprises. **The list itself is sorted by plan tier** (`compareByPlan()`): Go models first, then GOAT, Pro, Provider/Max, alphabetical within each tier — the models your plan can actually use lead the picker.
|
|
21
22
|
- **Deal and free-model annotations**: active discounts (`75% off`, `50% off`, `98% off`, `99% off`) and the `FREE` badge (Laguna S 2.1) show next to the plan tier (`KNOWN_DEALS`, synced from the [official pricing page](https://commandcode.ai/docs/resources/pricing-limits#deals)). **Expiry-aware**: each deal records its official end date and is hidden the moment it passes — an un-updated plugin never shows a lapsed discount as if it were live (only Gemini 3.7 Flash's 50% off is time-limited, through December 31, 2026; the rest are permanent).
|
|
@@ -31,7 +32,7 @@ npm i -g command-code@latest
|
|
|
31
32
|
cmd login # macOS/Linux; native Windows: cmdc login
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
`cmd login` opens a browser to authenticate; on success the key is written to `~/.commandcode/auth.json` — this plugin picks it up automatically (last-resort fallback). Alternatively create an API key in the browser ([Command Code Studio](https://commandcode.ai/studio/auth/cli)) and paste it into the
|
|
35
|
+
`cmd login` opens a browser to authenticate; on success the key is written to `~/.commandcode/auth.json` — this plugin picks it up automatically (last-resort fallback). Alternatively create an API key in the browser ([Command Code Studio](https://commandcode.ai/studio/auth/cli)) and paste it into **Settings → Command Code** (the dedicated page's API-key field), or `export COMMANDCODE_API_KEY="user_..."`.
|
|
35
36
|
|
|
36
37
|
## Install
|
|
37
38
|
|
|
@@ -131,7 +132,7 @@ Then restart the web app (`dsh web`, or restart the service). Verify the running
|
|
|
131
132
|
|
|
132
133
|
## Verify it works
|
|
133
134
|
|
|
134
|
-
After restart, in the web UI: **Settings → Models** shows a **Command Code** card; the model picker lists the live catalog under **commandcode** (54 models at the time of writing). Send a message with a model your plan includes — the default `deepseek/deepseek-v4-flash` works on entry-level plans; open-weight models (DeepSeek/Qwen/Kimi/MiniMax) generally do, while frontier models (Claude/GPT/Gemini/Grok) may require Pro/Max plans or on-demand usage (see FAQ).
|
|
135
|
+
After restart, in the web UI: **Settings → Command Code** shows the dedicated page — enter your API key there and click **Save** (the badge flips to 已配置/Configured once the Host holds it). **Settings → Models** shows a **Command Code** card; the model picker lists the live catalog under **commandcode** (54 models at the time of writing). Send a message with a model your plan includes — the default `deepseek/deepseek-v4-flash` works on entry-level plans; open-weight models (DeepSeek/Qwen/Kimi/MiniMax) generally do, while frontier models (Claude/GPT/Gemini/Grok) may require Pro/Max plans or on-demand usage (see FAQ).
|
|
135
136
|
|
|
136
137
|
## Usage dashboard
|
|
137
138
|
|
|
@@ -166,7 +167,9 @@ Each endpoint degrades independently: a temporary failure of one (e.g. the credi
|
|
|
166
167
|
|
|
167
168
|
## Configure
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
**Settings → Command Code** is the primary surface: an **API-key** field (stored in `$DSH_HOME/.credentials.yaml` via the credentials service; the field is write-only and reports whether a key is configured), plus **API base URL**, **working directory**, and the **request/stream timeout** fields — all written to the `llm-commandcode` section and applied to the next request. The model catalog is browsable without a key. The **working directory** is optional: leave it blank and the field's placeholder shows the process cwd it resolves to (it defaults to the dsh process's working directory, so there is nothing to configure unless you want to pin a specific path).
|
|
171
|
+
|
|
172
|
+
The same knobs live in the `llm-commandcode` section of `$DSH_HOME/settings.yaml` (overrides the bundle defaults per request, no restart needed):
|
|
170
173
|
|
|
171
174
|
```yaml
|
|
172
175
|
llm-commandcode:
|
package/README.zh-CN.md
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
- **插件包**:可通过 `dsh plugin add` 安装到任意 dsh 配置(npm 包,带 `dsh.bundle` 层)。
|
|
16
16
|
- **`commandcode` provider 路由**:注册在 `llm` 服务上,可在模型选择器中选择,并带 **实时模型目录**(从 `GET {apiBase}/provider/v1/models` 拉取,缓存于 `~/.commandcode/models-cache.json`)。
|
|
17
|
-
-
|
|
18
|
-
- **
|
|
17
|
+
- **专属"Command Code"设置页**(设置 → **Command Code**,与通用/模型/插件同级导航项),带 **API key 输入框** 以及连接参数(API 地址、工作目录、请求/流超时)。密钥通过 dsh 凭据服务存储(与 Models 页同一套机制);连接参数写入 `llm-commandcode` 设置段,改动对下一次请求即刻生效,无需重启。
|
|
18
|
+
- **Models 页面卡片**("Command Code"):反映该 provider 的存在(Models 页对未知适配器家族的通用卡片会禁用其编辑,因此请在专属设置页配置密钥)。
|
|
19
|
+
- **API key 解析顺序**:`config.apiKey` → 凭据引用 `apiKeyEnv`(Command Code 设置页写入,默认 `COMMANDCODE_API_KEY`)→ 启动环境变量 → 官方 Command Code CLI 认证文件(`~/.commandcode/auth.json`,由 `command-code login` 写入)。
|
|
19
20
|
- **推理强度(reasoning-effort)支持**:针对 Command Code 目录中标为推理模型的模型(如 `claude-opus-5`、`gpt-5.5`、`deepseek/deepseek-v4-pro`、`google/gemini-3.7-flash` 等),通过 `KNOWN_EFFORTS` 实现,与官方 command-code@1.26.0 内置模型表一致。没有可选推理档位但仍支持思考的模型(如 `MiniMaxAI/MiniMax-M3`、`moonshotai/Kimi-K3`、`moonshotai/Kimi-K2.5`)同样会思考——由 Command Code 自动控制推理深度,与官方 CLI 行为完全一致。
|
|
20
21
|
- **模型选择器标注套餐档位**:每个 Command Code 模型都标注了包含它的最低套餐(`KNOWN_PLANS`,与[官方套餐页](https://commandcode.ai/docs/plans/go)同步)——**Go**(33 个)、**GOAT**(+3)、**Pro**(+14)、**Provider/Max**(+5:Claude Opus/Fable、Fugu Ultra)。选择器的 `description` 以套餐标签开头,例如 *"Go · 50% off · Image · 1M"*、*"Pro · Image · 1M"*,切换前即可知道该模型需要什么套餐——不再等到第一次请求才收到 403 `MODEL_NOT_IN_PLAN`。**列表本身也按套餐排序**(`compareByPlan()`):Go 模型在最前,随后 GOAT、Pro、Provider/Max,档内按字母序——你当前套餐能用的模型总是排在列表最前面。
|
|
21
22
|
- **折扣与免费标注**:活动折扣(`75% off`、`50% off`、`98% off`、`99% off`)与 `FREE` 徽章(Laguna S 2.1)会显示在套餐旁(`KNOWN_DEALS`,与[官方定价页](https://commandcode.ai/docs/resources/pricing-limits#deals)同步)。**到期感知**:每个折扣记录官方结束日期,一旦过期立即隐藏——插件未更新时也不会把已失效的折扣当成仍在生效(只有 Gemini 3.7 Flash 的 50% off 限时,至 2026 年 12 月 31 日;其余为永久)。
|
|
@@ -31,7 +32,7 @@ npm i -g command-code@latest
|
|
|
31
32
|
cmd login # macOS/Linux;Windows 原生版:cmdc login
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
`cmd login` 会打开浏览器进行认证;成功后 key 写入 `~/.commandcode/auth.json`——本插件会自动读取(最后兜底)。也可以直接在浏览器创建 API key([Command Code Studio](https://commandcode.ai/studio/auth/cli))并粘贴到
|
|
35
|
+
`cmd login` 会打开浏览器进行认证;成功后 key 写入 `~/.commandcode/auth.json`——本插件会自动读取(最后兜底)。也可以直接在浏览器创建 API key([Command Code Studio](https://commandcode.ai/studio/auth/cli))并粘贴到 **设置 → Command Code** 专属页面的 API key 输入框,或者 `export COMMANDCODE_API_KEY="user_..."`。
|
|
35
36
|
|
|
36
37
|
## 安装
|
|
37
38
|
|
|
@@ -130,7 +131,7 @@ dsh web
|
|
|
130
131
|
|
|
131
132
|
## 验证是否生效
|
|
132
133
|
|
|
133
|
-
重启后,在 Web UI 中:**设置 → Models** 会显示 **Command Code** 卡片;模型选择器会在 **commandcode** 下列出实时目录(撰写本文时有 54 个模型)。发送一条消息,选择你套餐中包含的模型——默认的 `deepseek/deepseek-v4-flash` 适用于入门级套餐;开放权重模型(DeepSeek/Qwen/Kimi/MiniMax)通常都可用,而前沿模型(Claude/GPT/Gemini/Grok)可能需要 Pro/Max 套餐或按需计费(见 FAQ)。
|
|
134
|
+
重启后,在 Web UI 中:**设置 → Command Code** 显示专属页面——在那里填入 API key 并点击**保存**(徽章变为"已配置"即表示 Host 已存储)。**设置 → Models** 会显示 **Command Code** 卡片;模型选择器会在 **commandcode** 下列出实时目录(撰写本文时有 54 个模型)。发送一条消息,选择你套餐中包含的模型——默认的 `deepseek/deepseek-v4-flash` 适用于入门级套餐;开放权重模型(DeepSeek/Qwen/Kimi/MiniMax)通常都可用,而前沿模型(Claude/GPT/Gemini/Grok)可能需要 Pro/Max 套餐或按需计费(见 FAQ)。
|
|
134
135
|
|
|
135
136
|
## 用量面板
|
|
136
137
|
|
|
@@ -165,7 +166,9 @@ dsh web
|
|
|
165
166
|
|
|
166
167
|
## 配置
|
|
167
168
|
|
|
168
|
-
Command Code
|
|
169
|
+
**设置 → Command Code** 是主要配置入口:**API key** 输入框(通过凭据服务存储在 `$DSH_HOME/.credentials.yaml`,只写不回显,并显示是否已配置),以及 **API 地址**、**工作目录**、**请求/流超时**字段——全部写入 `llm-commandcode` 设置段,对下一次请求即刻生效。没有 key 也可以浏览模型目录。**工作目录可选**:留空即可——占位符会显示它实际使用的进程工作目录(默认就是 dsh 进程的 cwd,除非你想固定某个路径,否则无需配置)。
|
|
170
|
+
|
|
171
|
+
同一组选项也位于 `$DSH_HOME/settings.yaml` 的 `llm-commandcode` 一节(按请求覆盖 bundle 默认值,无需重启):
|
|
169
172
|
|
|
170
173
|
```yaml
|
|
171
174
|
llm-commandcode:
|
package/lib/client.js
CHANGED
|
@@ -4,7 +4,10 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
|
|
7
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/sessions.ts
|
|
8
11
|
/** Whether a selectModel rejection is the harness's image-session gate. */
|
|
9
12
|
function isImageSessionRejection(result) {
|
|
10
13
|
return !result.result.ok && result.result.error.code === "model-unavailable" && result.result.error.message.includes("does not accept image input");
|
|
@@ -31,20 +34,673 @@ window.__ModuleLoader__.load({
|
|
|
31
34
|
}
|
|
32
35
|
};
|
|
33
36
|
}
|
|
37
|
+
/** Install the wrapper on a connection's shared sessions face. */
|
|
38
|
+
function installFriendlyImageError(connection) {
|
|
39
|
+
connection.api.sessions = withFriendlyImageError(connection.api.sessions);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/client/settings.ts
|
|
43
|
+
/** The settings namespace the plugin registers (host half, src/index.ts). */
|
|
44
|
+
const COMMANDCODE_NS = "llm-commandcode";
|
|
45
|
+
/** Default credential reference the plugin resolves when none is named. */
|
|
46
|
+
const DEFAULT_API_KEY_REF = "COMMANDCODE_API_KEY";
|
|
47
|
+
/** A free-text field; an empty draft clears it. */
|
|
48
|
+
function textField(field) {
|
|
49
|
+
return {
|
|
50
|
+
field,
|
|
51
|
+
format: (value) => typeof value === "string" ? value : "",
|
|
52
|
+
parse: (text) => {
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
return trimmed === "" ? { kind: "clear" } : {
|
|
55
|
+
kind: "set",
|
|
56
|
+
value: trimmed
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** A whole-number field; an empty draft clears it, anything non-numeric blocks save. */
|
|
62
|
+
function numberField(field) {
|
|
63
|
+
return {
|
|
64
|
+
field,
|
|
65
|
+
format: (value) => typeof value === "number" ? String(value) : "",
|
|
66
|
+
parse: (text) => {
|
|
67
|
+
const trimmed = text.trim();
|
|
68
|
+
if (trimmed === "") return { kind: "clear" };
|
|
69
|
+
const parsed = Number(trimmed);
|
|
70
|
+
return Number.isFinite(parsed) ? {
|
|
71
|
+
kind: "set",
|
|
72
|
+
value: parsed
|
|
73
|
+
} : { kind: "invalid" };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** The fields this page edits inside the `llm-commandcode` namespace. */
|
|
78
|
+
const SECTION_FIELDS = [
|
|
79
|
+
textField("apiBase"),
|
|
80
|
+
textField("workingDir"),
|
|
81
|
+
numberField("requestTimeoutMs"),
|
|
82
|
+
numberField("streamIdleTimeoutMs")
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* Controller bridging the `llm-commandcode` scope and the credentials domain
|
|
86
|
+
* onto the page. Public API mirrors the harness's CardForm actions, so the
|
|
87
|
+
* component stays thin.
|
|
88
|
+
*/
|
|
89
|
+
var CommandCodeSettingsController = class {
|
|
90
|
+
scope;
|
|
91
|
+
api;
|
|
92
|
+
specs = new Map(SECTION_FIELDS.map((spec) => [spec.field, spec]));
|
|
93
|
+
staged = /* @__PURE__ */ new Map();
|
|
94
|
+
listeners = /* @__PURE__ */ new Set();
|
|
95
|
+
disposers = [];
|
|
96
|
+
disposed = false;
|
|
97
|
+
defaultWorkingDir;
|
|
98
|
+
credential = {
|
|
99
|
+
ref: DEFAULT_API_KEY_REF,
|
|
100
|
+
configured: false,
|
|
101
|
+
writable: true
|
|
102
|
+
};
|
|
103
|
+
saving = false;
|
|
104
|
+
failed = false;
|
|
105
|
+
/**
|
|
106
|
+
* @param scope - bound scope for the `llm-commandcode` namespace.
|
|
107
|
+
* @param api - credentials wire face.
|
|
108
|
+
* @param hostDescription - the Host-description observable whose `cwd` is
|
|
109
|
+
* shown as the placeholder a blank `workingDir` field resolves to.
|
|
110
|
+
*/
|
|
111
|
+
constructor(scope, api, hostDescription) {
|
|
112
|
+
this.scope = scope;
|
|
113
|
+
this.api = api;
|
|
114
|
+
this.disposers.push(scope.subscribe(() => {
|
|
115
|
+
this.recomputeCredentialRef();
|
|
116
|
+
this.publish();
|
|
117
|
+
}));
|
|
118
|
+
if (hostDescription !== void 0) {
|
|
119
|
+
this.defaultWorkingDir = hostDescription.getSnapshot()?.cwd;
|
|
120
|
+
this.disposers.push(hostDescription.subscribe(() => {
|
|
121
|
+
if (this.disposed) return;
|
|
122
|
+
const cwd = hostDescription.getSnapshot()?.cwd;
|
|
123
|
+
if (cwd !== this.defaultWorkingDir) {
|
|
124
|
+
this.defaultWorkingDir = cwd;
|
|
125
|
+
this.publish();
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
this.recomputeCredentialRef();
|
|
130
|
+
this.readCredential();
|
|
131
|
+
}
|
|
132
|
+
/** Release every subscription held on external sources. Idempotent. */
|
|
133
|
+
dispose() {
|
|
134
|
+
if (this.disposed) return;
|
|
135
|
+
this.disposed = true;
|
|
136
|
+
for (const dispose of this.disposers) dispose();
|
|
137
|
+
this.disposers.length = 0;
|
|
138
|
+
this.listeners.clear();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The credential reference the section names, or the provider default. A
|
|
142
|
+
* user who renamed `apiKeyEnv` in `settings.yaml` (or the composition
|
|
143
|
+
* config) gets a page that addresses the renamed ref instead of silently
|
|
144
|
+
* writing the default — mirroring the Models page's `refFor()`.
|
|
145
|
+
*/
|
|
146
|
+
recomputeCredentialRef() {
|
|
147
|
+
const snapshot = this.scope.getSnapshot();
|
|
148
|
+
const named = typeof snapshot.value?.apiKeyEnv === "string" && snapshot.value.apiKeyEnv.length > 0 ? snapshot.value.apiKeyEnv : DEFAULT_API_KEY_REF;
|
|
149
|
+
if (named === this.credential.ref) return;
|
|
150
|
+
this.credential = {
|
|
151
|
+
ref: named,
|
|
152
|
+
configured: false,
|
|
153
|
+
writable: true
|
|
154
|
+
};
|
|
155
|
+
this.readCredential();
|
|
156
|
+
}
|
|
157
|
+
/** Subscribe to state projections. @returns the disposer. */
|
|
158
|
+
subscribe(listener) {
|
|
159
|
+
this.listeners.add(listener);
|
|
160
|
+
return () => this.listeners.delete(listener);
|
|
161
|
+
}
|
|
162
|
+
/** Build the current page state face. */
|
|
163
|
+
state() {
|
|
164
|
+
const snapshot = this.scope.getSnapshot();
|
|
165
|
+
const plan = this.plan();
|
|
166
|
+
return {
|
|
167
|
+
available: snapshot.status === "ready",
|
|
168
|
+
writable: snapshot.writable,
|
|
169
|
+
apiKeyConfigured: this.credential.configured,
|
|
170
|
+
apiKeyWritable: this.credential.writable,
|
|
171
|
+
apiKey: {
|
|
172
|
+
text: this.staged.get("apiKey")?.text ?? "",
|
|
173
|
+
clear: false,
|
|
174
|
+
overridden: false,
|
|
175
|
+
invalid: false
|
|
176
|
+
},
|
|
177
|
+
apiBase: this.field("apiBase"),
|
|
178
|
+
workingDir: this.field("workingDir"),
|
|
179
|
+
defaultWorkingDir: this.defaultWorkingDir,
|
|
180
|
+
requestTimeoutMs: this.field("requestTimeoutMs"),
|
|
181
|
+
streamIdleTimeoutMs: this.field("streamIdleTimeoutMs"),
|
|
182
|
+
dirty: plan.length > 0,
|
|
183
|
+
invalid: plan.some((item) => item.run === void 0),
|
|
184
|
+
saving: this.saving,
|
|
185
|
+
failed: this.failed
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Stage one field's draft text. */
|
|
189
|
+
edit(field, text) {
|
|
190
|
+
this.staged.set(field, {
|
|
191
|
+
text,
|
|
192
|
+
clear: false
|
|
193
|
+
});
|
|
194
|
+
this.failed = false;
|
|
195
|
+
this.publish();
|
|
196
|
+
}
|
|
197
|
+
/** Reset one section field to its inherited (composition) value. */
|
|
198
|
+
resetField(field) {
|
|
199
|
+
if (field === "apiKey") {
|
|
200
|
+
this.staged.delete("apiKey");
|
|
201
|
+
this.failed = false;
|
|
202
|
+
this.publish();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const spec = this.spec(field);
|
|
206
|
+
this.staged.set(field, {
|
|
207
|
+
text: spec.format(this.baseValue(field)),
|
|
208
|
+
clear: true
|
|
209
|
+
});
|
|
210
|
+
this.failed = false;
|
|
211
|
+
this.publish();
|
|
212
|
+
}
|
|
213
|
+
/** Discard every staged edit. */
|
|
214
|
+
discard() {
|
|
215
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
216
|
+
this.staged.clear();
|
|
217
|
+
this.failed = false;
|
|
218
|
+
this.publish();
|
|
219
|
+
}
|
|
220
|
+
/** Write every staged edit, then re-read the Host's accepted state. */
|
|
221
|
+
async save() {
|
|
222
|
+
const plan = this.plan();
|
|
223
|
+
if (plan.length === 0 || this.saving) return;
|
|
224
|
+
const runs = [];
|
|
225
|
+
for (const item of plan) {
|
|
226
|
+
if (item.run === void 0) return;
|
|
227
|
+
runs.push(item.run);
|
|
228
|
+
}
|
|
229
|
+
this.saving = true;
|
|
230
|
+
this.failed = false;
|
|
231
|
+
this.publish();
|
|
232
|
+
let landed = true;
|
|
233
|
+
for (const run of runs) landed = await run() && landed;
|
|
234
|
+
this.saving = false;
|
|
235
|
+
this.failed = !landed;
|
|
236
|
+
if (landed) this.staged.clear();
|
|
237
|
+
this.publish();
|
|
238
|
+
}
|
|
239
|
+
spec(field) {
|
|
240
|
+
const spec = this.specs.get(field);
|
|
241
|
+
if (spec === void 0) throw new Error(`commandcode settings page has no field ${field}`);
|
|
242
|
+
return spec;
|
|
243
|
+
}
|
|
244
|
+
/** One field's rendered state: draft text, whether it is user-overridden, invalid. */
|
|
245
|
+
field(field) {
|
|
246
|
+
const spec = this.spec(field);
|
|
247
|
+
const staged = this.staged.get(field);
|
|
248
|
+
if (staged === void 0) return {
|
|
249
|
+
text: spec.format(this.sectionValue(field)),
|
|
250
|
+
clear: false,
|
|
251
|
+
overridden: this.stored(field),
|
|
252
|
+
invalid: false
|
|
253
|
+
};
|
|
254
|
+
const parsed = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
|
|
255
|
+
return {
|
|
256
|
+
text: staged.text,
|
|
257
|
+
clear: staged.clear,
|
|
258
|
+
overridden: parsed.kind === "set",
|
|
259
|
+
invalid: parsed.kind === "invalid"
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
sectionValue(field) {
|
|
263
|
+
return this.scope.getSnapshot().value?.[field];
|
|
264
|
+
}
|
|
265
|
+
baseValue(field) {
|
|
266
|
+
const base = this.scope.getSnapshot().base;
|
|
267
|
+
return typeof base === "object" && base !== null && !Array.isArray(base) ? base[field] : void 0;
|
|
268
|
+
}
|
|
269
|
+
userLayer() {
|
|
270
|
+
const user = this.scope.getSnapshot().user;
|
|
271
|
+
return typeof user === "object" && user !== null && !Array.isArray(user) ? user : void 0;
|
|
272
|
+
}
|
|
273
|
+
stored(field) {
|
|
274
|
+
const user = this.userLayer();
|
|
275
|
+
return user !== void 0 && Object.prototype.hasOwnProperty.call(user, field);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The writes a save would perform, in staged order. A field whose draft is
|
|
279
|
+
* not a value its spec accepts carries no write (the save refuses).
|
|
280
|
+
*/
|
|
281
|
+
plan() {
|
|
282
|
+
const plan = [];
|
|
283
|
+
for (const [field, staged] of this.staged) {
|
|
284
|
+
if (field === "apiKey") {
|
|
285
|
+
const value = staged.text.trim();
|
|
286
|
+
if (value !== "") plan.push({
|
|
287
|
+
field,
|
|
288
|
+
run: () => this.writeKey(value)
|
|
289
|
+
});
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const spec = this.spec(field);
|
|
293
|
+
if (staged.clear) {
|
|
294
|
+
if (this.stored(field)) plan.push({
|
|
295
|
+
field,
|
|
296
|
+
run: () => this.clear(field)
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
301
|
+
const parsed = spec.parse(staged.text);
|
|
302
|
+
if (parsed.kind === "invalid") plan.push({
|
|
303
|
+
field,
|
|
304
|
+
run: void 0
|
|
305
|
+
});
|
|
306
|
+
else if (parsed.kind === "clear") plan.push({
|
|
307
|
+
field,
|
|
308
|
+
run: () => this.clear(field)
|
|
309
|
+
});
|
|
310
|
+
else plan.push({
|
|
311
|
+
field,
|
|
312
|
+
run: () => this.store(field, parsed.value)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return plan;
|
|
316
|
+
}
|
|
317
|
+
async clear(field) {
|
|
318
|
+
await this.scope.unset(field);
|
|
319
|
+
return !this.stored(field);
|
|
320
|
+
}
|
|
321
|
+
async store(field, value) {
|
|
322
|
+
await this.scope.set(field, value);
|
|
323
|
+
return this.userLayer()?.[field] === value;
|
|
324
|
+
}
|
|
325
|
+
/** Write the staged key, then re-read whether the Host now holds one. */
|
|
326
|
+
async writeKey(value) {
|
|
327
|
+
try {
|
|
328
|
+
if (!(await this.api.credentials.set({
|
|
329
|
+
ref: this.credential.ref,
|
|
330
|
+
value
|
|
331
|
+
})).result.ok) return false;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
await this.readCredential();
|
|
336
|
+
return this.credential.configured;
|
|
337
|
+
}
|
|
338
|
+
/** Ask the credentials domain about the reference this page writes. */
|
|
339
|
+
async readCredential() {
|
|
340
|
+
const ref = this.credential.ref;
|
|
341
|
+
let response;
|
|
342
|
+
try {
|
|
343
|
+
response = await this.api.credentials.describe({ refs: [ref] });
|
|
344
|
+
} catch {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!response.result.ok) return;
|
|
348
|
+
const view = response.result.value.credentials[ref];
|
|
349
|
+
const next = {
|
|
350
|
+
ref,
|
|
351
|
+
configured: view?.configured ?? false,
|
|
352
|
+
writable: view?.writable ?? true
|
|
353
|
+
};
|
|
354
|
+
if (next.configured === this.credential.configured && next.writable === this.credential.writable) return;
|
|
355
|
+
this.credential = next;
|
|
356
|
+
this.publish();
|
|
357
|
+
}
|
|
358
|
+
publish() {
|
|
359
|
+
if (this.disposed) return;
|
|
360
|
+
for (const listener of this.listeners) listener();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/client/section.tsx
|
|
365
|
+
/**
|
|
366
|
+
* React component for the "Command Code" settings page (browser half).
|
|
367
|
+
*
|
|
368
|
+
* Renders as a `settings.section` entry — a page at the same settings-nav
|
|
369
|
+
* level as General / Models / Plugins. The shell supplies the nav row and
|
|
370
|
+
* renders this body inside the content column. All copy comes from the
|
|
371
|
+
* `settings.commandcode` locale namespace; all state comes from the
|
|
372
|
+
* `CommandCodeSettingsController` injected by the slot registration.
|
|
373
|
+
*
|
|
374
|
+
* The layout mirrors the harness's settings pages: a max-width content
|
|
375
|
+
* column, labelled fields with hints, a reset affordance, and a
|
|
376
|
+
* save/discard footer. Styles are injected once by the client entry
|
|
377
|
+
* (see src/client/index.ts) and class-prefixed `cc-` to stay local.
|
|
378
|
+
*/
|
|
379
|
+
/** One labelled field row in the page body. */
|
|
380
|
+
function Field({ id, label, hint, state, disabled, numeric, placeholder, onEdit, onReset, t }) {
|
|
381
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
382
|
+
className: "cc-field",
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
385
|
+
className: "cc-fieldHead",
|
|
386
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
387
|
+
className: "cc-label",
|
|
388
|
+
htmlFor: id,
|
|
389
|
+
children: label
|
|
390
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
391
|
+
className: "cc-badges",
|
|
392
|
+
children: [state.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
393
|
+
className: "cc-badge",
|
|
394
|
+
children: t("overridden")
|
|
395
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
396
|
+
type: "button",
|
|
397
|
+
className: "cc-reset",
|
|
398
|
+
disabled,
|
|
399
|
+
onClick: onReset,
|
|
400
|
+
children: t("reset")
|
|
401
|
+
})]
|
|
402
|
+
})]
|
|
403
|
+
}),
|
|
404
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
405
|
+
id,
|
|
406
|
+
className: state.invalid ? "cc-input cc-inputInvalid" : "cc-input",
|
|
407
|
+
type: "text",
|
|
408
|
+
inputMode: numeric ? "numeric" : void 0,
|
|
409
|
+
value: state.text,
|
|
410
|
+
placeholder,
|
|
411
|
+
disabled,
|
|
412
|
+
onChange: (event) => onEdit(event.target.value)
|
|
413
|
+
}),
|
|
414
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
415
|
+
className: state.invalid ? "cc-invalid" : "cc-hint",
|
|
416
|
+
children: state.invalid ? t("invalidNumber") : hint
|
|
417
|
+
})
|
|
418
|
+
]
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/** The API-key control: write-only, reports configured state, never echoes the key. */
|
|
422
|
+
function SecretKeyField({ label, hint, state, disabled, configured, configuredLabel, unconfiguredLabel, onEdit }) {
|
|
423
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
424
|
+
className: "cc-field",
|
|
425
|
+
children: [
|
|
426
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
427
|
+
className: "cc-fieldHead",
|
|
428
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
429
|
+
className: "cc-label",
|
|
430
|
+
htmlFor: "cc-api-key",
|
|
431
|
+
children: label
|
|
432
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
433
|
+
className: "cc-badges",
|
|
434
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
435
|
+
className: configured ? "cc-badge" : "cc-badgeMuted",
|
|
436
|
+
children: configured ? configuredLabel : unconfiguredLabel
|
|
437
|
+
})
|
|
438
|
+
})]
|
|
439
|
+
}),
|
|
440
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
441
|
+
id: "cc-api-key",
|
|
442
|
+
className: "cc-input",
|
|
443
|
+
type: "password",
|
|
444
|
+
autoComplete: "off",
|
|
445
|
+
value: state.text,
|
|
446
|
+
disabled,
|
|
447
|
+
onChange: (event) => onEdit(event.target.value)
|
|
448
|
+
}),
|
|
449
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
450
|
+
className: "cc-hint",
|
|
451
|
+
children: hint
|
|
452
|
+
})
|
|
453
|
+
]
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/** The settings page body: connection facts for the Command Code provider. */
|
|
457
|
+
function CommandCodeSettingsPage(props) {
|
|
458
|
+
const { t } = props;
|
|
459
|
+
const state = props.useCommandCodeSettings((snapshot) => snapshot);
|
|
460
|
+
const disabled = !state.writable;
|
|
461
|
+
const keyLocked = !state.apiKeyWritable;
|
|
462
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
463
|
+
className: "cc-section",
|
|
464
|
+
"aria-label": t("title"),
|
|
465
|
+
children: [
|
|
466
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
467
|
+
className: "cc-title",
|
|
468
|
+
children: t("title")
|
|
469
|
+
}),
|
|
470
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
471
|
+
className: "cc-intro",
|
|
472
|
+
children: t("intro")
|
|
473
|
+
}),
|
|
474
|
+
!state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
475
|
+
className: "cc-readOnly",
|
|
476
|
+
role: "status",
|
|
477
|
+
children: t("readOnly")
|
|
478
|
+
}) : null,
|
|
479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
480
|
+
className: "cc-card",
|
|
481
|
+
children: [
|
|
482
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretKeyField, {
|
|
483
|
+
label: t("apiKey"),
|
|
484
|
+
hint: keyLocked ? t("apiKeyLocked") : t("apiKeyHint"),
|
|
485
|
+
state: state.apiKey,
|
|
486
|
+
disabled: disabled || keyLocked,
|
|
487
|
+
configured: state.apiKeyConfigured,
|
|
488
|
+
configuredLabel: t("apiKeySet"),
|
|
489
|
+
unconfiguredLabel: t("apiKeyUnset"),
|
|
490
|
+
onEdit: (text) => props.edit("apiKey", text)
|
|
491
|
+
}),
|
|
492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
493
|
+
id: "cc-api-base",
|
|
494
|
+
label: t("apiBase"),
|
|
495
|
+
hint: t("apiBaseHint"),
|
|
496
|
+
state: state.apiBase,
|
|
497
|
+
disabled,
|
|
498
|
+
onEdit: (text) => props.edit("apiBase", text),
|
|
499
|
+
onReset: () => props.resetField("apiBase"),
|
|
500
|
+
t
|
|
501
|
+
}),
|
|
502
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
503
|
+
id: "cc-working-dir",
|
|
504
|
+
label: t("workingDir"),
|
|
505
|
+
hint: t("workingDirHint"),
|
|
506
|
+
state: state.workingDir,
|
|
507
|
+
disabled,
|
|
508
|
+
placeholder: state.defaultWorkingDir,
|
|
509
|
+
onEdit: (text) => props.edit("workingDir", text),
|
|
510
|
+
onReset: () => props.resetField("workingDir"),
|
|
511
|
+
t
|
|
512
|
+
}),
|
|
513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
514
|
+
id: "cc-request-timeout",
|
|
515
|
+
label: t("requestTimeoutMs"),
|
|
516
|
+
hint: t("requestTimeoutMsHint"),
|
|
517
|
+
state: state.requestTimeoutMs,
|
|
518
|
+
disabled,
|
|
519
|
+
numeric: true,
|
|
520
|
+
onEdit: (text) => props.edit("requestTimeoutMs", text),
|
|
521
|
+
onReset: () => props.resetField("requestTimeoutMs"),
|
|
522
|
+
t
|
|
523
|
+
}),
|
|
524
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
525
|
+
id: "cc-stream-idle-timeout",
|
|
526
|
+
label: t("streamIdleTimeoutMs"),
|
|
527
|
+
hint: t("streamIdleTimeoutMsHint"),
|
|
528
|
+
state: state.streamIdleTimeoutMs,
|
|
529
|
+
disabled,
|
|
530
|
+
numeric: true,
|
|
531
|
+
onEdit: (text) => props.edit("streamIdleTimeoutMs", text),
|
|
532
|
+
onReset: () => props.resetField("streamIdleTimeoutMs"),
|
|
533
|
+
t
|
|
534
|
+
})
|
|
535
|
+
]
|
|
536
|
+
}),
|
|
537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
538
|
+
className: "cc-footer",
|
|
539
|
+
children: [
|
|
540
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
541
|
+
className: "cc-failed",
|
|
542
|
+
role: "status",
|
|
543
|
+
children: t("saveFailed")
|
|
544
|
+
}) : null,
|
|
545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
546
|
+
variant: "ghost",
|
|
547
|
+
size: "sm",
|
|
548
|
+
disabled: !state.dirty || state.saving,
|
|
549
|
+
onClick: props.discard,
|
|
550
|
+
children: t("discard")
|
|
551
|
+
}),
|
|
552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
553
|
+
variant: "primary",
|
|
554
|
+
size: "sm",
|
|
555
|
+
disabled: !state.dirty || state.invalid || state.saving,
|
|
556
|
+
onClick: props.save,
|
|
557
|
+
children: t(state.saving ? "saving" : "save")
|
|
558
|
+
})
|
|
559
|
+
]
|
|
560
|
+
})
|
|
561
|
+
]
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/client/locales.ts
|
|
566
|
+
const zh = {
|
|
567
|
+
nav: "Command Code",
|
|
568
|
+
title: "Command Code",
|
|
569
|
+
intro: "配置 Command Code Provider 连接。API 密钥仅保存在本机凭据服务中,不会回显;其他字段写入用户设置,下次请求即生效。",
|
|
570
|
+
apiKey: "API 密钥",
|
|
571
|
+
apiKeyHint: "在 commandcode.ai 控制台创建。留空保存不会覆盖已存储的密钥。",
|
|
572
|
+
apiKeySet: "已配置",
|
|
573
|
+
apiKeyUnset: "未配置",
|
|
574
|
+
apiKeyLocked: "密钥由只读来源提供",
|
|
575
|
+
apiBase: "API 地址",
|
|
576
|
+
apiBaseHint: "默认 https://api.commandcode.ai,一般无需修改。",
|
|
577
|
+
workingDir: "工作目录",
|
|
578
|
+
workingDirHint: "可选。留空时使用占位符显示的进程工作目录;仅在需要固定路径时填写。",
|
|
579
|
+
requestTimeoutMs: "请求超时(毫秒)",
|
|
580
|
+
requestTimeoutMsHint: "等待响应首个字节的超时;默认 60000。",
|
|
581
|
+
streamIdleTimeoutMs: "流空闲超时(毫秒)",
|
|
582
|
+
streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 120000。",
|
|
583
|
+
overridden: "已覆盖",
|
|
584
|
+
reset: "重置",
|
|
585
|
+
invalidNumber: "无效数字",
|
|
586
|
+
readOnly: "当前配置为只读。",
|
|
587
|
+
unsaved: "未保存",
|
|
588
|
+
save: "保存",
|
|
589
|
+
saving: "保存中",
|
|
590
|
+
saveFailed: "保存失败,请重试。",
|
|
591
|
+
discard: "放弃",
|
|
592
|
+
cancel: "取消"
|
|
593
|
+
};
|
|
594
|
+
const en = {
|
|
595
|
+
nav: "Command Code",
|
|
596
|
+
title: "Command Code",
|
|
597
|
+
intro: "Configure the Command Code Provider connection. The API key is stored only in the local credential service and never echoed; other fields are written to user settings and take effect on the next request.",
|
|
598
|
+
apiKey: "API key",
|
|
599
|
+
apiKeyHint: "Create one in the commandcode.ai console. Saving with this field blank keeps the stored key.",
|
|
600
|
+
apiKeySet: "Configured",
|
|
601
|
+
apiKeyUnset: "Not configured",
|
|
602
|
+
apiKeyLocked: "Key provided by a read-only source",
|
|
603
|
+
apiBase: "API base URL",
|
|
604
|
+
apiBaseHint: "Defaults to https://api.commandcode.ai; usually leave as-is.",
|
|
605
|
+
workingDir: "Working directory",
|
|
606
|
+
workingDirHint: "Optional. Leave blank to use the process cwd shown as the placeholder; fill in only to pin a specific path.",
|
|
607
|
+
requestTimeoutMs: "Request timeout (ms)",
|
|
608
|
+
requestTimeoutMsHint: "Time to wait for the first response byte; default 60000.",
|
|
609
|
+
streamIdleTimeoutMs: "Stream idle timeout (ms)",
|
|
610
|
+
streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 120000.",
|
|
611
|
+
overridden: "Overridden",
|
|
612
|
+
reset: "Reset",
|
|
613
|
+
invalidNumber: "Invalid number",
|
|
614
|
+
readOnly: "Settings are read-only.",
|
|
615
|
+
unsaved: "Unsaved",
|
|
616
|
+
save: "Save",
|
|
617
|
+
saving: "Saving",
|
|
618
|
+
saveFailed: "Save failed, please retry.",
|
|
619
|
+
discard: "Discard",
|
|
620
|
+
cancel: "Cancel"
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/client/index.ts
|
|
624
|
+
/** CSS for the settings page, injected once (harness bundle convention). */
|
|
625
|
+
const PAGE_CSS = `
|
|
626
|
+
.cc-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}
|
|
627
|
+
.cc-title{margin:0;font-size:18px;font-weight:600}
|
|
628
|
+
.cc-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:1.5}
|
|
629
|
+
.cc-readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
630
|
+
.cc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px}
|
|
631
|
+
.cc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
632
|
+
.cc-field+.cc-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
633
|
+
.cc-fieldHead{align-items:center;gap:8px;display:flex}
|
|
634
|
+
.cc-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
635
|
+
.cc-badges{align-items:center;gap:8px;display:inline-flex}
|
|
636
|
+
.cc-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
637
|
+
.cc-badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}
|
|
638
|
+
.cc-reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}
|
|
639
|
+
.cc-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
640
|
+
.cc-reset:disabled{cursor:default;opacity:.5}
|
|
641
|
+
.cc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}
|
|
642
|
+
.cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
643
|
+
.cc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
644
|
+
.cc-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
645
|
+
.cc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
646
|
+
.cc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
647
|
+
.cc-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}
|
|
648
|
+
.cc-failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
649
|
+
`;
|
|
650
|
+
/** Inject the page stylesheet once (idempotent per tag). */
|
|
651
|
+
function injectPageCss() {
|
|
652
|
+
if (typeof document === "undefined") return;
|
|
653
|
+
const id = "@mars-sea/dsh-commandcode-provider/CommandCodeSettingsPage.module.css";
|
|
654
|
+
if (document.querySelector(`style[data-plugin-css="${id}"]`) !== null) return;
|
|
655
|
+
const tag = document.createElement("style");
|
|
656
|
+
tag.dataset.plugin = "@mars-sea/dsh-commandcode-provider";
|
|
657
|
+
tag.dataset.pluginCss = id;
|
|
658
|
+
tag.textContent = PAGE_CSS;
|
|
659
|
+
document.head.appendChild(tag);
|
|
660
|
+
}
|
|
34
661
|
/**
|
|
35
|
-
* Client plugin body
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* UI reads fresh on every call — so wrapping it once covers both the /model
|
|
40
|
-
* popup and the composer seat, across reconnects.
|
|
662
|
+
* Client plugin body. Gates on the services the settings page needs
|
|
663
|
+
* (`slots`, `locale`, `connection`, `remote`, `settingsScope`) plus the
|
|
664
|
+
* `connection` used by the friendly-error wrapper — the same inject list the
|
|
665
|
+
* harness's own settings-surface plugins declare.
|
|
41
666
|
*/
|
|
42
667
|
function apply(ctx) {
|
|
668
|
+
injectPageCss();
|
|
43
669
|
const connection = ctx.get("connection");
|
|
44
|
-
if (connection
|
|
45
|
-
|
|
670
|
+
if (connection !== void 0) installFriendlyImageError(connection);
|
|
671
|
+
ctx.effect(() => ctx.locale.register("settings.commandcode", {
|
|
672
|
+
zh,
|
|
673
|
+
en
|
|
674
|
+
}), "dsh-commandcode-provider: page copy");
|
|
675
|
+
const api = ctx.get("connection").api;
|
|
676
|
+
const hostDescription = ctx.get("connection").hostDescription;
|
|
677
|
+
const controller = new CommandCodeSettingsController(ctx.settingsScope.bind({ namespace: COMMANDCODE_NS }), { credentials: api.credentials }, hostDescription);
|
|
678
|
+
ctx.effect(() => () => controller.dispose(), "dsh-commandcode-provider: settings controller");
|
|
679
|
+
const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(controller.state());
|
|
680
|
+
controller.subscribe(() => store.set(controller.state()));
|
|
681
|
+
const injected = () => ({
|
|
682
|
+
hooks: { commandCodeSettings: store },
|
|
683
|
+
edit: (field, text) => controller.edit(field, text),
|
|
684
|
+
resetField: (field) => controller.resetField(field),
|
|
685
|
+
save: () => void controller.save(),
|
|
686
|
+
discard: () => controller.discard()
|
|
687
|
+
});
|
|
688
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
689
|
+
name: "settings.section",
|
|
690
|
+
id: "commandcode",
|
|
691
|
+
order: 12,
|
|
692
|
+
label: () => ctx.locale.bind("settings.commandcode")("nav"),
|
|
693
|
+
locale: "settings.commandcode",
|
|
694
|
+
inject: injected
|
|
695
|
+
}, CommandCodeSettingsPage));
|
|
46
696
|
}
|
|
47
|
-
const inject = [
|
|
697
|
+
const inject = [
|
|
698
|
+
"slots",
|
|
699
|
+
"locale",
|
|
700
|
+
"connection",
|
|
701
|
+
"remote",
|
|
702
|
+
"settingsScope"
|
|
703
|
+
];
|
|
48
704
|
//#endregion
|
|
49
705
|
exports.apply = apply;
|
|
50
706
|
exports.inject = inject;
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client/index.ts"],"sourcesContent":["/**\n * Browser half of the dsh-commandcode-provider bundle.\n *\n * The host rejects switching to a text-only model while the session already\n * contains images with a harness-level `model-unavailable` error\n * (`dsh-host-apiproxy`'s `session.selectModel` handler). That rejection is\n * intentional and cannot be relaxed from the plugin side — the adapter's\n * `inputModalities` is exactly what makes the guard work. What we CAN do is\n * make the error message friendlier: this client plugin wraps the shared\n * `connection.api.sessions.selectModel` face so a `model-unavailable`\n * rejection shows a clear, actionable hint (with the requested model name)\n * instead of the raw English harness message.\n *\n * The wrapper is deliberately narrow: only the `model-unavailable` code is\n * rewritten, only when the message matches the image-session gate, and only\n * the message text changes — the error code and details pass through\n * untouched so any caller that switches on `error.code` keeps working.\n *\n * The wire types are spelled structurally here (not imported from\n * `@deepseek-ai/dsh-host-apiproxy`) so this client bundle does not drag an\n * extra peer dependency into the package; the shapes are stable and the\n * client build inlines them anyway.\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n\n/** The `model-unavailable` error details: provider + model id. */\ninterface ModelUnavailableDetails {\n provider: string\n model: string\n}\n\n/** The narrow slice of the RPC error we need to inspect and rewrite. */\ninterface RpcErrorLike {\n code: string\n message: string\n details?: ModelUnavailableDetails\n}\n\n/**\n * The narrow slice of a unary RPC result we need to inspect and rewrite.\n * The wire shape from `sessions.selectModel` (via `AbstractApiClient.callUnary`)\n * is the full envelope `{ rpcId, result: { ok, error? } }` — the error lives\n * under `result.result`, not at the top level. `RpcResultLike` models that.\n */\ninterface RpcResultLike {\n rpcId: string\n result:\n | { ok: true; value?: unknown }\n | { ok: false; error: RpcErrorLike }\n}\n\n/** One selectModel call: payload in, envelope out. */\ntype SelectModelCall = (\n payload: { sessionId: string; provider: string; model: string; reasoningEffort?: string },\n signal?: AbortSignal,\n) => Promise<RpcResultLike>\n\n/** The shared sessions wire face we wrap. */\ninterface SessionsLike {\n selectModel: SelectModelCall\n}\n\n/** The connection handle shape we read `api.sessions` from. */\ninterface ConnectionLike {\n api: { sessions: SessionsLike }\n}\n\n/** Whether a selectModel rejection is the harness's image-session gate. */\nexport function isImageSessionRejection(\n result: RpcResultLike,\n): result is RpcResultLike & { result: { ok: false; error: RpcErrorLike } } {\n return (\n !result.result.ok &&\n result.result.error.code === 'model-unavailable' &&\n result.result.error.message.includes('does not accept image input')\n )\n}\n\n/** Wrap the shared sessions API so selectModel failures read friendlier. */\nexport function withFriendlyImageError(sessions: SessionsLike): SessionsLike {\n const selectModel = sessions.selectModel.bind(sessions)\n return {\n ...sessions,\n selectModel: async (payload, signal) => {\n const result = await selectModel(payload, signal)\n if (!isImageSessionRejection(result)) return result\n const model = result.result.error.details?.model ?? payload.model\n return {\n ...result,\n result: {\n ...result.result,\n error: {\n ...result.result.error,\n message:\n `当前会话已包含图片,而模型 ${model} 不支持图片输入;`\n + '请选择支持图片的模型,或先移除会话中的图片。',\n },\n },\n }\n },\n }\n}\n\n/**\n * Client plugin body: install the selectModel wrapper on the connection's\n * shared api. `inject: ['connection']` gates activation until the connection\n * service is provided (the same pattern the harness's own client plugins\n * use), and `connection.api.sessions` is a stable object the model-selection\n * UI reads fresh on every call — so wrapping it once covers both the /model\n * popup and the composer seat, across reconnects.\n */\nexport function apply(ctx: Context): void {\n const connection = ctx.get('connection') as ConnectionLike | undefined\n if (connection === undefined) return\n connection.api.sessions = withFriendlyImageError(connection.api.sessions)\n}\n\nexport const inject: readonly string[] = ['connection']\n"],"mappings":";;;;;;;;EAqEA,SAAgB,wBACd,QAC0E;GAC1E,OACE,CAAC,OAAO,OAAO,MACf,OAAO,OAAO,MAAM,SAAS,uBAC7B,OAAO,OAAO,MAAM,QAAQ,SAAS,6BAA6B;EAEtE;;EAGA,SAAgB,uBAAuB,UAAsC;GAC3E,MAAM,cAAc,SAAS,YAAY,KAAK,QAAQ;GACtD,OAAO;IACL,GAAG;IACH,aAAa,OAAO,SAAS,WAAW;KACtC,MAAM,SAAS,MAAM,YAAY,SAAS,MAAM;KAChD,IAAI,CAAC,wBAAwB,MAAM,GAAG,OAAO;KAC7C,MAAM,QAAQ,OAAO,OAAO,MAAM,SAAS,SAAS,QAAQ;KAC5D,OAAO;MACL,GAAG;MACH,QAAQ;OACN,GAAG,OAAO;OACV,OAAO;QACL,GAAG,OAAO,OAAO;QACjB,SACE,iBAAiB,MAAM;OAE3B;MACF;KACF;IACF;GACF;EACF;;;;;;;;;EAUA,SAAgB,MAAM,KAAoB;GACxC,MAAM,aAAa,IAAI,IAAI,YAAY;GACvC,IAAI,eAAe,KAAA,GAAW;GAC9B,WAAW,IAAI,WAAW,uBAAuB,WAAW,IAAI,QAAQ;EAC1E;EAEA,MAAa,SAA4B,CAAC,YAAY"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["Button","createSnapshotStore"],"sources":["../src/client/sessions.ts","../src/client/settings.ts","../src/client/section.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Friendly-error wrapper for the harness's image-session gate.\n *\n * The host rejects switching to a text-only model while the session already\n * contains images with a `model-unavailable` error\n * (`dsh-host-apiproxy`'s `session.selectModel` handler). That rejection is\n * intentional and cannot be relaxed from the plugin side — the adapter's\n * `inputModalities` is exactly what makes the guard work. What we CAN do is\n * make the error message friendlier: wrap the shared\n * `connection.api.sessions.selectModel` face so a `model-unavailable`\n * rejection shows a clear, actionable hint (with the requested model name)\n * instead of the raw English harness message.\n *\n * The wrapper is deliberately narrow: only the `model-unavailable` code is\n * rewritten, only when the message matches the image-session gate, and only\n * the message text changes — the error code and details pass through\n * untouched so any caller that switches on `error.code` keeps working.\n *\n * The wire types are spelled structurally here (not imported from\n * `@deepseek-ai/dsh-host-apiproxy`) so this client bundle does not drag an\n * extra peer dependency into the package; the shapes are stable and the\n * client build inlines them anyway.\n *\n * This module is deliberately free of React and other client-platform\n * imports so the node test runner can exercise it directly.\n */\n\n/** The `model-unavailable` error details: provider + model id. */\ninterface ModelUnavailableDetails {\n provider: string\n model: string\n}\n\n/** The narrow slice of the RPC error we need to inspect and rewrite. */\ninterface RpcErrorLike {\n code: string\n message: string\n details?: ModelUnavailableDetails\n}\n\n/**\n * The narrow slice of a unary RPC result we need to inspect and rewrite.\n * The wire shape from `sessions.selectModel` (via `AbstractApiClient.callUnary`)\n * is the full envelope `{ rpcId, result: { ok, error? } }` — the error lives\n * under `result.result`, not at the top level. `RpcResultLike` models that.\n */\ninterface RpcResultLike {\n rpcId: string\n result:\n | { ok: true; value?: unknown }\n | { ok: false; error: RpcErrorLike }\n}\n\n/** One selectModel call: payload in, envelope out. */\ntype SelectModelCall = (\n payload: { sessionId: string; provider: string; model: string; reasoningEffort?: string },\n signal?: AbortSignal,\n) => Promise<RpcResultLike>\n\n/** The shared sessions wire face we wrap. */\ninterface SessionsLike {\n selectModel: SelectModelCall\n}\n\n/** Whether a selectModel rejection is the harness's image-session gate. */\nexport function isImageSessionRejection(\n result: RpcResultLike,\n): result is RpcResultLike & { result: { ok: false; error: RpcErrorLike } } {\n return (\n !result.result.ok &&\n result.result.error.code === 'model-unavailable' &&\n result.result.error.message.includes('does not accept image input')\n )\n}\n\n/** Wrap the shared sessions API so selectModel failures read friendlier. */\nexport function withFriendlyImageError(sessions: SessionsLike): SessionsLike {\n const selectModel = sessions.selectModel.bind(sessions)\n return {\n ...sessions,\n selectModel: async (payload, signal) => {\n const result = await selectModel(payload, signal)\n if (!isImageSessionRejection(result)) return result\n const model = result.result.error.details?.model ?? payload.model\n return {\n ...result,\n result: {\n ...result.result,\n error: {\n ...result.result.error,\n message:\n `当前会话已包含图片,而模型 ${model} 不支持图片输入;`\n + '请选择支持图片的模型,或先移除会话中的图片。',\n },\n },\n }\n },\n }\n}\n\n/** The connection handle shape we read `api.sessions` from. */\nexport interface ConnectionLike {\n api: { sessions: SessionsLike }\n}\n\n/** Install the wrapper on a connection's shared sessions face. */\nexport function installFriendlyImageError(connection: ConnectionLike): void {\n connection.api.sessions = withFriendlyImageError(connection.api.sessions)\n}\n","/**\n * Browser controller for the \"Command Code\" settings page.\n *\n * The page lives at the same settings-nav level as General / Models / Plugins\n * (a `settings.section` entry, id `commandcode`). It exists because the\n * Models page renders an unknown-adapter-family card for the `commandcode`\n * provider and deliberately disables its submit — the API key cannot be\n * configured there. This page owns the connection facts the plugin resolves\n * per request:\n *\n * - API key -> written through the credentials domain under the reference\n * the plugin resolves (`apiKeyEnv`, default\n * `COMMANDCODE_API_KEY`). The literal never rides a response,\n * so the control only reports whether one is configured.\n * - API base -> the `llm-commandcode` settings namespace (`apiBase`), same\n * namespace the Models page card addresses.\n * - Working dir, request/stream timeouts -> the same namespace.\n *\n * The controller mirrors the plugin-card pattern from the harness's own\n * settings UI: it binds the `llm-commandcode` namespace through the\n * `settingsScope` service, keeps a staged draft of edits, and writes them on\n * save through `scope.set` / the credentials domain. The Host stays the\n * single fact source; the snapshot is republished after each accepted write.\n *\n * This module is deliberately free of JSX — it only produces the state face\n * the React component renders.\n */\n\nimport type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client'\n\n/** The settings namespace the plugin registers (host half, src/index.ts). */\nexport const COMMANDCODE_NS = 'llm-commandcode'\n/** Default credential reference the plugin resolves when none is named. */\nexport const DEFAULT_API_KEY_REF = 'COMMANDCODE_API_KEY'\n\n/** The narrow slice of the wire face this controller needs. */\nexport interface SettingsPageApi {\n credentials: {\n describe(request: { refs: string[] }): Promise<{\n result: { ok: true; value: { credentials: Record<string, { configured: boolean; writable: boolean }> } } | { ok: false; error: { message: string } }\n }>\n set(request: { ref: string; value: string }): Promise<{ result: { ok: true; value?: unknown } | { ok: false; error: { message: string } } }>\n }\n}\n\n/** The Host-description observable the page reads the process cwd from. */\nexport interface HostDescriptionSource {\n getSnapshot(): { cwd?: string } | undefined\n subscribe(fn: () => void): () => void\n}\n\n/** One editable text field's staged state (blank = keep stored value). */\nexport interface StagedField {\n /** Live draft text the input shows. */\n text: string\n /** Whether the user explicitly cleared the field (reset to inherited). */\n clear: boolean\n /** Whether the user layer carries this field (marks it overridden). */\n overridden: boolean\n /** Whether the staged draft fails to parse (blocks save). */\n invalid: boolean\n}\n\n/** The page's full state face, projected from the scope + drafts + credential. */\nexport interface SettingsPageState {\n /** Whether the namespace snapshot is ready. */\n available: boolean\n /** Whether the Host document accepts writes. */\n writable: boolean\n /** Whether the API key is currently configured (Host-reported). */\n apiKeyConfigured: boolean\n /** Whether the credentials domain can store the key. */\n apiKeyWritable: boolean\n /** The API key draft (write-only; starts blank, never echoes the stored key). */\n apiKey: StagedField\n /** apiBase draft. */\n apiBase: StagedField\n /** workingDir draft. */\n workingDir: StagedField\n /**\n * The working directory a blank `workingDir` resolves to: the Host\n * process cwd (`host.describe().cwd`). Shown as the field's placeholder so\n * the user sees what \"leave it empty\" means — no configuration needed.\n */\n defaultWorkingDir: string | undefined\n /** requestTimeoutMs draft. */\n requestTimeoutMs: StagedField\n /** streamIdleTimeoutMs draft. */\n streamIdleTimeoutMs: StagedField\n /** Whether any staged edit differs from the stored section. */\n dirty: boolean\n /** Whether a staged numeric field fails to parse (save blocked). */\n invalid: boolean\n /** Whether a save is in flight. */\n saving: boolean\n /** Whether the last save failed (drafts retained for correction). */\n failed: boolean\n}\n\n/** Parsed outcome of one field's draft. */\ntype Parsed = { kind: 'set'; value: string | number } | { kind: 'clear' } | { kind: 'invalid' }\n\n/** One field's staged draft (internal; the public face adds derived flags). */\ninterface Staged {\n text: string\n clear: boolean\n}\n\n/** A field conversion spec. */\ninterface FieldSpec {\n field: string\n format(value: unknown): string\n parse(text: string): Parsed\n}\n\n/** A free-text field; an empty draft clears it. */\nfunction textField(field: string): FieldSpec {\n return {\n field,\n format: (value) => (typeof value === 'string' ? value : ''),\n parse: (text) => {\n const trimmed = text.trim()\n return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }\n },\n }\n}\n\n/** A whole-number field; an empty draft clears it, anything non-numeric blocks save. */\nfunction numberField(field: string): FieldSpec {\n return {\n field,\n format: (value) => (typeof value === 'number' ? String(value) : ''),\n parse: (text) => {\n const trimmed = text.trim()\n if (trimmed === '') return { kind: 'clear' }\n const parsed = Number(trimmed)\n return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : { kind: 'invalid' }\n },\n }\n}\n\n/** The fields this page edits inside the `llm-commandcode` namespace. */\nconst SECTION_FIELDS: FieldSpec[] = [\n textField('apiBase'),\n textField('workingDir'),\n numberField('requestTimeoutMs'),\n numberField('streamIdleTimeoutMs'),\n]\n\n/**\n * Controller bridging the `llm-commandcode` scope and the credentials domain\n * onto the page. Public API mirrors the harness's CardForm actions, so the\n * component stays thin.\n */\nexport class CommandCodeSettingsController {\n private readonly scope: SettingsScope<Record<string, unknown>>\n private readonly api: SettingsPageApi\n private readonly specs = new Map(SECTION_FIELDS.map((spec) => [spec.field, spec]))\n private readonly staged = new Map<string, Staged>()\n private readonly listeners = new Set<() => void>()\n private readonly disposers: Array<() => void> = []\n private disposed = false\n private defaultWorkingDir: string | undefined\n private credential = { ref: DEFAULT_API_KEY_REF, configured: false, writable: true }\n private saving = false\n private failed = false\n\n /**\n * @param scope - bound scope for the `llm-commandcode` namespace.\n * @param api - credentials wire face.\n * @param hostDescription - the Host-description observable whose `cwd` is\n * shown as the placeholder a blank `workingDir` field resolves to.\n */\n constructor(\n scope: SettingsScope<Record<string, unknown>>,\n api: SettingsPageApi,\n hostDescription?: HostDescriptionSource,\n ) {\n this.scope = scope\n this.api = api\n this.disposers.push(scope.subscribe(() => {\n this.recomputeCredentialRef()\n this.publish()\n }))\n if (hostDescription !== undefined) {\n this.defaultWorkingDir = hostDescription.getSnapshot()?.cwd\n this.disposers.push(hostDescription.subscribe(() => {\n if (this.disposed) return\n const cwd = hostDescription.getSnapshot()?.cwd\n if (cwd !== this.defaultWorkingDir) {\n this.defaultWorkingDir = cwd\n this.publish()\n }\n }))\n }\n this.recomputeCredentialRef()\n void this.readCredential()\n }\n\n /** Release every subscription held on external sources. Idempotent. */\n dispose(): void {\n if (this.disposed) return\n this.disposed = true\n for (const dispose of this.disposers) dispose()\n this.disposers.length = 0\n this.listeners.clear()\n }\n\n /**\n * The credential reference the section names, or the provider default. A\n * user who renamed `apiKeyEnv` in `settings.yaml` (or the composition\n * config) gets a page that addresses the renamed ref instead of silently\n * writing the default — mirroring the Models page's `refFor()`.\n */\n private recomputeCredentialRef(): void {\n const snapshot = this.scope.getSnapshot()\n const named = typeof snapshot.value?.apiKeyEnv === 'string' && snapshot.value.apiKeyEnv.length > 0\n ? snapshot.value.apiKeyEnv\n : DEFAULT_API_KEY_REF\n if (named === this.credential.ref) return\n this.credential = { ref: named, configured: false, writable: true }\n void this.readCredential()\n }\n\n /** Subscribe to state projections. @returns the disposer. */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /** Build the current page state face. */\n state(): SettingsPageState {\n const snapshot = this.scope.getSnapshot()\n const plan = this.plan()\n return {\n available: snapshot.status === 'ready',\n writable: snapshot.writable,\n apiKeyConfigured: this.credential.configured,\n apiKeyWritable: this.credential.writable,\n apiKey: {\n text: this.staged.get('apiKey')?.text ?? '',\n clear: false,\n overridden: false,\n invalid: false,\n },\n apiBase: this.field('apiBase'),\n workingDir: this.field('workingDir'),\n defaultWorkingDir: this.defaultWorkingDir,\n requestTimeoutMs: this.field('requestTimeoutMs'),\n streamIdleTimeoutMs: this.field('streamIdleTimeoutMs'),\n dirty: plan.length > 0,\n invalid: plan.some((item) => item.run === undefined),\n saving: this.saving,\n failed: this.failed,\n }\n }\n\n /** Stage one field's draft text. */\n edit(field: string, text: string): void {\n this.staged.set(field, { text, clear: false })\n this.failed = false\n this.publish()\n }\n\n /** Reset one section field to its inherited (composition) value. */\n resetField(field: string): void {\n if (field === 'apiKey') {\n this.staged.delete('apiKey')\n this.failed = false\n this.publish()\n return\n }\n const spec = this.spec(field)\n this.staged.set(field, { text: spec.format(this.baseValue(field)), clear: true })\n this.failed = false\n this.publish()\n }\n\n /** Discard every staged edit. */\n discard(): void {\n if (this.staged.size === 0 && !this.failed) return\n this.staged.clear()\n this.failed = false\n this.publish()\n }\n\n /** Write every staged edit, then re-read the Host's accepted state. */\n async save(): Promise<void> {\n const plan = this.plan()\n if (plan.length === 0 || this.saving) return\n const runs: Array<() => Promise<boolean>> = []\n for (const item of plan) {\n if (item.run === undefined) return\n runs.push(item.run)\n }\n this.saving = true\n this.failed = false\n this.publish()\n let landed = true\n for (const run of runs) landed = (await run()) && landed\n this.saving = false\n this.failed = !landed\n if (landed) this.staged.clear()\n this.publish()\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private spec(field: string): FieldSpec {\n const spec = this.specs.get(field)\n if (spec === undefined) throw new Error(`commandcode settings page has no field ${field}`)\n return spec\n }\n\n /** One field's rendered state: draft text, whether it is user-overridden, invalid. */\n private field(field: string): StagedField {\n const spec = this.spec(field)\n const staged = this.staged.get(field)\n if (staged === undefined) {\n return {\n text: spec.format(this.sectionValue(field)),\n clear: false,\n overridden: this.stored(field),\n invalid: false,\n }\n }\n const parsed = staged.clear ? { kind: 'clear' as const } : spec.parse(staged.text)\n return {\n text: staged.text,\n clear: staged.clear,\n overridden: parsed.kind === 'set',\n invalid: parsed.kind === 'invalid',\n }\n }\n\n private sectionValue(field: string): unknown {\n return this.scope.getSnapshot().value?.[field]\n }\n\n private baseValue(field: string): unknown {\n const base = this.scope.getSnapshot().base\n return typeof base === 'object' && base !== null && !Array.isArray(base)\n ? (base as Record<string, unknown>)[field]\n : undefined\n }\n\n private userLayer(): Record<string, unknown> | undefined {\n const user = this.scope.getSnapshot().user\n return typeof user === 'object' && user !== null && !Array.isArray(user)\n ? (user as Record<string, unknown>)\n : undefined\n }\n\n private stored(field: string): boolean {\n const user = this.userLayer()\n return user !== undefined && Object.prototype.hasOwnProperty.call(user, field)\n }\n\n /**\n * The writes a save would perform, in staged order. A field whose draft is\n * not a value its spec accepts carries no write (the save refuses).\n */\n private plan(): Array<{ field: string; run: (() => Promise<boolean>) | undefined }> {\n const plan: Array<{ field: string; run: (() => Promise<boolean>) | undefined }> = []\n for (const [field, staged] of this.staged) {\n if (field === 'apiKey') {\n const value = staged.text.trim()\n if (value !== '') {\n plan.push({ field, run: () => this.writeKey(value) })\n }\n continue\n }\n const spec = this.spec(field)\n if (staged.clear) {\n if (this.stored(field)) plan.push({ field, run: () => this.clear(field) })\n continue\n }\n if (staged.text === spec.format(this.sectionValue(field))) continue\n const parsed = spec.parse(staged.text)\n if (parsed.kind === 'invalid') plan.push({ field, run: undefined })\n else if (parsed.kind === 'clear') plan.push({ field, run: () => this.clear(field) })\n else plan.push({ field, run: () => this.store(field, parsed.value) })\n }\n return plan\n }\n\n private async clear(field: string): Promise<boolean> {\n await this.scope.unset(field)\n return !this.stored(field)\n }\n\n private async store(field: string, value: string | number): Promise<boolean> {\n await this.scope.set(field, value)\n return this.userLayer()?.[field] === value\n }\n\n /** Write the staged key, then re-read whether the Host now holds one. */\n private async writeKey(value: string): Promise<boolean> {\n try {\n const response = await this.api.credentials.set({ ref: this.credential.ref, value })\n if (!response.result.ok) return false\n } catch {\n return false\n }\n await this.readCredential()\n return this.credential.configured\n }\n\n /** Ask the credentials domain about the reference this page writes. */\n private async readCredential(): Promise<void> {\n const ref = this.credential.ref\n let response: Awaited<ReturnType<SettingsPageApi['credentials']['describe']>>\n try {\n response = await this.api.credentials.describe({ refs: [ref] })\n } catch {\n return\n }\n if (!response.result.ok) return\n const view = response.result.value.credentials[ref]\n const next = {\n ref,\n configured: view?.configured ?? false,\n writable: view?.writable ?? true,\n }\n if (next.configured === this.credential.configured && next.writable === this.credential.writable) return\n this.credential = next\n this.publish()\n }\n\n private publish(): void {\n if (this.disposed) return\n for (const listener of this.listeners) listener()\n }\n}\n","/**\n * React component for the \"Command Code\" settings page (browser half).\n *\n * Renders as a `settings.section` entry — a page at the same settings-nav\n * level as General / Models / Plugins. The shell supplies the nav row and\n * renders this body inside the content column. All copy comes from the\n * `settings.commandcode` locale namespace; all state comes from the\n * `CommandCodeSettingsController` injected by the slot registration.\n *\n * The layout mirrors the harness's settings pages: a max-width content\n * column, labelled fields with hints, a reset affordance, and a\n * save/discard footer. Styles are injected once by the client entry\n * (see src/client/index.ts) and class-prefixed `cc-` to stay local.\n */\n\nimport { Button } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { SettingsCommandCodeKey } from './locales.ts'\nimport type { SettingsPageState, StagedField } from './settings.ts'\n\n/** Props composed by the slot registration: locale seat + injected face. */\nexport interface CommandCodeSettingsProps {\n t: Translate<SettingsCommandCodeKey>\n useCommandCodeSettings<T>(selector: (state: SettingsPageState) => T): T\n edit(field: string, text: string): void\n resetField(field: string): void\n save(): void\n discard(): void\n}\n\n/** One labelled field row in the page body. */\nfunction Field({\n id,\n label,\n hint,\n state,\n disabled,\n numeric,\n placeholder,\n onEdit,\n onReset,\n t,\n}: {\n id: string\n label: string\n hint: string\n state: StagedField\n disabled: boolean\n numeric?: boolean\n placeholder?: string | undefined\n onEdit(text: string): void\n onReset(): void\n t: Translate<SettingsCommandCodeKey>\n}) {\n return (\n <div className=\"cc-field\">\n <div className=\"cc-fieldHead\">\n <label className=\"cc-label\" htmlFor={id}>{label}</label>\n <span className=\"cc-badges\">\n {state.overridden ? <span className=\"cc-badge\">{t('overridden')}</span> : null}\n <button type=\"button\" className=\"cc-reset\" disabled={disabled} onClick={onReset}>{t('reset')}</button>\n </span>\n </div>\n <input\n id={id}\n className={state.invalid ? 'cc-input cc-inputInvalid' : 'cc-input'}\n type=\"text\"\n inputMode={numeric ? 'numeric' : undefined}\n value={state.text}\n placeholder={placeholder}\n disabled={disabled}\n onChange={(event) => onEdit(event.target.value)}\n />\n <p className={state.invalid ? 'cc-invalid' : 'cc-hint'}>\n {state.invalid ? t('invalidNumber') : hint}\n </p>\n </div>\n )\n}\n\n/** The API-key control: write-only, reports configured state, never echoes the key. */\nfunction SecretKeyField({\n label,\n hint,\n state,\n disabled,\n configured,\n configuredLabel,\n unconfiguredLabel,\n onEdit,\n}: {\n label: string\n hint: string\n state: StagedField\n disabled: boolean\n configured: boolean\n configuredLabel: string\n unconfiguredLabel: string\n onEdit(text: string): void\n}) {\n return (\n <div className=\"cc-field\">\n <div className=\"cc-fieldHead\">\n <label className=\"cc-label\" htmlFor=\"cc-api-key\">{label}</label>\n <span className=\"cc-badges\">\n <span className={configured ? 'cc-badge' : 'cc-badgeMuted'}>\n {configured ? configuredLabel : unconfiguredLabel}\n </span>\n </span>\n </div>\n <input\n id=\"cc-api-key\"\n className=\"cc-input\"\n type=\"password\"\n autoComplete=\"off\"\n value={state.text}\n disabled={disabled}\n onChange={(event) => onEdit(event.target.value)}\n />\n <p className=\"cc-hint\">{hint}</p>\n </div>\n )\n}\n\n/** The settings page body: connection facts for the Command Code provider. */\nexport function CommandCodeSettingsPage(props: CommandCodeSettingsProps) {\n const { t } = props\n const state = props.useCommandCodeSettings((snapshot) => snapshot)\n const disabled = !state.writable\n const keyLocked = !state.apiKeyWritable\n return (\n <section className=\"cc-section\" aria-label={t('title')}>\n <h2 className=\"cc-title\">{t('title')}</h2>\n <p className=\"cc-intro\">{t('intro')}</p>\n {!state.writable ? <p className=\"cc-readOnly\" role=\"status\">{t('readOnly')}</p> : null}\n <div className=\"cc-card\">\n <SecretKeyField\n label={t('apiKey')}\n hint={keyLocked ? t('apiKeyLocked') : t('apiKeyHint')}\n state={state.apiKey}\n disabled={disabled || keyLocked}\n configured={state.apiKeyConfigured}\n configuredLabel={t('apiKeySet')}\n unconfiguredLabel={t('apiKeyUnset')}\n onEdit={(text) => props.edit('apiKey', text)}\n />\n <Field\n id=\"cc-api-base\"\n label={t('apiBase')}\n hint={t('apiBaseHint')}\n state={state.apiBase}\n disabled={disabled}\n onEdit={(text) => props.edit('apiBase', text)}\n onReset={() => props.resetField('apiBase')}\n t={t}\n />\n <Field\n id=\"cc-working-dir\"\n label={t('workingDir')}\n hint={t('workingDirHint')}\n state={state.workingDir}\n disabled={disabled}\n placeholder={state.defaultWorkingDir}\n onEdit={(text) => props.edit('workingDir', text)}\n onReset={() => props.resetField('workingDir')}\n t={t}\n />\n <Field\n id=\"cc-request-timeout\"\n label={t('requestTimeoutMs')}\n hint={t('requestTimeoutMsHint')}\n state={state.requestTimeoutMs}\n disabled={disabled}\n numeric\n onEdit={(text) => props.edit('requestTimeoutMs', text)}\n onReset={() => props.resetField('requestTimeoutMs')}\n t={t}\n />\n <Field\n id=\"cc-stream-idle-timeout\"\n label={t('streamIdleTimeoutMs')}\n hint={t('streamIdleTimeoutMsHint')}\n state={state.streamIdleTimeoutMs}\n disabled={disabled}\n numeric\n onEdit={(text) => props.edit('streamIdleTimeoutMs', text)}\n onReset={() => props.resetField('streamIdleTimeoutMs')}\n t={t}\n />\n </div>\n <div className=\"cc-footer\">\n {state.failed ? <p className=\"cc-failed\" role=\"status\">{t('saveFailed')}</p> : null}\n <Button variant=\"ghost\" size=\"sm\" disabled={!state.dirty || state.saving} onClick={props.discard}>\n {t('discard')}\n </Button>\n <Button\n variant=\"primary\"\n size=\"sm\"\n disabled={!state.dirty || state.invalid || state.saving}\n onClick={props.save}\n >\n {t(state.saving ? 'saving' : 'save')}\n </Button>\n </div>\n </section>\n )\n}\n","/**\n * Locale copy for the \"Command Code\" settings page, and the declaration that\n * merges the page's namespace into the framework's `LocaleNamespaceMap` so\n * `ctx.locale.register` / `ctx.slots.register(..., { locale })` are typed.\n *\n * zh is the source of truth for the key set (repo convention); en must carry\n * the exact same keys — a mismatch is a compile error at the register site.\n */\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Copy of the Command Code settings page. */\n 'settings.commandcode': SettingsCommandCodeKey\n }\n}\n\n/** Dictionary keys of the Command Code settings page. */\nexport type SettingsCommandCodeKey =\n | 'nav'\n | 'title'\n | 'intro'\n | 'apiKey'\n | 'apiKeyHint'\n | 'apiKeySet'\n | 'apiKeyUnset'\n | 'apiKeyLocked'\n | 'apiBase'\n | 'apiBaseHint'\n | 'workingDir'\n | 'workingDirHint'\n | 'requestTimeoutMs'\n | 'requestTimeoutMsHint'\n | 'streamIdleTimeoutMs'\n | 'streamIdleTimeoutMsHint'\n | 'overridden'\n | 'reset'\n | 'invalidNumber'\n | 'readOnly'\n | 'unsaved'\n | 'save'\n | 'saving'\n | 'saveFailed'\n | 'discard'\n | 'cancel'\n\nexport const zh: Record<SettingsCommandCodeKey, string> = {\n nav: 'Command Code',\n title: 'Command Code',\n intro:\n '配置 Command Code Provider 连接。API 密钥仅保存在本机凭据服务中,不会回显;'\n + '其他字段写入用户设置,下次请求即生效。',\n apiKey: 'API 密钥',\n apiKeyHint: '在 commandcode.ai 控制台创建。留空保存不会覆盖已存储的密钥。',\n apiKeySet: '已配置',\n apiKeyUnset: '未配置',\n apiKeyLocked: '密钥由只读来源提供',\n apiBase: 'API 地址',\n apiBaseHint: '默认 https://api.commandcode.ai,一般无需修改。',\n workingDir: '工作目录',\n workingDirHint: '可选。留空时使用占位符显示的进程工作目录;仅在需要固定路径时填写。',\n requestTimeoutMs: '请求超时(毫秒)',\n requestTimeoutMsHint: '等待响应首个字节的超时;默认 60000。',\n streamIdleTimeoutMs: '流空闲超时(毫秒)',\n streamIdleTimeoutMsHint: '生成流停滞多久视为断连;默认 120000。',\n overridden: '已覆盖',\n reset: '重置',\n invalidNumber: '无效数字',\n readOnly: '当前配置为只读。',\n unsaved: '未保存',\n save: '保存',\n saving: '保存中',\n saveFailed: '保存失败,请重试。',\n discard: '放弃',\n cancel: '取消',\n}\n\nexport const en: Record<SettingsCommandCodeKey, string> = {\n nav: 'Command Code',\n title: 'Command Code',\n intro:\n 'Configure the Command Code Provider connection. The API key is stored only'\n + ' in the local credential service and never echoed; other fields are written'\n + ' to user settings and take effect on the next request.',\n apiKey: 'API key',\n apiKeyHint: 'Create one in the commandcode.ai console. Saving with this field'\n + ' blank keeps the stored key.',\n apiKeySet: 'Configured',\n apiKeyUnset: 'Not configured',\n apiKeyLocked: 'Key provided by a read-only source',\n apiBase: 'API base URL',\n apiBaseHint: 'Defaults to https://api.commandcode.ai; usually leave as-is.',\n workingDir: 'Working directory',\n workingDirHint: 'Optional. Leave blank to use the process cwd shown as the'\n + ' placeholder; fill in only to pin a specific path.',\n requestTimeoutMs: 'Request timeout (ms)',\n requestTimeoutMsHint: 'Time to wait for the first response byte; default 60000.',\n streamIdleTimeoutMs: 'Stream idle timeout (ms)',\n streamIdleTimeoutMsHint: 'How long a stalled stream is treated as dead; default 120000.',\n overridden: 'Overridden',\n reset: 'Reset',\n invalidNumber: 'Invalid number',\n readOnly: 'Settings are read-only.',\n unsaved: 'Unsaved',\n save: 'Save',\n saving: 'Saving',\n saveFailed: 'Save failed, please retry.',\n discard: 'Discard',\n cancel: 'Cancel',\n}\n","/**\n * Browser half of the dsh-commandcode-provider bundle.\n *\n * Two responsibilities:\n *\n * 1. A \"Command Code\" settings page (a `settings.section` entry at the same\n * nav level as General / Models / Plugins). The Models page renders an\n * unknown-adapter-family card for the `commandcode` provider and disables\n * its submit, so the API key cannot be configured there; this page is the\n * dedicated surface. It writes the API key through the credentials domain\n * (the `COMMANDCODE_API_KEY` reference the plugin resolves) and the\n * connection facts through the `llm-commandcode` settings namespace, so a\n * saved key or endpoint reaches the very next request.\n *\n * 2. The friendly-error wrapper for the harness's image-session gate — see\n * `./sessions.ts`. The wrapper is deliberately narrow: only the\n * `model-unavailable` code is rewritten, only when the message matches the\n * image-session gate, and only the message text changes.\n *\n * The wire types are spelled structurally in `./sessions.ts` (not imported\n * from `@deepseek-ai/dsh-host-apiproxy`) so this client bundle does not drag\n * an extra peer dependency into the package; the shapes are stable and the\n * client build inlines them anyway.\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only imports that pull in the client-service augmentations\n// (`slots`/`remote`/`locale` on Context) and the `settings.section` SlotMap\n// entry (`settingsScope` arrives through dsh-client-ui-settings).\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { installFriendlyImageError } from './sessions.ts'\nimport { CommandCodeSettingsController, COMMANDCODE_NS, type SettingsPageState } from './settings.ts'\nimport { CommandCodeSettingsPage } from './section.tsx'\nimport { zh, en } from './locales.ts'\n\nexport { isImageSessionRejection, withFriendlyImageError } from './sessions.ts'\nimport type { ConnectionLike } from './sessions.ts'\n\n/** CSS for the settings page, injected once (harness bundle convention). */\nconst PAGE_CSS = `\n.cc-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}\n.cc-title{margin:0;font-size:18px;font-weight:600}\n.cc-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:1.5}\n.cc-readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}\n.cc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px}\n.cc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}\n.cc-field+.cc-field{border-top:1px solid var(--dsw-alias-border-l2)}\n.cc-fieldHead{align-items:center;gap:8px;display:flex}\n.cc-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}\n.cc-badges{align-items:center;gap:8px;display:inline-flex}\n.cc-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}\n.cc-badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}\n.cc-reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}\n.cc-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}\n.cc-reset:disabled{cursor:default;opacity:.5}\n.cc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}\n.cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}\n.cc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}\n.cc-inputInvalid{border-color:var(--dsw-alias-label-error)}\n.cc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}\n.cc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}\n.cc-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}\n.cc-failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}\n`\n\n/** Inject the page stylesheet once (idempotent per tag). */\nfunction injectPageCss(): void {\n if (typeof document === 'undefined') return\n const id = '@mars-sea/dsh-commandcode-provider/CommandCodeSettingsPage.module.css'\n if (document.querySelector(`style[data-plugin-css=\"${id}\"]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.plugin = '@mars-sea/dsh-commandcode-provider'\n tag.dataset.pluginCss = id\n tag.textContent = PAGE_CSS\n document.head.appendChild(tag)\n}\n\n/**\n * Client plugin body. Gates on the services the settings page needs\n * (`slots`, `locale`, `connection`, `remote`, `settingsScope`) plus the\n * `connection` used by the friendly-error wrapper — the same inject list the\n * harness's own settings-surface plugins declare.\n */\nexport function apply(ctx: Context): void {\n injectPageCss()\n\n // Friendly image-gate error wrapper (unchanged behaviour).\n const connection = ctx.get('connection') as ConnectionLike | undefined\n if (connection !== undefined) {\n installFriendlyImageError(connection)\n }\n\n // The \"Command Code\" settings page: register the section once the\n // `settings.section` declaration is on the ledger (ui-settings-general\n // owns the shell; registration order relative to it is not constrained —\n // `slots.inject` waits for the declaration).\n ctx.effect(() => ctx.locale.register('settings.commandcode', { zh, en }), 'dsh-commandcode-provider: page copy')\n\n const api = ctx.get('connection').api\n const hostDescription = ctx.get('connection').hostDescription\n const scope = ctx.settingsScope.bind<Record<string, unknown>>({ namespace: COMMANDCODE_NS })\n const controller = new CommandCodeSettingsController(scope, { credentials: api.credentials }, hostDescription)\n ctx.effect(() => () => controller.dispose(), 'dsh-commandcode-provider: settings controller')\n const store = createSnapshotStore<SettingsPageState>(controller.state())\n controller.subscribe(() => store.set(controller.state()))\n const injected = () => ({\n hooks: { commandCodeSettings: store },\n edit: (field: string, text: string) => controller.edit(field, text),\n resetField: (field: string) => controller.resetField(field),\n save: () => void controller.save(),\n discard: () => controller.discard(),\n })\n\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'commandcode',\n order: 12,\n label: () => ctx.locale.bind('settings.commandcode')('nav'),\n locale: 'settings.commandcode',\n inject: injected,\n }, CommandCodeSettingsPage))\n}\n\nexport const inject: readonly string[] = [\n 'slots',\n 'locale',\n 'connection',\n 'remote',\n 'settingsScope',\n]\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars-sea/dsh-commandcode-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Unofficial DeepSeek Harness LLM provider plugin for Command Code, ported from pi-commandcode-provider (MIT). Registers the 'commandcode' provider route with a Models-page card and live model catalog.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"client": {
|
|
24
24
|
"inject": [
|
|
25
|
-
"@deepseek-ai/dsh-client-connection"
|
|
25
|
+
"@deepseek-ai/dsh-client-connection",
|
|
26
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
27
|
+
"@deepseek-ai/dsh-client-locale",
|
|
28
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
29
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
26
30
|
],
|
|
27
31
|
"platform": "web"
|
|
28
32
|
}
|
|
@@ -69,18 +73,33 @@
|
|
|
69
73
|
"license": "MIT",
|
|
70
74
|
"peerDependencies": {
|
|
71
75
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
76
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
|
72
77
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
|
|
78
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
79
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
80
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
73
84
|
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
|
|
74
85
|
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
75
86
|
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
|
|
76
87
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
77
88
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
78
89
|
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
|
|
79
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
90
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
91
|
+
"react": "^18.2.0"
|
|
80
92
|
},
|
|
81
93
|
"devDependencies": {
|
|
82
94
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
95
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
|
83
96
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
|
|
97
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
98
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
99
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
102
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
84
103
|
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
|
|
85
104
|
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
86
105
|
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
|
|
@@ -89,6 +108,7 @@
|
|
|
89
108
|
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
|
|
90
109
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
91
110
|
"@types/node": "^22.0.0",
|
|
111
|
+
"@types/react": "^18.3.1",
|
|
92
112
|
"tsdown": "^0.22.2",
|
|
93
113
|
"tsx": "^4.23.12",
|
|
94
114
|
"typescript": "^6.0.3"
|