RestRegular/dsh-skill-center

RestRegular/dsh-skill-center

当前会话的技能中心面板:列出可见技能及其提供方与来源根目录,阅读 SKILL.md,开关模型/用户调用,并可创建、软删除技能;随包附带 skill-authoring 技能与 skill_create 工具。

技能包

dsh-skill-center · 技能中心

English · 中文

A Skill Center panel for DeepSeek Harness: browse every skill a session can see, switch which session scope the panel follows, read its SKILL.md, flip the model/user invocation switches, create and delete skills — without leaving the app and without hand-editing frontmatter. The package ships icon.svg for the plugin manager and uses the same lightning mark for the sidebar entry, so the row and the plugin card match.

[!NOTE] The npm name dsh-skill-center is taken by an unrelated project, so this plugin is published as @restregular/dsh-skill-center. The plugin id stays dsh-skill-center / skill-center.

Sidebar                                   Main area
├ New session                            ┌───────────────────────────────────────────────┐
├ Plugins            ← order 0           │ Skill Center    8 skills · session scope  Back│
├ Skill Center       ← order 1 (this)    ├──────────────┬────────────────────────────────┤
├ Workspaces / Sessions                  │ auth0-quick… │ auth0-quickstart               │
└ Settings                               │ discover-…   │ provider / source / root       │
                                         │ office-docx  │ path / size                    │
                                         │ …            │ [model invocable ●] [user ●]   │
                                         │              │ [Read body]  [Delete]          │
                                         └──────────────┴────────────────────────────────┘

Features

