- 规则 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
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,.tsextension imports) - Build:
tsdown(rolldown). Dual output —lib/index.js+lib/invariant.js(host, ESM) andlib/client.js(client, self-contained bundle with a namedapplyexport) - 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
-
Namespace plugin export shape.
src/index.tsMUST export{ name, inject, apply }as named exports — never adefaultexport. A straydefaultmakes the cordisLoader.unwrapExportsdiscardname/inject/applyand the plugin silently fails to mount.tests/smoke.spec.tspins this. -
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 namedexport 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 viactx.betterSidebar/ctx.effect(read fromctx, not a separately-injectedhostApi). React is external (host-provided). Neverimporta Node builtin intosrc/client/**. src/invariant.tsis reachable from BOTH halves — it must stay dependency-free (no Node builtins, no @deepseek-ai imports).
- The host half (
-
dsh-better-sidebaris a peerDependency, not a bundled dep. The tsdown config lists it indeps.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. -
@deepseek-ai/cordisis devDependency (types only). The host provides the real cordis context at runtime. Do NOT add cordis todependenciesor todeps.neverBundle— that would force the client to resolve it. -
Tab IDs are
apes:*. Neverwb:*(legacy prefix) or any better-sidebar internal id. The five Phase 1 tabs:apes:tasks,apes:agents,apes:apps,apes:messages,apes:profile. -
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-sidebaritself. Do NOT add team/room features — those belong todsh-background-agents. -
Bilingual i18n. Every user-facing string goes through
t(key)insrc/client/locales.tswith BOTH azhand anenentry. No raw strings in JSX. -
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(picknode-pty,y). Without this,pnpm build's preinstall dep-check hard-fails. It is approved in the committedpnpm-lock.yamlstate — 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 (seetests/smoke.spec.ts) — never against a live DSH shell in CI. - The locale contract is tested against a mock
ApesLocaleService(seetests/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)
tsdown0.22 deprecatedexternal/noExternalin favour ofdeps.neverBundle/deps.alwaysBundle.externalanddeps.neverBundlecannot coexist — using both throws at build. Use only thedepsAPI.allowImportingTsExtensions(for.ts-extension imports) is set intsconfig.json.tsconfig.build.jsoninherits it but emitsd.ts, which is compatible (tsdown handles the strip). Iftscbuild complains, the declaration emit needsemitDeclarationOnly+noEmit: false— that is the currenttsconfig.build.jsonsetup.node-ptybuild approval is per-machine state inpnpm-lock.yaml+ the pnpm store. A freshpnpm installon 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.jsis a DSH browser__ModuleLoader__bundle, NOT plain CJS/ESM. Line 1 iswindow.__ModuleLoader__.load({...})— it is consumed by the DSH browser shell's module loader and cannot berequire()d in Node (top-levelwindowreference throwswindow 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 thedshCLI is absent. Unit specs test the sourcesrc/client/index.tsx(applycontract), 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 realTabDescriptor/TabComponentProps/Contextfromdsh-better-sidebar), i18n for all five views,ctx.effect-wrapped registration with HMR-safe disposer, and graceful degrade whenbetterSidebaris 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:agentsshow live count pills (running / subagent);apes:taskspersists ashowInactivepreference through the settings namespace (apesSidebar.tasks,applies: 'live'). The cordis scratch mount smoke and wiringapps/messages/profileto 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 adshCLI (absent locally) plus a DOM harness the Phase 0 convention deliberately avoids. See docs/ for the deferral rationale.