浏览器界面的很大一部分由约 30 个 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 拼装成四个部分:运行时份额(owner 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'。声明即占有:只有声明某子键的入口才被允许渲染它。
全部 ui-* 模块
| 包 | 职责 |
|---|---|
ui-slots | 纯扩展点内核(SlotCore)——无 React、无 Cordis |
ui-primitives | 共享构件(markdown、数学公式、高亮),供小组件使用 |
ui-theme | 主题注册表(--dsw-* token)、外观设置行 |
ui-layout | root 的 AppFrame:侧栏+会话+详情三列、shell.overlay、主题 presenter |
ui-sidebar | 标识、新建会话、折叠轨、Workspace/Settings 座 |
ui-workspace | 侧栏里的 WorkspaceBrowser + 首屏里的 WorkspacePicker |
ui-conversation | 骨架、聊天视图、composer/输入坞、详情壳、会话头操作 |
ui-input-trigger | / 与 @ 检测、候选菜单、ctx.inputTriggers 源清单 |
ui-commands | ctx.commandUi:命令目录缓存与三类派发 |
ui-skill | / 触发的技能调用源 |
ui-subagent | 子代理目录会话头操作、composer 链、@ 引用源 |
ui-jobs | 基于 jobsBySession 的后台任务列表会话头操作 |
ui-goal | 输入坞中的 GoalBar,读数来自 goal 投影 |
ui-plan | conversation.input.plan 的计划模式状态片 |
ui-tool | 工具调用呈现,按键派发 + 通用兜底 |
ui-trajectory | 轮次感知的事件台账,conversation.view 的一个标签页 |
ui-workflow-run | 把持久化工作流运行重构为聊天节点 |
ui-deliverables | 成品文件行 + 内联文件链接 |
ui-message-feedback | 助手操作条中的点赞/点踩 + 备注 |
ui-model-selection | /model popupSelect + composer 模型座 |
ui-permission-presets | 带风险确认的权限默认行 |
ui-agent-preset | Agent 预设选择 + 清单管理 |
ui-user-questions | 经 conversation.composer 渲染用户问题 |
ui-settings | 设置域底座:ctx.settingsScope + slot 契约 |
ui-settings-general | 设置壳、General 区、chrome |
ui-settings-models | DeepSeek / pi-ai 提供商与模型编辑 |
ui-settings-plugins | 宿主面插件配置卡片 |
ui-settings-plugin-inventory | 只读插件清单标签页 |
ui-directory-picker-browse | 应用内浏览式目录选择器 |
ui-directory-picker-native | 无渲染原生系统选择器驱动 |
ui-attachment | 附件渲染原语(平台种子模块) |
(另有 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);单元内优先级最低的存活入口渲染,因此优先级 0 是历史的「每个单元只允许一个入口」的响亮失败。 - 卸载会递归折叠所有已声明子项——单一生命周期轴。
onMutate/onEntryError桥接到ctx.emit与崩溃监管;snapshot()导出一棵 JSON 安全的声明树。
运行时 SlotRegistry(Cordis Service,packages/client/runtime/src/client/slots.ts)补上了 store 实例轴(按会话)、install(createSlotRenderer())(仅启动一次)、installLocale(localeFace),并把注册卸载经由调用方 fiber 路由。
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 渲染在固定的树位置。侧栏占用者接收来自让步求解的 { collapsed, width };会话占用者是 session-maybe(跨会话切换保持身份);详情列是严格会话并上报实时宽度。shell.overlay 是可叠加的、整个框架层的浮层座(徽标、toast)——想在整应用之上浮动一层表面时,这是推荐去注册的地方,而不是去与唯一的 root/sidebar/conversation 座硬碰。
界面上的具名座
取自模块 README 与源码合并的真实 slot 名:
| 座 | 声明者 | 占用者 |
|---|---|---|
root | 运行时(内置) | ui-layout AppFrame |
sidebar、conversation、details、shell.overlay | ui-layout | ui-sidebar、ui-conversation、(匿名入口)、overlay 列表 |
conversation.session.header.actions | ui-conversation | ui-subagent(目录)、ui-jobs(任务列表) |
conversation.input.dock | ui-conversation | ui-goal(GoalBar)、队列行 |
conversation.input.plan、conversation.input.model | ui-conversation | ui-plan、ui-model-selection |
conversation.chat.node | ui-conversation(keyed) | ui-tool、ui-workflow-run |
conversation.chat.turnTail | ui-conversation | ui-deliverables |
conversation.chat.assistant-actions | ui-conversation(list) | ui-message-feedback(feedback) |
conversation.composer | ui-conversation(chain) | ui-user-questions(question) |
conversation.view | ui-conversation(list) | ui-trajectory |
conversation.hero.workspace、.directoryFlow | ui-conversation | ui-workspace、ui-directory-picker-* |
sidebar.workspaces、sidebar.workspaces.directoryFlow | ui-sidebar | ui-workspace、ui-directory-picker-* |
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboarding | ui-settings | ui-settings-general 的 chrome/分区 |
settings.general.item | ui-settings-general | ui-locale(Language)、ui-theme(Appearance)、ui-permission-presets、ui-agent-preset |
tool.call.toolview | ui-tool(keyed) | 业务 Tool 视图 |
SlotCore 还给监管者留了一根缝:reportEntryError 可以把崩溃的入口从它的单元里让位(渲染器退休它,让 slot 回退到下一个幸存者),而注册本身在 disposer 运行前仍留在台账上。chain 类型从不让位——选举替代在 select 时解析。snapshot() 以 JSON 安全的方式投影活声明树(注册者、单元、优先级、active),让状态界面无需导入任何组件即可镜像贡献健康度。
模块作者的周期纪律
三条规则保障 ui-module 组合的安全:
- 在
ctx.effect内注册(或经由slots.register的 Service 包装,它把处置路由到调用方 fiber 上)。卸载随即自动级联注册 + 声明 + store 挂载。 - 一个 slot 只有一个声明者。声明已被别家入口声明过的子键会抛错并点名首个声明者——这条「独占渲染权」保证让
renderSlot可以安全地经 props 向下传递。 - store 属主按作用域。
SlotRegistry的实例轴映射handle × scope-key;会话级 store 按会话各一份,并在作用域死亡时被裁剪(pruneStoreScope),随它一起丢弃持久化状态。根级记录不受会话拆卸影响。
据此,一个模块可以被挂载或热重载(dsh-client-hmr)而无需任何中央注册表知道它存在——因为每个座都在 SlotMap 里可发现、每个 widget 都是一次性贡献,组合天然成立。
组合住在 bundle 补丁里
没有任何 ui-module 被代码硬连到另一个上。发布浏览器界面的清单住在 packages/bundle/web-app/cordis.patch.yml 的 dsh.client 行里(ui-theme、ui-layout、ui-sidebar、ui-conversation、ui-tool、ui-deliverables、ui-workspace、ui-settings*、ui-goal、ui-plan、ui-message-feedback、ui-model-selection…)。从补丁里删掉某一行,就整体移除该功能的贡献——视图、slot 与文案一起——因为 shell 里没有别处 import 它。这正是宿主用来在 agent 预设背后禁用 base 的 agent 面行的同一套「行覆盖」机制;浏览器半分只是一样骑在那张图里。
版本表(本页涉及的包)
它们依赖:@deepseek-ai/cordis(vendored)、@deepseek-ai/cordis-plugin-loader、@deepseek-ai/schemastery(vendored)。
延伸阅读
- 前端:客户端运行时与链路 —
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-layout/src/client/AppFrame.tsx— 三列框架——以及packages/bundle/web-app/cordis.patch.yml(ui 模块清单)。