# Writing an extension for DeepSeek Harness (DSH)

DeepSeek Harness is built on the **Cordis plugin framework** (Koishi-style).
An extension is a single npm package with up to two halves:

| Half | Runs in | What it can do |
|---|---|---|
| **Host** | the `dsh` process (Node) | register slash commands, provide services, schedule jobs, access sessions/agents/LLMs |
| **Client** (`./client`) | the browser GUI | register React UI into **slots**, client-side commands, stores |

A package may be host-only (services, commands), client-only (UI — this
package is an example), or both. This document uses the shipped
`deepseek-peak-pricing-hours` port as the running example — read its
`sources` alongside.

---

## 1. The two contracts in `package.json`

```jsonc
{
  "name": "deepseek-peak-pricing-hours",
  "type": "module",
  "main": "lib/index.js",                       // host entry
  "exports": {
    ".":          { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
    "./client":   { "types": "./lib/client/index.d.ts", "default": "./lib/client.js" },
    "./cordis.patch.yml": "./cordis.patch.yml"
  },
  "dsh": {
    "bundle": { "patch": "./cordis.patch.yml" },   // host: join the profile tree
    "client": {                                     // client: serve the browser half
      "inject": ["@deepseek-ai/dsh-client-runtime",
                 "@deepseek-ai/dsh-client-ui-layout",
                 "@deepseek-ai/dsh-client-ui-commands"],
      "platform": "web"
    }
  }
}
```

Three fields do all the wiring:

1. **`dsh.bundle.patch`** — makes this package a *bundle* (a profile patch
   layer). The `cordis.patch.yml` it points to inserts this package's row
   into the profile's Cordis tree, e.g.:

   ```yaml
   # cordis.patch.yml
   - insert:
       - id: deepseek-peak-pricing
         name: 'deepseek-peak-pricing-hours'
   ```

   `name` is what the loader instantiates as a host plugin; `id` is the
   tree row identity (this is what client-modules uses as the client id).
   A bundle that declares no patch row never activates — the row IS the
   activation.

2. **`exports["./client"]`** — the browser half. The client-modules service
   scans **active loader rows** for packages declaring `dsh.client` with
   `platform: "web"`, resolves `exports["./client"]` to a built JS file, and
   serves it at `/plugins/<name>/client.js` (see "Install & iterate").

