Files
apes-sidebar/AGENTS.md
T
figmar a94c43343d docs(apes-sidebar): 修正 client 契约描述 + 记录 __ModuleLoader__ bundle 坑
- 规则 2 / Layout / Stack 段:client 入口实为命名 export function apply(ctx)
  (tsdown 打包为自包含 bundle,导出 { apply, ... }),非 AGENTS 旧述的
  'CJS closure factory — module.exports = (hostApi) => {}'——hostApi 描述失实,
  实际经 ctx.betterSidebar / ctx.effect 接线
- Gotchas 新增:lib/client.js 第 1 行是 window.__ModuleLoader__.load({...}),
  是 DSH browser 模块加载器格式,Node 侧 require 直接抛 'window is not
  defined'。这坐实 cordis scratch 挂载冒烟无法进单测门禁——client 产物
  按构造是 browser-only,不仅是缺 dsh CLI

门禁:typecheck rc=0 · build rc=0 · test 29/29
2026-09-04 20:20:09 +08:00

158 lines
8.1 KiB
Markdown

# apes-sidebar — engineering conventions
A DSH (DeepSeek Harness) **sidebar plugin** that registers five information-layer
tabs into the `dsh-better-sidebar` host: `apes:tasks`, `apes:agents`,
`apes:apps`, `apes:messages`, `apes:profile`.
This repo is a **from-scratch v2.0.0 rewrite** of the legacy JS-bundle sidebar.
The engineering template is `dsh-better-sidebar` (TS + tsdown + host/client
split). If a convention here conflicts with `dsh-better-sidebar`, follow
better-sidebar and file a note in `docs/`.
## Stack
- **Language**: TypeScript (strict, `noUncheckedIndexedAccess`, ESM, `.ts` extension imports)
- **Build**: `tsdown` (rolldown). Dual output — `lib/index.js` + `lib/invariant.js`
(host, ESM) and `lib/client.js` (client, self-contained bundle with a named
`apply` export)
- **Type check**: `tsc --noEmit -p tsconfig.json`
- **Unit tests**: `vitest` (node environment, no DOM)
- **Package manager**: `pnpm` (Node ≥ 22.13 required — `node:sqlite` + pnpm 11)
- **Peer**: `dsh-better-sidebar` ≥ 0.18.0 (host-provided, never bundled)
- **Dev-only types**: `@deepseek-ai/cordis` (types; host provides at runtime)
## Commands
```bash
pnpm install # install deps (preinstall re-checks on build)
pnpm typecheck # tsc --noEmit (strict)
pnpm build # rm -rf lib && tsc (d.ts) && tsdown (bundles)
pnpm test # vitest run (unit only; no e2e in Phase 0)
pnpm build && pnpm test && pnpm typecheck # the full gate
```
Run the **full gate** before pushing. All three must be green.
## Layout
```
src/
index.ts # HOST entry: export name/inject/apply (namespace plugin shape)
invariant.ts # invariant surface (assert, semver, peer guard) — dep-free
context-types.ts # service face contracts (ApesContext = CordisContext & {...})
client/
index.tsx # CLIENT entry: named `apply(ctx)` registering apes:* tabs
locales.ts # i18n (zh default, en fallback) + useApesLocale hook
tests/
smoke.spec.ts # host plugin shape + apply + invariant surface
locales.spec.ts # i18n contract against a mock ApesLocaleService
tsdown.config.ts # dual build config (host ESM + client CJS)
vitest.config.ts
cordis.patch.yml # plugin registration into the DSH profile
```
## Hard rules
1. **Namespace plugin export shape.** `src/index.ts` MUST export
`{ name, inject, apply }` as named exports — **never** a `default` export.
A stray `default` makes the cordis `Loader.unwrapExports` discard
`name`/`inject`/`apply` and the plugin silently fails to mount.
`tests/smoke.spec.ts` pins this.
2. **Host / client split is load-bearing.**
- The **host** half (`lib/index.js`) runs in Node. It may use Node builtins.
- The **client** half (`lib/client.js`) runs in the DSH browser shell. Its
entry is a **named `export function apply(ctx: ApesClientContext)`** (not a
default export); tsdown bundles it to a self-contained module whose export
face is `{ apply, ... }`. The host wires it via `ctx.betterSidebar` /
`ctx.effect` (read from `ctx`, not a separately-injected `hostApi`).
React is external (host-provided). Never `import` a Node builtin into
`src/client/**`.
- `src/invariant.ts` is reachable from BOTH halves — it must stay
dependency-free (no Node builtins, no @deepseek-ai imports).
3. **`dsh-better-sidebar` is a peerDependency, not a bundled dep.** The tsdown
config lists it in `deps.neverBundle`. If a build inlines it, the client
bundle ships a second copy of the sidebar registry — tabs register into a
dead instance and never appear.
4. **`@deepseek-ai/cordis` is devDependency (types only).** The host provides
the real cordis context at runtime. Do NOT add cordis to `dependencies` or
to `deps.neverBundle` — that would force the client to resolve it.
5. **Tab IDs are `apes:*`.** Never `wb:*` (legacy prefix) or any better-sidebar
internal id. The five Phase 1 tabs: `apes:tasks`, `apes:agents`,
`apes:apps`, `apes:messages`, `apes:profile`.
6. **Scope lock.** This plugin is the **information layer only**:
tasks / agents / apps / messages / profile. Do NOT add file/terminal/browser
panels — those belong to `dsh-better-sidebar` itself. Do NOT add team/room
features — those belong to `dsh-background-agents`.
7. **Bilingual i18n.** Every user-facing string goes through `t(key)` in
`src/client/locales.ts` with BOTH a `zh` and an `en` entry. No raw strings in
JSX.
8. **pnpm build-script approval.** `node-pty` (a transitive runtime dep of
better-sidebar's terminal panel) needs its native build approved once:
`pnpm approve-builds` (pick `node-pty`, `y`). Without this, `pnpm build`'s
preinstall dep-check hard-fails. It is approved in the committed
`pnpm-lock.yaml` state — don't reset it.
## Testing conventions
- Unit tests run in a **node** vitest environment. No DOM, no jsdom (the
client React components are not rendered in Phase 0 — a full cordis scratch
mount smoke lands in Phase 1).
- The host `apply()` is tested against a **minimal fake context** (see
`tests/smoke.spec.ts`) — never against a live DSH shell in CI.
- The locale contract is tested against a **mock `ApesLocaleService`** (see
`tests/locales.spec.ts`).
- A test that needs the real DSH host is a **Phase 1+** concern (cordis
scratch profile); do not add such tests to Phase 0.
## Gotchas (learned, don't rediscover)
- `tsdown` 0.22 deprecated `external`/`noExternal` in favour of
`deps.neverBundle`/`deps.alwaysBundle`. **`external` and `deps.neverBundle`
cannot coexist** — using both throws at build. Use only the `deps` API.
- `allowImportingTsExtensions` (for `.ts`-extension imports) is set in
`tsconfig.json`. `tsconfig.build.json` inherits it but emits `d.ts`, which is
compatible (tsdown handles the strip). If `tsc` build complains, the
declaration emit needs `emitDeclarationOnly` + `noEmit: false` — that is the
current `tsconfig.build.json` setup.
- `node-pty` build approval is **per-machine state** in `pnpm-lock.yaml` + the
pnpm store. A fresh `pnpm install` on a clean checkout will re-prompt.
- The workspace path contains spaces and Chinese characters
(`文稿 - figmarLite的MacBook Pro/...`) — always quote it in shell commands.
- **`lib/client.js` is a DSH browser `__ModuleLoader__` bundle, NOT plain
CJS/ESM.** Line 1 is `window.__ModuleLoader__.load({...})` — it is consumed
by the DSH browser shell's module loader and **cannot be `require()`d in
Node** (top-level `window` reference throws `window is not defined`). This is
why the cordis scratch mount smoke cannot run in the unit gate: the client
artifact is browser-only by construction, not just because the `dsh` CLI is
absent. Unit specs test the **source** `src/client/index.tsx` (`apply`
contract), never the built bundle.
## Phase status
- **Phase 0 (done)**: engineering skeleton, host/client split, i18n, invariant
surface, build gate green, unit smoke green.
- **Phase 1 (done)**: real tab registration for the five `apes:*` panels
against a mock host registry (type-only real `TabDescriptor` /
`TabComponentProps` / `Context` from `dsh-better-sidebar`), i18n for all
five views, `ctx.effect`-wrapped registration with HMR-safe disposer, and
graceful degrade when `betterSidebar` is absent. Unit smoke: `tests/tabs.spec.ts`
(6 tests). The cordis scratch mount smoke (real DSH host process) is deferred
to Phase 2 — it needs a live host and is outside the unit gate.
- **Phase 2 (partial)**: badge counts + auto-collapse (visible-gated
subscription pause) + per-tab state landed for the information panels.
`apes:tasks` / `apes:agents` show live count pills (running / subagent);
`apes:tasks` persists a `showInactive` preference through the settings
namespace (`apesSidebar.tasks`, `applies: 'live'`). The cordis scratch mount
smoke and wiring `apps` / `messages` / `profile` to real feeds are **deferred**
— better-sidebar 0.18.0 exposes no workspace-apps / message-stream /
model-provider host feeds for those three panels, and a live scratch mount
needs a `dsh` CLI (absent locally) plus a DOM harness the Phase 0 convention
deliberately avoids. See docs/ for the deferral rationale.