@restregular/dsh-skill-center 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -15
- package/client/client.js +136 -15
- package/lib/index.js +467 -25
- package/package.json +6 -3
- package/skills/skill-authoring/SKILL.md +58 -0
- package/skills/skill-authoring/references/frontmatter.md +71 -0
- package/skills/skill-authoring/references/plugin-bundled.md +89 -0
- package/skills/skill-authoring/references/resources.md +51 -0
- package/skills/skill-authoring/references/roots.md +44 -0
- package/skills/skill-authoring/templates/minimal/SKILL.md +28 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# dsh-skill-center · 技能中心
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@restregular/dsh-skill-center)
|
|
4
4
|
[](./LICENSE)
|
|
@@ -35,12 +35,69 @@ Sidebar Main area
|
|
|
35
35
|
| Browse | Lists every skill visible in the **current session scope**, including skills embedded in plugins, each with provider, source root and invocation policy |
|
|
36
36
|
| Search / filter | Search by name or description; filter by provider source |
|
|
37
37
|
| Read body | Renders the raw `SKILL.md` Markdown inside the panel (no right-hand resource pane needed) |
|
|
38
|
-
| Invocation switches |
|
|
38
|
+
| Invocation switches | Flips `disable-model-invocation` / `user-invocable` locally — the switch moves at once, with no round trip — and writes them into the frontmatter when you press **Save** beside *Back to session* (the button reads 未修改 → 保存 → 已保存); the host pushes the refreshed catalog to the model immediately |
|
|
39
39
|
| Create | Creates `<name>/SKILL.md` under the user Agents or user DSH skills root |
|
|
40
40
|
| Delete | Moves the skill to `.trash/<timestamp>-<name>` in the same root — recoverable, never a hard delete |
|
|
41
41
|
| Read-only guard | Skills embedded in plugins (e.g. `office-docx`) are labelled *embedded* and their switches/deletion are disabled |
|
|
42
|
+
| Bundled skill | Ships an embedded **`skill-authoring`** skill (guidance + `references/` + a starter template) that teaches the agent which root a skill belongs in, what the frontmatter contract is, and where a skill's resource files go |
|
|
43
|
+
| Model tool | Registers **`skill_create`**, which writes `<root>/<name>/SKILL.md` plus any resource files in one call — the path that works for skill roots outside the session workspace, where the file-write tools are sandboxed |
|
|
42
44
|
| Security | Every write is confined to managed skill roots that really exist on disk; requests from non-loopback origins get `403` |
|
|
43
45
|
|
|
46
|
+
## Bundled skill and model tool
|
|
47
|
+
|
|
48
|
+
Beyond the panel, the host half publishes its own guidance for agents and one tool that acts on it.
|
|
49
|
+
|
|
50
|
+
### `skill-authoring` (embedded skill)
|
|
51
|
+
|
|
52
|
+
The package ships `skills/skill-authoring/` and registers it with `ctx.skills.registerProvider()`,
|
|
53
|
+
so it appears in the session catalog exactly like the built-in skills — the model can load it with
|
|
54
|
+
the `skill` tool, and the panel lists it as *embedded* (read-only, because the file belongs to the
|
|
55
|
+
package, not to a managed root).
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
skills/skill-authoring/
|
|
59
|
+
SKILL.md routing + the five-step short path
|
|
60
|
+
references/roots.md the root table, ranks, precedence, project-root detection
|
|
61
|
+
references/frontmatter.md every field, the boolean spellings, why a skill silently does not load
|
|
62
|
+
references/resources.md where resource files go and how the model is told to find them
|
|
63
|
+
references/plugin-bundled.md shipping skill content inside a plugin package
|
|
64
|
+
templates/minimal/SKILL.md starter bundle
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Because the candidate carries `resourceBase: { kind: 'directory', path }`, the loaded skill arrives
|
|
68
|
+
with `Base directory for this skill: <abs path>` in its `<skill_resources>` block, so the agent can
|
|
69
|
+
read `references/*.md` with ordinary file tools. Rank 600 means any project or user skill named
|
|
70
|
+
`skill-authoring` shadows this one — override it by writing your own.
|
|
71
|
+
|
|
72
|
+
### `skill_create` (model tool)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
skill_create({ name, description, whenToUse?, body?, root?, resources?, modelInvocable?, userInvocable?, overwrite? })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Argument | Meaning |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `name` | kebab-case skill name; also the bundle directory name |
|
|
81
|
+
| `description` | frontmatter description — the only routing signal the catalog shows |
|
|
82
|
+
| `whenToUse` | optional `whenToUse` frontmatter field |
|
|
83
|
+
| `body` | the Markdown after the frontmatter; defaults to a `# <name>` heading |
|
|
84
|
+
| `root` | `project-dsh` \| `project-agents` \| `user-dsh` \| `user-agents`; defaults to `user-agents` |
|
|
85
|
+
| `resources` | `[{ path, content }]` written inside the same bundle directory (e.g. `references/roots.md`) |
|
|
86
|
+
| `modelInvocable` / `userInvocable` | `false` writes `disable-model-invocation: true` / `user-invocable: false` |
|
|
87
|
+
| `overwrite` | required to replace an existing skill; the default fails instead of clobbering |
|
|
88
|
+
|
|
89
|
+
It exists because of a real obstacle: the file-write tools are sandboxed to the session workspace,
|
|
90
|
+
while `$DSH_HOME/skills` and `$DSH_AGENTS_HOME/skills` are not in it, so an agent asked to "make me
|
|
91
|
+
a skill" could only fail there. The tool writes with the same confinement as the panel — only the
|
|
92
|
+
four managed roots, only inside `<root>/<name>/` — and rejects absolute resource paths, `..`
|
|
93
|
+
segments, a resource that would overwrite `SKILL.md`, and a name that collides with an embedded
|
|
94
|
+
skill. It registers through `ctx.inject(['tools'], …)`, so a composition without `ctx.tools` simply
|
|
95
|
+
loses the tool instead of failing to mount.
|
|
96
|
+
|
|
97
|
+
> [!NOTE]
|
|
98
|
+
> The tool is registered globally, so **every agent on the host sees `skill_create`** (one extra
|
|
99
|
+
> tool schema in the prompt). Removing the plugin removes it; there is no switch.
|
|
100
|
+
|
|
44
101
|
## Requirements
|
|
45
102
|
|
|
46
103
|
- **DeepSeek Harness** desktop build that provides the `ctx.skills` registry and the loopback web server.
|
|
@@ -67,7 +124,7 @@ Then add the package to the profile's bundle list and restart:
|
|
|
67
124
|
// <profile>/package.json
|
|
68
125
|
{
|
|
69
126
|
"dsh": { "profile": { "bundles": [/* … */ "@restregular/dsh-skill-center"] } },
|
|
70
|
-
"dependencies": { "@restregular/dsh-skill-center": "^0.
|
|
127
|
+
"dependencies": { "@restregular/dsh-skill-center": "^0.3.1" }
|
|
71
128
|
}
|
|
72
129
|
```
|
|
73
130
|
|
|
@@ -80,7 +137,7 @@ because the client registers `sidebar.panellist` with `order: 1`.
|
|
|
80
137
|
> (`@restregular/dsh-skill-center`). `@deepseek-ai/dsh-client-modules` locates a row's
|
|
81
138
|
> `package.json` by that name and compares it strictly (`nearestPackage`). With a bare
|
|
82
139
|
> `dsh-skill-center` the host half still mounts, but the client half is dropped **silently**:
|
|
83
|
-
> no sidebar row,
|
|
140
|
+
> no sidebar row, `op=diagnose` reports `client.inGraph: false` with no `client.url`, and nothing is
|
|
84
141
|
> logged. 0.2.1 fixed exactly this; 0.2.0 shipped the bare name.
|
|
85
142
|
|
|
86
143
|
> [!IMPORTANT]
|
|
@@ -112,8 +169,11 @@ Nothing appears in the sidebar after install + restart? Check in this order:
|
|
|
112
169
|
2. **Did the client half reach the browser module graph?**
|
|
113
170
|
`http://127.0.0.1:19387/dsh-skill-center/api?op=diagnose` — `client.inGraph` must be `true`.
|
|
114
171
|
`false` while `host.mounted` is `true` is the loader-row-name / package-name mismatch above.
|
|
115
|
-
3. **Is the bundle served?**
|
|
116
|
-
|
|
172
|
+
3. **Is the bundle served?** The browser is only ever handed a **revisioned** URL
|
|
173
|
+
(`/plugins/@restregular/dsh-skill-center/client.js?rev=<12 hex>`), and the route deliberately rejects a
|
|
174
|
+
mismatched revision, so requesting the bare path answers `404` even on a healthy install. Read
|
|
175
|
+
`client.url` from `op=diagnose` (it now reports the row it matched in `client.entryId`) and fetch that
|
|
176
|
+
URL instead.
|
|
117
177
|
4. **Re-run the offline composition check** (no host restart needed):
|
|
118
178
|
`node verify-compose.mjs <profileDir>` — it now fails loudly on a loader row name that cannot be
|
|
119
179
|
resolved back to the package.
|
|
@@ -125,7 +185,10 @@ Nothing appears in the sidebar after install + restart? Check in this order:
|
|
|
125
185
|
1. Open a session (or stay on the home screen — see *Known limits*).
|
|
126
186
|
2. Click **Skill Center** in the sidebar.
|
|
127
187
|
3. Pick a skill on the left; the right pane shows provider, source, path, size and the two invocation switches.
|
|
128
|
-
4.
|
|
188
|
+
4. Flip switches as you like: they move immediately and stay staged in the panel. The button to the left of
|
|
189
|
+
*Back to session* tracks the state — **未修改** (greyed, nothing staged) → **保存** (active, changes staged;
|
|
190
|
+
a count appears when several skills are staged) → **已保存** (greyed, written). Editing anything again puts
|
|
191
|
+
it back to 保存. Press **Read body** for the Markdown, or **Create** / **Delete** to manage files.
|
|
129
192
|
|
|
130
193
|
## HTTP API
|
|
131
194
|
|
|
@@ -138,7 +201,7 @@ The host half mounts a **loopback-only** route at `/dsh-skill-center/api`:
|
|
|
138
201
|
| GET | `list` | `sessionId?` `cwd?` | skill catalog + read metadata |
|
|
139
202
|
| POST | `read` | `name` | raw `SKILL.md` text |
|
|
140
203
|
| POST | `setInvocation` | `name` `modelInvocable?` `userInvocable?` | rewrite invocation policy |
|
|
141
|
-
| POST | `create` | `name` `description` `body?` `rootId?` | create a skill bundle |
|
|
204
|
+
| POST | `create` | `name` `description` `body?` `rootId?` `whenToUse?` `resources?` `overwrite?` `modelInvocable?` `userInvocable?` | create (or, with `overwrite`, replace) a skill bundle and its resource files; shares one implementation with `skill_create` |
|
|
142
205
|
| POST | `delete` | `name` | move to `.trash` |
|
|
143
206
|
|
|
144
207
|
Requests from anything other than `127.0.0.1` / `::1` are rejected with `403`.
|
|
@@ -174,7 +237,17 @@ Two halves, no shared imports:
|
|
|
174
237
|
`<projectRoot>/.dsh/skills`, `<projectRoot>/.agents/skills`, `$DSH_HOME/skills`, `$DSH_AGENTS_HOME/skills`.
|
|
175
238
|
- Frontmatter edits are string-level: only the invocation keys are touched, everything else is preserved
|
|
176
239
|
byte for byte, including line-ending style.
|
|
240
|
+
- For skills inside a managed root, `list` takes the invocation policy **from the file on disk** instead of
|
|
241
|
+
the registry snapshot. The skill catalog is cached and the filesystem watcher invalidates it a moment
|
|
242
|
+
after a write, so a panel that trusted the snapshot would show its own stale value right after a save —
|
|
243
|
+
which is exactly what made a flipped switch snap back until a manual refresh.
|
|
177
244
|
- Delete is a `rename` into `.trash`.
|
|
245
|
+
- Also registers an embedded skill provider (`ctx.skills.registerProvider`) for `skills/skill-authoring/`
|
|
246
|
+
and the `skill_create` tool (`ctx.inject(['tools'], …)` + `ctx.tools.register`). The tool schema is a
|
|
247
|
+
hand-written JSON Schema using only the subset `@deepseek-ai/dsh-tools` accepts, so the package keeps
|
|
248
|
+
zero runtime dependencies and needs no host-side module resolution.
|
|
249
|
+
- The three parts degrade independently: a missing `tools` or `webServer` service, or a bundled-skill
|
|
250
|
+
asset that fails to parse, only logs a warning and leaves the rest mounted.
|
|
178
251
|
|
|
179
252
|
**Browser half** — [`client/client.js`](client/client.js)
|
|
180
253
|
|
|
@@ -195,6 +268,11 @@ Styles use host theme tokens only (`--dsw-alias-*`, `--dsw-radius-*`, `--dsw-foc
|
|
|
195
268
|
`<style>` tag carries `data-plugin="dsh-skill-center"` so the host's ownerless-style sweep does not
|
|
196
269
|
reclaim it.
|
|
197
270
|
|
|
271
|
+
Switch edits are staged in component state: the switch reads a local override first and the catalog
|
|
272
|
+
second, and the Save button's three states are derived from the staged set (`pendingEdits`) plus a
|
|
273
|
+
"saved once" flag. After a save the overrides are kept until a fresh catalog actually agrees with them
|
|
274
|
+
(`load()` drops the ones that match), so a lagging host snapshot can never bounce the UI back.
|
|
275
|
+
|
|
198
276
|
## Development
|
|
199
277
|
|
|
200
278
|
```bash
|
|
@@ -216,7 +294,8 @@ New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_m
|
|
|
216
294
|
-Target "D:\path\to\dsh-skill-center"
|
|
217
295
|
```
|
|
218
296
|
|
|
219
|
-
Self-checks (
|
|
297
|
+
Self-checks (the first two run against the local host route with a mini React shim; the third is
|
|
298
|
+
fully offline):
|
|
220
299
|
|
|
221
300
|
```powershell
|
|
222
301
|
$env:ELECTRON_RUN_AS_NODE = 1
|
|
@@ -224,13 +303,42 @@ $exe = "C:\path\to\DeepSeek Harness.exe"
|
|
|
224
303
|
$here = "D:\path\to\dsh-skill-center"
|
|
225
304
|
$env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # client half + real loopback route
|
|
226
305
|
& $exe "$here\verify-compose.mjs" # client half accepted by the module graph
|
|
306
|
+
node "$here\verify-skill.mjs" # bundled skill + skill_create, offline
|
|
227
307
|
```
|
|
228
308
|
|
|
309
|
+
`verify-compose.mjs` must run through the Electron binary: it reads the shipped `@deepseek-ai`
|
|
310
|
+
packages out of `app.asar`, which plain `node` cannot open. `verify-skill.mjs` is the opposite — it
|
|
311
|
+
imports `lib/index.js` with a stub context and needs no host at all. It checks the bundled skill's
|
|
312
|
+
contract (directory name vs frontmatter, non-empty description, `resourceBase` directory, every
|
|
313
|
+
`references/`-style path cited in a table or link actually existing), the tool's JSON Schema against
|
|
314
|
+
the subset `@deepseek-ai/dsh-tools` accepts, and then **really runs** `skill_create` inside a temp
|
|
315
|
+
dir: create, resource placement, overwrite, duplicate names, illegal names, path traversal, flat
|
|
316
|
+
files, explicit project roots, and the three degraded mount paths. It also asserts that a managed-root
|
|
317
|
+
skill's invocation policy comes from the file on disk even when the registry snapshot disagrees.
|
|
318
|
+
|
|
319
|
+
`client-smoke.mjs` renders the real panel against the live loopback route through a mini React shim,
|
|
320
|
+
and then drives the staged-save flow: the switch flips with no write request, Save issues exactly one
|
|
321
|
+
`setInvocation` carrying both flags, the button steps 未修改 → 保存 → 已保存 (and back to 保存 on the
|
|
322
|
+
next edit), and a lagging catalog snapshot cannot bounce the switch back. It intercepts POSTs, so the
|
|
323
|
+
run never rewrites a real `SKILL.md`.
|
|
324
|
+
|
|
229
325
|
> Renaming or relocating the package: the client module table caches package metadata by
|
|
230
326
|
> `(loader baseUrl, loader line name)` — no file timestamps — so a running host keeps the old path.
|
|
231
327
|
> The browser side uses the in-memory snapshot taken at activation and keeps working after a reload;
|
|
232
328
|
> the new path is only resolved after a host restart.
|
|
233
329
|
|
|
330
|
+
### Releasing
|
|
331
|
+
|
|
332
|
+
Bump `version`, commit, push, then `npm publish` from the repo root with a token that has publish
|
|
333
|
+
rights for the scope. The registry answers a successful publish with `202 {"success":true}` and then
|
|
334
|
+
holds the version in a **validating** state: until validation passes, both
|
|
335
|
+
`/@restregular/dsh-skill-center/<version>` and the tarball URL answer `404`, and `dist-tags` still
|
|
336
|
+
points at the previous release. That is the normal window, not a failure. Re-probe the version URL
|
|
337
|
+
with a cache-busting query (`?t=<ticks>`) rather than publishing again: a second attempt for the same
|
|
338
|
+
version fails with `E409 Cannot publish over previously staged version <version>` (and
|
|
339
|
+
`npm stage publish` with `E409 Cannot stage previously published version <version>`), while
|
|
340
|
+
`npm stage list` may show nothing at all.
|
|
341
|
+
|
|
234
342
|
## Known limits
|
|
235
343
|
|
|
236
344
|
- While the panel occupies the main area, the right-hand `dsh-resource://` preview is unavailable
|
|
@@ -241,6 +349,11 @@ $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # cli
|
|
|
241
349
|
embedded skills plus that cwd's project skills; the footer shows the effective cwd and scope tier.
|
|
242
350
|
- Catalog replacement is all-or-nothing: one switch change makes the host resend the whole skill
|
|
243
351
|
catalog message (a Harness design choice, not this plugin's).
|
|
352
|
+
- `skill_create` is registered host-wide, so every agent sees one more tool schema; it has no
|
|
353
|
+
per-agent switch, and only uninstalling the plugin removes it.
|
|
354
|
+
- The bundled `skill-authoring` skill is embedded and therefore read-only in the panel. Because it
|
|
355
|
+
sits at rank 600, a project or user skill with the same name replaces it entirely — that is the
|
|
356
|
+
intended way to customize it.
|
|
244
357
|
|
|
245
358
|
## License
|
|
246
359
|
|
|
@@ -264,12 +377,61 @@ $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # cli
|
|
|
264
377
|
| 浏览 | 列出**当前会话作用域**下模型/用户能看到的所有技能(含插件内嵌技能),带提供方、来源根、调用策略 |
|
|
265
378
|
| 搜索 / 筛选 | 按名称或描述搜索;按来源(provider source)筛选 |
|
|
266
379
|
| 查看正文 | 面板内直接读 `SKILL.md` 原文(Markdown 原文,不经过右栏) |
|
|
267
|
-
| 调用开关 |
|
|
380
|
+
| 调用开关 | 开关先改本地状态(**点了立刻切换**,不发请求),再按「返回会话」左边的 **保存** 按钮写回 frontmatter(按钮三态:未修改 → 保存 → 已保存,多个技能待保存时显示「保存 (n)」);写盘后监听器立刻把新目录推给模型 |
|
|
268
381
|
| 新建 | 在「用户 Agents 技能」或「用户 DSH 技能」根下创建 `<name>/SKILL.md` |
|
|
269
382
|
| 删除 | 移动到同根目录的 `.trash/<时间戳>-<名字>`(可手工恢复,不硬删) |
|
|
270
383
|
| 只读保护 | 插件内嵌技能(如 `office-docx`、`diagnose-windows-sandbox-acl`)标注「内嵌」,开关与删除按钮禁用 |
|
|
384
|
+
| 随包技能 | 附带一个内嵌的 **`skill-authoring`** 指导技能(正文 + `references/` + 起步模板):技能该放哪个根、frontmatter 契约是什么、资源文件放哪、怎么把技能打进插件包 |
|
|
385
|
+
| 模型工具 | 注册 **`skill_create`**:一次调用写出 `<root>/<name>/SKILL.md` 与全部资源文件 —— 技能根在工作区之外时(文件写入工具被沙箱挡住)这是唯一走得通的路径 |
|
|
271
386
|
| 安全 | 写操作只落在**真实存在于磁盘**的受管技能根内;非回环来源一律 `403` |
|
|
272
387
|
|
|
388
|
+
### 随包技能与模型工具
|
|
389
|
+
|
|
390
|
+
除了面板,宿主半边还会给 Agent 发一份指导技能和一个真正能动手的工具。
|
|
391
|
+
|
|
392
|
+
**`skill-authoring`(内嵌技能)**:包内 `skills/skill-authoring/` 通过
|
|
393
|
+
`ctx.skills.registerProvider()` 注册,和第一方内嵌技能一样出现在会话技能目录里 ——
|
|
394
|
+
模型可以用 `skill` 工具加载它,面板把它标成「内嵌」(只读,因为它属于包,不属于受管根)。
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
skills/skill-authoring/
|
|
398
|
+
SKILL.md 路由 + 五步短路径
|
|
399
|
+
references/roots.md 根表、rank、优先级、项目根判定
|
|
400
|
+
references/frontmatter.md 每个字段、布尔写法、为什么技能会静默不加载
|
|
401
|
+
references/resources.md 资源文件放哪、模型怎么被告知去找
|
|
402
|
+
references/plugin-bundled.md 把技能打进插件包的两种注册方式
|
|
403
|
+
templates/minimal/SKILL.md 起步模板
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
候选带上 `resourceBase: { kind: 'directory', path }` 之后,加载出来的
|
|
407
|
+
`<skill_resources>` 里会写明 `Base directory for this skill: <绝对路径>`,
|
|
408
|
+
Agent 因此能用普通文件工具去读 `references/*.md`。rank 600 意味着任何项目级/用户级同名技能都能盖过它 ——
|
|
409
|
+
想改写它就自己建一个同名技能。
|
|
410
|
+
|
|
411
|
+
**`skill_create`(模型工具)**:
|
|
412
|
+
|
|
413
|
+
| 参数 | 含义 |
|
|
414
|
+
|---|---|
|
|
415
|
+
| `name` | kebab-case 技能名,同时是目录名 |
|
|
416
|
+
| `description` | frontmatter 的 description:目录里唯一的路由信号 |
|
|
417
|
+
| `whenToUse` | 可选,写进 frontmatter |
|
|
418
|
+
| `body` | frontmatter 之后的 Markdown 正文;省略则写一个 `# <name>` 标题 |
|
|
419
|
+
| `root` | `project-dsh` / `project-agents` / `user-dsh` / `user-agents`,默认 `user-agents` |
|
|
420
|
+
| `resources` | `[{ path, content }]`,写进同一个技能目录(如 `references/roots.md`) |
|
|
421
|
+
| `modelInvocable` / `userInvocable` | 传 `false` 分别写 `disable-model-invocation: true` / `user-invocable: false` |
|
|
422
|
+
| `overwrite` | 覆盖已存在的技能必须显式传,默认宁可失败也不覆盖 |
|
|
423
|
+
|
|
424
|
+
它存在的理由很具体:文件写入工具被沙箱限制在会话工作区内,而 `$DSH_HOME/skills`、
|
|
425
|
+
`$DSH_AGENTS_HOME/skills` 不在工作区里 —— 用户说「给我做个技能」时,Agent 在那里根本写不进去。
|
|
426
|
+
这个工具用的是和面板同一套落盘实现:只写四个受管根、只写 `<root>/<name>/` 之内,
|
|
427
|
+
并拒绝绝对资源路径、`..` 段、会覆盖 `SKILL.md` 的资源,以及与内嵌技能重名。
|
|
428
|
+
它通过 `ctx.inject(['tools'], …)` 注册,所以没有 `ctx.tools` 的组合只是少这个工具,不会挂不上。
|
|
429
|
+
|
|
430
|
+
> [!NOTE]
|
|
431
|
+
> 工具是**宿主级全局注册**:同一宿主上每个 Agent 都会多看到 `skill_create`(多一份工具 schema)。
|
|
432
|
+
> 没有开关,卸载插件才会消失。
|
|
433
|
+
|
|
434
|
+
|
|
273
435
|
### 安装
|
|
274
436
|
|
|
275
437
|
**市场安装**:侧栏「插件」→ 搜索 `@restregular/dsh-skill-center` → 安装 → 重启宿主。
|
|
@@ -286,7 +448,7 @@ pnpm add @restregular/dsh-skill-center
|
|
|
286
448
|
// <profile>/package.json
|
|
287
449
|
{
|
|
288
450
|
"dsh": { "profile": { "bundles": [/* … */ "@restregular/dsh-skill-center"] } },
|
|
289
|
-
"dependencies": { "@restregular/dsh-skill-center": "^0.
|
|
451
|
+
"dependencies": { "@restregular/dsh-skill-center": "^0.3.1" }
|
|
290
452
|
}
|
|
291
453
|
```
|
|
292
454
|
|
|
@@ -297,8 +459,8 @@ pnpm add @restregular/dsh-skill-center
|
|
|
297
459
|
> patch 里那条 loader 行的 `name` **必须逐字等于包名**(`@restregular/dsh-skill-center`)。
|
|
298
460
|
> 宿主 `@deepseek-ai/dsh-client-modules` 是按这个行名去定位该行的 `package.json` 并**严格比对**
|
|
299
461
|
> `name` 的(`nearestPackage`)。写成裸名 `dsh-skill-center` 时:宿主半边照样挂得上,
|
|
300
|
-
> 但浏览器半边会被**静默丢弃** ——
|
|
301
|
-
>
|
|
462
|
+
> 但浏览器半边会被**静默丢弃** —— 侧栏没有「技能中心」、`op=diagnose` 里 `client.inGraph` 是 `false`
|
|
463
|
+
> 且没有 `client.url`,而且日志里一个字都没有。0.2.1 修的就是这个;0.2.0 发出去的是裸名。
|
|
302
464
|
|
|
303
465
|
> [!IMPORTANT]
|
|
304
466
|
> 客户端 bundle 还必须用**包名**注册:`__ModuleLoader__.load({ id: '@restregular/dsh-skill-center' })`,
|
|
@@ -325,8 +487,10 @@ pnpm add @restregular/dsh-skill-center
|
|
|
325
487
|
profile 的 `node_modules`。
|
|
326
488
|
2. **浏览器半边进模块图了吗**:`…/api?op=diagnose` 里的 `client.inGraph` 必须是 `true`。
|
|
327
489
|
若 `host.mounted` 为 `true` 而它是 `false`,就是上面那个「行名 ≠ 包名」。
|
|
328
|
-
3. **bundle
|
|
329
|
-
|
|
490
|
+
3. **bundle 有没有被服务**:浏览器只会拿到**带版本号**的地址
|
|
491
|
+
(`/plugins/@restregular/dsh-skill-center/client.js?rev=<12 位十六进制>`),而路由**故意拒绝**
|
|
492
|
+
版本不匹配的请求 —— 所以即使一切正常,直接要裸路径也会回 `404`。请从 `op=diagnose` 的
|
|
493
|
+
`client.url` 取地址(现在它还会在 `client.entryId` 里告诉你命中哪一行),再取那个地址。
|
|
330
494
|
4. **跑离线合成自检**(不用重启宿主):`node verify-compose.mjs <profileDir>`,
|
|
331
495
|
现在它会因为「行名解析不回本包」直接 FAIL。
|
|
332
496
|
5. `pnpm add` 必须**在 profile 目录里**执行;装到别处(全局 `npm i -g`、别的目录)profile 合成不到。
|
|
@@ -341,7 +505,16 @@ pnpm add @restregular/dsh-skill-center
|
|
|
341
505
|
- 写操作只针对**落在受管技能根里、真实存在于磁盘**的文件:
|
|
342
506
|
`<projectRoot>/.dsh/skills`、`<projectRoot>/.agents/skills`、`$DSH_HOME/skills`、`$DSH_AGENTS_HOME/skills`。
|
|
343
507
|
- frontmatter 改写是字符串级编辑,只动与调用策略相关的键,其余逐字保留(含行尾风格)。
|
|
508
|
+
- 受管根内的技能,`list` 的调用策略**以磁盘上的文件为准**,不照抄注册表快照:技能目录是缓存的,
|
|
509
|
+
而 watcher 的失效事件总在写盘之后一小会儿才到 —— 只信快照的面板会在刚保存完的瞬间把界面弹回
|
|
510
|
+
旧值,这正是「拨完开关又自己跳回去、手动刷新才变」的根因。
|
|
344
511
|
- 删除只 rename 进 `.trash`。
|
|
512
|
+
- 另注册一个内嵌技能提供方(`ctx.skills.registerProvider`,指向包内 `skills/skill-authoring/`)
|
|
513
|
+
和 `skill_create` 工具(`ctx.inject(['tools'], …)` + `ctx.tools.register`)。
|
|
514
|
+
工具 schema 是手写的原始 JSON Schema,只使用 `@deepseek-ai/dsh-tools` 接受的子集 ——
|
|
515
|
+
本包因此保持零运行时依赖,也不需要宿主侧的模块解析。
|
|
516
|
+
- 三块各自独立降级:缺 `tools` / `webServer`,或者随包技能的资产解析失败,
|
|
517
|
+
都只写一行日志,其余部分照常挂载。
|
|
345
518
|
|
|
346
519
|
**浏览器半边**(`client/client.js`)注册两处槽位,不 require 任何 Harness Client 包:
|
|
347
520
|
|
|
@@ -359,6 +532,11 @@ ctx.slots.inject('main', () => ctx.slots.register({
|
|
|
359
532
|
样式只用宿主主题令牌 `--dsw-alias-*` / `--dsw-radius-*` / `--dsw-focus-ring-*`,
|
|
360
533
|
`<style>` 带 `data-plugin="dsh-skill-center"`(避免被宿主的无主样式回收误删)。
|
|
361
534
|
|
|
535
|
+
开关的改动先落在组件状态里:开关的值取「本地覆盖 → 目录快照」的先后顺序,
|
|
536
|
+
保存按钮的三态由「待保存集合 + 是否保存过」推出。保存之后本地覆盖会继续留着,
|
|
537
|
+
直到某次 `list` 的快照真的与它一致(`load()` 只撤销已经追平的那些)——
|
|
538
|
+
所以宿主的目录快照慢半拍也不会把界面弹回去。
|
|
539
|
+
|
|
362
540
|
### API 示例
|
|
363
541
|
|
|
364
542
|
```powershell
|
|
@@ -373,6 +551,9 @@ Invoke-RestMethod -Method POST -Uri "http://127.0.0.1:19387/dsh-skill-center/api
|
|
|
373
551
|
|
|
374
552
|
非回环来源一律 403。
|
|
375
553
|
|
|
554
|
+
`create` 与模型工具 `skill_create` 共用一份实现:除 `name` / `description` / `body` / `rootId` 外,
|
|
555
|
+
还接受 `whenToUse`、`resources: [{ path, content }]`、`overwrite`、`modelInvocable`、`userInvocable`。
|
|
556
|
+
|
|
376
557
|
### 开发
|
|
377
558
|
|
|
378
559
|
| 改动 | 生效方式 |
|
|
@@ -390,12 +571,38 @@ $exe = "C:\path\to\DeepSeek Harness.exe"; $here = "D:\path\to\dsh-skill-center"
|
|
|
390
571
|
$env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs"
|
|
391
572
|
# 客户端半侧是否会被浏览器模块图接受(复现 dsh-client-modules 的合成校验)
|
|
392
573
|
& $exe "$here\verify-compose.mjs"
|
|
574
|
+
# 随包技能 + skill_create:纯离线,不碰宿主也不碰真实用户技能根
|
|
575
|
+
node "$here\verify-skill.mjs"
|
|
393
576
|
```
|
|
394
577
|
|
|
578
|
+
`verify-compose.mjs` 必须用 Electron 可执行文件跑:它要读 app.asar 里随发行版交付的
|
|
579
|
+
`@deepseek-ai` 包,普通 `node` 打不开 asar。`verify-skill.mjs` 正好相反 ——
|
|
580
|
+
它用桩 ctx 直接 import `lib/index.js`,完全不需要宿主:检查随包技能契约
|
|
581
|
+
(目录名 vs frontmatter、description 非空、`resourceBase` 目录真实存在、正文表格/链接里点到的
|
|
582
|
+
`references/` 等路径是否真的存在)、检查工具 schema 是否落在 `@deepseek-ai/dsh-tools` 支持的子集内,
|
|
583
|
+
然后**真的跑一遍** `skill_create`:新建、资源落盘、覆盖、重名、非法名、路径穿越、平坦文件、
|
|
584
|
+
显式项目根,以及三条降级路径;另外还断言:受管根技能的调用策略取自磁盘文件,即使注册表快照
|
|
585
|
+
给的是旧值。
|
|
586
|
+
|
|
587
|
+
`client-smoke.mjs` 用迷你 React 把真实面板渲染在真宿主回环路由上,再驱动「暂存 + 保存」全流程:
|
|
588
|
+
拨开关不发任何写请求、点保存只发一次带两个策略字段的 `setInvocation`、按钮按
|
|
589
|
+
未修改 → 保存 → 已保存 走(再改一次回到保存)、以及宿主目录快照慢半拍也弹不回开关。
|
|
590
|
+
它拦下了所有 POST,所以跑一遍不会改动任何真实 `SKILL.md`。
|
|
591
|
+
|
|
395
592
|
> 搬迁/改名注意:客户端模块表按 `(loader baseUrl, loader 行名)` 缓存包元信息,不含文件时间戳。
|
|
396
593
|
> 把包挪到别处后,运行中的宿主仍握着旧路径;浏览器端用的是激活时读进内存的快照,
|
|
397
594
|
> 所以刷新页面照常可用,**下次重启宿主**才会解析新路径。
|
|
398
595
|
|
|
596
|
+
### 发版
|
|
597
|
+
|
|
598
|
+
改 `version` → 提交 → 推送 → 在仓库根用有该 scope 发布权限的 token 执行 `npm publish`。
|
|
599
|
+
注册表对成功的发布回 `202 {"success":true}`,随后该版本进入 **validating** 阶段:验证通过之前,
|
|
600
|
+
`/@restregular/dsh-skill-center/<版本>` 与 tarball 地址都回 `404`,`dist-tags` 仍指向上一个版本。
|
|
601
|
+
这是正常窗口,不是失败。**用带时间戳参数的版本地址重新探测**(`?t=<ticks>`)即可,
|
|
602
|
+
不要重复发布:同一版本再发一次会回
|
|
603
|
+
`E409 Cannot publish over previously staged version <版本>`(`npm stage publish` 则回
|
|
604
|
+
`E409 Cannot stage previously published version <版本>`),而 `npm stage list` 可能什么都不显示。
|
|
605
|
+
|
|
399
606
|
### 已知边界
|
|
400
607
|
|
|
401
608
|
- 面板占中央区域时,右栏的 `dsh-resource://` 预览不可用(宿主规定:无会话面时 `sidebarRight.openResource` 抛错)。
|
|
@@ -404,6 +611,10 @@ $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs"
|
|
|
404
611
|
- 没有选中会话时退化为「按工作目录读取」,此时只能看到内嵌技能 + 该 cwd 的项目技能;
|
|
405
612
|
面板底栏会显示实际使用的 cwd 与作用域级别。
|
|
406
613
|
- 目录整体替换制:一次开关改动会让模型的技能目录消息整条重发(这是 Harness 自身的设计)。
|
|
614
|
+
- `skill_create` 是宿主级全局注册:每个 Agent 都会多看到一份工具 schema,没有按 Agent 的开关,
|
|
615
|
+
只有卸载插件才会消失。
|
|
616
|
+
- 随包 `skill-authoring` 是内嵌技能,面板里只读。它排在 rank 600,所以同名项目级/用户级技能会
|
|
617
|
+
整体盖过它 —— 这正是定制它的正道。
|
|
407
618
|
|
|
408
619
|
### 许可
|
|
409
620
|
|
package/client/client.js
CHANGED
|
@@ -83,6 +83,9 @@ window.__ModuleLoader__.load({
|
|
|
83
83
|
noBody: '(正文为空)',
|
|
84
84
|
created: '已创建',
|
|
85
85
|
saved: '已保存',
|
|
86
|
+
unmodified: '未修改',
|
|
87
|
+
save: '保存',
|
|
88
|
+
saveHint: '把改动写回 SKILL.md 的 frontmatter',
|
|
86
89
|
back: '返回会话',
|
|
87
90
|
retry: '重试',
|
|
88
91
|
modelOff: '模型关闭',
|
|
@@ -132,6 +135,9 @@ window.__ModuleLoader__.load({
|
|
|
132
135
|
noBody: '(empty body)',
|
|
133
136
|
created: 'Created',
|
|
134
137
|
saved: 'Saved',
|
|
138
|
+
unmodified: 'Unmodified',
|
|
139
|
+
save: 'Save',
|
|
140
|
+
saveHint: 'Write the changes back into the SKILL.md frontmatter',
|
|
135
141
|
back: 'Back to session',
|
|
136
142
|
retry: 'Retry',
|
|
137
143
|
modelOff: 'model off',
|
|
@@ -389,6 +395,17 @@ window.__ModuleLoader__.load({
|
|
|
389
395
|
const [body, setBody] = React.useState(undefined);
|
|
390
396
|
const [pendingDelete, setPendingDelete] = React.useState(false);
|
|
391
397
|
const [creating, setCreating] = React.useState(false);
|
|
398
|
+
/** 已点开关、还没点"保存"的改动:技能名 → 部分调用策略。 */
|
|
399
|
+
const [pendingEdits, setPendingEdits] = React.useState({});
|
|
400
|
+
/**
|
|
401
|
+
* 面板显示值(技能名 → 部分调用策略),覆盖目录快照里的同名字段。
|
|
402
|
+
* 为什么要本地留存:宿主的技能目录是**缓存**的,写盘之后 chokidar 失效
|
|
403
|
+
* 事件到达前那次 `list` 仍是旧值 —— 只信快照的话,刚点完开关的界面会
|
|
404
|
+
* 自己弹回去(这正是"点了 toggle 不切换、手动刷新才切换"的原因)。
|
|
405
|
+
*/
|
|
406
|
+
const [shownEdits, setShownEdits] = React.useState({});
|
|
407
|
+
/** 本次进入面板后是否保存过:决定按钮显示"未修改"还是"已保存"。 */
|
|
408
|
+
const [savedOnce, setSavedOnce] = React.useState(false);
|
|
392
409
|
|
|
393
410
|
const load = React.useCallback(async () => {
|
|
394
411
|
setBusy(true);
|
|
@@ -397,6 +414,29 @@ window.__ModuleLoader__.load({
|
|
|
397
414
|
const payload = await apiGet('list', pickScope(sessions, scopeCache));
|
|
398
415
|
setCatalog(payload);
|
|
399
416
|
setSelected((previous) => previous ?? payload.skills[0]?.name);
|
|
417
|
+
// 快照已经追平本地面板值的字段就地撤销覆盖,覆盖集合只留"宿主还没追上"的部分。
|
|
418
|
+
setShownEdits((previous) => {
|
|
419
|
+
const next = {};
|
|
420
|
+
let changed = false;
|
|
421
|
+
for (const [name, patch] of Object.entries(previous)) {
|
|
422
|
+
const skill = payload.skills.find((candidate) => candidate.name === name);
|
|
423
|
+
if (skill === undefined) { changed = true; continue; }
|
|
424
|
+
const fresh = {
|
|
425
|
+
modelInvocable: skill.modelInvocable !== false,
|
|
426
|
+
userInvocable: skill.userInvocable !== false,
|
|
427
|
+
};
|
|
428
|
+
const keep = {};
|
|
429
|
+
if (patch.modelInvocable !== undefined && patch.modelInvocable !== fresh.modelInvocable) {
|
|
430
|
+
keep.modelInvocable = patch.modelInvocable;
|
|
431
|
+
}
|
|
432
|
+
if (patch.userInvocable !== undefined && patch.userInvocable !== fresh.userInvocable) {
|
|
433
|
+
keep.userInvocable = patch.userInvocable;
|
|
434
|
+
}
|
|
435
|
+
if (Object.keys(keep).length > 0) next[name] = keep;
|
|
436
|
+
else changed = true;
|
|
437
|
+
}
|
|
438
|
+
return changed ? next : previous;
|
|
439
|
+
});
|
|
400
440
|
} catch (cause) {
|
|
401
441
|
setError(String(cause?.message ?? cause));
|
|
402
442
|
} finally {
|
|
@@ -423,7 +463,7 @@ window.__ModuleLoader__.load({
|
|
|
423
463
|
/**
|
|
424
464
|
* 跑一个写操作,然后重读目录。
|
|
425
465
|
* @param action - 返回 Promise 的动作。
|
|
426
|
-
* @param okMessage -
|
|
466
|
+
* @param okMessage - 成功提示;不传就不显示绿色提示条。
|
|
427
467
|
*/
|
|
428
468
|
const mutate = async (action, okMessage) => {
|
|
429
469
|
setBusy(true);
|
|
@@ -440,16 +480,77 @@ window.__ModuleLoader__.load({
|
|
|
440
480
|
}
|
|
441
481
|
};
|
|
442
482
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
483
|
+
/**
|
|
484
|
+
* 一个技能此刻该显示的调用策略:本地改动优先,其次目录快照。
|
|
485
|
+
* @param skill - 目录条目。
|
|
486
|
+
* @returns `{ modelInvocable, userInvocable }`。
|
|
487
|
+
*/
|
|
488
|
+
const shownInvocation = (skill) => {
|
|
489
|
+
const local = shownEdits[skill.name] ?? {};
|
|
490
|
+
return {
|
|
491
|
+
modelInvocable: local.modelInvocable ?? (skill.modelInvocable !== false),
|
|
492
|
+
userInvocable: local.userInvocable ?? (skill.userInvocable !== false),
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 拨一个开关:只改本地状态,界面立刻反映,写盘交给"保存"。
|
|
498
|
+
* @param skill - 目录条目。
|
|
499
|
+
* @param patch - 本次改动的字段。
|
|
500
|
+
*/
|
|
501
|
+
const editInvocation = (skill, patch) => {
|
|
502
|
+
setShownEdits((previous) => ({
|
|
503
|
+
...previous,
|
|
504
|
+
[skill.name]: { ...(previous[skill.name] ?? {}), ...patch },
|
|
505
|
+
}));
|
|
506
|
+
setPendingEdits((previous) => ({
|
|
507
|
+
...previous,
|
|
508
|
+
[skill.name]: { ...(previous[skill.name] ?? {}), ...patch },
|
|
509
|
+
}));
|
|
510
|
+
setSavedOnce(false);
|
|
511
|
+
setError(undefined);
|
|
512
|
+
setNotice(undefined);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* 保存全部未保存的调用策略改动:逐个 POST,然后重读目录。
|
|
517
|
+
* 重读拿到的可能仍是旧快照(目录缓存),所以显示值继续由 shownEdits 兜住,
|
|
518
|
+
* 直到快照追平 —— 撤销覆盖的逻辑在 load() 里。
|
|
519
|
+
*/
|
|
520
|
+
const saveEdits = async () => {
|
|
521
|
+
const entries = Object.entries(pendingEdits);
|
|
522
|
+
if (entries.length === 0) return;
|
|
523
|
+
setBusy(true);
|
|
524
|
+
setError(undefined);
|
|
525
|
+
setNotice(undefined);
|
|
526
|
+
try {
|
|
527
|
+
for (const [name, patch] of entries) {
|
|
528
|
+
const skill = skills.find((candidate) => candidate.name === name);
|
|
529
|
+
if (skill === undefined) continue;
|
|
530
|
+
const target = { ...shownInvocation(skill), ...patch };
|
|
531
|
+
await apiPost({
|
|
532
|
+
op: 'setInvocation',
|
|
533
|
+
...pickScope(sessions, scopeCache),
|
|
534
|
+
name,
|
|
535
|
+
modelInvocable: target.modelInvocable,
|
|
536
|
+
userInvocable: target.userInvocable,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
setPendingEdits({});
|
|
540
|
+
setSavedOnce(true);
|
|
541
|
+
await load();
|
|
542
|
+
} catch (cause) {
|
|
543
|
+
setError(String(cause?.message ?? cause));
|
|
544
|
+
} finally {
|
|
545
|
+
setBusy(false);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/** 有多少个技能带着未保存的改动。 */
|
|
550
|
+
const pendingCount = Object.keys(pendingEdits).length;
|
|
551
|
+
const saveLabel = pendingCount === 0
|
|
552
|
+
? (savedOnce ? t('saved') : t('unmodified'))
|
|
553
|
+
: pendingCount === 1 ? t('save') : `${t('save')} (${pendingCount})`;
|
|
453
554
|
|
|
454
555
|
const removeSkill = (skill) => mutate(
|
|
455
556
|
async () => {
|
|
@@ -457,6 +558,18 @@ window.__ModuleLoader__.load({
|
|
|
457
558
|
setSelected(undefined);
|
|
458
559
|
setBody(undefined);
|
|
459
560
|
setPendingDelete(false);
|
|
561
|
+
setPendingEdits((previous) => {
|
|
562
|
+
if (!(skill.name in previous)) return previous;
|
|
563
|
+
const next = { ...previous };
|
|
564
|
+
delete next[skill.name];
|
|
565
|
+
return next;
|
|
566
|
+
});
|
|
567
|
+
setShownEdits((previous) => {
|
|
568
|
+
if (!(skill.name in previous)) return previous;
|
|
569
|
+
const next = { ...previous };
|
|
570
|
+
delete next[skill.name];
|
|
571
|
+
return next;
|
|
572
|
+
});
|
|
460
573
|
},
|
|
461
574
|
t('deleted'),
|
|
462
575
|
);
|
|
@@ -481,6 +594,14 @@ window.__ModuleLoader__.load({
|
|
|
481
594
|
? t('subtitle')
|
|
482
595
|
: `${t('total', { n: skills.length })} · ${scopeText}`),
|
|
483
596
|
h('span', { className: 'dsh-sc-spacer', key: 'sp' }),
|
|
597
|
+
h('button', {
|
|
598
|
+
key: 'save',
|
|
599
|
+
type: 'button',
|
|
600
|
+
className: `dsh-sc-btn${pendingCount > 0 ? ' dsh-sc-btn-primary' : ''}`,
|
|
601
|
+
disabled: busy || pendingCount === 0,
|
|
602
|
+
title: t('saveHint'),
|
|
603
|
+
onClick: saveEdits,
|
|
604
|
+
}, saveLabel),
|
|
484
605
|
h('button', {
|
|
485
606
|
key: 'back',
|
|
486
607
|
type: 'button',
|
|
@@ -579,19 +700,19 @@ window.__ModuleLoader__.load({
|
|
|
579
700
|
h('div', { key: 'sw', className: 'dsh-sc-section' }, [
|
|
580
701
|
h(Switch, {
|
|
581
702
|
key: 'model',
|
|
582
|
-
checked: current.modelInvocable
|
|
703
|
+
checked: shownInvocation(current).modelInvocable,
|
|
583
704
|
disabled: busy || current.writable !== true,
|
|
584
705
|
label: t('model'),
|
|
585
706
|
hint: t('modelHint'),
|
|
586
|
-
onChange: (value) =>
|
|
707
|
+
onChange: (value) => editInvocation(current, { modelInvocable: value }),
|
|
587
708
|
}),
|
|
588
709
|
h(Switch, {
|
|
589
710
|
key: 'user',
|
|
590
|
-
checked: current.userInvocable
|
|
711
|
+
checked: shownInvocation(current).userInvocable,
|
|
591
712
|
disabled: busy || current.writable !== true,
|
|
592
713
|
label: t('user'),
|
|
593
714
|
hint: t('userHint'),
|
|
594
|
-
onChange: (value) =>
|
|
715
|
+
onChange: (value) => editInvocation(current, { userInvocable: value }),
|
|
595
716
|
}),
|
|
596
717
|
]),
|
|
597
718
|
current.writable !== true
|