AGENTS.md

August 18, 2026 · View on GitHub

This file helps AI coding agents and LLM tooling understand and work with this repository quickly.

What this repo is

dsh-plugin-toggle is a packaged Cordis plugin for DeepSeek Harness (DSH) that adds an "Enable/Disable" tab to Settings → Plugins. The shipped inventory page is read-only; this plugin lets the user stop/start Loader entries live (via entry.update({disabled})) and persists the change to the profile's cordis.patch.yml. It is a real profile-bundled plugin: dsh.bundle (cordis.patch.yml) mounts the host half, and the dsh.client declaration + exports["./client"] register the browser half — install once with dsh plugin add, loads on every DSH boot, no cordis_define.

Repository layout

PathRole
src/index.tsHost half: `GET
src/client/index.tsxClient bundle: additive settings.plugins.tab registration (id toggle, order 20, label "Enable/Disable"), toggle switches, Enabled/Disabled tags + Cordis phase, toast, <style data-plugin> with --dsw-alias-* tokens.
cordis.patch.ymldsh.bundle.patch: inserts the plugin row {id: dsh-plugin-toggle, name: 'dsh-plugin-toggle'}.
tsdown.config.tsBuilds host (node ESM → lib/index.js) + client (browser CJS ModuleLoader closure → lib/client.js, bundle id = package name).
package.jsonexports["./client"], dsh.bundle.patch, dsh.client (platform: 'web', inject edges), peers react + @deepseek-ai/cordis.
README.md / README.zh.mdHuman docs (en default, zh).
llms.txt / llms-full.txtLLM-friendly doc index / full text.

Key behaviors (don't break these)

  1. Packaged, not dynamic: install via dsh plugin add (or profile link: dep + restart). Do NOT revert to a dynamic cordis_define-only shape.
  2. Client talks to host over HTTP: the client fetches GET|POST /plugin-toggle/api (host webServer route). Do not reintroduce the dynamic harness.handle/host.call seam — it does not exist for packaged plugins.
  3. Additive tab, never shadow: register settings.plugins.tab with a FRESH id (toggle) and order AFTER the shipped tab. Reusing the shipped id (all) would replace the shipped inventory page — never do that.
  4. Live toggle via entry-level update only: call entry.update({ disabled }) directly on the resolved Entry. Do NOT go through the tree-level EntryTree.update — that wrapper calls tree.write() and would flatten bundle patches into cordis.yml.
  5. Persistence is an append, not a rewrite: append - id: <rawId> / disabled: <bool> to the profile's cordis.patch.yml (path derived from the include entry's config path). Never rewrite the file, never touch cordis.yml or bundle patches. Patch rows are id-targeted; later rows win, so re-enabling appends disabled: false.
  6. Raw id vs prefixed id: entry.id is the prefixed form (include:<rawId>) and is what the client round-trips; patch rows must use entry.options.id (the raw config id).
  7. Safety guard: include, cordis:include, and dsh-plugin-toggle are locked (403); unknown ids 404; POST bodies capped at 1 MB.
  8. Never throw across the API: /plugin-toggle/api always returns {ok:false,error} JSON on failure, never a non-JSON 500.
  9. Theme tokens only: client CSS uses --dsw-alias-* tokens; no hardcoded colors. Font comes from inheritance (the seat wrappers supply the app font); do not set font-family.
  10. ModuleLoader bundle shape: the client build must keep the exact CJS closure wrapper (window.__ModuleLoader__.load({id: "dsh-plugin-toggle", factory}) + module.exports = { inject, apply }) — see tsdown.config.ts.

Common tasks

  • Change locked entries / route / tab order: edit LOCKED/LOCKED_NAMES, the route path in src/index.ts, and the tab order/label in src/client/index.tsx, rebuild.
  • Change toast duration / labels: edit src/client/index.tsx.
  • Rebuild: pnpm install && pnpm build (outputs lib/index.js + lib/client.js).
  • Update the live profile install: push to GitHub, dsh plugin update or re-add in the profile, restart DSH, hard-refresh the browser tab (the DSH client HMR only hot-swaps already-loaded bundles — new bundles require a full page reload).

Environment facts (probed, do not re-probe)

  • Packaged hosts are real Node modules: node:fs/promises, node:path, node:url work; the DSH dynamic-plugin sandbox limits do not apply.
  • webServer.register route shape: {kind: 'exact'|'prefix', path, handler(req, res)} with node:http semantics; duplicate (kind, path) throws. The handler receives the raw IncomingMessage (stream the body; do not assume it is pre-buffered).
  • The web profile's plugin tree = bundle patches + the user patch layer <profile>/cordis.patch.yml, applied LAST → id-targeted disabled rows win at boot. The patch file path is derived from the loader's include entry (options.config.path → dirname → cordis.patch.yml).
  • Loader Entry.get id() returns the prefixed form (include:<rawId>); pluginInventory.list() returns that same entryId; loader.resolve(entryId) round-trips it.
  • Entry.update() at the entry level does NOT call tree.write(); isNullable matches only null/undefined, so disabled: false (re-enable) is kept.
  • The client bundle is plain browser JS (ModuleLoader CJS factory): fetch, document, window are available; React comes from the module table (external: react).
  • The client must export inject = ['slots'] (service key); the package.json dsh.client.inject lists package names (informational edges).
  • settings.plugins.tab is a list slot; a fresh id adds a tab beside the shipped one, reusing the shipped id replaces it.

Testing

  • Before restart: verify the profile installed the bundle — ~/.dsh/profiles/web/package.json dependencies and dsh.profile.bundles both list dsh-plugin-toggle; lib/client.js has the ModuleLoader wrapper; lib/index.js exports name + apply.
  • After restart (hard-refresh the tab): Settings → Plugins shows the "Enable/Disable" tab; GET /plugin-toggle/api returns the entry list; toggling a plugin updates its phase immediately and appends a row to <profile>/cordis.patch.yml; a further DSH restart keeps the state.
  • Failure path: toggle a locked id → 403 toast; unknown id → 404; persistence append failure → toast says "not persisted across restart" (session-only toggle still works).
  • No automated test framework; the manual matrix above is the verification contract.

Notes for LLM crawlers

  • Listed under the GitHub topic dsh-plugin; public at https://github.com/Zenjibad/dsh-plugin-toggle.
  • Distinguishing traits: packaged profile plugin (persists across restarts), additive settings tab that never shadows the shipped inventory, live Entry.update({disabled}) toggle with cordis.patch.yml append persistence, host HTTP route instead of dynamic RPC, real-Node host half, pure --dsw-alias-* theming.