feat(apes-sidebar): Phase 1 — 5 个 apes:* tab 真实注册

- client/index.tsx 改用 dsh-better-sidebar 真实 TabDescriptor/
  TabComponentProps/Context(type-only,擦除不打包)
- 注册 5 个面板:apes:tasks(读 sessions,running/current/subagent
  标记)、apes:agents(subagent 过滤 + 按 parent 分组)、apes:apps /
  apes:messages / apes:profile(占位)
- 全部走 t() i18n,ctx.effect 包裹注册(HMR 安全 disposer)
- betterSidebar 缺失时优雅降级(bare DSH 不破坏 shell)
- context-types.ts 删除被取代的 Phase 0 占位镜像,保留 ApesLocaleService
- package.json devDeps 补 dsh-better-sidebar(dev-only 类型)
- tests/tabs.spec.ts: mock registry 验证 5 个 apes:* id 注册 +
  disposer 撤销 + 缺失降级 + namespace 锁

门禁全绿:typecheck / build / test(20/20)
cordis scratch 真实挂载冒烟留 Phase 2(需真实 DSH host 进程)
This commit is contained in:
figmar
2026-09-04 17:42:57 +08:00
parent 0e91e99f49
commit f86c1d82e6
6 changed files with 509 additions and 202 deletions
+21 -97
View File
@@ -1,109 +1,33 @@
/**
* Structural types for the cordis services apes-sidebar consumes, plus the
* Context face both halves share.
* Structural types for the cordis services apes-sidebar consumes.
*
* The type base is the `@deepseek-ai/cordis` Context; the service members
* this plugin touches are restated below as structural mirrors and combined
* with the base by INTERSECTION. Intersection (not `declare module`
* augmentation) is deliberate: DSH's own packages already augment
* `@deepseek-ai/cordis` and the host and client packages declare *different*
* types for the same member — intersecting keeps every face available.
* PHASE 1: the sidebar tab + session types now come from the REAL
* `dsh-better-sidebar` package (type-only imports in `src/client/index.tsx`):
* `TabDescriptor` / `TabComponentProps` / `BetterSidebarService` /
* `SidebarSessionSummary` / `SidebarSessionList`. The Phase 0 placeholder
* mirrors (`ApesSidebarTabDescriptor`, `ApesSidebarService`,
* `ApesSessionSummary`, `ApesSessionList`, `ApesSessionsService`, and the
* `ApesContext` intersection) were deleted — they are superseded by the
* peer's authoritative types and would drift.
*
* What remains here is the ONE service face apes-sidebar still defines
* itself: the locale runtime mirror (`ApesLocaleService`). `dsh-better-sidebar`
* publishes its own `SidebarLocaleService`, but locales.ts pre-dates the type
* import and stays on this local mirror to keep the i18n module independent
* of the peer (it must compile even when the peer is absent).
*
* This file must stay FREE of Node.js types: it is part of the
* CLIENT-reachable declaration graph (the `Context` in TabComponentProps and
* the `apesSidebar` augmentation), so a Node import here would leak into
* CLIENT-reachable declaration graph, so a Node import here would leak into
* browser-only consumer builds.
*/
import type { Context as CordisContext } from '@deepseek-ai/cordis'
/** One session row the sidebar reads (client sessions list feed). */
export interface ApesSessionSummary {
id: string
cwd?: string
displayTitle: string
/** Coarse durable origin for navigation filtering (subagent children). */
origin?: 'subagent'
/** Durable direct parent session id (present on subagent children). */
parentId?: string
/** Whether the session's agent is currently running. */
running?: boolean
}
/** The client session list snapshot the sidebar subscribes to. */
export interface ApesSessionList {
current: string | undefined
byId: Record<string, ApesSessionSummary>
}
/** The client sessions service face (only the list feed + open are needed). */
export interface ApesSessionsService {
list: {
getSnapshot(): ApesSessionList
subscribe(fn: () => void): () => void
}
/** Select a listed session as current. */
open?(id: string): void
}
/** The client locale service face (mirror of dsh-client-locale's LocaleRuntime). */
/**
* The client locale service face (mirror of dsh-client-locale's LocaleRuntime).
* Only the slices apes-sidebar's i18n module touches: snapshot read, snapshot
* subscription, and per-namespace dictionary registration.
*/
export interface ApesLocaleService {
getSnapshot(): { active: string }
subscribe(fn: () => void): () => void
register(ns: string, locale: string, dict: Record<string, string>): () => void
}
/** The client slots service face (register returns the disposer). */
export interface ApesSlotsService {
register(options: Record<string, unknown>, component: unknown): () => void
inject(key: string, callback: () => () => void): () => void
}
/**
* The `ctx.apesSidebar` service face: a registry that lets apes-sidebar
* (and future sibling plugins) contribute sidebar tab descriptors to the
* better-sidebar host. For Phase 0 this is a minimal self-registry; in
* Phase 1 it is re-exported from the better-sidebar `BetterSidebarService`
* (type-only import through `dsh-better-sidebar/client/service`).
*
* NOTE: this is a PLACEHOLDER contract for the skeleton. Phase 1 replaces
* it with the real better-sidebar `BetterSidebarService` + `TabDescriptor`
* types (type-only imports, which are erased and never reach the bundle
* purity gate).
*/
export interface ApesSidebarTabDescriptor {
/** Unique id; also the `SidebarTab.type` value (`'apes:tasks'`, …). */
id: string
title: string | (() => string)
icon?: unknown
/** + menu sort order (ascending); default 100. */
order?: number
hidden?: boolean
/** Single-instance sugar. */
single?: boolean
/** Tab-strip badge: a number renders as a count, a string as-is. */
badge?: (ctx: ApesContext, state: unknown) => string | number | null | undefined
component: (props: Record<string, unknown>) => unknown
}
/** The `ctx.apesSidebar` service face (Phase 0 placeholder). */
export interface ApesSidebarService {
registerTab(descriptor: ApesSidebarTabDescriptor): () => void
getTabs(): readonly ApesSidebarTabDescriptor[]
subscribe(listener: () => void): () => void
}
/**
* The Context face this plugin's code resolves against. Intersecting the
* cordis base with the structural service mirrors keeps every face
* available without module-level conflicts.
*/
export type ApesContext = CordisContext & {
/** The client-side registry (client half only). */
apesSidebar?: ApesSidebarService
/** The client sessions feed (client half only). */
sessions?: ApesSessionsService
/** The client locale runtime (client half only). */
locale?: ApesLocaleService
/** The client slots service (client half only). */
slots?: ApesSlotsService
}