# ui-shell 实现计划(ui-layout + web 壳 + tsdown client preset) > owner=ui-shell(常驻)。契约=api-contracts v3 §5/§9.1/§9.3/§0.3(immediately=先行装载组、loader 机件壳静态持有);验收面=plugins.md §0.1 规则 1/2/5/6 + dispatch ui-shell 行。T0 骨架刀完成前不动 packages/。 ## 1. ui-layout ### 1.1 AppFrame 让步链(核心算法,抽纯函数) ``` computeColumns(viewportW, sidebar: PanelState, details: PanelState) → { sidebarW, centerW, detailsW } ``` 输入=观看态原值(persist 的用户偏好永不被让步链改写);输出=本帧生效列宽。链序(写死): 1. 期望值:sidebarW = sidebar.open ? sidebar.width : 0;detailsW = details.open ? details.width : 0;centerW = viewport − 两侧。 2. centerW ≥ 640 → 完成。 3. clamp details:detailsW = max(300, viewport − sidebarW − 640)。 4. 仍不足 → clamp sidebar:sidebarW = max(240, viewport − detailsW − 640)。 5. 仍不足 → auto close details:detailsW=0(**派生关闭,不写 details.open**——窗口回宽自动恢复,persist 语义不被让步链污染)。⚠假设待 main 确认。 6. 兜底:sidebar 已到 240、details 已 0 仍不足 → centerW=剩余全部(可 <640,中栏兜底)。 纯函数单测穷举边界;组件层只做接线。 ### 1.2 AppFrame 组件 - 三栏 grid:`gridTemplateColumns: ${sidebarW}px minmax(0,1fr) ${detailsW}px`;viewport 宽经 window resize 监听(rAF 节流)进本地 state。 - 两条拖拽把手:sidebar 右缘 + details 左缘。pointerdown+setPointerCapture → pointermove 记最新 x → rAF flush 调 layout.setSidebarWidth/setDetailsWidth(service 内 clamp [240,420]/[300,520])→ pointerup 释放。把手命中区 ≥8px 宽(视觉窄条+padding)。 - **details 收起/让步=0 宽不 unmount**:第三列 0px + overflow hidden,子树保留。 - 组件零框架 import:布局态经 props 注入 hooks(useSidebar/useDetails selector)+ actions(引用恒定);三坑一律 `slots.renderSlot`;中右两坑包在 SessionProvider 内(组件经 props 收 `SessionProvider: FC`)。 - CSS Modules,只用 `var(--dsw-*)`;sidebar 右描边/背景等 token 见 figma sidebar §5。 ### 1.3 LayoutService(四面观看态,zustand+persist) - `current: SnapshotStore`、`sidebar/details: SnapshotStore`——createSnapshotStore(persist opt-in),四面全 persist(刷新恢复选中+布局)。 - 默认:sidebar {open:true, width:300};details {open:false, width:360}。P-I details 全局不随 session(规则5,per-session keyed 升级位=将来把 details 换 keyed store,接口不动)。 - `open(id)`:校验存在于 ctx.sessions.list,不存在=throw(fail loud)。`openView(id,view)` 写 viewFor。 - prune:订 sessions.list → removed id 清 viewFor 条目;current.sessionId 指向已删会话时置 undefined(⚠后者契约未明写,按 fail-safe 补,待 main 确认)。 - SlotMap declare merge 三坑 + apply 里 ctx.slots.define 三条(single;sidebar=root,conversation/details=session)。 ## 2. web 壳(packages/client/web) ### 2.1 boot 时序 1. 入口(vite 产物):静态 import cordis/react/react-dom/ui-slots/web-react/ui-primitives + **loader 机件**(代码家在 runtime 包,壳静态 import——见 §4 对界)。 2. new cordis Context(root ctx)→ 实例化 loader → 挂 ctx.loader → 播种模块表六实体(与壳同实例,插件 require 到同一 React/cordis)。 3. import ui-theme 的 src/styles/ 两份 CSS 为 base 样式表(vite 静态引入;t0-checklist 已知坑:font-family base 变量 T0 补档)。 4. createRoot → `` boot loading 页(纯壳组件:logo+loading,零插件依赖,样式独立不依赖插件 CSS)。 5. `loader.start()`(读 __DSH_BOOT__:immediately 组并行→其余拓扑)→ `await loader.settled()`。 6. settled 成功 → AppRoot 一次切换真 UI;单插件失败 → loading 页显式列出 failed 插件 id(订 loader.status store),不做部分可用。 ### 2.2 真 UI 装配闭合(settled 后,壳内一处) - `SessionProvider = createSessionProvider({ useCurrent: ()=>ctx.layout.current.useSelector(s=>s.sessionId), resolveBinding: id=>ctx.sessions.binding(id) })`。⚠契约缺口:SessionProviderDeps 无 slots/core 注入位,provider 要渲染 conversation+details 两坑拿不到 renderSlot 来源——已报 main 仲裁(fw-react 属地)。 - `slots = scopedSlots(core, 'sidebar','conversation','details')`(layout define 的三坑显式转授壳)。 - AppFrame 组件经 loader 模块表 `require('@deepseek-ai/dsh-client-ui-layout')` 取导出面(壳持有 loader,settled 后可读),props 注入 §1.2 全套后渲染。 - SessionProvider 渲染的两坑需落进 grid 第 2/3 列:需要 provider 输出 display:contents 兼容结构(与 fw-react 对齐 DOM 形状,随上条仲裁一并定)。 ### 2.3 构建管线拓扑 ``` packages/client/web: vite build(壳 bundle:react/react-dom/cordis/纯库三包/loader 机件/AppRoot/boot) index.html 模板(__DSH_BOOT__ 占位由 host 注入) 8 个插件包: tsdown -c 引用共享 preset → dist/client.js(闭包工厂,CSS 内联,external→require) dsh web serve: host webserver 托管壳 dist + GET /plugins//client.js + GET / 注入 —— serve/注入归 rt-core;壳 dist 位置与构建归我 dev 工作流: 壳 vite build --watch;插件 tsdown --watch;手动刷新(无 HMR) ``` ## 3. tsdown client preset(T0 模板后接手) - `packages/client/tsdown.client.ts` export 工厂(入参 {id}): - banner `window.DSHClientProxy.loadPlugin({ id: '', factory: (require) => {`;footer `return <模块导出面>; } })`——导出面含 apply,loader 装载后登记模块表。 - external=模块表清单(react/react-dom/cordis/ui-slots/web-react/ui-primitives + 全部 @deepseek-ai/dsh-client-* 插件名)→ 编译为 require('') 调用。 - CSS Modules 内联:css 文本进 bundle,执行时注入 `