浏览器的很大一部分由 38 个 ui-* 模块构成,每个都是一个给表面涂上一块的 Cordis 插件。它们不是靠编辑某个中央布局来接线;每个模块都把组件注册进一个具名 slot——由声明它的属主声明的扩展点。组合逻辑在 cordis.patch.yml(web-app bundle)里,渲染树挂在一个 root slot 上。
模块范式
每个 ui-* 包都带一个浏览器面(./client),形状相同:
- 合并
SlotMap(以及LocaleNamespaceMap)来声明它渲染或拥有的 slot 契约。 ctx.slots.register({ name, children?, store?, locale?, inject? }, Component)——一次调用贡献组件,并(可选)声明子 slot、共享/独占 store、locale 命名空间与业务 inject 面。- 注册/声明发生在
ctx.effect内,卸载自动级联。
框架把每个组件的 props 合成自四份:运行时份额(属主 props + 标准套件——useSession、useSessions、useWorkspaces、sessionId、useProjection)、渲染份额(renderSlot/renderSlotChain/SessionProvider)、store 份额(useStore/actions)与inject 面(绑定到你的 <inject> 工厂的业务钩子)。见 packages/client/ui-slots/src/index.ts。
SlotKind 是 'single' | 'list' | 'keyed' | 'chain';SlotScope 是 'root' | 'session-maybe' | 'session'。声明即认领:只有声明某子键的入口才能渲染它。
全部 38 个 ui-* 模块
| 包 | 职责 |
|---|---|
ui-slots | 纯扩展点核心(SlotCore)——无 React、无 Cordis |
ui-renderer | 运行时注册表:SlotRegistry(一个 Cordis Service,即 ctx.slots)、React slot 绑定、ctx.uiRenderer.mount |
ui-primitives | 共享基础件(markdown、math、highlight),供 widget 使用 |
ui-theme | 主题注册表(--dsw-* 令牌)、Appearance + FontSize 设置行 |
ui-layout | root AppFrame:sidebar+conversation+details 三列、shell.overlay、主题呈现器、ctx.layout 面板服务 |
ui-sidebar | 导航壳:品牌行(sidebar.brand.mark/sidebar.brand.name)、New Session、折叠导轨、承载会话树的浏览区、底部钉住的 sidebar.settings/sidebar.footer.action |
ui-workspace | 侧栏里的 Workspace/Session 浏览器 + hero 里的 WorkspacePicker——分组或平铺行、搜索、状态圆点、fork/archive |
ui-session | Session Controller 的 React 适配:useSession/useSessions 钩子与会话作用域 slot 数据 |
ui-conversation | 组装座椅:聊天骨架、composer/input 停靠区、details 壳、header 动作(即 ui-chat 等占用的座椅) |
ui-chat | Chat 目标:拥有 conversation.chat.node(keyed)与 conversation.message.images、节点定义、details、滚动状态 |
ui-input-trigger | / 与 @ 检测、候选菜单、ctx.inputTriggers 来源名册 |
ui-commands | ctx.commandUi:命令目录缓存与三种分派 |
ui-skill | / 触发的技能调用来源 |
ui-subagent | 子代理目录 header 动作、composer 链、@ 引用来源 |
ui-jobs | 基于 jobsBySession 的后台任务列表 header 动作 |
ui-goal | input 停靠区里的 GoalBar,基于 goal 投影 |
ui-plan | conversation.input.plan 里的计划模式状态芯片 |
ui-tool | 工具调用呈现、keyed 分派 + 通用兜底(tool.call.toolview)、conversation.details.tool 里的工具详情 |
ui-trajectory | 轮次感知的事件账本,conversation.view 标签页 |
ui-workflow-run | 作为 Chat 节点的持久 workflow 运行 |
ui-deliverables | 产物文件行 + 内联文件链接 |
ui-message-feedback | 助手动作条里的赞/踩 + 备注 |
ui-model-selection | /model popupSelect + composer 模型座椅 |
ui-permission-presets | 权限默认行 + 风险确认 |
ui-agent-preset | Agent 预设选择 + 名册管理 |
ui-user-questions | 经 conversation.composer 渲染用户提问 |
ui-approval | 作用域化 Remote Event 瀑布之上的审批 composer 接管(conversation.approval.detail);零运行时依赖 |
ui-reference | composer 的统一 Web @file/@session 引用来源 |
ui-schedule | Session header 里的只读活跃 Schedule 目录 |
ui-settings | 设置域基础:ctx.settingsScope + 设置 schema 服务 + slot 契约 |
ui-settings-general | 设置壳、General 分区、chrome |
ui-settings-models | DeepSeek / pi-ai provider + 模型编辑器 |
ui-settings-plugins | 宿主平面插件配置卡片 |
ui-settings-plugin-inventory | 只读插件清单标签页 |
ui-directory-picker-browse | 应用内浏览目录选择器 |
ui-directory-picker-native | 无渲染的原生 OS 选择器驱动 |
ui-attachment | 附件渲染原语——在 web-app bundle 里组合,不再是平台种子模块 |
(另有 ui-cordis,Cordis 动态插件定义卡片——它是 web-app cordis.patch.yml 里的普通一行,不是仅开发用的表面。)
ui-slots:扩展点
SlotCore(packages/client/ui-slots/src/index.ts)无框架依赖,构造时就声明内置 root 座椅:
constructor() {
const root = this.record('root')
root.spec = { kind: 'single', scope: 'root' }
root.declaredBy = '(built-in)'
root.declarationEpoch = 1
}值得知道的注册语义:
- 注册进未声明的 slot 会抛;声明已被声明过的子键会抛。
- 遮蔽:single/keyed/list 单元按
priority(默认 0)升序;单元里最低的活跃条目渲染,因此 priority 0 是历史上「单占位、失败即声」的默认。 - 卸载会递归折叠每个已声明子键——一条生命周期轴。
onMutate/onEntryError接入ctx.emit与崩溃监管;snapshot()导出 JSON 安全的声明树。
运行时 SlotRegistry(Cordis Service,packages/client/ui-renderer/src/client/registry.ts,构造函数约在第 95 行)增加了按 scope 的 store 实例轴、install(createSlotRenderer())(仅启动一次)、installLocale(localeFace)、installScope,并把注册销毁路由到调用者的 fiber。scope 属主死亡时,其 store 实例随之一并修剪。
ui-layout:组装 shell
ui-layout 通过一次 register 调用(packages/client/ui-layout/src/client/index.ts)把 AppFrame 贡献进运行时 root slot,同时声明它的四个子 slot:
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'root': { kind: 'single'; scope: 'root' }
'sidebar': { kind: 'single'; scope: 'root' }
'conversation': { kind: 'single'; scope: 'session-maybe' }
'details': { kind: 'single'; scope: 'session' }
'shell.overlay': { kind: 'list'; scope: 'root' }
}
}AppFrame.tsx 渲染带拖拽手柄(指针捕获 + rAF 节流)的三列网格、对面板宽度做让步求解,并在固定树位置渲染每个子 slot。属主 props 已消失:details 占用者(ui-conversation 的 DetailsPanel)不再从框架收到列几何——框架注入 session 作用域的会话 id 与钩子,而新的 ctx.layout 面板服务(packages/client/ui-layout/src/client/service.ts)掌管列是否打开。ctx.layout 是一个小型跨插件面——toggleSidebar()、openDetails()、closeDetails()——由根入口的布局 store 动作支撑(attachPanels 在根挂载后接线一次),因此任何插件里的 widget 都能驱动面板迁移,无需伸进框架内部。shell.overlay 依旧是叠加式、全帧浮动座椅(徽章、toast)。
全界面的具名座椅
取自模块 README 与源码合并的真实 slot 名:
| 座椅 | 声明者 | 占用者 |
|---|---|---|
root | 运行时(内置) | ui-layout AppFrame |
sidebar、conversation、details、shell.overlay | ui-layout | ui-sidebar、ui-conversation、(匿名条目)、overlay 列表 |
sidebar.brand.mark、sidebar.brand.name | ui-sidebar(single) | ui-brand-official(仅官方构建) |
sidebar.workspaces、sidebar.workspaces.directoryFlow | ui-sidebar | ui-workspace、ui-directory-picker-* |
sidebar.settings | ui-sidebar(single) | ui-settings-general(Settings 触发器 + 面板) |
sidebar.footer.action | ui-sidebar(list) | 功能页脚动作 |
conversation.session、conversation.session.header | ui-conversation(组装) | 组合行 |
conversation.session.header.lineage、.utilities | ui-conversation | 功能 header 格子(fork 谱系、工具) |
conversation.session.header.actions | ui-conversation | ui-subagent(目录)、ui-jobs(任务列表) |
conversation.view | ui-conversation(list) | ui-chat(id: 'chat')、ui-trajectory |
conversation.chat.node | ui-chat(keyed,scope: session) | ui-tool、ui-workflow-run、ui-deliverables 行 |
conversation.chat.commandview | ui-chat(keyed) | 命令行 |
conversation.message.images | ui-chat(single) | 随附画廊(ui-chat 自身) |
conversation.chat.turnTail | ui-chat(chain) | ui-deliverables |
conversation.chat.assistant-actions | ui-chat(list) | ui-message-feedback(feedback) |
conversation.details.tool | ui-chat(single) | ui-tool(ToolDetails) |
conversation.approval.detail | ui-approval(single) | ui-approval 审批 composer |
conversation.composer | ui-conversation(chain) | ui-user-questions(question) |
conversation.composer.bar | ui-conversation(single,session-maybe) | composer 条 |
conversation.composer.dock | ui-conversation(list) | input 区行 |
conversation.input.left、.right、.overlay | ui-conversation(list) | 功能 input 单元格 |
conversation.input.attachments | ui-conversation | 附件预览 |
conversation.input.dock | ui-conversation | ui-goal(GoalBar)、队列行 |
conversation.input.plan、conversation.input.model | ui-conversation | ui-plan、ui-model-selection |
conversation.hero.workspace、.directoryFlow | ui-conversation | ui-workspace、ui-directory-picker-* |
conversation.hero.brand.mark、conversation.hero.agentPreset | ui-conversation(root) | 品牌标记占用者、ui-agent-preset |
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboarding | ui-settings | ui-settings-general chrome/分区 |
settings.general.item | ui-settings | ui-theme(Appearance + FontSize)、locale(Language)、ui-permission-presets、ui-agent-preset |
tool.call.toolview | ui-tool(keyed) | 业务 Tool 视图 |
两处值得点名的所有权变化:聊天节点与图片画廊座椅(conversation.chat.node、conversation.message.images)由 ui-chat 声明并注册——ui-conversation 只保留组装座椅(conversation.session.*、conversation.view、composer、hero.*、input.*),负责框定那些占用者渲染的位置。
SlotCore 还给监管者一条缝:reportEntryError 可以把崩溃条目让位出它的单元(渲染器将其退役,slot 落到下一个幸存者),而注册本身在 disposer 运行前始终留在台账上。Chain 种类永不让位——选举替代在选中时解析。snapshot() 以 JSON 安全方式投影活声明树(注册者、单元、优先级、活跃态),让状态面无需导入任何组件即可镜像贡献健康度。
模块作者的生命周期纪律
三条规则保证 ui 模块组合安全:
- 在
ctx.effect内注册(或经slots.registerService 包装,后者把卸载路由到调用者的 fiber)。卸载随之自动级联注册 + 声明 + store 挂载。 - 每个 slot 只有一个声明者。 声明已被其他条目声明过的子键会抛并点名首个声明者——这正是让
renderSlot可安全经 props 下传的独占渲染权保证。 - Store 所有权按 scope。
SlotRegistry实例轴映射handle × scope-key;会话作用域 store 每个会话一份,scope 死亡即被修剪、丢弃持久状态。根作用域记录不受会话拆除影响。
遵守这些,模块就可以在没有任何中央注册表知情的情况下被挂载或热重载(dsh-client-hmr)——组合天然成立,因为每个座椅都能在 SlotMap 里被发现、每个 widget 都是可销毁的贡献。
组合活在 bundle 补丁里
没有 ui 模块在代码层面硬接进另一个。随产品发布的浏览器表面名册在 packages/bundle/web-app/cordis.patch.yml 里,作为 dsh.client 行(ui-theme、ui-layout、ui-sidebar、ui-conversation、ui-chat、ui-tool、ui-deliverables、ui-workspace、ui-settings*、ui-goal、ui-plan、ui-message-feedback、ui-model-selection、ui-approval、ui-reference、ui-schedule…)。从补丁删掉一行就移除该功能的全部贡献——视图、slot 与文案一起——因为 shell 里没有别的代码 import 它。这与宿主在 agent 预设背后禁用基础 agent 平面行的「行覆盖」机制相同;浏览器面只是骑在同一张图上。
版本表(本页涉及的包)
延伸阅读
- 前端:客户端运行时与链路 —
ctx.slots、ctx.sessions、ctx.workspaces立在什么之上。 - 前端:Web 前端 — web-app bundle 挂载哪些 ui 模块、shell 如何渲染
root。 - 前端:设置 Schema 与表单 — ui-settings 拥有的设置模型层。
- 前端:本地化 —
LocaleNamespaceMap合并与t座椅。 packages/client/ui-slots/src/index.ts—SlotCore、SlotKind、SlotScope、ComposedProps。packages/client/ui-renderer/src/client/registry.ts—SlotRegistry(运行时服务)与packages/client/ui-layout/src/client/AppFrame.tsx(三列框架)。packages/bundle/web-app/cordis.patch.yml— ui 模块名册。