学会做插件以后,脑子里会冒出各种想法:
- 在会话标题栏加一个按钮,点一下就跑测试。
- 在右侧栏加一页,列出项目里所有的 TODO 注释。
- 让左侧栏里的工作区能分组嵌套。
每个想法落到实处,都要先回答两个问题:
- DSH 插件到底做不做得了?
- 如果做得了,该照着谁的代码写?
dsh-ccgui 是个现成的参照物。它在 DSH 上动了十几处地方,插件能用上的办法,它几乎都用了一遍。
这一篇我按功能点把它过一遍。每一处改动我只讲清楚两件事:
- 它是靠 DSH 的什么机制做到的;
- 对应的代码写在哪个文件里。
读完这一篇,你能做到两件事:
- 拿到一个新想法,能判断出插件做不做得了、该用哪种办法;
- 能在 dsh-ccgui 源码里精准找到同类功能的代码,直接喂给 AI 当范本。
这一篇不用敲代码,但最好把 dsh-ccgui 的源码开在旁边对照着看。代码就在上一篇用过的 dsh-branch-chip/.reference/dsh-ccgui 文件夹里,用 VS Code 打开就行。
一、界面改动盘点
上一篇讲了 dsh-ccgui 最核心的一件事:把 Claude Code 这些 CLI 接成 DSH 的模型。那部分全在后台跑,界面上看不见。
界面上看得见的改动,主要有下面这些:

| 位置 | 加了什么 |
|---|---|
| 输入框下方 | CLI 切换器;模型菜单 |
| 会话标题栏 | 启动脚本;终端按钮 |
| 左侧栏 | 每个会话前的 CLI 图标;会话列表的树形连线;项目右键「新建 Worktree…」和 Worktrees 分组;设置里的「CC GUI 设置」页 |
| 右侧栏 | 「变更」页;一展开就显示工作区文件;只显示图标的紧凑页签;文件右键「添加到对话」 |
源码一共 113 个文件,其中 70 个在前端界面这一半。文件看着多,其实归结起来只用了五种办法:

二、两个代码入口
读一个插件的源码,先抓它的两个入口文件。古法开发那一篇讲过:
- 后台那一半从
src/index.ts开始; - 界面那一半从
src/client/index.tsx开始。
后台入口
先看它向 DSH 要了哪些服务。打开 src/index.ts,找到这一行:
static inject = ['llm', 'agents', 'subprocess', 'tools']dsh-ccgui 的后台入口写成了一个类,所以用 static inject。作用和前面写过的 export const inject 一样,用来列出“少了它我就没法干活”的核心服务:
| 服务 | dsh-ccgui 用它干什么 |
|---|---|
llm |
把每个 CLI 登记成一个模型 |
tools |
登记那个只记录、不执行的 cli_tool |
subprocess |
启动 CLI,跑 git |
agents |
查出每个会话背后的智能体,好知道它选了哪个 CLI |
还有几个服务没在这个列表里,属于“有就用,没有就算了”:
webServer和connection:给变更页开接口用;approval:弹审批卡片用。
上一篇审方案时讲过“必须有”和“有就用”要分开。当时让 AI 照着写的范本,就是这个文件。
界面入口
打开 src/client/index.tsx,拉到文件末尾看 apply 的结尾:
const settings = createFeatureSettings(BROWSER_STORAGE)
registerSettingsPage(ctx, settings, cliPrefs)
registerSwitchableFeatures(ctx, settings)
registerAddToChat(ctx)
}这里 registerSettingsPage 负责登记设置页,registerAddToChat 负责登记“添加到对话”。中间的 registerSwitchableFeatures 展开后,又依次调用了五个注册函数:
registerTerminalEntryregisterFilesDefaultregisterChangesregisterWorktreeregisterLaunchScripts
代码组织得很规整:一个功能对应一个 register 函数,各自放在独立的文件里。
你想看哪个功能,就在 src/client 里搜对应的 register 函数。用了哪些插槽、调了哪些服务,函数里都写着。
package.json 依赖声明
古法那一篇提过,package.json 里的 dsh.client.inject 用来声明“你要往哪块界面积木里放东西”。做分支查看时只声明了管输入框的那一块,而 dsh-ccgui 一口气声明了九块:
"inject": [
"@deepseek-ai/dsh-api-terminal-controller",
"@deepseek-ai/dsh-client-ui-context-menu",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-layout",
"@deepseek-ai/dsh-client-ui-model-selection",
"@deepseek-ai/dsh-client-ui-session",
"@deepseek-ai/dsh-client-ui-sidebar",
"@deepseek-ai/dsh-client-ui-sidebar-right",
"@deepseek-ai/dsh-client-ui-workspace"
]看包名就能猜出大概用途:
context-menu管右键菜单;sidebar管左侧栏;sidebar-right管右侧栏;model-selection管模型选择。
这份清单也是张地图。以后你的想法要动哪块界面,直接去读对应积木的文档就行。
目录结构
前后台代码在目录里是分开存放的,中间只留了一个共用文件:

