# P-I API 契约终版 v3(冻结;teammate 照抄实现) > **本文件即冻结契约本体**——实现中发现契约错 → SendMessage 主会话仲裁改本文件并广播;不许自行改接口。配套:[dispatch.md](dispatch.md)(任务分派)、[plugins.md](plugins.md)(业务+布局规则)、[architecture.md](architecture.md)(机制讲解)、figma-analysis/(视觉)。services.md 已并入本版作废。 > 包名前缀 `@deepseek-ai/dsh-client-`(下文省略)。ESM 源码、strict TS、类型 import 一律 `import type`。 ## 0. 包清单、依赖图、装载链 ### 0.1 包(12 个) ``` packages/client/ ui-slots/ SlotCore 纯注册表 + SlotMap 类型 + ScopedSlots 双形态类型 ui-primitives/ 纯 React 原子件:State四色点/ic_ds_图标族/按钮胶囊菜单/markdown族(零 cordis 依赖) web-react/ store(zustand 引擎)+ObservableSnapshot+bindSnapshotSelector+SessionProvider+outlet 实现+useInvoke connection/ wire 类型封装+网络连接(现文件原样搬家) runtime/ cordis 启动+SlotsService 包装+sessions(scope 树)+loader(渐进式) ui-layout/ layout 插件:三栏 AppFrame+拖拽+ctx.layout service(壳级观看态总归口:导航+面板) ui-sidebar/ sidebar 插件(树列表全量) ui-conversation/ conversation 骨架+chat 视图+toolviews+details 极简+bash toolview 样例 ui-trajectory/ trajectory+waterfall 占位视图(独立 teammate,不要求效果) ui-theme/ theme 注册表+CSS vars i18n/ i18n 注册表+t 绑定 web/ 入口壳:global 挂载+boot+AppRoot+vite/构建管线 ``` ### 0.2 依赖方向(import 面) ``` ui-slots ←─ web-react ←─ runtime ←─┬─ ui-layout ←──── web ↑ ↑ ↑ ├─ ui-sidebar ui-primitives ──┘ connection └─ ui-conversation ←─ ui-trajectory (ui-* 均可用 ui-slots 类型/web-react hooks/ui-primitives 组件;ui-theme、i18n 零依赖旁路) ``` ### 0.3 装载链(v4 修订:默认等全量加载完再画真 UI) ``` GET / → host webserver 渲染 index.html,注入 window.__DSH_BOOT__(§9.2) → web 壳: 挂 ctx.loader(loader 机件由壳静态持有,不能经自己装载) → createRoot 渲染 的 **boot loading 页**(纯壳组件,不依赖任何插件) → loader.start(): 读 __DSH_BOOT__,immediately 组并行拉取+组内 inject 拓扑 apply (connection/runtime(slots/sessions)/ui-theme/i18n) → 全组就位后 → 其余插件按 inject 拓扑逐个拉 bundle(layout 等) → await loader.settled() → AppRoot 切换渲染真 UI(此刻全部注册已到位,一次成型) ``` **默认=等所有插件加载完才开始画实际 UI**(loading 页→整 UI 一次切换);Suspense/渐进点亮/局部依赖加载全部后置(台账)——loader 的渐进能力(status/单插件失败标记)保留,只是 AppRoot 不利用它做渐进渲染。单插件 load 失败:P-I 直接在 loading 页显示错误(fail loud),不做部分可用。 插件(含基建四包)全部经 bundle loader 动态加载(CSS 随插件注入+隔离 §9.1)——生产模式不可重打包主入口,壳静态内置的只有 loader 机件与纯库。dev 与 prod 同链:插件改动=tsdown watch 重编译+刷新,**无 HMR**(后置)。 **双入口包形态(每个 UI 插件包)**——UI 插件本身就是 host 插件;**client 产物靠声明发现,不靠 apply 里调注册**: ``` package.json: "dshClient": { "inject": ["ui-layout"], "platform": "web", "immediately": true? } ← 声明位 "exports": { ".": ..., "./client": "./dist/client.js" } ← client 产物走标准 exports,不进 dshClient "."(node 半边) : cordis host 插件。纯 UI 插件的 apply 可以是空函数—— 存在的意义=让插件出现在 host cordis.yml/Loader 里(装载与生命周期跟 host 走); 有 host 逻辑的插件(将来 approvals/swarm)在此加码 "./client" : 浏览器半边(tsdown 闭包 bundle,§9.1) "./shared" : 仅当有跨线契约时出现(P-I 无) ``` dshClient 字段语义(2026-07-21 修订 immediately):`inject: string[]`=client 半边的装载依赖(**与 cordis inject 同名同念**——按它拓扑排序);`platform: 'web'`=目标平台(暂一插件一平台,将来 electron 等再扩枚举);`immediately: true`=**先行装载组**——仍是标准动态 bundle(有 url,与其余插件同构),loader.start() 先并行拉取该组、按组内 inject 拓扑 apply,**全组就位后才开始装载其余插件**(layout 等)。 **声明范围(八包)**:connection/runtime/ui-theme/i18n 带 `immediately: true`(基建;runtime 的 client 半边供 slots/sessions;**loader 机件例外**——装载器不能经自己装载,由 web 壳静态持有并在 boot 时挂 ctx.loader,代码家仍在 runtime 包);ui-layout/ui-sidebar/ui-conversation/ui-trajectory 不带。纯库 ui-slots/ui-primitives/web-react 与壳 web 不声明。host cordis.yml 全列八包(config 同源:装/卸=改 yml 重启)。 **发现机制(HostWebPluginRegistry,归 webserver 属地/rt-core 实施)**:订阅 host Loader 的插件加载面(实施时对照 vendor/loader 实际事件名,无则 registry 遍历+update 事件),对每个已加载插件解析其 package.json:有 `dshClient` 且 platform 匹配 → 进 web 插件表(id=包名;client 产物路径=解析该包 `exports["./client"]`);插件卸载=移出表。webserver 由此表生成 __DSH_BOOT__ 与 /plugins//client.js。**apply 零仪式**——插件作者只写 package.json 声明;config 同源落地=装/卸 UI 插件就是改 host cordis.yml 重启。类型宇宙隔离照旧:client 编译单不得见 node 半边 merge,verify-client-closure 扩到 12 包(T0 接)。 ## 1. ui-slots ```ts export interface SlotMap {} // owner declare merge;条目={ kind; scope; props } export type SlotKind = 'single' | 'list' | 'keyed' export type SlotScope = 'root' | 'session' export interface SlotEntryDef { kind: SlotKind; scope: SlotScope; props: object } // inject 装配语境(apply 世界专用,永不进 props) export interface SessionBinding { readonly sessionId: string // web-react re-export 收窄为 SessionId readonly session: SessionAccess // { useSelector },web-react 收窄 readonly ctx: unknown // 该 session 的 scoped cordis ctx(runtime 充实) } export interface RootBinding { readonly ctx: unknown } export interface SessionAccess { readonly useSelector: unknown } export type InjectFactory = (b: E['scope'] extends 'session' ? SessionBinding : RootBinding) => Record export interface SlotSpec { kind: E['kind']; scope: E['scope'] } export type SlotOptions = E['kind'] extends 'keyed' ? { key: string; inject?: InjectFactory } : E['kind'] extends 'list' ? { id: string; order?: number; label?: string; inject?: InjectFactory } : { inject?: InjectFactory } // 纯注册表核心(零 cordis;事件发射归 runtime 的 Service 包装) export interface SlotEntry { component: FC; options: SlotOptions } export class SlotCore { define(key: K, spec: SlotSpec): () => void register( key: K, component: FC, options?: SlotOptions): () => void entries(key: K): readonly SlotEntry[] spec(key: string): SlotSpec | undefined subscribe(key: string, fn: () => void): () => void // 微任务合批 getVersion(key: string): number onMutate(fn: (key: string) => void): () => void // Service 包装的事件桥挂点 } // 渲染注入面类型(实现住 web-react;P-I 单形态——Suspense/局部依赖加载后置,接口预留位见台账)。 // ★交付形态=props 注入:owner 组件的 props 类型里带 `slots: ScopedSlots<'a'|'b'>`(白名单泛型), // 由注册处 inject 工厂/框架标配注入下发——组件不 import 任何渲染器,能开哪些坑在签名上强类型可见。 export interface ScopedSlots { renderSlot(key: Key, props: OwnerProps, opts?: RenderOpts): ReactNode } export interface RenderOpts { entryKey?: string; only?: string; fallback?: ReactNode } export type OwnerProps = /* E['props'] 去掉 inject 键与标配注入键 */ ``` 行为规格:define 前 register=throw;single 重复 register=throw;keyed 重 key=throw;register/define 返回 disposer。零依赖(React 仅 `import type`)。 ## 2. web-react ```ts // ── 子路径 web-react/store —— zustand 为引擎 ── export interface ObservableSnapshot { getSnapshot(): T; subscribe(fn: () => void): () => void } export interface SnapshotStore extends ObservableSnapshot { update(mutator: (draft: T) => void): void // immer 中间件 set(next: T): void readonly useSelector: SnapshotSelectorHook } export type SnapshotSelectorHook = (sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S export function createSnapshotStore(init: T, opts?: { flush?: 'raf' | 'sync'; persist?: { name: string } }): SnapshotStore // = zustand vanilla + immer + subscribeWithSelector + 自研 rafFlush 中间件 + persist(opt-in) + dev freeze // flush 缺省='sync'(受控输入要同 tick 回响;帧驱动 store 由创建方显式传 'raf'——Notifier 双通道纪律的 store 面) export function shallowEqual(a: unknown, b: unknown): boolean // re-export zustand/shallow // ── 主入口(React 面) ── export function bindSnapshotSelector(w: ObservableSnapshot): SnapshotSelectorHook export type UseSession = SnapshotSelectorHook export interface SessionBinding { sessionId: SessionId; session: { useSelector: UseSession }; ctx: Context } // SessionProvider(依赖倒置,不 import runtime;2026-07-21 仲裁修订:两坑渲染归装配方) export interface SessionProviderDeps { useCurrent: () => SessionId | undefined resolveBinding: (id: SessionId) => SessionBinding | undefined renderBody: (id: SessionId) => ReactNode // 装配方(web 壳)闭包自己的 scopedSlots 渲染 conversation+details 两坑——坑归 layout own,Provider 不认识坑名 } export function createSessionProvider(deps: SessionProviderDeps): FC<{ renderEmpty?: () => ReactNode }> // 订 current → resolveBinding(恒等引用) → → renderBody(id) // RootBinding 供给通道(2026-07-21 仲裁新增):root 坑 inject 工厂需要 b.ctx(如 sidebar 拿 layout/sessions), // 由壳顶部挂一次 ;SlotOutlet 渲染 root 坑 inject 时读取,缺失=throw export const RootBindingProvider: FC<{ value: RootBinding; children?: ReactNode }> // ScopedSlots 工厂(outlet 实现;不全局导出组件——一切经注入面下发,白名单收窄) export function scopedSlots(core: SlotCore, ...keys: K[]): ScopedSlots // renderSlot: 订注册表(subscribe+getVersion 走 uSES),按 kind 渲染,每 entry 包 ErrorBoundary, // 合并 props={...标配注入(session 坑=useSession), ...cachedInject(binding), ...ownerProps} // inject 缓存: session 坑 per-(entry×session) WeakMap;root 坑 per-entry export function useInvoke(fn: () => Promise): [invoke: () => void, pending: boolean] ``` 白名单纪律:组件能开哪些坑写在它的 props 类型上(`slots: ScopedSlots<'a'|'b'>`)——来源=自己 define 的坑+owner 显式转授;越权=编译错。 ## 3. connection —— 接口零变化搬家 现 web-runtime wire 消费层原样改名搬家:IApiClient、connect/ConnectionHandle(SSE 双流+重连+since)、MuxFrame/HostFrame、createFixtureApi。契约=现状导出面(rt-core 搬家时附实际清单于本节尾)。不 import cordis。 ### 3.1 附注:apiproxy 纯度原则(2026-07-21 裁决;rt-core 对账清单的判据) apiproxy 只准含两种代码:①窄形↔全形协议胶水;②wire 契约明文承诺的读投影(如分页边界对齐)。判据:**换个消费面(ACP/headless)还需要吗?** 需要→下沉 host 能力层;只有浏览器需要→上移 client;只有这条 wire 契约需要→留下。 存量三类判决: | 存量 | 判决 | |---|---| | agentFor(resume-on-prompt)、summarizeCold(cold 列表合并) | **下沉 host 能力位**——P-I 原样使用,rt-core 对账清单标 TODO(P-II) 下沉 | | paginate | **留**(wire 契约的读投影) | | viewFor / backscanArgs(ToolEventView 呈现计算) | **P-I 随 toolviews 迁移整体删除**(呈现归 client) | 语义边界:隐式 resume 保留(cold→live 迁移是 host 内政,in-flight 表去重),**永不隐式 create**。可议项:history 的隐式 resume 应改纯持久化读(看历史不该拉起 agent)——记入 rt-core 对账清单,P-I 不改。 ### 3.2 附录:实际导出清单(rt-core 对账 2026-07-21;本节尾附清单=契约授权动作) 搬家后接口零变化实测(`export *` 已改精确清单)。§3 正文「connect/ConnectionHandle」为旧稿措辞,现状实体=ConnectionController/ConnectionSinks(对账认现状)。 | 来源文件 | 值导出 | 类型导出 | |---|---|---| | api.ts | RpcId、AbstractApiClient、resultOf、transportError | ApiProxy/SessionsApi/SessionSummary/HostApi/EventsApi/MuxFrame/HostFrame/ApprovalResponsePayload/QuestionResponsePayload/HistoryEntry/ToolEventView/ToolCallView/ToolResultView/RpcRequest/RpcResponse/RpcResult/RpcError/RpcErrorCode/ClientRequest/ServerResponse/ServerRequest/ClientResponse/RpcMessage/RpcReceipt/IApiClient/SessionId/SessionEvent/ContentBlock/StreamChunk | | connection.ts | ConnectionController | ConnectionConfig/ConnectionSinks/ConnectionState | | web-api-client.ts | WebApiClient | — | | fixture.ts | FixtureApiClient、createFixtureApi | — | 溶解项(不入 connection 导出面):events.ts(WEB_EVENTS/WebEventName)随死码退役刀删除(web-cordis pre-provision,零消费者,2026-07-22);intents.ts 整文件死——rpc-log 五 intent+pingHost 随 D18/RpcLog 族退役,refreshSessions/createSession 职责归 runtime SessionsService;store.ts(ConnectionSlice 观看态)不属 wire 层,connection 状态可见性归 runtime 侧挂账。ToolEventView/ToolCallView/ToolResultView 的 re-export 随 §3.1 viewFor 删除刀同步移除(toolviews 迁移触发,届时更新本表)。 ## 4. runtime ### 4.0 类型宇宙隔离(2026-07-22 终裁——host/client 双 TS program) > 实证:client 包 `declare module 'cordis'` 与 host 侧同名键撞车(`sessions` 撞 core/session 的 SessionStore、`loader` 撞 vendor/loader)。~~ClientContext extends Context 方案~~已作废(extends 消不掉父类——若 program 仍装载 host declare,继承照样撞)。 > > **终裁(用户拍板)=多 tsconfig 拆分**,已全案落地(细案与执行记录=同目录 clientcontext-audit.md):①typecheck 拆两 program——`tsc -b tsconfig.json tsconfig.client.json`(根=host 聚合,client 独立聚合);②A 类类型链切断——session/llm/tools/approval/interaction 出纯类型子路径(./types、./presentation、llm/brand),connection/apiproxy/runtime 的 import 全量切换,client program 不再装载 host 的 cordis augmentation;③client 包照常 `declare module 'cordis'` 声明自家服务键(sessions: SessionsService/loader: ClientLoader/layout/conversation/toolviews/theme/i18n)——双 program 下与 host 声明永不同室。ui-theme/i18n 对 runtime 的 type-only devDep 豁免保留(§0.2 零依赖旁路指零运行时依赖)。 ```ts // ── slots Service 包装(真 cordis Service,直接 ctx.emit) ── declare module 'cordis' { interface Events { 'slots/changed'(key: string): void } } declare module 'cordis' { interface Context { slots: SlotsService } } export class SlotsService extends Service { // static provide='slots' private core = new SlotCore() // core.onMutate(k => this.ctx.emit('slots/changed', k)) // define/register/entries/spec/subscribe/getVersion 代理 core } // ── sessions(含 scope 树,P-I 就做) ── declare module 'cordis' { interface Context { sessions: SessionsService } } export interface SessionSummary { id: SessionId; title: string; cwd?: string; parentId?: SessionId; running: boolean; updatedAt: number } export interface SessionListState { ids: SessionId[]; byId: Record } export class SessionsService { readonly list: SnapshotStore // list RPC 快照+host 流增量;重连重拉 readonly manager: SessionManager // 现有类原样 create(opts: { cwd?: string }): Promise scope(id: SessionId): Context | undefined // scoped ctx 视图(用完即弃) binding(id: SessionId): SessionBinding | undefined // SessionProvider.resolveBinding 供给 ancestry(id: SessionId): SessionSummary[] // 面包屑供数:沿 parentId 回溯(list store 内查,含自身,根在前) } // scope 树:dsh-scope mintScope 模式照搬(no-op 插件 Fiber+extend({[kScope]:id})); // 生命周期=观看驱动(首次解析惰性建;列表 removed 且无人观看才拆;host 死≠scope 死,frozen 保留) export function scopeOf(ctx: Context): SessionId | undefined // ── Session 类改造(仅加法两行) ── class Session implements ObservableSnapshot { readonly useSelector: UseSession // = bindSnapshotSelector(this) } // ── loader(渐进式;机件壳静态持有——runtime 出 `./loader` 专用子路径供壳 import, // 壳 bundle 不得捎带 runtime 整个 client 半边(双实例禁令)) ── // 工厂签名(2026-07-22 仲裁,ui-shell/rt-core 接缝): // export function createClientLoader(opts: { ctx: Context; modules: Record }): ClientLoader // —— modules=壳播种的纯库实体表(seed.ts 形状);loader 拥有该表并在装载后回登记 bundle 导出面 // export interface ClientLoader { requireModule(spec: string): unknown } // 追加成员:壳装配面取 // 已装载模块导出(如 layout 的 CenterColumn),查无=throw——与递给 factory 的 require 同一实现 declare module 'cordis' { interface Context { loader: ClientLoader } } export interface BootPluginEntry { id: string; url: string; inject: string[]; immediately?: boolean } export interface ClientLoader { start(): void // 读 __DSH_BOOT__:immediately 组并行拉+组内 inject 拓扑 apply,全组就位后其余按拓扑装,不阻塞 load(id: string): Promise // script 注入→DSHClientProxy 单槽 handoff→ctx.plugin(apply)→style 归属登记 unload(id: string): Promise // P-I 空实现(throw not-implemented)——HMR 立项时补(Fiber dispose→级联收回→移 style) settled(): Promise // 全部到位——AppRoot 等它后才画真 UI(见 §0.3 v4 修订) readonly status: SnapshotStore> } ``` ## 5. ui-layout(单插件:AppFrame + ctx.layout service) ```ts declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { sidebar: { kind: 'single'; scope: 'root'; props: SidebarRootProps } conversation: { kind: 'single'; scope: 'session'; props: ConvRootProps } details: { kind: 'single'; scope: 'session'; props: DetailsProps } 'conversation.empty': { kind: 'single'; scope: 'root'; props: {} } // ↑ 2026-07-22 仲裁(EmptyState 挂载点缺口):无选中会话时 SessionProvider.renderEmpty // 由壳 renderSlot('conversation.empty') 渲染——EmptyState 归 ui-conversation 包, // 经注册表进坑(inject 工厂供 startSession/cwd 派生);壳不得经 requireModule 点名 // 插件导出件(防幽灵耦合)。转场同组件裁决不变:EmptyState 与常态 InputBar 共用输入组件。 declare module 'cordis' { interface Context { layout: LayoutService } } export interface NavState { sessionId?: SessionId; viewFor: Record } export interface PanelState { open: boolean; width: number } export class LayoutService { // 壳级观看态总归口(zustand+persist) readonly current: SnapshotStore readonly sidebar: SnapshotStore // 默认 300,[240,420] readonly details: SnapshotStore // 默认 360,[300,520];P-I 全局不随 session open(id: SessionId): void // 校验存在于 sessions.list openView(sessionId: SessionId, view: ViewId): void toggleSidebar(): void; setSidebarWidth(px: number): void openDetails(): void; closeDetails(): void; setDetailsWidth(px: number): void } export type ViewId = keyof ConversationViewMap & string // prune: 订 sessions.list 的 removed → 清 viewFor 等 keyed 条目(单向);current.sessionId 指向 removed 会话时同刀置 undefined(回落 EmptyState) // 让步链第4步 auto-close details=派生 0 宽,不写 details.open 的 persist 态(窗口回宽自动恢复) // AppFrame: 三栏 grid+两条拖拽把手(pointer+raf 节流)+让步链(中栏 min640→clamp details→clamp sidebar→auto close details) // details 收起=0 宽不 unmount;各坑一律 renderSlot; // AppRoot(web 壳)第一帧的 boot 骨架被 layout 插件到达后替换 ``` ## 6. ui-sidebar register 进 `sidebar` 坑(整栏独占)。SidebarRoot 结构照 figma(Logo 行+折叠钮/NewSession/Search/区头+Group-by 菜单/树列表/Foot Settings 占位)。 treeStore=插件自建 SnapshotStore:订 sessions.list → 物化派生(cwd 分组→project 节点;parentId→session 树;排序)+expandedIds+query。组件订 treeStore,不在 render 里派生。 注入面(inject from RootBinding):`{ useTree: treeStore.useSelector, actions: { open, create, toggleSidebar } }`(经 b.ctx.layout/b.ctx.sessions 绑定)。 状态点:P-I 数据源两态(running→ongoing 环/无);四色组件齐(ui-primitives State 件),done/error/琥珀数据源记台账。 ## 7. ui-conversation ```ts // ── ViewMap(强类型视图注册) ── export interface ConversationViewMap {} // 本包 merge {chat};ui-trajectory merge {trajectory,waterfall} // merge 目标路径(2026-07-22 对账,§6b 双入口后类型真身在 client 半边): // declare module '@deepseek-ai/dsh-client-ui-conversation/client'(对根路径 merge 挂不到真身) declare module '@deepseek-ai/dsh-client-ui-conversation' { interface ConversationViewMap { chat: {} } } export interface ViewEntry { id: Id; label: string; order?: number component: FC chrome?: { header?: FC; footer?: FC } // 统计条挂点(chat 用 footer) } export interface ChromeProps { sessionId: SessionId; useSession: UseSession } // ── ConversationService(scope 寻址,无 sessionId 参数) ── declare module 'cordis' { interface Context { conversation: ConversationService } } export interface SelectionTarget { turnSeq: number; stepSeq?: number; callId?: CallId; toolName?: string } export class ConversationService { // 方法从 caller ctx 读 scope(scopeOf);root ctx 调用=throw。跨 session: ctx.sessions.scope(id)!.conversation.xxx send(text: string, mode: 'queue' | 'steer'): Promise cancel(): Promise readonly selection: SnapshotStore // per-scope 实例账(挂 scope 树) openDetails(target: SelectionTarget): void // 写 selection + ctx.layout.openDetails()(编排;布局态归 layout) registerView(entry: ViewEntry): () => void readonly drafts: SnapshotStore // per-scope;persist keyed by sessionId } export interface ConvViewProps { sessionId: SessionId; useSession: UseSession useSelection: SnapshotSelectorHook actions: { openDetails(t: SelectionTarget): void; loadOlder(): void } slots: ScopedSlots // chat P-I 无子坑转授(toolview 走具名注册表) } // ── ToolViewRegistry(agent/session 维度 P-I 生效) ── declare module 'cordis' { interface Context { toolviews: ToolViewRegistry } } export interface ToolViewProps { callId: CallId; toolName: string; block: ToolCallBlock // frozen 切片 useSession: UseSession actions: { openDetails(): void } // 已绑本 call t: Translate } export class ToolViewRegistry { register(tool: string, component: FC, opts?: { scope?: (sessionId: SessionId) => boolean; inject?: InjectFactory }): () => void resolve(tool: string, sessionId: SessionId): ResolvedToolView | undefined // 解析序:scope 匹配(注册序后者优先) > 无 filter 全局(同样后者优先——后装插件可覆写默认渲染) > undefined(渲染方 fallback GenericToolCard) } // 验收样例:bash 专属行注册两份(全局一份+某 scope filter 一份)证明差异渲染链路 // 组件归属: 骨架=ConversationRoot(Header面包屑+按钮排+ViewSwitcher+composer)/InputBar 平移/EmptyState 同组件转场 // chat=ChatView/MessageItem/AssistantMarkdown 平移改注入取数/ToolRow 5形态/GenericToolCard/统计行(chrome.footer) // details=DetailsPanel 极简(关闭钮+选中 call 的 args/result 展示) // 模型/effort 选择器: P-I 只显示(host describe 供数)不可改——写回窗口等后端能力,记台账 // ── 空态首发链(先创建再 send;figma NEW SESSION 屏)── // 无选中会话时 conversation 坑渲染 EmptyState:居中大输入框+左上角 project(cwd)下拉 // (选项=sessions.list 派生的 cwd 集合+"新目录"输入;figma New Session 按钮的隐藏下拉箭头即此) // 提交: conversation.startSession({ cwd?, text, mode }) —— root 语境专用方法(不吃 scope): // ① await ctx.sessions.create({cwd}) ② ctx.layout.open(id)(SessionProvider 随之建 scope) // ③ 经新 scope 的 conversation.send(text) —— 三连由 service 内部编排,组件一次调用 // EmptyState 的输入框与常态 InputBar 是同一组件(空态转场裁决§0.1-8),仅 onSend 绑定不同 ``` ## 8. ui-trajectory / ui-theme / i18n / ui-primitives - **ui-trajectory**:merge ConversationViewMap{trajectory,waterfall}+registerView 两条+占位实现(span 粗糙即可,不要求效果);chrome.header 挂统计条(第二客户实证)。**不挂任何 ctx service**——判断:它只消费(registerView+useSession 派生渲染),无人需要调用它;插件≠必有 service,纯消费型插件零 declare merge(这也是"最小插件"样板)。 - **ui-theme**:`ThemeService { register(id, tokens): ()=>void; apply(id): void; current(): string }`。**token 体系=用户提供的 cssdesign/ 两份文件(design-platform.css + gradient-shadow-text.css),统一用这套 `--dsw-*` 变量,不沿用现有代码任何色值**(figma 报告 token 表仅对照参考)。结构:static 色阶层(`--dsw-static-*`)+ alias 语义层(`--dsw-alias-*`)+ 渐变/阴影/blur/字体;暗色=`body[data-ds-dark-theme]` 属性整段覆写——**apply(id) 的实现=切换该 body 属性**。两份 CSS 由 web 壳作为 base 样式表直接引入(成品变量表,不经 register 转译);ThemeService 管"选哪套"+将来第三方主题(=覆写同名 `--dsw-alias-*`)。**全体组件只准用 `--dsw-*` 变量,禁 hardcode 色值/阴影/渐变**(lint 面)。单占冲突 throw。 - **i18n**:`I18nService { register(ns, locale, dict): ()=>void; bind(ns): Translate; locale: SnapshotStore }`;`Translate=(key, params?)=>string`;zh/en 空结构起步。 - **ui-primitives**:纯 React 组件(零 cordis):``四色件/ic_ds_* SVG 图标族(figma-flows 供数)/Button/Pill/Menu/Input 原子件/markdown 渲染族(现 components/ 迁入)。全部只吃 props+token vars。 ## 9. 构建、分发、装载(bundle loader 全链) ### 9.1 插件构建(每个 UI 插件包)——闭包+依赖注入,无 global bundle 编译为**闭包工厂**:external 依赖不挂全局,作为初始化入参注入(require 形态,但解析源=cordis 依赖注入实体): ```js // dist/client.js 的产物形状(tsdown banner/footer 包装): window.DSHClientProxy.loadPlugin({ id: 'ui-sidebar', factory: (require) => { // require: (spec: string) => unknown const React = require('react') // ← loader 从模块表供给 const { createSnapshotStore } = require('@deepseek-ai/dsh-client-web-react') ...bundle 主体(tsdown external→require 调用)... return { apply, ...其余导出 } // 模块导出面:apply=cordis 插件本体;整面登记进模块表供后装者 require }, }) ``` - **模块表归 client loader**(机件由壳静态持有,§0.3):壳 boot 时播种纯库实体 `{ react, react-dom, cordis, ui-slots, web-react, ui-primitives }`;**插件 bundle 的 factory 返回其模块导出面(含 apply),loader 装载成功后以包名登记进模块表**——inject 拓扑保证后装者可 require 先装者(如 ui-sidebar require runtime 的 scopeOf、i18n 的消费面)。loader 在 load 时把绑定了模块表的 `require` 递给 factory;要什么查什么,查无=fail loud(明确报缺依赖)。无 window.DSH.* 命名空间、无 import map;将来换机制只动 loader 的 require 实现。 - zod 等非共享依赖照旧内联。 - **CSS 随插件**:CSS Modules 产物内联进 client.js(执行时注入 `