CapabilityDetail
BrowseLists every skill visible in the current session scope, including skills embedded in plugins, each with provider, source root and invocation policy
Switch scopeThe Scope dropdown in the toolbar is a custom popup menu that browses another session's skill scope, with sessions grouped by workspace while archived and blank sessions are left out. Auto (current session) keeps the original follow-the-main-view pick, while any listed session makes list and every write resolve from that session's cwd and preset. It is disabled while invocation edits are staged, so a switch can never carry them into a different scope
Search / filterSearch by name or description; filter by provider source
Read bodyRenders the raw SKILL.md Markdown inside the panel (no right-hand resource pane needed)
Invocation switchesFlips 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
CreateCreates <name>/SKILL.md under the user Agents or user DSH skills root; the form pairs Create with Cancel, and a hint under Write to explains what the choice means
Export / importExport downloads the selected bundle as a .zip — SKILL.md plus every resource file under it, flat skills become <name>.md. Import skill accepts that .zip (a single wrapping folder is stripped) or a bare SKILL.md; it refuses entries whose paths escape the bundle, names that already exist, and files without frontmatter name + description
DeleteMoves the skill to .trash/<timestamp>-<name> in the same root — recoverable, never a hard delete; the confirmation puts 取消 / Cancel to the left of 确认删除? / Confirm delete?
TrashThe Trash button in the toolbar lists every .trash entry across the managed roots, with its root and deletion time. Restore renames one back into its original root (refusing to overwrite a skill that already exists there); Delete in the detail pane removes the selected entry permanently (after 确认删除?, Cancel on its left). Empty trash sits in the top toolbar and clears every entry, also behind a 确认清空? step that pairs a 取消 / Cancel on its left
No success toastsThe panel shows no "saved / created / deleted / restored" notice: the state change itself is the feedback (the save button's three states, a row appearing in or leaving the list, a trash entry leaving the trash). Only failures render, as a red bar with a retry button
Read-only guardSkills embedded in plugins (e.g. office-docx) are labelled embedded and their switches/deletion are disabled
Bundled skillShips 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
Model toolRegisters 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
SecurityEvery write is confined to managed skill roots that really exist on disk; requests from non-loopback origins get 403

Write location: project roots vs the two user roots

Write to (写入位置) is the directory the new skill's files are written into; it decides who can see the skill and how it competes with same-named skills. The form only offers the two user roots.

ChoicePathApplies toNotes
User Agents skills~/.agents/skills ($DSH_AGENTS_HOME/skills)every projectThe cross-agent convention: other tools that scan ~/.agents/skills see it too. This is the panel's default.
User DSH skills~/.dsh/skills ($DSH_HOME/skills)every projectDeepSeek Harness only. Rank 400, so a same-named skill here shadows the ~/.agents/skills one (rank 500).

Project roots — <projectRoot>/.dsh/skills (rank 100) and <projectRoot>/.agents/skills (rank 200) — are not offered in the form (the API and skill_create accept them via rootId). They only apply to that repository and outrank both user roots, which is why a project copy of a skill wins over a user copy with the same name.

Bundled skill and model tool

Beyond the panel, the host half publishes its own guidance for agents and one tool that acts on it.

skill-authoring (embedded skill)

The package ships skills/skill-authoring/ and registers it with ctx.skills.registerProvider(), so it appears in the session catalog exactly like the built-in skills — the model can load it with the skill tool, and the panel lists it as embedded (read-only, because the file belongs to the package, not to a managed root).

skills/skill-authoring/
  SKILL.md                        routing + the five-step short path
  references/roots.md             the root table, ranks, precedence, project-root detection
  references/frontmatter.md       every field, the boolean spellings, why a skill silently does not load
  references/resources.md         where resource files go and how the model is told to find them
  references/plugin-bundled.md    shipping skill content inside a plugin package
  templates/minimal/SKILL.md      starter bundle

Because the candidate carries resourceBase: { kind: 'directory', path }, the loaded skill arrives with Base directory for this skill: <abs path> in its <skill_resources> block, so the agent can read references/*.md with ordinary file tools. Rank 600 means any project or user skill named skill-authoring shadows this one — override it by writing your own.

skill_create (model tool)

skill_create({ name, description, whenToUse?, body?, root?, resources?, modelInvocable?, userInvocable?, overwrite? })
ArgumentMeaning
namekebab-case skill name; also the bundle directory name
descriptionfrontmatter description — the only routing signal the catalog shows
whenToUseoptional whenToUse frontmatter field
bodythe Markdown after the frontmatter; defaults to a # <name> heading
rootproject-dsh | project-agents | user-dsh | user-agents; defaults to user-agents
resources[{ path, content }] written inside the same bundle directory (e.g. references/roots.md)
modelInvocable / userInvocablefalse writes disable-model-invocation: true / user-invocable: false
overwriterequired to replace an existing skill; the default fails instead of clobbering

It exists because of a real obstacle: the file-write tools are sandboxed to the session workspace, while $DSH_HOME/skills and $DSH_AGENTS_HOME/skills are not in it, so an agent asked to "make me a skill" could only fail there. The tool writes with the same confinement as the panel — only the four managed roots, only inside <root>/<name>/ — and rejects absolute resource paths, .. segments, a resource that would overwrite SKILL.md, and a name that collides with an embedded skill. It registers through ctx.inject(['tools'], …), so a composition without ctx.tools simply loses the tool instead of failing to mount.

[!NOTE] The tool is registered globally, so every agent on the host sees skill_create (one extra tool schema in the prompt). Removing the plugin removes it; there is no switch.

Requirements

  • DeepSeek Harness desktop build that provides the ctx.skills registry and the loopback web server.
  • Node.js ≥ 22.13.0 for the package metadata/engine field (the plugin itself is plain ESM and has zero runtime dependencies).

Install

One command (CLI)

dsh plugin --profile web add @restregular/dsh-skill-center

From the plugin marketplace (GUI)

  1. Open the Plugins entry in the sidebar.
  2. Search for @restregular/dsh-skill-center and install it.
  3. Restart the host (or reload the profile) so the host half mounts.

Manually

# inside your DSH profile directory, e.g. ~/.dsh/profiles/desktop
pnpm add @restregular/dsh-skill-center

Then add the package to the profile's bundle list and restart:

// <profile>/package.json
{
  "dsh": { "profile": { "bundles": [/* … */ "@restregular/dsh-skill-center"] } },
  "dependencies": { "@restregular/dsh-skill-center": "^0.3.11" }
}

The package ships its own cordis.patch.yml, so no manual loader edit is required — it inserts the host plugin (id: skill-center) and the client half. The sidebar row appears below Plugins, because the client registers sidebar.panellist with order: 1.

[!IMPORTANT] The loader row's name must equal the package name verbatim (@restregular/dsh-skill-center). @deepseek-ai/dsh-client-modules locates a row's package.json by that name and compares it strictly (nearestPackage). With a bare dsh-skill-center the host half still mounts, but the client half is dropped silently: no sidebar row, op=diagnose reports client.inGraph: false with no client.url, and nothing is logged. 0.2.1 fixed exactly this; 0.2.0 shipped the bare name.

[!IMPORTANT] The client bundle must also call __ModuleLoader__.load({ id: '@restregular/dsh-skill-center' }) — the package name, not the plugin id. The browser module graph numbers rows by package name, and ClientModuleSystem.invalidate(id, rev) (what the HMR rebuilt frame and entries.reload use) deletes factory entries by row id only. A registration id that differs from the row id leaves the stale factory in place, so the next bundle revision throws client-modules: duplicate factory registration … (bundle executed twice without invalidate?) and the whole web boot reports 1 entry did not activate. 0.2.2 fixed this; 0.2.0/0.2.1 used the bare plugin id. The bundle additionally skips registration when that id is already present, so a double execution can no longer take the boot down.

