DeepSeek Harness 插件

omdsh-basemode

The session-mode system for the DeepSeek Harness web GUI: the segment registry every mode plugin registers into, the switch that renders them, and the sidebar dots that colour their conversations(英文原文)

跳到安装方式

来源信息

GitHub 仓库
omdsh-plugins/omdsh-basemode
最近更新
2026年8月20日
分类
记忆
GitHub stars
1
载体类型
plugin
目录证据
上游声明已找到 dsh.bundle
证据路径
package.json#dsh.bundle
核对版本
0.1.0-rc.8
上游核对日期
2026-08-20

该证据由上游目录提供。本站没有安装、运行或安全审核这个插件。

安装

默认先复制一段 Prompt,让 Agent 读 GitHub 仓库和源码;需要自己装时再切到命令。

复制这段 Prompt,发给 DSH、Codex 或其他 Agent,让它先读 GitHub 仓库和源码。

请先不要安装或执行任何命令。阅读这个插件的 GitHub 仓库、README 和关键源码,然后用清楚、直接的方式回答以下问题,帮助我判断它是否适合我的需求:

1. 这个插件是什么,解决什么问题;
2. 适合哪些用户和典型使用场景;
3. 安装后如何使用,并给出一个最小使用示例;
4. 有哪些已知限制,以及隐私、安全、兼容性或维护风险;
5. 给出“推荐 / 有条件推荐 / 不推荐”的明确建议和理由。

请区分仓库明确说明、根据源码推断和未知信息。证据不足时请明确说明,不要猜测或照抄 README。

GitHub:https://github.com/omdsh-plugins/omdsh-basemode
插件名:omdsh-basemode
作者:omdsh-plugins

检查来源文件

安装前先看这个插件目录里的 README 和其他文件。

文件资源管理器4 个文件
README.zh.md来源说明 · 只读预览
README 语言

omdsh-basemode

English | 中文

DeepSeek Harness 网页版的会话模式系统:所有模式插件都要注册进去的分段注册表、渲染它们的开关,以及侧栏上的两种标记——一种说明这段会话属于哪个模式,一种说明你此刻看的到底是哪一段。

