@fanchaozz/provider-manager 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -59
- package/README_EN.md +16 -22
- package/commands.ts +1 -13
- package/components.ts +75 -57
- package/forms.ts +57 -154
- package/package.json +5 -2
- package/store.ts +1 -0
- package/sync.ts +29 -3
- package/ui.ts +201 -107
package/README.md
CHANGED
|
@@ -14,29 +14,15 @@
|
|
|
14
14
|
pi install npm:@fanchaozz/provider-manager
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
依赖:`@earendil-works/pi-coding-agent`(pi 自带)。jiti 向上找 `node_modules`,**不需要**在扩展目录内 `npm install`。
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
# ~/.npmrc
|
|
21
|
-
@fanchaozz:registry=https://npm.pkg.github.com/
|
|
22
|
-
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
token 只需要 `read:packages` scope(包是 public 的)。之后 `npm install` / `pi install` 对所有 `@fanchaozz/*` 包都不用再配置。
|
|
26
|
-
|
|
27
|
-
### 如果没法访问 GitHub Packages
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
git clone https://github.com/fanchaozz/provider-manager.git
|
|
31
|
-
ln -s "$(pwd)/provider-manager" ~/.pi/agent/extensions/provider-manager
|
|
32
|
-
# Windows: mklink /D "%USERPROFILE%\.pi\agent\extensions\provider-manager" "%CD%\provider-manager"
|
|
33
|
-
```
|
|
19
|
+
安装后,首次加载时会自动创建 `~/.pi/agent/provider-manager.json`(见 [用户配置](#用户配置--provider-managerjson))。删除该文件即可回退到代码默认。
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
---
|
|
36
22
|
|
|
37
|
-
|
|
23
|
+
## 最近变更
|
|
38
24
|
|
|
39
|
-
|
|
25
|
+
版本变更记录看 [CHANGELOG.md](./CHANGELOG.md)。
|
|
40
26
|
|
|
41
27
|
## 快速开始
|
|
42
28
|
|
|
@@ -45,8 +31,8 @@ ln -s "$(pwd)/provider-manager" ~/.pi/agent/extensions/provider-manager
|
|
|
45
31
|
| 打开仪表盘 | `/providers` |
|
|
46
32
|
| 列出 provider + 它们的 model | `/providers ls`(过滤:`/providers ls kdapi`) |
|
|
47
33
|
| 新增 provider | 仪表盘 Providers 面板按 `n`,或 `/providers add [<id>]` |
|
|
48
|
-
| 新增 model |
|
|
49
|
-
| 编辑 provider / model |
|
|
34
|
+
| 新增 / 删除 model | **仅通过 sync**(仪表盘 Models 面板按 `y`)。model 不可手动新增 |
|
|
35
|
+
| 编辑 provider / model | 仪表盘选中后按 `Enter`(表单里 Enter 进入字段 edit、确认) |
|
|
50
36
|
| 删除 | 仪表盘按 `d`(确认对话框) |
|
|
51
37
|
| 从 provider 的 API 拉取新 model 列表 | 仪表盘按 `y`,或 `/providers sync [<pid>]` |
|
|
52
38
|
| 探测 auth + 可达性 + 1-token 测试调用 | 仪表盘 `t`(当前 model)或 `T`(provider 内全部) |
|
|
@@ -54,7 +40,7 @@ ln -s "$(pwd)/provider-manager" ~/.pi/agent/extensions/provider-manager
|
|
|
54
40
|
| 打印命令帮助 | `/providers help` |
|
|
55
41
|
| 关闭仪表盘 | `q` 或 `Esc` |
|
|
56
42
|
|
|
57
|
-
`sync` 命令是给全新 provider 填充 model 列表最快的方式:拉取远端 model 列表,显示 checklist
|
|
43
|
+
`sync` 命令是给全新 provider 填充 model 列表最快的方式:拉取远端 model 列表,显示 checklist,把选中的写回。**model 只能通过 sync 增删**(仪表盘 `n` 在 model 面板已停用)。
|
|
58
44
|
|
|
59
45
|
---
|
|
60
46
|
|
|
@@ -62,10 +48,10 @@ ln -s "$(pwd)/provider-manager" ~/.pi/agent/extensions/provider-manager
|
|
|
62
48
|
|
|
63
49
|
`/providers` 打开两栏 TUI:
|
|
64
50
|
|
|
65
|
-
- **左栏** —— provider(id + model
|
|
66
|
-
- **右栏** —— 选中 provider 的 model(id + `[R]` reasoning / `[I]` image 标记 + ctx / max
|
|
67
|
-
-
|
|
68
|
-
- **底栏** ——
|
|
51
|
+
- **左栏** —— provider(id + auth 状态 + model 数;0 model 时 ⚠ 提示)
|
|
52
|
+
- **右栏** —— 选中 provider 的 model(id + `[R]` reasoning / `[I]` image 标记 + ctx / max;R / I 实际值,未启用是 `-`)
|
|
53
|
+
- **详情面板** —— 选中行分组:Identity / Endpoint / Auth / Capabilities / Limits / Thinking levels / Cost
|
|
54
|
+
- **底栏** —— 按面板调整的按键提示(按 `?` 看完整 help)
|
|
69
55
|
|
|
70
56
|
### 按键绑定
|
|
71
57
|
|
|
@@ -73,29 +59,37 @@ ln -s "$(pwd)/provider-manager" ~/.pi/agent/extensions/provider-manager
|
|
|
73
59
|
|---|---|
|
|
74
60
|
| `↑↓` / `j k` | 在当前面板上下移动 |
|
|
75
61
|
| `g` / `G` | 跳到顶 / 底 |
|
|
76
|
-
|
|
|
77
|
-
| `n` |
|
|
78
|
-
| `
|
|
62
|
+
| `←` / `→` | 切换 Providers ↔ Models 面板 |
|
|
63
|
+
| `n` | **仅 Providers 面板**:新增 provider。Model 面板已停用(model 只能 sync) |
|
|
64
|
+
| `Enter` | 选中行进入 edit 表单(Provider 或 Model) |
|
|
79
65
|
| `d` | 删除(带确认对话框) |
|
|
80
66
|
| `y` | 同步(拉取选中 provider 的远端 model 列表) |
|
|
81
|
-
| `t` / `T` |
|
|
82
|
-
| `?` |
|
|
67
|
+
| `t` / `T` | **仅 Models 面板**:探测当前 model / provider 内全部 model |
|
|
68
|
+
| `?` | 切换帮助覆盖层(按面板显示特有键) |
|
|
83
69
|
| `q` / `Esc` | 关闭仪表盘 |
|
|
84
70
|
|
|
85
|
-
provider
|
|
71
|
+
底部提示按面板动态调整(provider 面板显示 `n`、model 面板显示 `t` / `T`),不会出现在不该出现的面板里。
|
|
86
72
|
|
|
87
73
|
---
|
|
88
74
|
|
|
89
75
|
## 同步流程
|
|
90
76
|
|
|
91
|
-
`sync` 是批量加 model
|
|
77
|
+
`sync` 是批量加 model 的唯一方式。它会拉取选中 provider 的远端 model 列表并显示 checklist。
|
|
92
78
|
|
|
93
|
-
checklist
|
|
79
|
+
**checklist 展示该 provider 的所有 model**(existing + remote new 都有):
|
|
94
80
|
|
|
95
81
|
- 已有 model 标 `<id> (existing)`,默认勾选。取消勾选 = 删除。
|
|
96
82
|
- 远端新 model 只标 `<id>`,默认不勾选。勾选 = 添加。
|
|
97
83
|
|
|
98
|
-
按 `Enter` 写入结果,按 `Esc`
|
|
84
|
+
按 `Enter` 写入结果,按 `Esc` 取消。保存时最终 `models.json` 是 (勾选的 existing) + (勾选的 new) 的并集;远端 new 优先于 local(这样能拉到最新的 `reasoning` / `input` / `ctx` / `maxTokens` / `thinkingLevelMap`)。
|
|
85
|
+
|
|
86
|
+
**新加的 model 字段来自 `~/.pi/agent/provider-manager.json` 的 `defaultModel` 段**(不是代码内置默认)—— 这是 `loadDefaultModelConfig()` 的行为。在 sync 前编辑这个文件可以定制 sync 出来的 model 模板。
|
|
87
|
+
|
|
88
|
+
**proxy 字段**:provider 编辑表单里可填 `proxy`(形如 `http://127.0.0.1:7890`)。sync 时设到 `HTTPS_PROXY` / `HTTP_PROXY` 环境变量,请求结束后还原。其他并发 fetch 会临时看到同一 proxy(env 是进程全局的,sync 一次只 1 个 fetch)。
|
|
89
|
+
|
|
90
|
+
**apiKey 字符约束**:sync 拒绝 `code > 255` 的 apiKey(含 `•`、中文、emoji 等,常见于复制粘贴残留)。错误是 actionable:`apiKey contains non-Latin-1 character at position 7 (U+2022). Re-enter the key in the provider form.`
|
|
91
|
+
|
|
92
|
+
**检测 noise**:默认过滤 `embed*` / `tts` / `whisper` / `dall-e` / `clip` / `moderation` / `image-*` 等 embedding/tts/image-gen 类 model。
|
|
99
93
|
|
|
100
94
|
---
|
|
101
95
|
|
|
@@ -147,40 +141,43 @@ checklist **展示该 provider 的所有 model —— existing + remote new 都
|
|
|
147
141
|
|
|
148
142
|
## 表单编辑器(新增/编辑 model/provider)
|
|
149
143
|
|
|
150
|
-
`addProviderFlow` / `editProviderFlow` / `
|
|
144
|
+
`addProviderFlow` / `editProviderFlow` / `editModelFlow` / `deleteProviderFlow` / `deleteModelFlow` 都共用一个 TUI 表单(`components.ts:FormEditor`)。**`addModelFlow` 已停用**——model 只能通过 sync 增删。
|
|
151
145
|
|
|
152
146
|
### 字段类型
|
|
153
147
|
|
|
154
148
|
| 类型 | 行为 |
|
|
155
149
|
|---|---|
|
|
156
150
|
| `text` | 自由文本输入 |
|
|
157
|
-
| `secret` |
|
|
151
|
+
| `secret` | 同 `text`,但用 `Enter` 进入 edit 后显示真实值(不显示 masked `••••Xn`),切 field 不写回 masked 覆盖原 key |
|
|
158
152
|
| `number` | 自由数字输入,提交时校验 |
|
|
159
|
-
| `select` | 选项列表;按 `
|
|
153
|
+
| `select` | 选项列表;按 `Enter` 进 edit,`Space` 选中,`↑↓` / `jk` 导航,再 `Enter` 退出 |
|
|
160
154
|
| `multiselect` | 类似 `select` 但可多选;`Space` 切换每项 |
|
|
161
155
|
| `levelmap` | 7 行(`off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`);`Space` 切换每项;提交时归一化缺失的 key 为 `null` |
|
|
162
156
|
| `readonly` | 仅展示,不可编辑 |
|
|
163
157
|
|
|
164
|
-
###
|
|
158
|
+
### 按键绑定(统一 view / edit 两态模型)
|
|
165
159
|
|
|
166
|
-
| 键 |
|
|
167
|
-
|
|
168
|
-
|
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `Space` | edit
|
|
173
|
-
|
|
|
174
|
-
|
|
|
160
|
+
| 键 | view 模式 | edit 模式 |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `↑↓` / `j k` | 切字段 | non-typeable:选项内 nav;typeable:no-op(j/k 是字符) |
|
|
163
|
+
| `Enter` | 进 edit | commit + 退出 edit(留在原字段) |
|
|
164
|
+
| `Esc` / `q` | 取消整个 form | 退出 edit(commit) |
|
|
165
|
+
| `s` | 保存整个 form | no-op(typeable 里 s 是字符;non-typeable 忽略) |
|
|
166
|
+
| `Space` | non-typeable 快捷进 edit | non-typeable 切换 / 选中 |
|
|
167
|
+
| `Backspace` | no-op | typeable 删最后一个字符 |
|
|
168
|
+
| 字符 | no-op | typeable 追加到 draft |
|
|
175
169
|
|
|
176
|
-
|
|
170
|
+
**view 模式不接受任何字符输入**(含 `s` / `q` / 数字 / 字母)。需要先按 `Enter` 进 edit 才能改。
|
|
177
171
|
|
|
178
|
-
`
|
|
172
|
+
**typeable 字段的 `j` / `k` 在 view 模式是 nav**(不当作字符),在 edit 模式是字符。
|
|
179
173
|
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
**secret 字段的两种保护**(避免保存的 key 被 masked 显示覆盖):
|
|
175
|
+
1. 进入 edit 时 `draft` 恢复为真实值(不是 masked `••••Xn`),用户能看见 / 修改真 key
|
|
176
|
+
2. commitDraft 在 `draftIsOriginal=true`(用户没改)时不写回,保证原 key 完整
|
|
182
177
|
|
|
183
|
-
`
|
|
178
|
+
### `addProviderFlow` 与 `editProviderFlow` 结构同形
|
|
179
|
+
|
|
180
|
+
两个流程都用同一个 FormEditor 模板(含 `id` / `name` / `baseUrl` / `apiKey` / `api` / `authHeader` / `proxy` 字段)。`addProviderFlow` 的 `id` 字段有 `validate`:字符集 `[a-z0-9_-]+` + 不能与已有 provider 重复(`json.providers[s]` 已存在则报 "provider already exists")。错误留在 FormEditor 自带的 `⚠ ...` 行显示,不弹 notify 打断流程。
|
|
184
181
|
|
|
185
182
|
---
|
|
186
183
|
|
|
@@ -218,21 +215,25 @@ cp ~/.pi/agent/models.json.bak ~/.pi/agent/models.json
|
|
|
218
215
|
|
|
219
216
|
## 故障排查
|
|
220
217
|
|
|
221
|
-
**`pi install` 报 `E404` 或 "no such package"。**
|
|
218
|
+
**`pi install` 报 `E404` 或 "no such package"。** npm registry 没收到这个包名。先 `npm view @fanchaozz/provider-manager` 看是否发布成功;如果成功还报,检查 `npm config get registry` 输出不是 `npm.pkg.github.com`。
|
|
222
219
|
|
|
223
220
|
**仪表盘打开是空的。** 你的 `models.json` 里没有自定义 provider。本扩展只管 `models.json` — pi 内置 provider(anthropic / openai / google 等)不显示,用 pi 内置的 `/model`。
|
|
224
221
|
|
|
225
|
-
**Sync 报 `ECONNREFUSED` / `ENOTFOUND`。** 选中 provider 的 `baseUrl`
|
|
222
|
+
**Sync 报 `ECONNREFUSED` / `ENOTFOUND`。** 选中 provider 的 `baseUrl` 不通。用仪表盘选中该 provider 后按 `Enter` 进 edit 表单改。
|
|
226
223
|
|
|
227
224
|
**Sync 报 `HTTP 500` / `HTTP 401`。** `baseUrl` 错或 `apiKey` 缺失/错。在 provider 编辑表单里核对。
|
|
228
225
|
|
|
229
|
-
|
|
226
|
+
**Sync 报 `Cannot convert argument to a ByteString`。** apiKey 含非 Latin-1 字符(`•`、中文、emoji 等,常见于复制粘贴残留)。**重新打开 provider 的 edit 表单,把 apiKey 字段清空再贴一次**真 key。
|
|
227
|
+
|
|
228
|
+
**Sync 总是 `ctx=128000` / `max=16384`(默认值),看起来没读到 user 配置。** 检查 `~/.pi/agent/provider-manager.json` 是否存在 + 路径正确(必须正好是这个文件名,不是 `providers.json` 等)。校验:`node -e "JSON.parse(require('fs').readFileSync(process.env.HOME+'/.pi/agent/provider-manager.json','utf8'))"`。
|
|
230
229
|
|
|
231
|
-
|
|
230
|
+
**Dashboard 显示的 `apiKey status` 与真状态不一致。** dashboard 不通过 pi runtime 检测 key 是否真有效(避免 OAuth / runtime / env / extension 多路径歧义),只自检 `models.json` 里 `apiKey` 字段是否非空。**真认证测试用 `t` / `T`**。
|
|
232
231
|
|
|
233
|
-
|
|
232
|
+
**保存 secret 字段后 apiKey 变成了 `••••Xn`。** 这是 0.1.x 的老 bug(切 field 时 commitDraft 把 masked 字符串写回原 key)。0.2.0 已修:进入 edit 时 draft 恢复真实值,未改时 commitDraft 跳写。**0.2.0 起,re-enter edit 表单不会改变 key**。如果是老版本升上来:edit 表单里把 apiKey 清空再贴一次真 key。
|
|
233
|
+
|
|
234
|
+
**设的 thinking level 一保存就消失。** pi 可能不支持该 level — 换别的,或者填 `null` 禁用。
|
|
234
235
|
|
|
235
|
-
**按 `n` / `
|
|
236
|
+
**按 `n` / `Enter` / `d` / `y` 后 Esc 仪表盘消失。** 当前版本不应发生 — 仪表盘会自动恢复。如果发生了,请带 `~/.pi/agent/provider-manager.log` 反馈。
|
|
236
237
|
|
|
237
238
|
---
|
|
238
239
|
|
package/README_EN.md
CHANGED
|
@@ -14,29 +14,15 @@ A pi extension that manages custom providers and models in `~/.pi/agent/models.j
|
|
|
14
14
|
pi install npm:@fanchaozz/provider-manager
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The
|
|
17
|
+
The extension depends on `@earendil-works/pi-coding-agent` (shipped with pi). jiti walks up `node_modules`, so **no `npm install` is needed** inside the extension directory.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
# ~/.npmrc
|
|
21
|
-
@fanchaozz:registry=https://npm.pkg.github.com/
|
|
22
|
-
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The token only needs the `read:packages` scope (it's a public package). After that, `npm install` / `pi install` works for all `@fanchaozz/*` packages without further setup.
|
|
26
|
-
|
|
27
|
-
### If you can't reach GitHub Packages
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
git clone https://github.com/fanchaozz/provider-manager.git
|
|
31
|
-
ln -s "$(pwd)/provider-manager" ~/.pi/agent/extensions/provider-manager
|
|
32
|
-
# or, on Windows: mklink /D "%USERPROFILE%\.pi\agent\extensions\provider-manager" "%CD%\provider-manager"
|
|
33
|
-
```
|
|
19
|
+
After install, `~/.pi/agent/provider-manager.json` is auto-created on first load (see [User config](#user-config--provider-managerjson)). Delete it to fall back to the code default.
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
---
|
|
36
22
|
|
|
37
|
-
|
|
23
|
+
## Recent changes
|
|
38
24
|
|
|
39
|
-
|
|
25
|
+
Version history in [CHANGELOG.md](./CHANGELOG.md).
|
|
40
26
|
|
|
41
27
|
## Quick start
|
|
42
28
|
|
|
@@ -88,14 +74,22 @@ When the provider list is empty, pressing `n` on the Models pane switches to the
|
|
|
88
74
|
|
|
89
75
|
## Sync flow
|
|
90
76
|
|
|
91
|
-
`sync` is the
|
|
77
|
+
`sync` is the **only** way to add a batch of models. It fetches the remote model list for the selected provider and shows a checklist.
|
|
92
78
|
|
|
93
79
|
The checklist **shows every model for that provider — both existing and remote new**:
|
|
94
80
|
|
|
95
81
|
- Existing models are labelled `<id> (existing)`, default checked. Uncheck to delete.
|
|
96
82
|
- Remote new models are labelled `<id>` only, default unchecked. Check to add.
|
|
97
83
|
|
|
98
|
-
Press `Enter` to write the result, `Esc` to cancel. On save, the final `models.json` is the union of (checked existing) + (checked new); remote new is preferred over local
|
|
84
|
+
Press `Enter` to write the result, `Esc` to cancel. On save, the final `models.json` is the union of (checked existing) + (checked new); remote new is preferred over local (so you pick up the fresh `reasoning` / `input` / `ctx` / `maxTokens` / `thinkingLevelMap`).
|
|
85
|
+
|
|
86
|
+
**Newly added models' fields come from `~/.pi/agent/provider-manager.json`'s `defaultModel` block** (not the code default) — this is the `loadDefaultModelConfig()` behavior. Edit that file before sync to customise the model template.
|
|
87
|
+
|
|
88
|
+
**proxy field**: a provider's edit form accepts a `proxy` URL (e.g. `http://127.0.0.1:7890`). On sync it's set as `HTTPS_PROXY` / `HTTP_PROXY` env vars and restored after the request. Other concurrent fetches in the same process will see the same proxy while it is set (env is process-global; sync runs at most one fetch at a time).
|
|
89
|
+
|
|
90
|
+
**apiKey character constraint**: sync rejects apiKeys with any character whose code > 255 (`•`, CJK, emoji, etc., typical from copy-paste). The error is actionable: `apiKey contains non-Latin-1 character at position 7 (U+2022). Re-enter the key in the provider form.`
|
|
91
|
+
|
|
92
|
+
**Noise filter**: embedding / tts / image-gen models are filtered by default (`embed*`, `tts`, `whisper`, `dall-e`, `clip`, `moderation`, `image-*`).
|
|
99
93
|
|
|
100
94
|
---
|
|
101
95
|
|
|
@@ -218,7 +212,7 @@ cp ~/.pi/agent/models.json.bak ~/.pi/agent/models.json
|
|
|
218
212
|
|
|
219
213
|
## Troubleshooting
|
|
220
214
|
|
|
221
|
-
**`pi install` fails with `E404` or "no such package".**
|
|
215
|
+
**`pi install` fails with `E404` or "no such package".** The npm registry doesn't have this package. First run `npm view @fanchaozz/provider-manager` to confirm the publish succeeded; if it has, check `npm config get registry` is not `npm.pkg.github.com`.
|
|
222
216
|
|
|
223
217
|
**Dashboard opens but is empty.** Your `models.json` has no custom providers. The extension only manages `models.json` — built-in pi providers (anthropic / openai / google / …) are not shown. Use pi's built-in `/model` for those.
|
|
224
218
|
|
package/commands.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { openDashboard } from "./ui.ts";
|
|
|
11
11
|
import {
|
|
12
12
|
addProviderFlow,
|
|
13
13
|
deleteProviderFlow,
|
|
14
|
-
addModelFlow,
|
|
15
14
|
restoreFromBackupFlow,
|
|
16
15
|
syncFlow,
|
|
17
16
|
} from "./forms.ts";
|
|
@@ -58,17 +57,6 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
58
57
|
await deleteProviderFlow(ctx, id, () => undefined);
|
|
59
58
|
return;
|
|
60
59
|
}
|
|
61
|
-
case "model": {
|
|
62
|
-
const tokens = rest.split(/\s+/);
|
|
63
|
-
const pid = tokens[0];
|
|
64
|
-
if (tokens[1] === "add") {
|
|
65
|
-
if (!pid) { ctx.ui.notify("用法: /providers model <pid> add", "warning"); return; }
|
|
66
|
-
await addModelFlow(ctx, pid, () => undefined);
|
|
67
|
-
} else {
|
|
68
|
-
ctx.ui.notify("用法: /providers model <pid> add", "warning");
|
|
69
|
-
}
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
60
|
case "reset": {
|
|
73
61
|
await restoreFromBackupFlow(ctx, () => undefined);
|
|
74
62
|
return;
|
|
@@ -219,13 +207,13 @@ function cmdHelp(ctx: ExtensionCommandContext): void {
|
|
|
219
207
|
" /providers ls [filter] List providers and their models",
|
|
220
208
|
" /providers add <id> Add a new provider (TUI: open dashboard, press n)",
|
|
221
209
|
" /providers remove <id> Remove a provider (with confirm)",
|
|
222
|
-
" /providers model <pid> add Add a model to a provider (TUI form)",
|
|
223
210
|
" /providers sync [provider-id] Pick a provider (or pass id), fetch remote models, multi-select, write back. TUI: y on selected provider.",
|
|
224
211
|
" /providers test <provider>/<model> Test model (auth + reachable + 1-shot generation). TUI: t on selected model.",
|
|
225
212
|
" /providers test-all [provider] Batch test all models of a provider (concurrency 3). TUI: T on selected provider.",
|
|
226
213
|
" /providers reset Restore models.json.bak (with confirm)",
|
|
227
214
|
" /providers help This help",
|
|
228
215
|
"",
|
|
216
|
+
"Models can only be added/removed via sync (y on a provider).",
|
|
229
217
|
"Switching models is NOT done by this extension — use Ctrl+L or /model.",
|
|
230
218
|
];
|
|
231
219
|
const out = lines.join("\n");
|
package/components.ts
CHANGED
|
@@ -317,7 +317,13 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
317
317
|
if (f.validate) { const err = f.validate(raw); if (err) { this.error = `${f.label}: ${err}`; return { ok: false }; } }
|
|
318
318
|
} else { // text, secret
|
|
319
319
|
if (f.validate) { const err = f.validate(raw); if (err) { this.error = `${f.label}: ${err}`; return { ok: false }; } }
|
|
320
|
-
|
|
320
|
+
// secret: 避免覆写原值。draft 是 masked display ("••••Xn"),如果用户没改(draftIsOriginal=true),
|
|
321
|
+
// 切 field / Enter 退出 edit 都会调 commitDraft,不跳这会写回 masked 字符串覆盖真 key。
|
|
322
|
+
if (f.type === "secret" && this.draftIsOriginal) {
|
|
323
|
+
// no-op,保持 values[f.key] 原值
|
|
324
|
+
} else {
|
|
325
|
+
(this.values as any)[f.key] = raw;
|
|
326
|
+
}
|
|
321
327
|
}
|
|
322
328
|
this.error = null;
|
|
323
329
|
return { ok: true };
|
|
@@ -364,11 +370,14 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
364
370
|
const f0 = this.fields[this.cursor];
|
|
365
371
|
const isNonInput = f0?.type === "select" || f0?.type === "levelmap" || f0?.type === "multiselect";
|
|
366
372
|
const isTypeable = f0 && (f0.type === "text" || f0.type === "secret" || f0.type === "number" || f0.type === "json");
|
|
373
|
+
const isReadonly = f0?.type === "readonly";
|
|
374
|
+
|
|
375
|
+
// 统一模型:所有字段都遵 view / edit 两态。
|
|
376
|
+
// view(editing=false,默认):只响应 Enter(进 edit)、↑↓/j/k(切字段)、s(保存)、Esc/q(取消)
|
|
377
|
+
// edit(editing=true):可修改(typeable 输字符 / non-typeable 按 Space/↑↓)、Enter(提交+退出)、Esc(退出)、↑↓(提交+切字段)
|
|
367
378
|
|
|
368
|
-
// Esc / q →
|
|
369
|
-
|
|
370
|
-
if (matchesKey(data, "escape") || (data === "q" && !isTypeable)) {
|
|
371
|
-
// edit 模式下 Esc = 退出 edit 模式(commit 当前值)
|
|
379
|
+
// 1. Esc / q → edit 中退出(commit);view 模式取消整个 form
|
|
380
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
372
381
|
if (this.editing) {
|
|
373
382
|
this.commitDraft();
|
|
374
383
|
this.editing = false;
|
|
@@ -379,45 +388,66 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
379
388
|
return;
|
|
380
389
|
}
|
|
381
390
|
|
|
382
|
-
// s → 保存整个 form
|
|
383
|
-
|
|
384
|
-
if (data === "s" && !isTypeable) {
|
|
391
|
+
// 2. s → 保存整个 form(仅在 view 模式;edit 模式下 s 是字符 / no-op)
|
|
392
|
+
if (data === "s" && !this.editing) {
|
|
385
393
|
const result = this.commitDraft();
|
|
386
394
|
if (!result.ok) { this.invalidate(); return; }
|
|
387
395
|
this.onSave(this.values);
|
|
388
396
|
return;
|
|
389
397
|
}
|
|
390
398
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
this.invalidate();
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
// Enter 行为
|
|
398
|
-
// edit 模式下 → 退出 edit 模式(commit 当前值)
|
|
399
|
-
// 输入字段 → 提交 + 移动到下一字段
|
|
400
|
-
// 其他(readonly)→ 提交整个 form 并保存
|
|
399
|
+
// 3. Enter → toggle edit(readonly 不响应)
|
|
400
|
+
// view: 进 edit(所有 typeable / non-typeable)
|
|
401
|
+
// edit: commit + 退出 edit(不走下一字段,留在原字段;用户用 ↑↓ 切)
|
|
401
402
|
if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
|
|
402
|
-
if (
|
|
403
|
+
if (isReadonly) return;
|
|
404
|
+
if (this.editing) {
|
|
403
405
|
this.commitDraft();
|
|
404
406
|
this.editing = false;
|
|
405
407
|
this.invalidate();
|
|
406
408
|
return;
|
|
407
409
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
this.
|
|
410
|
+
// 进 edit。secret field:draft = 原值(不是 masked 显示),用户可看/可改真 key;
|
|
411
|
+
// commitDraft 会配合 draftIsOriginal 避免覆写未改的原值。
|
|
412
|
+
if (f0?.type === "secret") {
|
|
413
|
+
const orig = (this.values as any)[f0.key];
|
|
414
|
+
this.draft = (orig === undefined || orig === null) ? "" : String(orig);
|
|
415
|
+
this.draftIsOriginal = true;
|
|
416
|
+
}
|
|
417
|
+
this.editing = true;
|
|
418
|
+
this.invalidate();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 4. ↑↓:
|
|
423
|
+
// view / edit + typeable:切字段(edit 模式下 commit + 退 edit)
|
|
424
|
+
// edit + non-typeable:在 step 7 处理(options 内 nav)
|
|
425
|
+
if (matchesKey(data, "down") || matchesKey(data, "up")) {
|
|
426
|
+
if (!(this.editing && isNonInput)) {
|
|
427
|
+
this.commitDraft();
|
|
428
|
+
this.move(matchesKey(data, "up") ? -1 : 1);
|
|
413
429
|
this.editing = false;
|
|
414
430
|
return;
|
|
415
431
|
}
|
|
416
|
-
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 5. j/k:
|
|
435
|
+
// view:切字段
|
|
436
|
+
// edit + typeable:字符(下面 printable 处理)
|
|
437
|
+
// edit + non-typeable:options 内 nav(在 step 7 处理)
|
|
438
|
+
if ((data === "j" || data === "k") && !this.editing) {
|
|
439
|
+
this.move(data === "j" ? 1 : -1);
|
|
417
440
|
return;
|
|
418
441
|
}
|
|
419
442
|
|
|
420
|
-
//
|
|
443
|
+
// 6. Space:view + non-typeable 快捷进 edit(与 Enter 等价)
|
|
444
|
+
if (data === " " && isNonInput && !this.editing) {
|
|
445
|
+
this.editing = true;
|
|
446
|
+
this.invalidate();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 7. edit + non-typeable:↑↓/j/k 在 options 内移动,Space pick
|
|
421
451
|
if (this.editing && isNonInput) {
|
|
422
452
|
if (f0!.type === "levelmap") {
|
|
423
453
|
if (matchesKey(data, "down") || data === "j") {
|
|
@@ -454,7 +484,7 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
454
484
|
}
|
|
455
485
|
return; // edit 模式下其他键不响应
|
|
456
486
|
}
|
|
457
|
-
// multiselect
|
|
487
|
+
// multiselect 字段:↑↓/j/k 选 option,Space toggle(在值里加/去)
|
|
458
488
|
if (f0!.type === "multiselect" && f0!.options) {
|
|
459
489
|
const opts = f0!.options;
|
|
460
490
|
if (matchesKey(data, "down") || data === "j") {
|
|
@@ -481,38 +511,24 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
481
511
|
}
|
|
482
512
|
}
|
|
483
513
|
|
|
484
|
-
//
|
|
485
|
-
if (
|
|
486
|
-
this.commitDraft();
|
|
487
|
-
this.move(1);
|
|
488
|
-
this.editing = false; // 切到新字段退出 edit
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (matchesKey(data, "up") || (isNonInput && data === "k")) {
|
|
492
|
-
this.commitDraft();
|
|
493
|
-
this.move(-1);
|
|
494
|
-
this.editing = false;
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// readonly 字段:忽略
|
|
499
|
-
if (f0?.type === "readonly") return;
|
|
514
|
+
// 8. readonly:后续输入不响应
|
|
515
|
+
if (isReadonly) return;
|
|
500
516
|
|
|
501
|
-
//
|
|
502
|
-
if (
|
|
503
|
-
this.editing = true;
|
|
504
|
-
this.invalidate();
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
517
|
+
// 9. view 模式:不接受任何字符输入(需先 Enter 进 edit)
|
|
518
|
+
if (!this.editing) return;
|
|
507
519
|
|
|
508
|
-
//
|
|
520
|
+
// 10. Backspace:仅在 edit + typeable 删除 draft
|
|
509
521
|
if (matchesKey(data, "backspace")) {
|
|
522
|
+
if (!isTypeable) return;
|
|
510
523
|
if (this.draft.length > 0) this.draft = this.draft.slice(0, -1);
|
|
511
524
|
this.draftIsOriginal = false;
|
|
512
525
|
this.invalidate();
|
|
513
526
|
return;
|
|
514
527
|
}
|
|
528
|
+
|
|
529
|
+
// 11. 可打印字符:仅在 edit + typeable 追加 draft
|
|
515
530
|
if (data.length === 1 && data.charCodeAt(0) >= 32 && data.charCodeAt(0) < 127) {
|
|
531
|
+
if (!isTypeable) return;
|
|
516
532
|
// number 字段:第一个数字替换(避免 100 + "2" = 1002),后续 append
|
|
517
533
|
if (f0?.type === "number" && /^\d$/.test(data) && this.draftIsOriginal) {
|
|
518
534
|
this.draft = data;
|
|
@@ -671,8 +687,9 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
671
687
|
valueStr = isActive ? raw : th.fg("text", raw);
|
|
672
688
|
}
|
|
673
689
|
const prefix = isActive ? th.fg("accent", "▸ ") : " ";
|
|
690
|
+
const editMarker = isActive && this.editing ? th.fg("accent", " [● edit]") : "";
|
|
674
691
|
const hint = f.hint ? " " + th.fg("muted", f.hint) : "";
|
|
675
|
-
const line = prefix + labelStr + valueStr + hint;
|
|
692
|
+
const line = prefix + labelStr + valueStr + editMarker + hint;
|
|
676
693
|
lines.push(truncateForRender(line, width));
|
|
677
694
|
}
|
|
678
695
|
|
|
@@ -680,12 +697,13 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
680
697
|
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
681
698
|
const f = this.fields[this.cursor];
|
|
682
699
|
const hints: string[] = ["↑↓ field"];
|
|
683
|
-
if (f?.type === "multiselect") hints.push(this.editing ? "↑↓ option · Space toggle · Enter commit" : "
|
|
684
|
-
else if (f?.type === "select") hints.push(this.editing ? "↑↓ option · Space pick · Enter commit" : "
|
|
685
|
-
else if (f?.type === "levelmap") hints.push(this.editing ? "↑↓ level · Space toggle · Enter commit" : "
|
|
686
|
-
else hints.push("
|
|
687
|
-
hints.push("
|
|
688
|
-
hints.push("
|
|
700
|
+
if (f?.type === "multiselect") hints.push(this.editing ? "↑↓ option · Space toggle · Enter commit" : "Enter edit · Space toggle");
|
|
701
|
+
else if (f?.type === "select") hints.push(this.editing ? "↑↓ option · Space pick · Enter commit" : "Enter edit · Space pick");
|
|
702
|
+
else if (f?.type === "levelmap") hints.push(this.editing ? "↑↓ level · Space toggle · Enter commit" : "Enter edit · Space toggle");
|
|
703
|
+
else if (f?.type === "readonly") hints.push("readonly");
|
|
704
|
+
else hints.push(this.editing ? "type to edit" : "Enter edit · type");
|
|
705
|
+
hints.push(this.editing && f && (f.type === "text" || f.type === "secret" || f.type === "number" || f.type === "json") ? "Backspace del" : "Backspace");
|
|
706
|
+
hints.push(this.editing ? "Enter commit" : "s save");
|
|
689
707
|
hints.push("Esc cancel");
|
|
690
708
|
lines.push(th.fg("dim", " " + hints.join(" · ")));
|
|
691
709
|
|