3. **`dsh.client.inject`** — client fibers that must be ACTIVE before this
   client applies (mirror it in the client module's `inject` export, below).
   Think of it as "wait until these plugin packages have run their `apply`".

## 2. Host half — a Cordis plugin

```ts
// src/index.ts — the loader accepts a default export OR named exports
export function apply(ctx: Context): void {
  ctx.commands.register({
    name: 'ds-status',                       // no leading slash
    description: 'Print current DeepSeek peak-pricing status',
    handler: async ({ agent }) => {
      const info = getPeakInfo();
      return {
        kind: 'success',
        text: `DeepSeek ${info.label} — ${info.nextTransition}`,
      };
    },
  });
}
```

- Host services live on `ctx`: `ctx.commands` (register/execute slash
  commands, `@deepseek-ai/dsh-commands`), `ctx.sessions`, `ctx.agent`,
  `ctx.projection`/`sessionProjections`, … `ctx.effect(fn)` scopes a
  disposer to the plugin's lifecycle.
- Activation is **service-availability driven**: the loader activates a row
  once every service its `inject` lists is provided. Declare
  `export const inject = ['commands']` (fiber-level) when you need one.
- A *surface* plugin (no host behavior — the port's case) ships an empty
  `export function apply(): void {}`; the row still activates and the client
  half still gets served.

> A client command (`/ds-peak` in this package) **collides** with a host
> command of the same name ("fails loud at candidate synthesis"), so pick
> one side per command name.

## 3. Client half — the browser Cordis context

The client bundle is a single **factory file** (not a browser-native ESM):

```js
window.__ModuleLoader__.load({
  id: "deepseek-peak-pricing-hours",
  factory: (require) => {
    var module = { exports: {} };
    var exports = module.exports;
    Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    // …CJS body; bare require() of seed words / other plugin packages…
    return module.exports;
  },
});
```

The runtime `require` resolves: **seed words** (`react`, `react/jsx-runtime`,
`@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-ui-slots`,
`@deepseek-ai/dsh-client-ui-primitives`, …), shell-own statics, and **graph
rows** (every other active client plugin — require its package name). So your
bundle should leave `react*` and `@deepseek-ai/*` external and inline
everything else (see `build.mjs`).

The module's named exports are the client plugin:

```ts
// src/client/index.tsx
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'; // SlotMap: shell.overlay

export const inject = ['@deepseek-ai/dsh-client-runtime',
                       '@deepseek-ai/dsh-client-ui-layout',
                       '@deepseek-ai/dsh-client-ui-commands'];

export function apply(ctx: ClientContext): void {
  ctx.effect(() => {
    const dispose = ctx.slots.register({
      name: 'shell.overlay',          // which slot to render into
      id: 'deepseek-peak-pricing',    // list-slot entry id
      order: 50,                       // visual order among entries
    }, PeakPill);
    return dispose;
  }, 'peak-pricing: pill');
}
```

### Slots — where UI plugs in

Slots are a typed registry (`@deepseek-ai/dsh-client-ui-slots`). Each slot
has a **kind** (`single` — replace the occupant; `list` — add an entry; `keyed`
— one cell per key; `chain` — routed by selectors) and a **scope** (`root` —
global; `session` — per session with `useSession` hooks; `session-maybe`).
`SlotMap` is augmented per declaring package, so `name` is checked at compile
time — include the declarer's client types in your program (the
`import type {} from '@deepseek-ai/dsh-client-ui-layout/client'` above).

Useful seats today:

| Slot | Declared by | Kind/Scope | Use for |
|---|---|---|---|
| `sidebar.footer.action` | ui-sidebar | list/root | sidebar-footer buttons |
| `conversation.session.header.actions` **/ `.utilities`** | ui-conversation | list/session | header controls |
| `conversation.input.overlay` | ui-conversation | single/session | composer-anchored popups |
| `shell.overlay` | ui-layout | list/root | **frame-wide pills/badges** (this port) |
| `conversation` / `sidebar` / `details` | layout / sidebar | single | replace whole columns (!) |

Components receive a composed props kit: owner props decided by the render
site, framework session hooks for `session` scope (`useSession`,
`sessionId`, `useProjection`), global hooks (`useSessions`,
`useWorkspaces`), plus your declared `inject` business face. The overlay
layer is click-through; interactive entries set `pointerEvents: 'auto'`.

### Client commands

`ctx.commandUi.register({ name, description, available, ui })` adds a `/`
-menu entry whose behavior lives on the client (`ui.kind` today:
`popupSelect`; decorated host commands are also supported via
`ctx.commandUi.decorate`). The port's `/ds-peak` opens a small select that
hides/shows the pill or expands/collapses the bar, driving a tiny vanilla
`useSyncExternalStore` store. Refresh spins a 30 s `setInterval` that bumps
the store version — matching the pi original's auto-refresh.

## 4. Building

`build.mjs` (reflect it in any DSH extension):

1. **Host**: esbuild → `lib/index.js`, `format: 'esm'`, `platform: 'node'`,
   `external: ['@deepseek-ai/*']`.
2. **Client**: esbuild → `lib/client.js`, `format: 'cjs'`,
   `platform: 'browser'`, `external: ['react', 'react/jsx-runtime',
   'react-dom*', '@deepseek-ai/*']`, with the `__ModuleLoader__` header as
   `banner` and `return module.exports` as `footer` (copy the wrapper from
   any shipped `lib/client.js`).
3. **Types**: `tsc --emitDeclarationOnly` → `lib/*.d.ts`.

Typecheck with the real DSH packages as devDependencies (pinned
`^0.1.0-rc.N`): `dsh-client-runtime/client` for `ClientContext`,
`dsh-client-ui-layout/client` for the slot augmentations,
`dsh-client-ui-commands/client` for `ctx.commandUi`.

## 5. Install & iterate

```bash
# from the package checkout (or any npm/git spec):
dsh plugin --profile web add ./dsh        # pnpm forwarder; anchors ./ to cwd
dsh plugin --profile web remove deepseek-peak-pricing-hours
```

After `pnpm add` the CLI **reconciles `dsh.profile.bundles`** from installed
state: a dependency whose manifest declares `dsh.bundle` joins the layer
stack automatically — no manual tree editing. Verify the composed tree
without touching the running server:

```bash
dsh --profile web --dump-config
```

Then **restart `dsh web`** (web HMR is disabled in rc builds — the loader
composes the tree at boot, and the client-modules scan caches package
verdicts per process). The browser fetches
`/plugins/deepseek-peak-pricing-hours/client.js?rev=<hash>`; the rev changes
when the file content changes, so in development rebuild with
`npm run build` and reload once — or restart the server if the running
process caches the bundle path.

### Iteration loop

```
npm install && npm run build            # in the extension package
dsh plugin --profile web add ./dsh      # first time only
dsh --profile web --dump-config         # sanity check the row
<restart dsh web>                       # activate / re-serve the client bundle
```

## 6. Reference card

- **Patch layers**: profile = base bundle → web bundle → your bundle →
  the user's `cordis.patch.yml` → `--patch` overlays; last write wins per
  row `id`; `- insert:` adds rows, `config:` restates a row, `disabled: true`
  kills one.
- **Host plugin shape**: named `apply(ctx)` (+ optional `inject`, `name`,
  `schema`) or a default-export class; loader normalizes with
  `exports.default ?? exports`.
- **Client plugin shape**: named `inject: string[]` + `apply(ctx)` in the
  `./client` module; loaded only for ACTIVE loader rows; injection waits for
  listed client fibers.
- **Style tokens**: theme-aware CSS vars, e.g. `--dsw-alias-label-primary`,
  `--dsw-alias-bg-module-platform`, `--dsw-alias-border-l2`,
  `--dsw-alias-state-error-primary` (red), `--dsw-alias-state-success-primary`
  (green), `--dsw-alias-brand-primary`.