Skip to content

浏览器界面的很大一部分由约 30 个 ui-* 模块构成,每个都是一个 Cordis 插件,负责绘制界面的一个切片。它们不是靠编辑某个中央布局来接线,而是各自把组件注册进一个具名 slot——即声明者所声明的扩展点。组装关系由 cordis.patch.yml(web-app bundle)决定,渲染树则挂在唯一的 root slot 之下。

模块范式

每个 ui-* 包都带有一个浏览器面(./client),结构一致:

  1. 合并 SlotMap(以及 LocaleNamespaceMap)来声明它要渲染或拥有的 slot 契约。
  2. ctx.slots.register({ name, children?, store?, locale?, inject? }, Component) ——一次调用同时贡献组件并(可选)声明子 slot、共享/独占 store、locale 命名空间与业务 inject 面。
  3. 注册/声明均包裹在 ctx.effect 内,卸载自动级联。

框架把组件 props 拼装成四个部分:运行时份额(owner props + 标准件——useSessionuseSessionsuseWorkspacessessionIduseProjection)、渲染份额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-layoutroot 的 AppFrame:侧栏+会话+详情三列、shell.overlay、主题 presenter
ui-sidebar标识、新建会话、折叠轨、Workspace/Settings 座
ui-workspace侧栏里的 WorkspaceBrowser + 首屏里的 WorkspacePicker
ui-conversation骨架、聊天视图、composer/输入坞、详情壳、会话头操作
ui-input-trigger/@ 检测、候选菜单、ctx.inputTriggers 源清单
ui-commandsctx.commandUi:命令目录缓存与三类派发
ui-skill/ 触发的技能调用源
ui-subagent子代理目录会话头操作、composer 链、@ 引用源
ui-jobs基于 jobsBySession 的后台任务列表会话头操作
ui-goal输入坞中的 GoalBar,读数来自 goal 投影
ui-planconversation.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-presetAgent 预设选择 + 清单管理
ui-user-questionsconversation.composer 渲染用户问题
ui-settings设置域底座:ctx.settingsScope + slot 契约
ui-settings-general设置壳、General 区、chrome
ui-settings-modelsDeepSeek / 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:扩展点

SlotCorepackages/client/ui-slots/src/index.ts)与框架无关,构造时即声明内置的 root 座:

ts
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 Servicepackages/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

ts
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
sidebarconversationdetailsshell.overlayui-layoutui-sidebar、ui-conversation、(匿名入口)、overlay 列表
conversation.session.header.actionsui-conversationui-subagent(目录)、ui-jobs(任务列表)
conversation.input.dockui-conversationui-goal(GoalBar)、队列行
conversation.input.planconversation.input.modelui-conversationui-plan、ui-model-selection
conversation.chat.nodeui-conversation(keyed)ui-tool、ui-workflow-run
conversation.chat.turnTailui-conversationui-deliverables
conversation.chat.assistant-actionsui-conversation(list)ui-message-feedback(feedback
conversation.composerui-conversation(chain)ui-user-questions(question
conversation.viewui-conversation(list)ui-trajectory
conversation.hero.workspace.directoryFlowui-conversationui-workspace、ui-directory-picker-*
sidebar.workspacessidebar.workspaces.directoryFlowui-sidebarui-workspace、ui-directory-picker-*
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboardingui-settingsui-settings-general 的 chrome/分区
settings.general.itemui-settings-generalui-locale(Language)、ui-theme(Appearance)、ui-permission-presets、ui-agent-preset
tool.call.toolviewui-tool(keyed)业务 Tool 视图

SlotCore 还给监管者留了一根缝:reportEntryError 可以把崩溃的入口从它的单元里让位(渲染器退休它,让 slot 回退到下一个幸存者),而注册本身在 disposer 运行前仍留在台账上。chain 类型从不让位——选举替代在 select 时解析。snapshot() 以 JSON 安全的方式投影活声明树(注册者、单元、优先级、active),让状态界面无需导入任何组件即可镜像贡献健康度。

模块作者的周期纪律

三条规则保障 ui-module 组合的安全:

  1. ctx.effect 内注册(或经由 slots.register 的 Service 包装,它把处置路由到调用方 fiber 上)。卸载随即自动级联注册 + 声明 + store 挂载。
  2. 一个 slot 只有一个声明者。声明已被别家入口声明过的子键会抛错并点名首个声明者——这条「独占渲染权」保证让 renderSlot 可以安全地经 props 向下传递。
  3. store 属主按作用域SlotRegistry 的实例轴映射 handle × scope-key;会话级 store 按会话各一份,并在作用域死亡时被裁剪(pruneStoreScope),随它一起丢弃持久化状态。根级记录不受会话拆卸影响。

据此,一个模块可以被挂载或热重载(dsh-client-hmr)而无需任何中央注册表知道它存在——因为每个座都在 SlotMap 里可发现、每个 widget 都是一次性贡献,组合天然成立。

组合住在 bundle 补丁里

没有任何 ui-module 被代码硬连到另一个上。发布浏览器界面的清单住在 packages/bundle/web-app/cordis.patch.ymldsh.client 行里(ui-themeui-layoutui-sidebarui-conversationui-toolui-deliverablesui-workspaceui-settings*ui-goalui-planui-message-feedbackui-model-selection…)。从补丁里删掉某一行,就整体移除该功能的贡献——视图、slot 与文案一起——因为 shell 里没有别处 import 它。这正是宿主用来在 agent 预设背后禁用 base 的 agent 面行的同一套「行覆盖」机制;浏览器半分只是一样骑在那张图里。

版本表(本页涉及的包)

它们依赖:@deepseek-ai/cordis(vendored)、@deepseek-ai/cordis-plugin-loader@deepseek-ai/schemastery(vendored)。

延伸阅读

  • 前端:客户端运行时与链路ctx.slotsctx.sessionsctx.workspaces 所依托的东西。
  • 前端:Web 前端 — web-app bundle 挂载哪些 ui 模块、shell 如何渲染 root
  • 前端:Schema 表单 — ui-settings 所拥有的设置编辑器。
  • 前端:本地化LocaleNamespaceMap 合并与 t 座。
  • packages/client/ui-slots/src/index.tsSlotCoreSlotKindSlotScopeComposedProps
  • packages/client/ui-layout/src/client/AppFrame.tsx— 三列框架——以及 packages/bundle/web-app/cordis.patch.yml(ui 模块清单)。