To uninstall: remove the bundle entry and the dependency, then restart.

[!WARNING] Do not leave a bare-name junction/link for this package in the profile's node_modules (e.g. node_modules\dsh-skill-center → the repo). Both the link and the scoped package register the same plugin, which is exactly the two-sources situation behind the duplicate-registration crash. Install the scoped package and nothing else.

Troubleshooting

Nothing appears in the sidebar after install + restart? Check in this order:

  1. Is the host half alive? http://127.0.0.1:19387/dsh-skill-center/api?op=ping should answer {"ok":true,…}. A 404 means the loader row never mounted — the package is not in the profile's dsh.profile.bundles, or is not installed under the profile's node_modules.
  2. Did the client half reach the browser module graph? http://127.0.0.1:19387/dsh-skill-center/api?op=diagnose — client.inGraph must be true. false while host.mounted is true is the loader-row-name / package-name mismatch above.
  3. Is the bundle served? The browser is only ever handed a revisioned URL (/plugins/@restregular/dsh-skill-center/client.js?rev=<12 hex>), and the route deliberately rejects a mismatched revision, so requesting the bare path answers 404 even on a healthy install. Read client.url from op=diagnose (it now reports the row it matched in client.entryId) and fetch that URL instead.
  4. Re-run the offline composition check (no host restart needed): node verify-compose.mjs <profileDir> — it now fails loudly on a loader row name that cannot be resolved back to the package.
  5. pnpm add inside the profile is what makes the package resolvable; installing it elsewhere (a global npm i -g, an unrelated directory) installs nothing the profile can compose.

Usage

  1. Open a session (or stay on the home screen — see Known limits).
  2. Click Skill Center in the sidebar.
  3. Pick a skill on the left; the right pane shows provider, source, path, size and the two invocation switches.
  4. Flip switches as you like: they move immediately and stay staged in the panel. The button to the left of Back to session tracks the state — 未修改 (greyed, nothing staged) → 保存 (active, changes staged; a count appears when several skills are staged) → 已保存 (greyed, written). Editing anything again puts it back to 保存. Press Read body for the Markdown, Create for a new skill, or Delete — whose confirmation pairs 取消 with 确认删除?, cancel sitting on the left. Successful actions print no notice; only failures show a red bar with a retry button.

HTTP API

The host half mounts a loopback-only route at /dsh-skill-center/api:

MethodopParametersPurpose
GETping—liveness probe
GETdiagnosesessionId? cwd?is the host mounted, did the client half reach the browser module graph, resolved skill roots
GETlistsessionId? cwd?skill catalog + read metadata
POSTreadnameraw SKILL.md text
POSTsetInvocationname modelInvocable? userInvocable?rewrite invocation policy
POSTcreatename 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
POSTdeletenamemove to .trash
GETtrashsessionId? cwd?list .trash entries across the managed roots (newest first)
POSTrestorerootId entryrename one .trash entry back into its original root; fails instead of overwriting
POSTdeleteTrashrootId entrypermanently delete one .trash entry
POSTemptyTrashsessionId? cwd? rootId?permanently delete trash entries (all managed roots unless rootId is given)
POSTexportnamebundle the skill as a .zip and return it as base64 (filename, bytes)
POSTimportSkillbase64 filename? rootId?import a .zip bundle or a bare .md; writes into rootId (default user-agents) and rejects traversal paths and existing names

Requests from anything other than 127.0.0.1 / ::1 are rejected with 403.

# list the catalog for a session
curl "http://127.0.0.1:19387/dsh-skill-center/api?op=list&sessionId=<session-id>"

# turn off model invocation for one skill
curl -X POST "http://127.0.0.1:19387/dsh-skill-center/api" \
  -H 'Content-Type: application/json' \
  -d '{"op":"setInvocation","sessionId":"<id>","name":"web-react","modelInvocable":false}'