| 位置 | 放什么 |
|---|---|
src/index.ts |
后台入口 |
src/cli-session.ts、src/session/ |
一个 CLI 会话怎么跑,上一篇讲的 ACP 那一套 |
src/git/ |
变更页和 Worktree 背后跑 git 的接口 |
src/client/ |
界面那一半,所有看得见的东西 |
src/constants.ts |
两半唯一共用的文件,只放几个名字 |
界面代码绝对不能导入后台文件,这在项目里是条硬规矩:src/client 下的代码,只允许导入 src/client 内部模块和 src/constants.ts。
三、插槽放组件
这是最常用的一种办法,之前做分支查看用的就是它。界面积木提前留好空位,你把写好的组件塞进去。
dsh-ccgui 用插槽做了下面这些界面:
| 加了什么 | 放进哪个插槽 | 代码在哪 |
|---|---|---|
| CLI 切换器 | conversation.input.left |
src/client/index.tsx |
| 模型菜单 | conversation.input.right |
src/client/model-menu.tsx |
| 启动脚本 | conversation.session.header.utilities |
src/client/launch-script-register.ts |
| 终端按钮 | conversation.session.header.utilities |
src/client/terminal-entry.tsx |
| 会话前的 CLI 图标 | sidebar.session.row.leading |
src/client/index.tsx |
| 设置页 | settings.section |
src/client/settings-register.ts |
第一行的 conversation.input.left 就是分支查看器用的那个插槽,CLI 切换器和你的分支徽章刚好挨着。

启动脚本和终端按钮塞进了同一个插槽。谁排左边,由 order 决定。
打开 src/client/terminal-entry.tsx,看终端按钮的登记代码:
const SLOT = 'conversation.session.header.utilities'
/** Just left of the header's overflow menu (order 0), right of the schedule chip (-5). */
const ENTRY_ORDER = -1 child.slots.inject(SLOT, () =>
child.slots.register(
{
name: SLOT,
id: 'dsh-ccgui.terminal-entry',
order: ENTRY_ORDER,
inject: () => ({ sidebar: child.sidebarRight }),
},
TerminalEntry,
),
)先调 slots.inject 再调 register,套路和之前写过的一样,只是多了一个 order 字段。
order 相当于排队的号码,数字越小越靠前。源码注释里记了左右邻居的号码:
- 标题栏最右边的
…更多菜单是0; - 定时任务标记是
-5; - 终端按钮设为
-1,刚好卡在…菜单左边; - 启动脚本设为
-2,紧挨在终端按钮左边,两个和终端相关的按钮就凑一块儿了。
插槽清单在哪
直接查官方的插槽清单,地址在上一篇已经写进了 AGENTS.md。我写这一篇时数过,清单里一共有 112 个插槽。
每个插槽在界面什么位置、会传给组件什么参数,清单里都列得很清楚。看名字前缀就能定位区域:
conversation.在对话区;sidebar.在左侧栏;sidebar.right.在右侧栏;settings.在设置页。