d0ublecl1ck/dsh-skill-dollar ↗★ 0
dsh-skill-dollar
Invoke DeepSeek Harness skills with a $name gesture — conflict-free with / commands, with composer completion and blue text-ref parity. 用 $name 手势调用 DSH skill(与 / 命令不冲突)。 适合需要避免 / 命令冲突、习惯使用 $ 符号快捷调用技能的用户。
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:d0ublecl1ck/dsh-skill-dollarドキュメント
README 全文を読む ↗dsh-skill-dollar
Invoke DeepSeek Harness skills with a $ gesture instead of /.
DSH already uses /name for both host commands and user-invocable skills. This
bundle keeps / for commands and adds a parallel $name gesture for skills,
with the same composer menu, fuzzy ranking, keyboard pick, and deterministic
host-side injection that the / skill path uses.
What it does
- Client half (
client.js) registers a candidate source under the trigger char$over the session skill catalog (skills/list). Typing$in the composer opens the normal trigger menu (title "Skills" / "技能"). Candidates are ordered by match quality first — name prefix, name substring, name subsequence, then description-only — and ties prefer the most recently picked skill, then the most frequently picked, then the host's own order. Picking a row inserts the literal text$skill-name. - Host half (
index.js) listens onagent/pre-step, recognises the whitespace-bounded$nametoken in direct user messages, resolves it through theskillsservice, and appends the canonical `` block — byte-identical to whatdsh-tool-skilldoes for/name.
Because the bridge is plain draft text, a menu pick and a hand-typed $name
are the same gesture, exactly like the built-in slash pipeline.
How the client half plugs into the core
The input-trigger package hard-codes '/' | '@' in its detectTrigger, but
the rest of the pipeline (source registry, menu reducer, keyboard arbitration,
span CAS insertion) is keyed by an opaque trigger string. The client half:
- declares
@deepseek-ai/dsh-client-ui-input-triggerindsh.client.inject; requires that module from its factory and patchesInputTriggerController.prototype.track;- on a live
$querytoken, synthesises a hit withtrigger: '$'and drives the controller's ownreduce/fetchCandidates/refreshHeadersagainst the'$'roster, so MenuView rendering and keyboard arbitration are reused without forking the core package.
When no $token is live the original track runs untouched, so / and @
behaviour is unchanged. Both halves depend on literals DSH may reformat, so
neither fails silently: check-core-surface.mjs reports drift with a non-zero
exit, the client half logs a console error when the controller shape is gone,
and the host half warns once per served bundle when the reference scan no
longer matches. The host half still makes a hand-typed $name work.
Install into a profile
The plugin directory is this repository root.
dsh plugin --profile web add /absolute/path/to/dsh-skill-dollar
dsh --profile web --dump-config
dsh plugin forwards to pnpm inside the profile and rebuilds
dsh.profile.bundles. Then restart the web app so the new client module joins
the boot graph: the host composes the client module manifest at startup, so a
running dsh web must be restarted to serve client.js.
Install into DSH Desktop
DSH Desktop runs its own harness home — ~/Library/Application Support/dsh-desktop/harness
on macOS — and snapshots every installed plugin into a content-addressed
"generation" under profiles/.generations/live/. Two consequences matter:
- a
link:install in~/.dsh/profiles/webdoes not reach Desktop; and - a running Desktop keeps the generation it booted from, so a plugin edit is only picked up after the profile is refreshed and Desktop restarts.
After editing this repo, reinstall/update dsh-skill-dollar in Desktop's
profile (its plugin market or settings surface) so a new generation is built.
Generation installs are restart-based: the running harness keeps the code it
booted with, so press Cmd/Ctrl+Shift+R ("Restart Harness") — or quit and
reopen DSH Desktop — to load the new generation. The version is bumped with
each behavior change so the update is visible. Nothing else needs a manual
step: the decoration is applied at boot by the plugin's host half (see below),
never by editing a file inside the app bundle.
Verify
Repo-local, no external tooling:
node --test # transform, drift guard, registry hook, ranking
node check-core-surface.mjs # DSH core anchors still hold (non-zero on drift)
node --test asserts the textual transform, the served-bytes hook, the drift
warning, and the pure ranking/usage helpers against fixtures, and it re-runs
the transform against every DSH install it can find on this machine. The client
half is browser-side: verify it by opening the web app, typing $ in the
composer, and confirming the skill menu appears and a pick loads the skill.
Blue text-ref decoration ($ parity with /)
The composer's blue "this is a reference" treatment is produced by a plain-text
scan in @deepseek-ai/dsh-client-ui-conversation. Its token regex is
hard-coded to / and @, and the trigger set is closed there — the
input-trigger lexicon is already keyed by arbitrary trigger strings, and this
plugin already publishes its skill names under $, but the scan never looks
for $. No plugin API registers another text-ref trigger, so exact parity
needs two literal substitutions in that bundle:
TEXT_REF_REbecomes/(^|\s)([/@$])([\w-]+)/g;- the
/-only end-boundary rule (a token must end at whitespace or the draft end) is applied to$as well.
Served at runtime (works in DSH Desktop)
Editing the file on disk is not an option inside DSH Desktop: the bundle lives
in app.asar/app.asar.unpacked, whose unpacked files carry SHA-256
integrity hashes (ElectronAsarIntegrity in Info.plist), so a modified
byte fails Electron's integrity check and invalidates the code signature.
Instead the host half (index.js) rewrites the bundle while it is served.
@deepseek-ai/dsh-client-modules snapshots every client bundle in memory at
boot and hands responses back through
ClientModuleRegistry.bundleResource; the plugin wraps that method,
substitutes $ into the scan, and returns the patched bytes. Degradation
stays graceful:
- the wrapper only touches
text/javascriptbodies that contain the shipped scan, so source maps and unrelated bundles pass through untouched; - if there is no
clientModulesservice (older DSH), the plugin logs a hint and the$menu and host injection still work; - the substitution is idempotent, so replaying it changes nothing.
Immutable /plugins URLs are cached by the browser for a year, so different
bytes under the same URL would never be fetched. The plugin also salts the
conversation row's revision (captureArtifactBaseline) and calls
rebuilt(), which recomposes the boot graph with a fresh URL and forces the
fetch. Bump TEXT_REF_PATCH_VERSION in bundle-patch.mjs when the patched
bytes change.
A host that derives revisions from bundle content instead of file metadata
does not see that salt (DSH 0.1.5 and earlier, including the current
npx dsh web). On such a host the plugin logs a hint and you run the legacy
patch once; the host's content-derived revision then changes and busts the
cache by itself.
Legacy fallback
patch-core.mjs still applies the same two substitutions to a bundle on disk
for hosts whose bundle revisions come from content (DSH 0.1.5 and earlier),
which cannot be cache-busted in memory:
node patch-core.mjs # resolve the bundle from the dsh install
node patch-core.mjs --check # exit non-zero when the patch is missing
node patch-core.mjs --file
Re-run it after a DSH upgrade only if you are on such a host; on DSH 0.1.7+ the served-at-runtime patch leaves nothing to re-apply.
Self-check
Everything this plugin adds beyond the official inputTriggers.registerSource
pipeline hangs off two literals in DSH core:
ui-input-triggerdetects only/and@(type TriggerChar = '/' | '@'), soclient.jshooksInputTriggerController.prototype.track;ui-conversation's plain-text scan is/(^|\s)([/@])([\w-]+)/g, so the host half rewrites the served bytes.
Run the check after every DSH upgrade and before shipping a change to either patch:
node check-core-surface.mjs # auto-locate installed DSH homes
node check-core-surface.mjs --root
node check-core-surface.mjs --json
It prints one line per installed surface, quotes the offending scan when one
changed shape, exits 1 on drift, and exits 2 when no installed DSH was
found. If upstream ever widens the scan to $, it reports
decoration-obsolete: the decoration patch and the TEXT_REF_PATCH_VERSION
salt can then be deleted.
Design notes
- The trigger char is deliberately fixed at
$. The host and client halves must agree on it, and a host-only config knob would silently break the picked text, so there is no such knob. - The host half adds no bare package imports (only the
node:cryptobuiltin and its own relative./bundle-patch.mjs) so alink:-installed profile can resolve it; the two small framework helpers it needs (renderSkillContent,createUserMessage) are inlined byte-for-byte from@deepseek-ai/dsh-skilland@deepseek-ai/dsh-llm. - The decoration never writes to the DSH install. Both the served bytes and the
salted revision are applied in memory, so DSH Desktop's integrity-sealed
app.asarand code signature stay valid. $VARSand shell-style tokens do not open the menu: a live query must match^[a-z0-9-]*$, the skill-name grammar.- The ranking usage table lives in
localStorageunderdsh-skill-dollar/usage, is capped at 200 skills, and is best-effort: a locked-down profile or a corrupt value degrades to host order instead of throwing. It adds no runtime dependency.