# PowerShell equivalent (UTF-8 safe)
$body = '{"op":"setInvocation","sessionId":"<id>","name":"web-react","modelInvocable":false}'
Invoke-RestMethod -Method POST -Uri "http://127.0.0.1:19387/dsh-skill-center/api" `
  -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body))

Architecture

Two halves, no shared imports:

Host half — lib/index.js

  • Reads the catalog through ctx.skills, resolving scope with the same three-tier fallback as the official ctx.remote.skills: live agent → session-recorded preset scope (agentPresets.acquireScope) → global registry. The first two tiers decide whether the preset's skill-filesystem mount is visible (with the global tier only, embedded skills are all you see).
  • Writes only touch files that live in a managed skill root and exist on disk: <projectRoot>/.dsh/skills, <projectRoot>/.agents/skills, $DSH_HOME/skills, $DSH_AGENTS_HOME/skills.
  • Frontmatter edits are string-level: only the invocation keys are touched, everything else is preserved byte for byte, including line-ending style.
  • For skills inside a managed root, list takes the invocation policy from the file on disk instead of the registry snapshot. The skill catalog is cached and the filesystem watcher invalidates it a moment after a write, so a panel that trusted the snapshot would show its own stale value right after a save — which is exactly what made a flipped switch snap back until a manual refresh.
  • Delete is a rename into .trash; trash lists those entries, restore renames one back (never overwriting an existing skill), deleteTrash removes one for good, and emptyTrash clears them all. Trash operations resolve only the working directory — they do not need the skill registry, so they keep working when the catalog read fails.
  • Also registers an embedded skill provider (ctx.skills.registerProvider) for skills/skill-authoring/ and the skill_create tool (ctx.inject(['tools'], …) + ctx.tools.register). The tool schema is a hand-written JSON Schema using only the subset @deepseek-ai/dsh-tools accepts, so the package keeps zero runtime dependencies and needs no host-side module resolution.
  • The three parts degrade independently: a missing tools or webServer service, or a bundled-skill asset that fails to parse, only logs a warning and leaves the rest mounted.

Browser half — client/client.js

Registers two slots and requires no Harness client package:

ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
  name: 'sidebar.panellist', id: 'skill-center', order: 1,   // 1 → right below plugins (order 0)
  label: () => t('panel'), locale: NS,
}, SkillCenterIcon));

ctx.slots.inject('main', () => ctx.slots.register({
  name: 'main', key: 'skill-center', locale: NS,             // `main` is keyed: use `key`
}, Page));

Styles use host theme tokens only (--dsw-alias-*, --dsw-radius-*, --dsw-focus-ring-*), and the <style> tag carries data-plugin="dsh-skill-center" so the host's ownerless-style sweep does not reclaim it.

Switch edits are staged in component state: the switch reads a local override first and the catalog second, and the Save button's three states are derived from the staged set (pendingEdits) plus a "saved once" flag. After a save the overrides are kept until a fresh catalog actually agrees with them (load() drops the ones that match), so a lagging host snapshot can never bounce the UI back.

The Scope dropdown is a custom popup (not a native <select>), so it can group candidates by workspace with sticky headings and mark the running session. It builds those candidates from sessions.list and workspaces.list: non-subagent sessions, archived and blank (never-messaged) sessions removed, one group per workspace in host order, ungrouped sessions bucketed by cwd, at most 12 sessions per workspace (most recently updated first). The picked sessionId / cwd rides on every request, reads and writes alike. Auto keeps the original three-step follow (session shown in the main area → last hit → most recently updated); picking a session explicitly does not update that cache, so switching back to Auto returns to the followed session instead of sticking to the manual choice. The source filter and the create form's write-location picker use the same dropdown component.

The whole client bundle is wrapped in an IIFE. Plugin bundles are classic scripts in one page and share a single global lexical environment that survives a plugin's removal, so re-installing or upgrading the plugin while the page is open re-executes the script in the same realm: a top-level const MODULE_ID would throw Identifier 'MODULE_ID' has already been declared, skip the whole script, register no factory, and the sidebar row would only come back after a page reload. client-smoke.mjs executes the bundle twice in one vm global-script context to keep this from regressing.

Development

git clone https://github.com/RestRegular/dsh-skill-center.git
cd dsh-skill-center
ChangeTakes effect
client/client.jsafter a page reload — the host's /plugins/<id>/client.js route re-reads from disk and bumps the rev URL when ctime/size change
lib/index.js (host)after a host restart. Source-level HMR must be configured before launch: add config: { root: ['.', '<abs path to this repo>'] } to the profile's hmr entry — changing it while running has no effect
package.json dsh.*after a host restart

For local development the repo can be mounted as a junction instead of being installed:

# profile is  <DSH_HOME>\profiles\desktop
New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-skill-center" `
  -Target "D:\path\to\dsh-skill-center"

Self-checks (the first two run against the local host route with a mini React shim; the third is fully offline):

$env:ELECTRON_RUN_AS_NODE = 1
$exe  = "C:\path\to\DeepSeek Harness.exe"
$here = "D:\path\to\dsh-skill-center"
$env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs"   # client half + real loopback route
& $exe "$here\verify-compose.mjs"                                          # client half accepted by the module graph
node "$here\verify-skill.mjs"                                              # bundled skill + skill_create, offline

verify-compose.mjs must run through the Electron binary: it reads the shipped @deepseek-ai packages out of app.asar, which plain node cannot open. verify-skill.mjs is the opposite — it imports lib/index.js with a stub context and needs no host at all. It checks the bundled skill's contract (directory name vs frontmatter, non-empty desc