メインコンテンツへスキップ
L

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-workspace

README

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:

WhereWhat it controls
Settings → SkillsThe skills directory (defaults to ~/.dsh/skills)
The ✦ icon in the chat title barWhich 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 / userInvocable are both false.

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:

  1. Register in the global layer only, with rank 350 (ahead of the built-in user root at 400), 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).
  2. Also register on agent.ctx from agent/created (an agent's own layer is nearer than the preset's), same skill → it disappears.
  3. 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
  • name is required and must be kebab-case;
  • description is required — it is the only thing the model routes on;
  • optional whenToUse, disable-model-invocation, user-invocable (booleans accept true/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):

MethodInputReturns
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.

License

MIT

関連プラグイン