dsh-skill-workspace
liufeicc/dsh-skill-workspace
Moves skills into a configurable directory and lets each workspace choose which skills it enables, avoiding context spent on unused skills.
インストール
dsh plugin --profile web add github:liufeicc/dsh-skill-workspaceREADME
dsh-skill-workspace
English | 中文
Keep every skill in one configurable directory, and let each workspace decide which of them it may use.
Two entry points, two jobs:
| Where | What it controls |
|---|---|
| Settings → Skills | The skills directory (defaults to ~/.dsh/skills) |
| The ✦ icon in the chat title bar | Which skills the current workspace enables (checkboxes) |
Why this exists
DSH's built-in skill-filesystem has a fixed user root at ~/.dsh/skills, and it treats
every session the same way: either all skills are visible (and cost context) or none are.
Once you have a few of them, every session pays for skills it will never use.
This plugin does not touch the built-in scanner. It registers its own provider and, for each skill in the directory, returns a candidate under the same name:
- the workspace has it checked → a normal candidate;
get()reads the body; - not checked → a same-name candidate whose
modelInvocable/userInvocableare bothfalse.
The registry merges by "same-layer duplicates are decided by rank, while a nearer layer
overrides a farther one outright", and the invocation policy travels with the winning
entry. An unchecked skill therefore really disappears from the model catalog, and
/name cannot reach it either.
⚠️ The catch: the provider has to be registered twice
The built-in skill-filesystem is mounted inside each agent preset, so its candidates
land in the preset's scope layer. Registering only in the global layer means the preset
layer overrides us — and this is not theory, it is what testing showed:
- Register in the global layer only, with rank
350(ahead of the built-in user root at400), then drop a skill into~/.dsh/skills→ it still shows up in the session catalog (rank only decides within one layer; across layers it is irrelevant). - Also register on
agent.ctxfromagent/created(an agent's own layer is nearer than the preset's), same skill → it disappears. - Check that skill for the current workspace → it comes back.
So apply() registers twice: once globally (a fallback for scopes with no nearer provider)
and once per agent (the one that actually beats the preset layer). Registrations on an
agent's scope are not released when the plugin unloads, so releaseAll() tears them all down
on unload or hot reload, leaving no provider pointing at a dead service.
Project-level skills (<workspace>/.dsh/skills, rank 100) sit on the same layer chain but
from a different source: they are never affected by the checkboxes and stay always
available. This plugin governs the skill library directory only.
Storage
$DSH_HOME/storages/skill_workspace_policy.json
{
"version": 1,
"dir": "/home/liufei/.dsh/skills",
"byWorkspace": {
"/abs/path/to/workspace": ["git-commit-zh", "review"]
}
}
No entry for a workspace means nothing is selected there. That is a product decision, matching the MCP policy, not missing data: an unchecked skill must not cost context.
⚠️ Editing this file by hand does not take effect immediately (the host reads it once when the plugin loads, keeps it in memory afterwards, and does not watch the file). Checking boxes in the UI is immediate: write to disk →
invalidate()→ the catalog refreshes on the next turn.
When changes take effect
- Checking a box: the skill registry is invalidated right away, so sessions in that
workspace see the new catalog on their next turn. The turn already in flight does not
change (the catalog is taken at
agent/pre-step). - Changing the directory: same as above. A missing directory is created with
mkdir -p. - Neither requires a restart or a remount.
Directory layout and file format
Exactly one level is scanned: <dir>/<name>/SKILL.md or <dir>/<name>.md. Nested
**/SKILL.md files are deliberately not supported (same as the built-in provider).
---
name: git-commit-zh
description: Write Chinese commit messages following the team convention. Use when committing code or summarising changes.
---
# The actual instructions
nameis required and must be kebab-case;descriptionis required — it is the only thing the model routes on;- optional
whenToUse,disable-model-invocation,user-invocable(booleans accepttrue/false,yes/no,on/off,1/0).
Malformed files (missing frontmatter, bad name, misspelled boolean) are collected and listed in the UI rather than skipped silently the way the built-in scanner skips them.
Remote API
The client calls these through ctx.remote.skillWorkspace (hand-written remote descriptors,
following dsh-mcp-workspace-policy):
| Method | Input | Returns |
|---|---|---|
list | {} | { dir, defaultDir, missing, skills, errors, shadowed, byWorkspace, storePath, recentErrors } |
set | { workspace, selected } | { applied, workspace, selected, unknown, persisted } |
setDir | { dir } (empty string restores the default) | { applied, dir, created, persisted } |
Development
No dependencies, no build step. The host half is plain ESM; the client half is a
window.__ModuleLoader__ factory.
node test/skill-workspace.mjs # parser, scanner and candidate logic
Changelog
- 2026-10-26 First implementation: configurable directory plus per-workspace checkboxes.
The title-bar MCP button was turned into an icon in the same pass (that change lives in
dsh-split-workspaces:client.skill.js,client.pane.js,client.menu.js). - 2026-10-26 The SKILL popover now reloads its list every time it opens; previously it only reloaded when its state was unhealthy, so a directory changed in Settings did not show up until the page was refreshed.