--- description: "给 DeepSeek Harness Web GUI 用的 Codex 风格侧边栏皮肤:项目 + 最近 两个分区,以及一行不选项目就能直接开聊的新对话 —— 它会当场新建一个临时工作目录,并在这次对话被弃用后立刻回收。" kind: "package-reference" --- # dsh-plugin-codex-ui [English](README.md) | 中文 一个给 DeepSeek Harness Web GUI 用的 **Codex 风格侧边栏皮肤**。 它把系统自带的侧边栏浏览区换成两个可折叠分区 —— **项目** 和 **最近** —— 上面压一行 **新对话**:不用选择项目就能直接开始,插件会像 Codex 一样自动新建一个工作目录。 ``` DeepSeek Harness ⇤ ← 品牌行(系统自带,改了文案) + 新对话 ← 在新建的临时目录里直接开一个真实会话 ───────────────────────────────────────────── 项目 + ← 分区标题,点它就是折叠 📁 pi ← 每个项目一行,不带箭头 📁 dsh-plugin-codex-ui 最近 20260912-135505-27c0 ← 只列不属于任何项目的对话,只显示标题 … 显示更多 ───────────────────────────────────────────── ⚙ 设置 ← 系统自带的设置入口,保留 ``` 两个分区都能折叠,折叠状态刷新后依然保留;点某个项目会在它下面展开该项目的对话。 分区标题和行的读法与 Codex 侧边栏一致:分区标题是一行安静的小字,左边距与它下面各行的图标 对齐;行高 32px,标题独占整行宽度 —— 不带箭头、不显示相对时间 —— 图标和标题用深色的 `label-primary`,分区标题和折叠后的计数用 `label-tertiary`。悬停只改背景不改字色,当前对话 靠实心胶囊标记。 ## 改了什么 | 位置 | 变化 | | --- | --- | | 侧边栏品牌名 | 显示 **DeepSeek Harness**,替换原来的本地构建版本号。 | | 侧边栏浏览区 | 换成本插件的「项目 + 最近」。 | | 全局面板行 | 隐藏。本皮肤没有 Pull Request / 已安排 / 插件 这几行。 | | 系统自带的「新会话」按钮 | 侧边栏展开时隐藏,由本皮肤的 **新对话** 行替代;折叠成窄栏时保留系统自带的图标按钮。 | | `--dsh-sidebar-inline-padding` | 从 12px 收紧到 10px。 | 其余都还是系统自带的外壳:品牌行、折叠动画、56px 窄栏、底部设置区,都没有改动。 ## 安装 需要 DeepSeek Harness 的 `web` profile(Node ≥ 20),以及 `PATH` 里有 `pnpm` —— `dsh plugin` 会在 profile 目录里转发给 pnpm。 ```sh # 从 GitHub 安装 dsh plugin --profile web add github:ice-ai-lab/dsh-plugin-codex-ui # 或者从本地目录 dsh plugin --profile web add /absolute/path/to/dsh-plugin-codex-ui ``` 本包声明了 `dsh.bundle.patch`,所以 `dsh plugin add` 会自动把它加进 `dsh.profile.bundles`,下次启动自动生效。 **然后需要重启 `dsh web`。** 浏览器端是在启动时组装进 `window.__DSH_BOOT__` 的, 只刷新页面不会加载新插件。 卸载: ```sh dsh plugin --profile web remove dsh-plugin-codex-ui ``` ## 两个不只是「换皮」的地方 ### 不选项目直接开始 **「新对话」开的是一个真实会话。** 它先让 node 端新建一个工作目录,把该目录注册成真正的 Workspace,在其中创建会话并打开 —— 顺序和系统自带的「新建会话」完全一样,只是目录不用你自己 挑。随后出现的输入框就是系统原装的,而且从第一次敲键起就是活的:模型选择器、权限选项、`+`、 `@`、上下文计量全都能用。 这里没有自绘的假输入框,也没有任何延迟创建:能选模型的对话必须真实存在,而会话不可能没有工作 目录。目录就是随对话一起建的。 清理规则就是为这件事准备的。插件会记住这一行最后开出的那个对话,读者一旦没用它就离开,就立刻 回收:归档会话、注销 Workspace 注册、删除目录。连点十次也只会留下一个目录。**用过**的对话永远 不会被回收 —— 有消息的它就是普通对话;打过字但没发送的,它连同目录一起保留(行也继续列出来, 文字仍然点得回去),直到发送或清空为止。至于插件开了却失去记录的(创建与登记之间崩了、浏览器 存储被清了),会在下次加载时兜底扫掉:本皮肤根目录下没有任何 workspace 和会话引用的目录,或者 只装着没人碰过的对话的已注册 workspace,只要超过宽限期就一并清理。 为什么这次注册很关键:一个只有 cwd、不属于任何 workspace 的会话,在对话区看来是**不完整**的 —— 输入框会变成灰的「选择一个工作区开始」,模型选择器一起失效。走 workspace 之后,得到的会话 和「在项目里新建会话」逐字段完全一致,唯一区别只是它显示的 workspace 名字不同。 接下来把这类 workspace 挡在「项目」之外,是本皮肤自己的事,而不是副作用:node 端会报告它 分配目录的根路径,凡是在这个根下的 workspace 都算「无项目」。它的会话因此落在「最近」里 —— 无项目的对话本来就该在那儿。 ### 项目 **项目** 列出 DSH workspace 注册表里的工作目录。项目行只有一个文件夹图标加名字,**不带箭头**: 折叠状态由文件夹本身表达(收起时是闭合文件夹,展开时是打开文件夹),这样一行只有一个前导 标记,而不是两个。 点项目行会展开它的会话;行上的 `+` 在该项目里开新对话;🗑 只移除注册关系(目录和会话日志 都保留)。 用分区标题上的 `+`(或空状态里的「选择项目」按钮)添加项目:直接调起 Host 的目录选择器, 中间没有任何表单,选完即添加。取消选择不算失败,什么都不会变。 这个选择器就是 Host 自己的目录选择能力,和系统自带「选择工作区」用的是同一个。如果当前 组合没有装目录选择相关插件,就无法添加项目 —— 此时皮肤会就地给出提示,而不是默默没反应。 ### 最近 **最近** 只列出**不属于任何项目**的对话 —— 也就是用 **新对话** 起的那些,以及注册表没有 归属到任何项目的会话。项目自己的对话通过展开该项目查看;如果这里也列一遍,同一个对话就会 在两个标题下各出现一次。 一个对话一旦被归属到某个项目,就会从「最近」移到该项目下面;整个过程它的工作目录不变, 磁盘上什么都没有被移动。 会话行右侧有一个悬停出现的 🗑,它做的事是**归档**这个对话 —— `archiveSession` 是 Host 唯一的 会话移除手段:对话会从所有分组界面消失,而它的日志文件仍留在磁盘上。归档是**单向**的:客户端 API、Host 命令、系统自带界面里都没有「取消归档」。所以这个操作刻意分成两步:🗑 会把那一行变成 问题「归档「<标题>」?」,把要归档的是哪个对话写在问题里,只有再按「归档」才真的执行。悬停出现、 一点生效、且不可撤销的按钮,正好长在读者准备点开那一行的位置,那是陷阱;问一句就不是。 行会在归档后立刻消失,这是皮肤自己的事:会话列表本身不带归档集合,所以皮肤从 Workspace 投影里 读出这个集合并自己减掉 —— 系统自带的浏览区也是这么做的。 ## 无项目会话的目录位置 ``` $DSH_HOME/codex-workspaces/-/ # $DSH_HOME 默认是 ~/.dsh ``` 刻意**不用**其他侧边栏皮肤用的 `scratch` 目录:两个插件共用一个根目录时,任何一方的 清理逻辑都可能删掉另一方正在使用的会话目录。 这个目录不会自动清理,也不会被当成项目显示 —— 无项目的会话出现在 **最近** 里,这正是想要 的效果。不想要某个会话时,手动删掉对应目录即可。 有一件事值得知道:每点一次 **新对话** 都会新建一个目录**并注册**它,所以 workspace 注册表会 按「无项目对话」的数量增长 —— 一个新对话配一个新工作目录,和 Codex app 的做法一致。这些记录 不会出现在「项目」里,但它们是真实的注册项;想清掉其中一条(🗑 只对项目行显示,所以要用系统 自带的 workspace 界面或直接改 `workspace.json`),移除注册不会删目录、也不会删会话日志。 ## 自定义 **改品牌文案。** `lib/client.js` 里的 `BrandName`,改它返回的字符串即可。 **不再隐藏全局面板行。** 删掉 `lib/client.js` 中 `CSS` 里的这一行: ```css nav[class*="_panelList"] { display: none !important; } ``` 它存在的原因是:动态加载的客户端插件可能注册全局面板行(Pull Request / 已安排 / 插件 这类行就是这么进侧边栏的)。默认组合里一行都没有,所以有这条规则时侧边栏就只有「项目」 和「最近」;删掉它之后,插件贡献的面板行就会重新可用。 ## 这个皮肤不做什么 系统自带的浏览区还有会话搜索、重命名、fork、拖拽排序、子代理血缘等功能。 本皮肤刻意都没做 —— 它是皮肤,不是第二个浏览器。(移除对话是做了的:一个不能清理的列表, 不算你自己的列表。)如果你需要其余这些,卸载插件即可。(把 `lib/client.js` 里的 `SHADOW_PRIORITY` 调到大于 `0`,两个被占用的槽位都会交还给系统自带实现,等于不卸载 就关掉这个皮肤。) ## 实现要点 * **Node 端**(`lib/index.js`)在 harness web 服务器上挂两条 `exact` 路由:分配工作目录 (同时报告根路径和已经分配过的所有目录),以及删除本插件分配过的目录。目录名完全由服务端 生成;删除只接受名字,每个名字都校验为单个路径片段,并在解析后重新做包含性检查。调用方提供 的路径永远不会到达文件系统。浏览器端加载时的那次兜底清理读的就是这份清单 —— 用来区分 「没有任何东西引用的目录」和「还有活的东西住在里面的目录」。 * **浏览器端**(`lib/client.js`)占用两个 `single` 槽位:`sidebar.brand.name` 放品牌文案, `sidebar.workspaces` 放浏览区。`single` 槽位是「抢占」而不是「叠加」的 —— 注册表会拒绝 同优先级的第二次注册 —— 所以本皮肤用 `SHADOW_PRIORITY`(`-10`)注册,数值最小者渲染。 插件卸载、或该条目因渲染崩溃而退出时,系统自带的浏览区会自动拿回这个槽位,无需刷新。 * **两处「减法」用 CSS 实现**,而不是遮蔽组件。隐藏系统自带的面板行和「新会话」按钮本质上 是减法;为了做减法去遮蔽整个 `sidebar` 槽位,就意味着要重新实现品牌行、折叠动画、窄栏和 底部设置区 —— 而这四件事本皮肤并没有意见。 ## 校验源码 ```sh npm run check # 对插件的两端各跑一次 node --check ``` ## 生态 本插件的发布方式,就是 harness 对第三方插件的要求:仓库打上 [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic 以便被检索到,安装走 `dsh plugin add github:…`,而不是给核心仓库提 PR —— 官方贡献指南写得很清楚,核心仓库 [目前不接受外部 PR](https://github.com/deepseek-ai/deepseek-harness/blob/master/CONTRIBUTING.zh.md), 贡献路径指向生态。 ## License MIT