它自己不发明任何一个模式。 Chatomdsh-chatmode 到来,Codeomdsh-codemode 到来,两者在这里谁也不比谁更"原生"。它唯一贡献的姿态,本来就在屏幕上:Work,harness 自己的会话列——把它做成一个分段,只是为了让开关有个地方可以切回去。它只在需要时才出现,见[基线姿态](#基线姿态)。

它提供什么

界面从哪来
sessionModes 服务ctx.provide,每个模式插件注册自己的分段都要走的那扇门
模式开关shell.overlay 里的一个条目——ui-layout 那层横跨整个框架的浮层;它对准会话列居中,没人伸手时自动让位
Work 分段——harness 自己的列registerBaseline:这个包为自己做的一次注册,一旦有模式插件带来这个姿态,就立刻撤走
侧栏每行最前面那个按模式着色的圆点row-marks.ts——在 harness 自己的行上,不是渲染出来的;由注册表的 toneowns 驱动
新建会话落在一段真正新建的对话上同一处改写:没有分段接下的请求会新建一段,而不是复用工作区里那段旧的空白对话——这样它的行才会出现在组首
侧栏的高亮,落在会话列正在显示的那段对话上同一支画笔——只在会话列与选中项不一致时才写;「会话列不是网页对话」的模式,正是在这种情况下把 harness 自带的高亮落在了原地
新建会话先递给正占着列的那个模式workspaces.startSession 的一层接管:先问激活的分段要不要,没人接才交给框架,并把这件事广播出去
sessionModes.column —— 会话列此刻真正显示的是什么激活的分段自己声明的 scope,没声明就是选中的那段对话

harness 一行没改。它注册的那个槽位是公开座位,接管只是往原型方法上盖一个自有属性,撤掉这一行,两样都原样奉还。

它也不注册任何 settings 命名空间,这是刻意的,不是漏掉的。分段注册表身上,没有什么是需要人来配置的——哪个姿态占着列,由每个标签页自己推导;存在哪些姿态,是 profile 的属性——所以它在插件中心里的卡片没有表单。

为什么要单独成包

它原本是 Chat 模式的一部分,而那是一次分层倒置,代价很具体。

模式插件离不开注册表。要是注册表装在某一个具体模式里,其他每个模式想往开关上放一颗药丸,都得依赖那个模式的整个包——它的托管工作区、它的 agent preset、它输入框上方那行说明。"想要 Code 模式却不想要 Chat 模式"是无解的。而且这样组出来的 profile 不只是少一个分段:omdsh-codemode 的浏览器半边会因为一个没人组装的服务永远停在 pending,客户端的启动审计会因此让整个页面失败。

把座位和姿态拆开,依赖关系才诚实:每个模式插件依赖这个包,这个包不依赖任何模式,"存在哪些姿态"也变成了 profile 的属性,不再取决于注册表碰巧归谁所有。

基线姿态

开关得有个能切回去的地方。以前,只装了这个包和 omdsh-codemode、别的什么都不装的 profile,拿到的是一个只有 Code 一颗药丸的控件——只有一个分段,进了终端就出不来,侧栏里每条会话也都没有圆点,因为没人认领它们。而缺的那个姿态,从来不该由某个插件来贡献:它就是 harness 自己的会话列,那块本来就在那儿的屏幕。

所以这个包自己把它注册进来,名字沿用 omdsh-chatmode 给它起的——Work——措辞、颜色和图标也照搬那个插件的,让人看不出开关上这个分段是哪个包给的。按下它,就是把列交还回去,显示当前选中的对话;它不启动什么、不记住什么,也不需要 profile 提供任何东西。

它只在需要时才立在那儿,这句话的两半同样重要:

  • 别的什么都没注册时,就根本没有开关。 只有基线一个分段时,开关不渲染——只有一段的控件没什么可切的。这也守住了当初的承诺:装了模式系统、却没装任何模式插件的 profile,屏幕上什么都不显示。
  • 有人认领"其余一切"这个姿态时,它让位。 omdsh-chatmode 的 Work,就是这个姿态外加一份"你上次离开的是哪段对话"的记忆,所以渲染出来的是它那一段。任何声明了 fallback 或占了同一个 id 的注册都会顶掉基线,那个插件一卸载,基线立刻回来。两个"其余一切"分段,会让同一段对话挂上两种颜色的圆点。

它的 active 是推导出来的,不是写进去的:只要它立在那儿、而且没有贡献者拿走列,列就归它。正因如此,模式插件无论是崩了、卸载了,还是仅仅自己不再 active,列都会自动交还,不用谁惦记着去还。

契约

一个模式插件注册一个分段,自己的事自己答:

// 绝不写进顶层 inject —— 见约定第 9 条。
ctx.inject(['sessionModes'], (mctx) => {
  const modes = mctx.get('sessionModes') as SessionModes | undefined
  if (modes === undefined) return

  mctx.effect(() => modes.register({
    id: 'code',
    order: 20,
    label: t('mode.code'),          // 已经是读者的语言
    hint: t('mode.code.hint'),
    tone: 'var(--dsw-alias-state-error-primary)',
    icon: createElement(IconCodeOutline16, { size: 14 }),
    owns: isCodeSessionId,          // "这段对话是我的吗?"
    available: true,
    enter: () => { /* 一次按下所执行的导航 */ },
    newSession: (workspaceId) => { /* 自己起了一个就返回 true */ },
  }))
})

SessionModes 是一个类型,来自 @omdsh-plugins/omdsh-basemode/client。用 import type 引入,永远不要作为值引入:跨插件的导入,要么把这个包的运行时再内联一份进你的产物,要么去问 shell 那张冻结的模块表要一个它答不上来的 specifier,而客户端产物的纯度门会因此让构建直接失败。上面那段代码之所以用字符串 'sessionModes' 解析服务,而不用这个包导出的 SESSION_MODES 常量,原因也在这里:服务名是与运行时共享的线上名字,不是与某个包共享的符号。本集合里的两个模式插件,都为此各自把它写成字面量。

动手之前,有五件事值得知道。

同一时刻只有一个分段是激活的,这条规则由注册表强制执行,不靠贡献者自觉:激活一个,就清掉其余所有。贡献者也是靠它得知自己丢了列的——看着自己那条变成 false,就把自己的界面收起来。

文案递进来时就已经本地化好了。 开关照单渲染 labelhintunavailableHint;它自己只有两个词(switch.aria,以及某个模式不可用又没说原因时的兜底),不知道任何模式叫什么。记得在 locale/change 时重新 update 你的分段。

按下是一次导航,绝不是一次状态写入。 enter 被调用后,这个分段要负责把世界变成真的——打开一段对话、起一段新的、或者把列拿过来——然后由负责推导它 active 标志的那一方去汇报。这里没有任何东西跨刷新记住模式;故意不做 node 半边,所以也不存在一份能让两个标签页吵起来的存储姿态。

owns 是按显示顺序逐个会话问的,第一个认领的赢;标了 fallback 的分段,接走所有没人认领的。侧栏就是这样按模式标满整张列表的,而画这些行的代码,对任何具体模式都只字不提。某个判别器抛了异常,只代表它放弃这段对话,不会把整个浏览区拖下水。

inProject 说的是一个模式的对话是不是住在有人干活的地方。 默认是 true,因为这个产品里的一段对话本来就是这样;把自己的对话归档进自己那套存储的模式,才声明 false——Chat 就是那一个。任何从屏幕上这段对话推导目录的界面都要读它:以前在一段聊天旁边按 Code,终端会开在聊天归档的那个文件夹里。去问注册表,正是为了把「这几个里哪个是 Chat」挡在其他每个插件之外。

「列在显示什么」和「选中了什么」不是一回事

sessions.current 回答的是"哪段对话被选中了",而它跟"屏幕上是什么",只在所有模式的列都是网页对话时才是同一个问题。Code 模式的列是终端,而且它故意从不选中终端驱动的那段对话——被选中的对话,是 Web host 会去 resume 的对话,而那份日志归另一个进程所有。于是选中项停留在终端背后打开的那段对话上。

所以,立在列旁边、却去读选中项的界面,不会缺一块,而是错得无声无息omdsh-sidepanel 的文件树曾立在一个项目的终端旁边,描述的却是另一个项目。column 才是那个诚实的答案:

// 列不是网页对话的模式,自己声明它在显示什么。
modes.register({ id: 'code', /* … */, scope: controller.scope })

// 列旁边的一切都跟着它走,而不是跟着选中项。
const scope = modes.column.getSnapshot()   // { sessionId, cwd }

只在该分段激活时才读,这是刻意的:贡献者的 scope 无论占没占着列,通常都是活的(Code 模式会一直推导它将会显示什么),把它报出去,就是在描述一块没人在看的屏幕。没声明 scope 的模式——Chat 和 Work,它们的列就是网页对话——按选中项报告,所以消费者只需要一条路,不用两条。

点一条会话,意思是「让我看它」

光靠选中项承载不了这个请求,而这道缝隙是个上手一分钟就能撞见的 bug:从某条工作对话进入 Code 模式——那条对话仍是选中的,因为终端不是它——然后去侧栏点它那一行。运行时选中的是已经选中的东西,什么都没变、什么都没发布,终端继续盖在这次点击想看的那条对话上面。想回去,就只剩模式开关可点,而这不是点侧栏一行的含义。

所以这里给 sessions.open 包了一层:每次 open,都把列交给"显示这条对话的那个模式"(showConversation),不管选中项动不动。声明了自己 scope 的模式会被跳过——这种点击它自己会先接住(omdsh-codemode 对自己的那些 id 就是这么做的),能走到这里,说明它已经拒绝了。

新建会话属于按下它时所在的那个模式

"再来一段和这个一样的对话",在不同姿态下含义不同:随包发布的那几个模式,要的是框架的空白会话;列里跑着终端的姿态,要的是框架压根没听说过的东西。所以这个请求先递给当前激活的分段(newSession),没人接,才落到框架手里。

分段一旦拒绝,就连同请求一起把列交出去——这是对的默认:用户要的是一段对话,而框架马上就要显示一段。

落到框架的这条路,会广播出去(onNewSession),因为这个手势可能是唯一什么痕迹都不留的导航:已经站在某个工作区的空白对话上时按下新建会话,不移动任何选中项、不改任何列表、不发布任何 store。一个从"用户在哪"推导自己标志的模式,会继续汇报那段用户正想离开的对话。"问了"这件事本身,就是全部的事实。

而且它落在一段真正新建的对话上

框架自带的这个手势会复用工作区里已有的空白对话,而把那一行摆错位置的,正是这个复用。浏览列表只把两种行提到组首:账上从没见过的,和 updatedAt 变大的。被交回来的对话两条都不沾,于是停在原位——排在第五位之后,折叠起来的组根本不画它。按下新建会话却看不到新行,不过是同一件事的另一面。

所以,没有分段接下的请求会去新建一段对话(sessions.create),不再复用。复用只留给它唯一正确的场景:那一行已经在屏幕上。站在某个工作区的空白对话上再按新建会话,要的就是已经打开的这一段,所以什么都不发生——这也是连按几下不会变出好几段对话的原因。

代价是:按了新建会话、没说话、又走开,会留下一段空白对话。它看不见(除了当前那一行,列表不画任何空白行),跑出第一轮之前不往磁盘写任何东西,而框架启动时的复用会把它消耗掉。

安装

dsh plugin --profile web add @omdsh-plugins/omdsh-basemode

这个包已经发布,所以写名字就够了——不用 git specifier,也不用 pnpm 构建白名单。这次安装也可以只是点一个按钮:只要 profile 里已经有插件中心,按钮就在设置 → 插件 → 插件中心里这个插件的卡片上。

把开关填满的那些模式插件,走插件中心的命令——它会从这套集合的 registry 里解析:

npx @omdsh-plugins/omdsh-plughub add omdsh-chatmode omdsh-codemode

两个都是可选的,开关如实反映组进来的东西:只装这个包,什么都不显示;加上 omdsh-codemode,显示 Work · Code;加上 omdsh-chatmode,显示 Chat · Work;两个都装,则是 Chat · Work · Code

顺序只是可读性上的偏好,不是要求:模式插件是在受限 fiber 上按名字解析 sessionModes 的,所以排在这个包前面的,只会等,不会失败。

也可以从检出安装,你在改的那份构建要走的就是这条路。dsh web 启动前 lib/ 必须存在——loader 直接 import lib/index.js,而按路径安装的包不会跑 prepare,没有任何环节替你构建:

pnpm install && pnpm run build
dsh plugin --profile web add "$PWD"

同理,改完源码要重新构建。

卸载同理:

dsh plugin --profile web remove @omdsh-plugins/omdsh-basemode

之后每个模式插件都会自己安静下来——分段和开关没了,各插件其余的界面照常站着。这就是受限 fiber 买来的东西,也是"模式关掉了"和"页面死了"之间的差别。

命令

pnpm install
pnpm run build       # tsc 产出 lib/types,tsdown 打包浏览器半边
pnpm run typecheck   # 先包源码,再测试
pnpm run test        # vitest

这个包对着哪个 harness 编译,是可以切换的:

pnpm run harness:npm                             # 提交状态:锁定的已发布版本
pnpm run harness:local ../../deepseek-harness    # 同级检出,用于开发
pnpm run check:harness-pin                       # 只要还有 link: 就失败

只有 registry 状态可以提交。 link: 是相对声明它的那份 manifest 解析的,提交一条,就等于把某台机器的目录布局写死进包里——而且 pnpm 不会大声报错:它建出悬空符号链接、报告安装成功,然后构建阶段每个 harness import 都是 TS2307check:harness-pin 就是用来在提交前拦住这件事的。

已知限制

  • 开关的座位是借来的。 shell.overlay 也横跨侧栏和详情面板,所以这枚药丸自己对准带 data-conversation-scroll 的盒子居中,指针不在附近时就隐藏。不带这个属性的列会让它失去锚点,开关便弹到整个框架的正中间。
  • 行上的标记是画上去的,不是渲染出来的。 侧栏的行是 harness 的,所以两个标记——模式圆点和会话列的高亮——都是通过 MutationObserver 写到 DOM 上的,不是组装进去的。harness 改了行的结构,这个包就得跟着改选择器。
  • 标记读的是一个私有结构。 一行是哪段对话,取自它的 React props——那不是公开契约。将来浏览器改版或 React 换了形状,结果是行上不标,而不是标错,开关里的字形也不受影响。另一条路——按行标题匹配——比不标更糟:同一个项目里,没标题的会话彼此同名。
  • 搜索结果不带圆点。 圆点画在浏览用的行上;搜索结果是两行的堆叠,第二行本来就写着所属工作区。
  • 没有行的对话拿不到高亮。 高亮是移到某一行上的,所以模式正在显示的东西,如果会话列表压根没听说过——比如第一轮还没落盘的 Code 终端——侧栏就一行都不高亮。等这段对话落盘、行出现了,高亮才会落上去。
  • 移过来的高亮,盖得过一行自己的菜单。 压掉 harness 自带的高亮,靠的是一条优先级更高的背景规则,而它多管了一种状态:一行如果同时是选中的、不在会话列上、又展开了自己的省略号菜单,那么在这三件事都成立期间,它会失去菜单那份背景色。菜单本身不受影响。
  • 按了新建会话又不用,会留下一段空白对话。 按下、什么都没说、又去了别处,那段对话就留在工作区的账上,等着框架启动时的复用把它捡走。在那之前,它哪儿都不画,也没有任何东西落到磁盘上。
  • 没有模式能活过一次刷新。 激活的姿态,是每个标签页从"当前对话住在哪"推导出来的,各贡献者各推各的。打开应用,永远落在当前对话所指的地方,而不是你上次待的地方。
  • 基线的 Work 什么都不记。 按下它,只是把列交还回去、显示当前选中的对话——它不会带你回到上一段工作对话;新标签页里什么都没选中时,落到的是工作区选择页。那份记忆是 omdsh-chatmode 的 Work 的,这也正是装了那个插件后由它的分段顶替这一段的原因。