Files
apes-sidebar/AGENTS.md
T
figmar 856d744139 feat(apes-sidebar): Phase 2 — badge 计数 + auto-collapse(visible 门控暂停)
- apes:tasks badge = running 会话数;apes:agents badge = subagent 数
  (纯 (ctx)=>number|null 函数,0→null 隐藏,host 封顶 99+)
- useSessionList 接受 visible:非活动 tab 切 no-op subscribe,
  停止 live session-list churn 重渲染隐藏面板(better-sidebar
  'live views pause otherwise' 契约 = auto-collapse)
- tests/tabs.spec.ts:新增 5 个 badge 契约测试(6→11),mock
  sessions feed 直测 badge 纯函数
- AGENTS.md:Phase 2 标 partial(per-tab state / scratch 冒烟 /
  apps·messages·profile feed 延后,better-sidebar 0.18.0 无相应 API/数据源)

门禁:typecheck rc=0 · build rc=0(client.js 15.74→20K)· test 25/25
2026-09-04 20:04:47 +08:00

7.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, CJS closure factory)
  • 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: closure factory 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. It is a CJS closure factory — module.exports = (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.

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) landed for apes:tasks / apes:agents. Per-tab state, the cordis scratch mount smoke, and wiring apps / messages / profile to real feeds are deferred — better-sidebar 0.18.0 exposes no meta-write API for plugin tab state, no workspace-apps / message-stream / model-provider host feeds for those three panels, and no local dsh CLI to drive a live scratch mount. See docs/ for the deferral rationale.