chore(gui): mission work logs — cordis design finalization, tool-card wire archive, incident records chore: missions chore: missions chore(gui): mission ledger — batch-2 answers, parallel dispatch state, jsdom coverage re-scope chore(gui): ledger — night-mode standing orders (self-commit small, no push, 5-min refresh) chore(gui): ledger — jsdom batches 2-4 landed (233 green), coverage probe next chore(gui): ledger — web-ui coverage probe 65%, four-tier fill plan approved chore(gui): ledger — cordis-impl B1 state after third API drop, decisions on file chore(gui): ledger — 01:32 patrol snapshot (jsdom tier-1 landed, coverage-fixer probed) chore(gui): ledger — 01:37 patrol (peer src trio landed, coverage-fixer still silent) chore(gui): ledger — 01:42 patrol (jsdom tier-2 landed, peer committed x2, coverage-fixer 2nd probe) chore(gui): ledger — coverage diagnosis complete (6-file gap list), web-ui at 91.4% chore(gui): ledger — 01:46 patrol (B1 done, jsdom tier-3 landed, coverage fix batch running) chore(gui): ledger — 01:51 patrol (jsdom tails x2 landed, B2 underway) chore(gui): ledger — 01:56 patrol (gateway.ts 337 lines, checkpoint T-7min) chore(gui): ledger — hold/pending split ruling, coverage-fixer externalize-or-restart ultimatum chore(gui): ledger — 02:01 patrol (B2 done, jsdom final arms, coverage ultimatum pending) chore(gui): ledger — 02:03 checkpoint executed (fixer2 respawn, four lanes released, three owners cold-started) chore(gui): ledger — all six lanes acked, type isolation first live proof (client closure clean) chore(gui): ledger — 02:08 patrol (all seven lanes active, wire carrier assembled) docs(gui): respond-design task checkpoint — apiproxy wire-layer recon done chore(gui): ledger — P0-2 contributor AGENTS.md landed (dd28a5019) docs(gui): respond-design checkpoint 2 — host-side recon (stub respond, frame types, approval seam, ACP answerer precedent) docs(gui): OOP debt inventory — seven territories, 2 real debts (createApiProxy, createFixtureApi), rest ruled keep-as-is docs(gui): disambiguation note on the archived i18n design task chore(gui): ledger — 02:12 patrol (exclude removed, mixed-knife incident under reconciliation) chore(gui): ledger — 02:15 wave (jsdom mission closed, OOP audit done, B3 isolation proof, mixed-knife resolved) chore(gui): ledger — 02:17 patrol (attribution reversal filed, arch-session probed) chore(gui): ledger — 02:22 patrol (B4 done, B5+B6 merged batch, arch-session deadline set) docs(gui): respond-design checkpoint 3 — client-side recon (pending map, PendingCard onRespond stub, AbstractApiClient.respond ready, bootHost missing approval mounts) docs(gui): peer carrier territory review — 2 fixes (SSE cancel leak, route-reservation guard), 1 ruling ask (RPC-log visibility), compliance ledger chore(gui): ledger — 02:27 (arch-shell respawn, territory review verdicts routed, ask-deny finding flagged) docs(gui): P1-5 respond design page complete — pending registry, wire answerer, client state machine, first-wins arbitration chore(gui): ledger — 02:31 patrol (respond design complete, B5 wire smoke green, shell knife 1 underway) docs(gui): respond design — add §0 status warning (web host ask defaults to deny), mount-behavior delta, no-timeout ruling with Config discipline chore(gui): ledger — 02:42 patrol (respond line closed pending review, B7 last piece underway) docs(gui): respond design contract review — direction pass, 2 doc fixes (settle-order contradiction, answering-state race), A/B/C compliance ledger docs(gui): respond design — contract review fixes (R1 verify-before-delete arbitration, R2 answering+resolved-frame transition, ask dual-source wording, rejected-is-ok-value note) chore(gui): ledger — 02:46 patrol (respond line final, two user decisions distilled, arch-shell deadline) docs(gui): respond review addendum — contract-gap ruling: approve plan A (ApprovalRequest.id), wire unchanged, drop plan B backscan chore(gui): ledger — cordis B7 summit: real-browser 10/10 green, user acceptance criterion proven docs(gui): respond design — contract gap #4 approved as plan A (ApprovalRequest.id), backscan fallback retired, blade 0 prepended docs(gui): respond design — final polish (owner-approval vs user-go-ahead wording, implementation handoff notes) chore(gui): ledger — 02:51 (shell-exec third respawn with operational script, respond line 4-knife final) chore(gui): ledger — 02:53 wave (respond five-knife true final, B7 closed 12/12, client.ts green) chore(gui): ledger — 02:56 patrol (cordis closeout bounced pending R1/R2/N1, shell-exec first sign of life) chore(gui): ledger — 03:01 patrol (R1/R2/N1 remediation in flight across four files) chore(gui): ledger — cordis line officially closed and archived, verified on disk (24 knives, 12/12, reviews closed) chore(gui): ledger — 03:06 patrol (shell-exec final window, lowered first-knife bar) chore(gui): ledger — 03:11 patrol (shell line iced-broken: three registries on disk) chore(gui): ledger — 03:15 patrol (quiet window, both active lanes within threshold) chore(gui): ledger — 03:20 patrol (shell five files up, api-proxy plan reported) chore(gui): ledger — 03:25 patrol (shell migration in flight with history-preserving moves, webserver green) chore(gui): ledger — 03:30 patrol (shell knife-1 in verification, api-proxy patching) chore(gui): ledger — shell knife 1 accepted (694cecc53), knife 2 released chore(gui): ledger — 03:40 patrol (knife 2 pre-move stage, cold-list spec appears) chore(gui): ledger — 03:45 patrol (rpclog moves staged, api-proxy two specs in flight) chore(gui): ledger — 03:54 patrol (knife-2 code done, coverage full-run final check) docs(gui): coverage-fixer task ledger — fixer2 takeover, per-file fix log, isolated reportsDirectory pitfall chore(gui): ledger — coverage lane closed and accepted (a19f069a5), the PR #443 CI fix knife chore(gui): ledger — 04:04 patrol (knife-2 calibration, sole active lane) chore(gui): ledger — shell knife 2 accepted (f8fb77b95), knife 3 released as final night task chore(gui): ledger — 04:19 patrol (knife-3 past half: callback chain through, ToolCallDetail up) chore(gui): ledger — night closeout summary: seven lanes closed, wake-up decision sheet chore: missions chore: missions chore: missions chore(gui): mission-local browser/probe verify scripts under missions/scripts/ The six acceptance/probe scripts move here as mission-side working material (headers and relative imports adjusted for the new location): carrier-errors, rpclog-panel, session, session-real, webserver-backpressure, webserver-hardening. chore(gui): verify-relocate mission log chore(gui): verify-relocate mission log — R1 guard addendum chore(gui): gates-continue mission log — CI-equivalent sequence all green chore(gui): VS Code 扩展体系双边设计调研报告 chore(gui): 调研追加 4.5 节——git 扩展数据面与 scope 绑定 docs(gui): web plugin system RFC — walkthrough + design notes docs(gui): RFC — restore existing SSE/POST as the v1 transport; envelope rides on it (D16) docs(gui): RFC — envelope demoted to chan-dispatch, scope out of envelope, rpc-log cut, peer deferred, scope tree is native cordis (D17-D21) docs(gui): RFC — hooks re-derived from component needs: useWatch/useAction only, useService removed; sessionHub cut, projections user-space, router rename, loader-only root (D22-D25) docs(gui): RFC — drop stale fork vocabulary (vendored cordis has Fiber only; scope = mintScope pattern), hook idempotence contract (D26-D27) docs(gui): RFC — session precision seam: plugins read scope key (host paradigm), React gets it from tree position via SlotOutlet (D28) docs(gui): RFC — domain hooks owned by plugins over framework primitives; useConversation paradigm carried over (D29) docs(gui): RFC — ctx services are the inter-plugin API (cordis proper); declarations are wire-only; get(id) returns scoped ctx (D30) docs(gui): RFC — full ctx.conversation walkthrough: root-singleton scope-sensitive service, caller-ctx scope key, get(key) as scoped ctx (D31) docs(gui): RFC — no client-side agents collection: session state machine already expresses the duality; agent resolution stays host authority (D32) docs(gui): RFC — v1 stays session-precision, no agent-level isolation; incarnation/agent-axis designs archived in ledger (D33) docs(gui): RFC walkthrough — full rewrite to final state (D16-D33 consolidated), end-to-end chain restored docs(gui): RFC — apiproxy demoted to generic channel routing; domain RPCs dissolve into owner plugins (D34) docs(gui): RFC — TS-interface-first wire contract (zod internal), conversation owns the dialogue frame with pluggable views (D35) docs(gui): RFC — page skeleton (sidebar+conversation), projects as plugin not service, nested slots via owner registries (D36) docs(gui): RFC — SlotMap declaration-merging slot model: single register API, inject-as-ownership, FC-typed registration, typed outlets (D37) docs(gui): RFC — slot props whitelist: identity, display params, materialized snapshot slices, stable UI callbacks (D38) docs(gui): RFC — end-to-end data flow: three transforms, equality protocol table, immer placement; i18n/theme kept standard (D39-D40) docs(gui): RFC — full external-injection model: props carry values + stable injected hooks; shared/client/react example rewritten (D41-D43) docs(gui): RFC final trio — modules.md (agent implementation spec), architecture.md (human walkthrough), plugins.md (business plugin inventory) docs(gui): RFC — props three-source merge (scope-standard useSession auto-injected); keyed key vs list id disambiguated (D44) docs(gui): RFC — inject comment says what it is (the React-facing props bundle); SessionHandle rename; snapshot-production story unified on buildSnapshot docs(gui): RFC architecture — full React component tree walkthrough: props three sources, slot vs plain children, hook taxonomy per node docs(gui): RFC — module map finalized (ui-slots/web-react/connection/runtime/ui-*/web); slots onChange replaced by cordis events; toolcall dimension; detail sidebar default-collapsed with toolName-keyed routing docs(gui): RFC plugins — openDetail relay chain: toolcard calls chat-view injected action, chat-view relays to conversation sidebar chore(gui): progress ledger — full archive rewrite: RFC outcome digest, open gaps, dispatch plan, cold-start entry docs(gui): RFC grill pass 1 — SlotScope axis (root/session) on declares, Gate dependency inversion, inject handle by scope, W5 acceptance list, gantt relay chain fixed docs(gui): figma analysis — sidebar/projects/sessions 区域交互视觉理解报告 docs(gui): figma 解析报告 — details 面板/多视图 tabs/未来功能区盘点 + slot 需求清单 docs(gui): figma 对话主区解析报告 — 消息流/tool calls 变体/审批接管输入框/Header tabs/视觉 token docs(gui): plugins.md rewritten from figma analysis — three-column layout, full slot reservation table, selection channel, composer-takeover approvals, phased scope docs(gui): layout dynamics ruled (drag+collapse both rails, details yields first, composer swap-panel, same-component transition); toolviews promoted to named scope-aware registry docs(gui): P-I scope locked (details minimal, dual theme, chat-view, custom toolview sample); teammate dispatch plan — 6 owners by package, dependency-driven waves, contract arbitration docs(gui): P-I api-contracts (full inter-package API spec) + dispatch plan (T0 skeleton knife, 7-dev roster, task briefs, milestones) docs(gui): api-contracts v2 — scope tree in P-I, bundle loader + per-plugin CSS isolation in P-I, agent-scoped toolviews live, zustand engine, renames (SessionProvider/ObservableSnapshot/SessionBinding), router owns all shell view-state docs(gui): services roster + progressive loading (no blocking loadAll), SlotsService as real cordis Service, renderSlot/renderSuspenseSlot duo, ui-traj teammate docs(gui): loading-chain gaps ruled — dev=rebundle no HMR, ui-primitives package, externals on globals (no import map), host injects __DSH_BOOT__ into HTML (zero round-trip) docs(gui): api-contracts v3 + dispatch v2 final — 12 packages, services merged in, progressive loader, global externals, __DSH_BOOT__ injection, 8-dev roster with convo split and ui-traj docs(gui): v3 amendments — router renamed ctx.layout, ui-trajectory has no service (pure consumer sample), wait-for-settled loading (no Suspense in P-I, ledger 6b) chore(gui): progress — pre-compact final state: v3 revision chain, 8-dev roster, T0 procedure, doc authority order docs(gui): authority banners — modules/architecture get v3 term-mapping headers, walkthrough marked as archived process doc docs(gui): cssdesign token set is THE theme source (--dsw-* variables, data-ds-dark-theme switch); recorded in contracts + progress docs(gui): architecture.md full v3 rewrite — loading chain, 12-package map, service roster, slot/inject/toolviews, data flow, component tree, perf model, all current docs(gui): contracts — UI plugins are dual-entry host plugins (node half serves client asset via ctx.webPlugins; __DSH_BOOT__ derives from it; client-closure gate back in scope) docs(gui): contracts — closure-factory bundles with DI require (no globals), package.json dshWeb declarative discovery (no serve ritual), create-then-send empty state with project picker, ancestry() for breadcrumb, unload stubbed until HMR, props.renderSlot confirmed docs(gui): dshClient declaration (inject/platform/immediately, exports./client), closure-DI require loading — synced across contracts/dispatch/modules/architecture/walkthrough chore(gui): progress — record final loading-chain rulings (dshClient declaration, closure-DI require, startSession) before compact docs(gui): architecture.md — developer-facing whole-web architecture on master baseline 6b16a67cb: what exists, what is new, no process narrative docs(gui): architecture.md — self-contained whole-web architecture: absorbs still-valid substance from the branch RFCs (host layering, four-quadrant RPC, object layer, testing tiers) under the new plugin system as the override chore(gui): progress — final pre-compact snapshot: contracts digest, apiproxy purity ruling, T0 procedure with first-action list docs(gui): api-contracts v3 §3.1 — apiproxy purity principle with three-way existing-code verdicts docs(gui): api-contracts v3 — immediately reinterpreted as static-infra group (8-package dshClient scope, boot manifest reconciliation) docs(gui): api-contracts v3 — immediately corrected to early-load dynamic group (prod shell must not rebundle); loader shell-held; bundles register their export surface into module table docs(gui): architecture — align with immediately=early-load dynamic group ruling; loader shell-held; module-table registration of loaded bundles docs(gui): T0 checklist — 12-package skeleton table, 4-cut sequence, mv/attic/rewire rules (pre-drafted, awaiting go) docs(gui): t0-checklist — pin figma-flows findings (missing font-family base vars, three alias vars behind upstream) docs(gui): dispatch v2.1 — drop cordis-web salvage wording, two-wave staffing, loader/immediately boundary updates docs(gui): progress + t0-checklist ledger — T0 landed, staffing status, execution accounting docs(gui): api-contracts v3 — arbitration round 1: renderBody deps, RootBindingProvider, flush default sync, prune current, loader subpath, config-source P-I bar docs(gui): v3 §3.2 connection 导出清单附录(rt-core 对账)+ rt-core 实现计划档案 docs(gui): progress — T1 milestone, arbitration round 1 ledger, fw-react timeout escalation docs(gui): progress rolling update — per-line battlefield state at 00:2x, mailbox-vs-contract lesson, small-batch discipline reinforced docs(gui): fw-react notes — v3 §2 complete, seven knives, T1/T2 follow-ups docs(fw-slots): archive — four packages landed, open tails logged docs(gui): progress — framework layer complete (web-react five, fw-slots four packages), T2 gated on rt-core runtime knife only docs: api-contracts docs: style-spec docs docs(gui): tsconfig convergence ruling — no host.json, root resumes host-aggregate duty, typecheck = root + client aggregates docs(gui): missions 根三份 07-18 世代档案加「已被取代」头注——指向 web-plugin-rfc 现行权威并注明新旧对应 missions docs(gui): progress rewritten for post-closeout state — wave ledger, architecture finale, teammate roster with handover notes, pending-user-command queue
35 KiB
dsh Web 架构
面向开发者的整体架构说明——读完可独立开发任一层。本文是完整自含的:底座(协议/对象层/host 分层,随本分支交付)与插件化体系(本次新建)都在正文里,不需要再读别的决策文档。接口精确签名见 api-contracts.md;业务插件清单与布局规则见 plugins.md;任务分派见 dispatch.md;主题变量表在 cssdesign/;视觉稿解析在 figma-analysis/。
1. 总纲
dsh Web 是 harness 的浏览器界面,与 host(node 侧 harness 运行时)经 HTTP/SSE 通信。三句话:
- 两端都是 cordis:host 是一棵 cordis 插件树;浏览器里同样跑一棵 client cordis 树——UI 的每一块功能(布局、侧栏、对话、主题、语言包)都是 client cordis 插件,由 loader 动态装载。
- cordis ctx 是一切运行时事实的宿主,React 是纯投影:数据与逻辑活在 ctx 的 service/store 上;React 组件零框架依赖、只吃 props;zustand 只存「怎么看」(导航/面板宽/偏好/草稿),永远不存运行时数据的副本。
- wire 与 host 权威层稳定:apiproxy RPC + SSE 双流是插件化体系的既定地基;host 侧仅两小件为 Web 插件化服务(插件产物分发端点、HTML 启动注入)。
┌─ Host ────────────────────────────┐ ┌─ 浏览器 ─────────────────────────────────────────┐
│ sessions/agents/SessionLog │ │ client cordis root ctx │
│ apiproxy: RPC + mux/host 双流 │◀──▶│ ├ loader 机件(壳静态持有,不能经自己装载) │
│ webserver: │ │ ├ immediately 先行组(动态 bundle,并行先装): │
│ ├ 静态托管 + /api 桥 + SSE 写出 │ │ │ connection/runtime(slots/sessions)/theme/i18n │
│ ├ GET /plugins/<id>/client.js │ │ ├ 后续组(先行组齐后装): layout/sidebar/ │
│ │ │ │ │ conversation/trajectory │
│ └ GET / 注入 window.__DSH_BOOT__ │ │ └ session scope ×N(观看驱动,惰性建) │
└───────────────────────────────────┘ │ └ Session 对象(对象层核,可订阅快照源) │
│ React: loading 页 → 全部插件就绪 → 整 UI 一次成型 │
│ zustand: 观看态(导航/布局/偏好/草稿) │
└───────────────────────────────────────────────────┘
2. 分层模型:host 三包与方向纪律
目录按能力提供方分层(不按产品形态):packages/host/* 只供 host 侧能力,packages/client/* 只供 client 侧能力(每包单侧),apps/ 放对外应用形态(host/client 混合体的装配——混合体永远不成包,装配写在 app 里)。
| 层 | 包 | 职责 | 关键纪律 |
|---|---|---|---|
| 前置层 | dsh-host-apiproxy |
TS/zod 契约定义(api/)+ fetch 抽象(handler + 客户端基类) | node 与浏览器皆可 import;client 只准消费 /api、/client 两个浏览器安全子路径(type-only+基类);消费端不得绕过 api 直连 ctx |
| 装配层 | dsh-host-runtime |
插件组合 + ApiProxy 集成;host 级配置的家 | 挂哪些插件、什么默认值只在这里定;壳不得改装配 |
| 载体层 | dsh-host-webserver |
Web 形态 HTTP:静态托管 + /api/*→handler 转发 + SSE 写出 + 关闭语义;插件分发端点 + DSH_BOOT 注入 |
零 workspace 依赖——webserver 不依赖 runtime,靠 {fetch} 形注入(运行时注入关系,非包依赖) |
| client 侧 | packages/client/* 12 包 |
§4 起 | 消费面只经 ApiProxy |
| 应用形态 | @deepseek-ai/dsh(apps/cli) |
bin 分发 + 每形态一个装配模块(web.ts/headless.ts,动态 import 互不加载) | dist 位置这类 workspace 知识只住 app |
命名规则:host/、client/ 两组的包名必含目录组前缀(dsh-host-runtime、dsh-client-web-react);tsconfig.base.json 需逐包显式 paths 条目。
类型宇宙隔离:typecheck=host/client 双 TS program(tsc -b tsconfig.json tsconfig.client.json:根=host 聚合、tsconfig.client.json=client 聚合)——两侧都对 cordis Context declare merge 同名键(sessions/loader),双 program 使其永不同室;client 经 session/llm/tools/approval/interaction 的纯类型子路径(./types 等)消费 wire 词汇表,不装载 host 的 augmentation(细案=missions/tasks/20260721-1520-web-plugin-rfc/clientcontext-audit.md)。
新形态接入清单:①选 fetch 形态(浏览器同源 HTTP / 进程内 host.handler.fetch 注入 / 自定义 doFetch 子类如将来 Electron IPC)②在 apps/ 写装配模块(startHost + 客户端子类 + 该形态的信号/打印/退出语义)③需要 HTTP 载体才 import webserver,否则零端口。现有两形态即模板:apps/cli/src/web.ts(startHost+startWebServer+信号关停)、headless.ts(startHost+InProcessApiClient 同构直调,零 HTTP 零端口)。
3. wire 协议:四象限消息模型
物理通道各异(HTTP/SSE、进程内直调、将来 IPC),协议因此与通道解耦:每条逻辑消息按「谁发起 × 请求/应答」两轴四格:
client 发起 server 发起
request ① ClientRequest ③ ServerRequest
(POST /api/<method> body) (SSE 帧:session 事件、审批/问答 requested)
response ② ServerResponse ④ ClientResponse
(该 POST 的 HTTP 应答体) (POST /api/respond body,回填 ③ 的 rpcId)
- rpcId 纪律(branded string):谁发起谁 mint;应答只回显、永不新 mint。server-request 分两种(method 静态区分,无第三种):可应答帧(approval/question requested——受理时 mint 一次稳定逻辑 id,基线重放原样复用,client 应答回显)与纯推送帧(session/event 等——每次推送新 mint)。业务代码永不 mint:unary 收敛在客户端基类
callUnary,帧收敛在 host 侧。 - 签名窄形:领域接口只见
RpcRequest<P> = {rpcId, payload}/RpcResponse<T> = {rpcId, result};载体层把窄形补全为带 type/method 的全形——方向永不从通道推断。RpcResult<T> = {ok:true; value} | {ok:false; error: RpcError},方法不 throw 业务错误。 - 类型系统单一事实源:方法参数/返回结构只活在领域接口签名里;
RpcMethodMap登记方法本身,其余位置(handler/client/测试)一律引用派生泛型(RequestPayload<K>/ResponseValue<K>)——抄字面量或引入扁平命名类型是禁令。错误码全集=RpcErrorDetailsMap(code 判别、details 必填,新码=一行 map+一条 schema 分支,漏写=编译错)。传输故障(网络断、host 未起)由载体层抛异常,与业务错误两层永不混。 - 双向 zod 校验:全形 schema 一次(type/rpcId/method 结构)→ 按 method/帧型分发的业务 payload 第二次;拒收=
bad-request。透传纪律:wire 上的事件/消息/内容块就是核心类型(SessionEvent/ContentBlock,type-only 链直达浏览器),无第二套 DTO;SessionEventMap merge-extensible,未知类型走 documented-default(忽略),信封保持 strict。 - RpcReceipt:④ 的 HTTP 应答体是载体回执
{accepted} | {accepted:false; reason:'not-pending'|'bad-response'}——不是 RpcMessage(应答没有应答);迟到/重复应答得not-pending,逻辑收敛面是*/resolved帧。
客户端基类族(AbstractApiClient):协议不变量全在基类(mint→POST 全形→schema parse→rpcId 回显核对→窄形出);平台差异只有两个切面——抽象方法 doFetch(传输)+ 可覆写 onEnvelope(观察,实例级微任务合批缓冲,RPC 调试面板即其纯订阅者)。子类表:
| 子类 | doFetch | 用途 |
|---|---|---|
InProcessApiClient |
注入的 {fetch} handler |
同构点:new InProcessApiClient(toFetchHandler(api)) 不碰网络但真跑 wire 序列化/zod/SSE 分帧——headless 与协议层测试都用它 |
WebApiClient |
globalThis.fetch 同源 /api/* |
浏览器形态 |
FixtureApiClient |
不用(协议层覆写 callUnary/openMux 等) | 无服务器 UI 开发:自身即假 server,?fixture URL 切入,装配零分叉 |
| (将来)IPC 子类 | IPC 往返 | 只换 doFetch,契约与基类不动 |
session 语义承诺(impl 侧):history=事件重放(一次 fold 在 client;分页边界对齐消息边界;无第二套物化快照系统);重连=重建(断线重开流+重拉 history,subscribed.lastSeq 对缝一次回补;无 cursor 续传);cold session 隐式 resume(history/prompt 打到未附着会话时自动拉起,in-flight 表去重);审批/问答=requested 帧稳定 rpcId+先答先赢+host 内存 pending 表唯一裁判,审计走持久 log(帧=活控制面,事件=持久审计);无协议版本号(client/host 绑定发布)。扩展是机械五步:领域接口加签名→map 一行→schema 对→UNARY_ROUTES 一行→impl 实现。
4. client 侧包地图(12 包)
| 包 | 角色 | 关键物 |
|---|---|---|
ui-slots |
slot 注册表纯核(零依赖) | SlotMap 类型、SlotCore、ScopedSlots 类型 |
ui-primitives |
纯 React 原子件(零 cordis) | StateDot 四色、ic_ds_* 图标族、Button/Pill/Menu、markdown 族 |
web-react |
ctx↔React 全部胶水 | createSnapshotStore、bindSnapshotSelector、SessionProvider、renderSlot 实现、useInvoke |
connection |
wire 消费层 | IApiClient 子类族、ConnectionController(SSE 双流+重连)、fixture |
runtime |
client cordis 启动+核心 service | SlotsService、SessionsService(scope 树+对象层)、ClientLoader |
ui-layout |
壳插件 | AppFrame 三栏、ctx.layout(导航+面板观看态总归口) |
ui-sidebar |
左栏插件 | 会话多级树、搜索、分组、状态点 |
ui-conversation |
对话域(最大包) | conversation 骨架、chat 视图、ctx.toolviews、details 面板 |
ui-trajectory |
Trajectory/Waterfall 视图 | 零 service 的纯消费型插件(最小插件样板) |
ui-theme |
主题 | ThemeService;token=cssdesign/ 的 --dsw-* 体系 |
i18n |
文案 | I18nService、bind(ns)→t |
web |
入口壳 | boot、AppRoot、require 模块表供给、vite/构建管线 |
依赖方向:ui-slots ← web-react ← runtime ← ui-*(并列)← web;ui-primitives/ui-theme/i18n 零依赖旁路。全部 ESM、strict TS。
5. 数据对象层(runtime/sessions,React-free)
流式事件驱动的会话 UI 里,业务状态(事件窗口、流式累积、pending 交互、连接状态机)若散在 React 组件和全局 store,每个 token chunk 都会摇动渲染树。对象层因此独立于 React 存在(zero React import,grep 可断言):
mux/host 帧(ConnectionController 泵入,sinks 单向注入)
▼
SessionManager.handleMux/HostEnvelope 带 sessionId 的帧只投已存在实例
▼ (审批/问答 requested 例外:入 pendingBuffers 缓冲,实例化时重放)
Session.handleMuxEnvelope ──► events 窗口(seq 连续升序,seq 是唯一去重键)
│ │ 定稿事件 │ chunk
│ ▼ ▼
│ FoldAdapter PartialAccumulator
│ (→ nodes) (→ partial,delta 只换该块引用)
▼
Notifier 微任务合批 ──► ConversationSnapshot 缓存(结构共享)──uSES──► React
- Session(懒建常驻,切走切回秒显、草稿天然存续):操作面 prompt/cancel/open(拉尾页 history,幂等)/loadOlder(向上翻页防重入)/resync(重连重建);订阅面 subscribe/getSnapshot(恒返缓存引用)。open/缝合语义:open 落定后按 seq 合并 liveBuffer 去重;
subscribedLastSeq超窗口尾且未覆盖则回补一次。 - ConversationSnapshot(不可变快照契约):nodes(定稿节点,fold 产物)/partial/runningCalls/pending/running/removed/openState/hasMore/promptError/draft 等。引用纪律(memo 与 uSES 的前提):顶层对象每变必新;nodes 数组重建但元素引用来自缓存(分段 rev:calls/pending/nodes 各持版本号,事件只 bump 相关段,未变段复用旧引用)。
- FoldAdapter 复用核心 SurfaceManager(增量折叠;分页窗口 seq>0 用哨兵垫齐;跨窗口 replace 触发时降级容错线性扫描并置 foldDegraded)。PartialAccumulator:chunk 不进 fold(O(1) 跳过),累积成 AssistantBlock[];定稿 assistant/message 到达即弃累积器(同一批内,partial 区无闪烁提升)。成本模型:每 chunk=一次字符串拼接+一个脏标记;未订阅 Session 在帧风暴下只花脏标记。
- Notifier 双通道:
markDirty()(默认,帧驱动一律用)微任务合批,N 变化=1 通知=1 重渲染;notifyNow()(仅用户手势直接回响)同 tick 重建+通知——受控输入(textarea 由快照 draft 控制)延迟到微任务会导致 DOM 回滚+光标跳尾。帧驱动/异步回调用 notifyNow=合批塌回逐帧渲染,禁。 - ConnectionController:开 mux/host 双流 for-await 泵入,断线指数退避重连(500ms 翻倍至 10s 封顶,抖动,无限重试),代号围栏丢弃旧代残帧;sinks 单向注入(Controller 不认识 Session);
onConnected(双流开+describe 成功,含首连)→ refreshList + 各已打开 Session resync。
插件化体系对对象层的改造只有两行:Session 补 implements ObservableSnapshot<ConversationSnapshot>(subscribe/getSnapshot 本就同形)+ 构造时 useSelector = bindSnapshotSelector(this)。buildSnapshot 与上述全部机制原样保留。被替换的是旧消费面:hooks 直连 manager 单例(getSessionManager())的形态、旧壳三 registry、--bg-* 旧 token——由 §7 起的插件模型接管。
6. UI 插件模型:双入口 host 插件
每个 UI 插件同时是一个 host 插件——UI 插件出现在 host 的 cordis.yml 里、由 host Loader 管辖生命周期,浏览器端装载清单由此推导,装/卸插件=改 host 配置重启(config 同源)。
my-plugin/
package.json "dshClient": { "inject": ["ui-layout"], "platform": "web", "immediately": true? }
"exports": { ".": <node半边>, "./client": "./dist/client.js" }
src/index.ts node 半边:cordis host 插件(纯 UI 插件=空 apply,存在意义=受 Loader 管辖;
带 host 逻辑的插件在此加码)
src/client/index.ts 浏览器半边:apply(ctx) —— 挂 service/建 store/注册 slot 与 toolview
src/client/*.tsx React 组件(零框架 import,只认 props)
src/client/*.module.css CSS Modules,只用 var(--dsw-*)
dist/client.js tsdown 闭包工厂产物(单文件,CSS 内联,external→require DI)
dshClient 字段:inject: string[]——client 半边装载依赖(与 cordis inject 同名同念,决定装载拓扑序);platform: 'web'(一插件一平台);immediately: true——先行装载组(基建四包:connection/runtime/ui-theme/i18n;并行拉取+组内 inject 拓扑 apply,全组就位后才装其余插件;仍是标准动态 bundle,非静态打包)。client 产物路径取标准 exports["./client"]。./shared 入口(跨线私有契约)仅在插件自带 host 数据面时出现——当前无此类插件。类型宇宙隔离:client 半边编译单不得看见 node 半边的 Context merge(verify-client-closure gate 全覆盖)。
7. 装载链:从刷新到可用
GET /
→ host webserver 渲染 index.html,注入 window.__DSH_BOOT__ =
{ plugins: [{ id, url: '/plugins/<id>/client.js', inject, immediately? }] }
清单来自 HostWebPluginRegistry:订阅 host Loader 加载面,收集带 dshClient 声明的已加载插件
——HTML 到手即知要拉什么,零额外往返
→ web 壳启动:挂 ctx.loader(机件由壳静态持有——装载器不能经自己装载),
播种纯库实体(react/react-dom/cordis/ui-slots/web-react/ui-primitives)进 require 模块表
→ createRoot 渲染 boot loading 页(纯壳组件,不依赖任何插件)
→ loader.start(): immediately 组(connection/runtime/theme/i18n)并行拉取+组内 inject 拓扑
apply,全组就位后其余按 inject 拓扑逐个装载:
<script> 注入 → bundle 执行 DSHClientProxy.loadPlugin({ id, factory })(id 对账,错配 fail loud)
→ loader 调 factory(require) —— bundle 是闭包工厂,external 依赖经 require(spec)
从模块表取(cordis 依赖注入实体;无 global、无 import map;查无=fail loud 报缺依赖)
→ factory 返回模块导出面(含 apply) → ctx.plugin(apply) → 导出面以包名登记进模块表
(inject 拓扑保证后装者可 require 先装者) → 内联 CSS 注入 <style data-plugin="<id>">
→ await loader.settled() → AppRoot 从 loading 页切换渲染真 UI(一次成型)
- 一次成型:默认等全部插件装载完再画实际 UI;单插件失败=loading 页显式报错,不做部分可用(渐进点亮/Suspense 为后续项)。
- 卸载:
loader.unload(id)当前空实现(完整链——Fiber dispose→注册级联收回→移除 style——随 HMR 立项补齐)。 - CSS 隔离:CSS Modules 哈希类名 + style 标签带插件 id 归属;插件包禁
:global越界与裸元素选择器。 - dev 工作流:与 prod 同链——插件包
tsdown --watch重编译,改完刷新;无 HMR;vite 只管 web 壳。fixture 模式注入同一份 DSH_BOOT 协议,双模式同构。
8. service 名册(插件间唯一 API 面)
service 是插件对外能力的唯一形态——UI 组件与注入面都不是 API;没有被调用需求的插件就不挂 service(ui-trajectory 即样板)。
| service | 提供者 | 职责 |
|---|---|---|
ctx.connection |
connection | api client + 双流句柄挂载面 |
ctx.slots |
runtime(核在 ui-slots) | define/register/entries;变化发 'slots/changed' cordis 事件 |
ctx.theme |
ui-theme | register/apply(=切 body[data-ds-dark-theme])/current |
ctx.i18n |
i18n | register(ns, locale, dict) / bind(ns) / locale store |
ctx.sessions |
runtime | list store、manager、create、scope(id)/binding(id)、ancestry(id) |
ctx.loader |
runtime(代码家);机件由 web 壳静态持有并 boot 时挂载——装载器不能经自己装载 | start/load/unload/settled/status |
ctx.layout |
ui-layout | current+viewFor+sidebar+details 四面观看态;open/openView/openDetails 导航动作 |
ctx.conversation |
ui-conversation | send/cancel(scope 寻址)、startSession、selection、openDetails 编排、registerView、drafts |
ctx.toolviews |
ui-conversation | 具名工具渲染注册表(§12) |
scope 寻址范式(与 host 侧 tool/system-prompt 同构):service 是 root 单例、scope 敏感——方法不收 sessionId 参数,从 caller ctx 读 scope 标(scopeOf(ctx))。per-session 语境里 ctx.conversation.send('hi','queue') 自动打到本会话;root 语境定向=换 ctx:ctx.sessions.scope(id)!.conversation.send(...)(用完即弃);root 语境直接调 scoped 方法=throw。
9. session scope 树
client 侧按会话建 cordis 子 scope,机制与 host 的 agent scope 完全同构(dsh-scope mintScope:ctx.plugin(no-op) 得 Fiber 载体 + extend({[kScope]: id}) 打标):
- 生命周期权威=观看需求:首次解析(选中会话/binding(id))惰性建;host 端会话结束 ≠ scope 拆除(转 frozen 只读视窗);列表 removed 且无人观看才拆。
- scoped ctx 承载三件事:service 的 scope 寻址(§8)、per-session 状态挂账(conversation 的 selection/drafts 按 scope 实例化)、注入面的会话绑定(§11)。
- 两侧 session 范畴不对称(公理):host session=过程(启动→事件流→结束,历史的作者);client session=视窗(任意时刻连上/离开,要处理 history 回补)——cold(纯历史)→ live(续流)→ frozen(终局只读)状态机。client 不建第二套 agents 集合:agent 的存活即状态机 live 态,session→agent 解析(含 resume-on-prompt)是 host 权威。
10. slot 系统:页面怎么拼
页面是一棵坑位树,谁拥有区域谁声明坑位;类型与所有权由两个机制拼出:
① SlotMap 声明合并(仓库 typed-events 惯例复用)——坑的契约在 declare 处一次定死:
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 }
} }
ctx.slots.define('sidebar', { kind: 'single', scope: 'root' }) // declare=类型,define=落账
ctx.slots.register('conversation', ConversationRoot) // 组件函数直接注册,FC<props> 编译期锁
- 三型:
single独占(重复注册 throw)、list多注册(id/order/label)、keyed按 key 运行时分发(重 key throw)。register 到未 define 的 key=运行期 throw。 - scope 轴:
root(无会话语境)/session(有)——决定注入面形态(§11)。 - ② inject 链即所有权链:注册进谁的坑就
inject:谁——没 inject 到 owner=装配期 fail loud;owner 卸载=下游注册级联收回(cordis 原生语义)。
渲染侧没有全局组件:owner 组件从 props 拿到白名单收窄的 slots: ScopedSlots<'a'|'b'>,用 slots.renderSlot(key, props) 渲染——能开哪些坑写在组件签名上(自己 define 的+被显式转授的),越权=编译错。渲染方供参与注册方组件签名来自同一条 SlotMap 条目,改契约两侧同时报错。
顶层坑位(layout 声明):sidebar(左栏)、conversation(中栏)、details(右栏)。conversation 再开子坑:conversation.views、conversation.statusline。完整坑位留位表见 plugins.md §1。
11. React 面:完全外部注入
组件文件对框架零 import;所需一切经 props 注入;唯一铁律=注入物引用稳定。
export function BashRow({ callId, block, useSession, actions, t }: ToolViewProps) {
const running = useSession(s => s.runningCalls.some(c => c.callId === callId))
const [open, opening] = useInvoke(() => actions.openDetails())
// ...纯渲染,样式只用 var(--dsw-*)
}
props 三源合并(渲染基件做,owner 只写第一份):
① owner 供参: renderSlot 的 props 位——身份(sessionId/callId)/展示参数/frozen 切片
② scope 标配注入: session 坑自动获得 useSession(绑定正确的 Session 对象);root 坑没有
③ 注册方私有注入: register 的 inject 工厂——per-(注册项×session) 只调一次并缓存
inject: (b) => ({ useMyStore: store.useSelector, actions: {...}, t: ctx.i18n.bind('ns') })
b = SessionBinding{ sessionId, session, ctx } 或 RootBinding{ ctx }
—— apply 世界的装配句柄,用完即弃,永不进 React
- props 白名单五类(其余禁止,尤其 class 实例/ctx/可订阅源):branded 身份、纯展示参数、owner 已物化的 frozen 切片、框架注入的 hook/动作/文案(引用稳定)、罕见纯 UI 协调回调。判据:props 传「定位你的已物化数据」,活数据自己用注入的 useXxx 订。
- hooks 的 uSES 契约四条(防撕裂,新 hook 逐条自检):getSnapshot 恒返缓存引用、调用中不计算;subscribe 引用稳定(实例常驻保证切换外零重订);纯 CSR 不传 getServerSnapshot;双源一致(running 位同时在列表项与会话快照——同一帧驱动、同一微任务批 flush,单渲染周期内一致)。
- 跨层动作走转交链:组件只调自己坑 owner 注入的动作;owner inject 别家 service 转调。范本=openDetails:工具行→(chat 注入面)→chat 视图→(inject conversation)→conversation 写 selection + 调 ctx.layout.openDetails()。组件永远不跨层认识非 owner 的 service。
- SessionProvider(框架内建,开发者不写):订
layout.current→sessions.binding(id)(常驻恒等引用)→ Providerkey={id}→ 渲染 conversation+details 两坑。切会话=右侧子树重挂(数据在对象层保温);不切=零刷新。全树只有这一个 Provider。
12. toolviews:具名的、按会话维度的工具渲染注册表
工具行渲染不走 SlotMap——它被 chat/trajectory/waterfall 多视图共同消费、且要按会话差异化,是独立的具名 service:
ctx.toolviews.register('bash', BashRow) // 全局注册
ctx.toolviews.register('bash', SwarmBashRow, { scope: id => isSwarm(id) }) // 特定会话族专属形态
// 渲染方: resolve(tool, sessionId) —— scope 匹配 > 全局 > undefined(fallback GenericToolCard)
视觉形态(figma 定稿):单行摘要行(16px 图标⇄chevron 槽+标题+摘要),5 变体(think/search/read/bash/others 兜底),无行内输出——完整输出走 details 面板。
13. 数据流全景与状态归属
SSE/RPC → 对象层(§5:Session/store,活对象,命令式) → ObservableSnapshot 面
→ frozen 快照(结构共享) → useXxx(selector) → 组件 → memo(shallow) → DOM
- 数据契约只有一个:
ObservableSnapshot<T>(getSnapshot/subscribe)。两种源同构满足:Session 类(§5,天然满足)与createSnapshotStore(zustand vanilla + immer + subscribeWithSelector + rafFlush 中间件 + persist opt-in + dev freeze——插件自家数据与壳观看态用)。 - 相等性协议:生产端=结构共享;消费端=selector 等值短路(Object.is 默认,选对象声明 shallowEqual);React.memo shallow。深比较全链禁止。数据形态转换全链只有三次(wire JSON→类型化事件→内部可变态→frozen 快照)——组件/容器里 map/filter 出新引用即打穿 memo。
- 动作方向:注入 actions 引用恒定;结果不回传——后果以事件回声从读方向回来(无乐观插入:自己的消息也等 log 回声,多端/刷新/重放零特判;乐观 UI 只准表达"发送中"按钮态与草稿乐观清空-失败回填);useInvoke 只管 pending。
- 「状态住哪」决策树:per-session 且要跨切换存续 → Session 对象/scope 挂账;单视图容器私有(滚动等瞬时)→ 组件 useState;壳级观看态(选中/面板/偏好/草稿)→ ctx.layout 与 conversation.drafts 的 zustand store;业务数据 → 永远对象层,永不进观看态 store。
- store 共享=export store 对象或放注入面(owner 给出口);无全局 store;插件不开自己的 React Provider。
- 空态首发链:无会话时 conversation 坑渲染 EmptyState(居中大输入框+project/cwd 下拉);提交走
conversation.startSession({cwd?, text, mode})——service 内部编排 create→open→send 三连。
14. 布局与导航:ctx.layout
壳级观看态总归口(zustand+persist):current(选中会话)、viewFor(每会话激活视图)、sidebar/details(open+width)。
三栏动态规则(全文 plugins.md §0.1):两侧栏可拖宽+可开合(Handle=拖拽把手,折叠按钮在 Logo 行);窗口过小自动缩两侧、优先缩 details(details→sidebar→中栏 min 640 兜底);输入框随中栏对等缩(736/776 为上限);details 收起=0 宽不 unmount;composer banner 堆叠不设上限;审批=composer 整体换新面板(非顶部加条);空态→有内容=同一输入框组件位移;树深层 ... 截断。
15. 样式与主题
- token 唯一来源=cssdesign/ 两份文件:
--dsw-static-*色阶层 +--dsw-alias-*语义层 + 渐变/阴影/blur/字体。暗色=body[data-ds-dark-theme]整段覆写;theme.apply(id)=切换该 body 属性——换肤是 CSS 级联,React 零渲染。两份 CSS 由 web 壳直接引入为 base 样式表;第三方主题=覆写同名 alias 变量。 - 工程约束:CSS Modules + clsx,无组件库、无 tailwind;组件禁 hardcode 色值/阴影/渐变(一律
var(--dsw-*));字号不 token 化——组件里写 px 且成对写行高(常用对 16/24 气泡、14/22 UI 默认、12/18 辅助),间距用 4 的倍数;代码字体栈末位不放monospace(防 Windows 中文回退宋体)。 - i18n:ns×locale 字典注册+fallback 链;
bind(ns)产物引用恒定可进注入面;locale 切换整树重渲染(低频可接受)。zh/en 起步。
16. 完整组件树
打开会话 S、chat 视图、一条消息带自定义 bash 行(■=框架件 □=插件件):
■ <AppRoot> loading 页 → await loader.settled() → 整 UI
└─ □ <AppFrame> ui-layout;三栏 grid+两条拖拽把手
├─ (左列) renderSlot('sidebar')
│ └─ ■ ErrorBoundary → □ <SidebarRoot useTree actions> root 坑:无 useSession
│ └─ □ <SessionRow …/>×N props:父自己 map(普通 children,不过注册表)
│ 点击 → actions.open(id) ← 注入③→ctx.layout.open
├─ ■ <SessionProvider key="S"> 订 layout.current;恒等 Session 引用
│ ├─ (中列) renderSlot('conversation', {sessionId})
│ │ └─ ■ EB → □ <ConversationRoot sessionId useSession slots> ①+②+③
│ │ ├─ □ <Header/> <ViewSwitcher/> 骨架自有 UI(views 清单+layout.viewFor)
│ │ ├─ slots.renderSlot('conversation.views', {sessionId}, {only:'chat'})
│ │ │ └─ ■ EB → □ <ChatView useSession useSelection actions>
│ │ │ └─ □ <MessageItem node/>×N props:frozen 切片,memo 短路
│ │ │ 工具块→ ■ <ToolViewOutlet tool="bash"> 查 ctx.toolviews
│ │ │ └─ ■ EB → □ <BashRow callId block useSession actions t>
│ │ ├─ slots.renderSlot('conversation.statusline', {sessionId})
│ │ └─ □ <InputBar draft running onSend onStop> 骨架自有;draft=conversation.drafts
│ └─ (右列) renderSlot('details', {sessionId})
│ └─ ■ EB → □ <DetailsPanel …> 订 selection,显示选中 call 的 args/result
读树四规律:跨插件边界=renderSlot(注册表+ErrorBoundary+注入合并),插件内部=普通 children/map;useSession 只在 session 坑自动合入;inject 注入只出现在声明了私有注入面的注册项上;全树 Provider 只有 SessionProvider 一个(值恒等),其余传递全走 props。
17. 渲染性能模型
| 场景 | 行为 |
|---|---|
| 流式 token 风暴 | 只有正在流式的气泡每帧渲染(对象层合批+partial 单块换引用);历史 MessageItem memo 短路;统计行/列表/details 零渲染;未订阅 Session 只花一个脏标记 |
| 切换会话 | sidebar 零渲染(没订 current);右侧重挂但数据在对象层保温(实例常驻),毫秒级 |
| 打开/拖宽 details | 布局列宽变化;组件不 unmount |
| 长对话 | 分页加载 + content-visibility + 工具行默认收起 |
| markdown | 已闭合块 memo,只重 parse 尾部未闭合块 |
| 换主题 | body 属性切换,CSS 级联,React 零渲染 |
hook 幂等五条(并发渲染安全的根):getSnapshot 纯读+引用稳定;subscribe 稳定;selector 走等值短路位、不在组件体二次加工;invoke 稳定且 pending 不走 setState;render 体零副作用。
18. 测试体系(三层)
| 层 | 被测物 | 关键手法 | 位置 |
|---|---|---|---|
| 1 协议同构 | AbstractApiClient + toFetchHandler(双向数据/rpcId/zod/SSE 分帧/合批/超时) | 同构点全链:InProcessApiClient(toFetchHandler(脚本化 impl))——零浏览器纯 node,真跑 wire 序列化 |
apiproxy/tests |
| 2 对象层编排 | Session/Manager/Connection(缝合/去重/分页/乐观清稿/pendingBuffers/重连/退避) | 「事件序列进→快照出」金路径:可编程 fake + deferred 控时序 + fake timer 控退避 | runtime(sessions)/connection tests |
| 3 浏览器冒烟+插件链 | 构建产物×真浏览器(boot/装载链/一轮会话) | 裸 playwright chromium;fixture 级+真 host 级(无 key 自跳过) | web/tests e2e |
层间纪律:各层测各层,上层不复测下层——冒烟只证接线活着,交互细节归 verify 脚本(missions/scripts/),wire 语义归 1 层,数据语义归 2 层。纯函数层(lineage/partial/notifier/fold)零 fake 直测。每修一个 bug 钉一条断言(浏览器可见 bug 钉 verify 脚本回归段,数据层 bug 钉对应 spec);fixture 全绿不算完,真 host 必须过(fixture 短路的恰是 wire 载体链——node:http 桥关闭语义、真网络时序,两次实证 bug 都藏在那)。插件化新增验收:装载链 e2e(DSH_BOOT→settled→单插件失败 fail loud)、toolviews 差异渲染、三栏拖宽让步、暗色切换、React Profiler 计数断言(流式期间统计行=0 等)。
19. 当前范围与预留
本期交付:装载链全通(DSH_BOOT→闭包 DI→settled 一次成型)+ 三栏壳(拖宽/开合/让步)+ sidebar 会话树 + conversation 骨架(Header/tabs/composer/空态)+ chat 视图(消息流+工具行 5 形态+统计行)+ details 极简(选中 call 的 args/result)+ selection 通道 + 双主题 + bash 自定义工具行样例(注册表全链实证)。
预留位已留、实现后置:审批 composer 换面板与追问队列(协议帧与 pending 语义已在 wire 层就绪,host pending 表未实现——respond 现为 stub 恒 not-pending,PendingCard 显示为主);slash 命令注册表;Trajectory/Waterfall 真实现与锚点深链;details 三段(Input/Output/Metadata)与步进;附件上传;context 进度指示;toast 通知;HMR 与渐进渲染;unload 完整实现;双半边插件私有 wire 通道;agent 级隔离;虚拟化长列表;i18n 存量文案抽取。协议侧预留缝(实现即五步机械展开):session.fork、prompt.mode 'inject'、task.list、host.listModels、describe 的 hostInstanceId。