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

8.1 KiB

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

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.