在对话顶部用标签页切换各个聊天会话
30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/9 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
该插件未发布到 npm,按作者文档从 GitHub 源安装即可(静态检查只覆盖 npm 路径,不代表插件不可用)。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add rifkyazizf/dsh-session-tabs该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:需留意
是什么
dsh 原生插件 · ui
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 26 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
⚠︎ 实装验证未通过(runtime_never_loaded · 2026/11/2) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-session-tabs(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/13 04:29:01
安全扫描自动 · 静态分析源码v2.7.0🟢 扫描:未发现敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 顶部标签页切换会话
给 DeepSeek Harness(dsh)桌面应用做的小巧会话标签页插件
README
由 DeepSeek 最新模型翻译生成dsh-session-tabs
给 DSH Web GUI 用的 JetBrains 风格会话标签页:每个聊天会话一个标签,排在对话面板顶部的一条标签栏里。
在会话标签之间切换
│ ● Add DeepSeek… × │ ● Fix the login bug │ ● vendor sync │
发过消息的会话会保留自己的标签。只是打开过的会话占用一个临时标签,下一次打开会把它替换掉,所以在侧边栏里点来点去不会留下一串标签。
规则
这是 JetBrains 的编辑器标签行为,只不过把文件换成了会话:
| 会话 | 标签 |
| 你在里面发过消息 | 普通标签,一直留着 |
| 你只是打开过 | 唯一的「预览」标签,下一次打开就被替换 |
「留下」这个信号不由本插件跟踪。会话列表已经用 blank 携带了它:
空白会话是所选工作区里临时的 New Session 行。
所以 blank === false 就意味着这个会话有真实内容。
展开完整文档(README 剩余 1 节)
dsh-session-tabs
给 DSH Web GUI 用的 JetBrains 风格会话标签页:每个聊天会话一个标签,排在对话面板顶部的一条标签栏里。
在会话标签之间切换
│ ● Add DeepSeek… × │ ● Fix the login bug │ ● vendor sync │
发过消息的会话会保留自己的标签。只是打开过的会话占用一个临时标签,下一次打开会把它替换掉,所以在侧边栏里点来点去不会留下一串标签。
规则
这是 JetBrains 的编辑器标签行为,只不过把文件换成了会话:
| 会话 | 标签 |
| 你在里面发过消息 | 普通标签,一直留着 |
| 你只是打开过 | 唯一的「预览」标签,下一次打开就被替换 |
「留下」这个信号不由本插件跟踪。会话列表已经用 blank 携带了它:
空白会话是所选工作区里临时的 New Session 行。
所以 blank === false 就意味着这个会话有真实内容。宿主在第一条被接受的提示词上设置它(promptAttempted,在第一次 await 之前同步设置),而且它的 reducer 刻意拒绝在之后把会话重新置为空白:
handleBlank(blank) {
if (blank === this.blankBit) return;
if (blank && (this.promptAttempted || this.running)) return; // sticky
...
}
也就是说,「发过消息」是宿主自己持久化的判断,已经算好,也已经固定下来。交给它来判断,标签栏就不会和 UI 其他部分在「会话是否被用过」这件事上产生分歧,也没有本地记账会漂移或需要持久化。
由此得到两个性质,两者都有测试覆盖:
- 顺序是使用顺序,不是最近访问顺序。重新点开旧标签不会让标签栏在指针底下重新洗牌。
- 预览标签永远在最后,因为它就是那个会被替换掉的。
子代理会话被排除在外:它们是通过父会话的面包屑进入的,而不是靠切换对话,所以给它们一个标签会让人误解标签栏的作用。标签数量不设上限、也不回收:每个用过的会话都按使用顺序保留一个标签,标签多到放不下时整行会横向滚动(普通滚轮也行),并让当前标签保持在视野内。
位置——对话的第一行
标签栏注册在 conversation.input.dock——这是输入框上方的一个列表座位,和 todo、queue、goal 这些 dock 共用——并绘制在本插件锚定的一行里,这一行排在对话已有的所有行之前,所以标签是对话里的第一样东西:
div.root ← the conversation column (flex column)
├── div[data-session-tabs-host] ← the strip's row (inserted by this plugin)
├── header ← title row + Chat/Trajectory view tabs
└── div[data-conversation-content] ← transcript + composer
为什么它不是一个座位
应用没有在对话内容上方声明任何座位: conversation.header 是单个席位,由随包发布的 ui-conversation 插件声明并占用。以相同优先级再次注册会抛出 single slot "conversation.header" already has a registration …;在 apply 阶段注册——此时席位还不存在——会抛出 slot "conversation.header" is not declared,这会让插件的整个 fiber 失败,并中止 web 启动。
它唯一未被占用的子席位 conversation.header.leading 位于标题栏内部(窗口拖拽区域),因此无法容纳整行。
header 的其他子席位(…session.header.actions、.utilities)是标题旁边的窄控件簇。
因此放置通过 DOM 完成:dock 注册提供席位和会话 props,组件插入一个宿主行,作为会话列的第一个子节点,然后用 createPortal 把条带渲染进去(shell 把 react-dom 暴露为平台种子词,因此客户端 bundle 可以 require 它)。从席位自己的占位符即可到达该列,所以它始终是该席位所属的那一列。只要宿主离开文档就会被重新锚定,因此会话列重新渲染不会把条带遗留在游离节点中。
该行位于 header 的 data-window-drag 区域之外,所以 chip 可以正常点击和滚动;窗口拖拽仍由标题行和侧边栏负责。
为什么记忆的是完整顺序,而不是可见标签页
stepTabs 返回条带以及下次要记住的顺序,也就是完整的参与顺序——绝不是恰好被渲染出来的那个窗口。这看起来像细节;它却是静止条带与旋转条带之间的区别:
| 记住的内容 | 10 个已参与会话重新渲染 6 次时显示什么 |
| 可见标签页(一个窗口) | 5 个不同窗口——条带每次重新渲染都旋转 |
| 完整参与顺序 | 1 个窗口,不变 |
打字会不断重新渲染条带(会话 store 在 tick),所以旋转的条带正是用户打字时看到的闪烁。verify.mjs 通过 stepTabs 重放这个循环——也就是组件调用的那个函数——如果出现多于一个窗口就失败。
为什么记忆是模块状态,而不是 ref
切换会话会重新挂载会话 Factory 实例——它的 key 携带会话 generation——所以一旦你打开另一个会话,ref 就失效了。因此保存在 ref 里的关闭状态会在下一次点击别处时立刻回来,参与顺序也随之消失,每次切换都重新洗牌标签页。dismissedTabs 和 engagementOrder 正是因此成为模块状态,而 verify.mjs 驱动真实的重新挂载(全新的 ref、保留的模块记忆)来断言两者都能存活。
页面刷新仍会清空它们,而 shell 会在应用重启时重新加载页面,所以同一份记忆会写入 localStorage,键为 dsh.session-tabs.v1——这是随包发布的客户端插件使用的 dsh.<name>.v1 约定(对话记录宽度、右侧边栏布局)——并在加载时读回。
这个键存放在 dsh-app://app 源下,桌面端把它注册为 standard + secure,所以重启后依然保留。存储被拦截、键缺失、数据损坏,都按冷启动处理,而不是报错;离线测试套件覆盖了往返读写、数据校验,以及一个预置的「页面加载」。
滚动条归 shell 管
这条标签栏故意不设置 scrollbar-width/scrollbar-color。ui-theme 会安装不带作用域的 ::-webkit-scrollbar 规则(--dsh-scrollbar-width = 5px,滑块圆角,颜色用 --dsw-alias-scrollbar-bg-l1,悬停色来自 --dsw-alias-scrollbar-hover-l1),这样滚动条就和对话记录、侧边栏保持一致。只要元素设置了标准属性,Chromium 就会忽略这些规则,而这条标签栏以前设了 scrollbar-width: thin——所以它显示的是 macOS 原生粗滚动条,而应用其余部分都是细的主题滚动条。如果某个配置档禁用了 ui-theme,这条标签栏的滚动条就会和其他所有滚动条一样变回原生样式。
外框是浏览器风格,以及为什么轨道靠绘制
这条标签栏画成浏览器标签栏的样子,而不是一排 chip:一条底色与面板自身表面略有差异的横条,标签坐在底部一条细线轨道上,活动标签向下压成面板的颜色,看起来像是连着下面那一行。chip 高 30px,肩部 8px 圆角,底部是直角(borderRadius: "8px 8px 0 0"),横条高 36px。
关于这套画法,有三点是刻意为之,每一点都是被 CSS 合成滚动容器的方式逼出来的:
- 横条的填充是半透明中性色(rgba(127,127,127,0.12)),不是表面 token。在深色模式下,shell 的表面都高于面板——--dsw-alias-bg-base 就是面板,而 --dsw-alias-bg-layer-1…3 比它更亮——所以没有任何一个表面 token 能在两种主题下都表示「比面板略高一点的横条」:一种主题会把横条抹掉,另一种会画出一张凸起的卡片。在面板上叠 12% 中性色,一个值就能在深色主题下读作凸起的横条,在浅色主题下读作凹陷的横条。
- 活动 chip 用 --dsw-alias-bg-base 填充——也就是面板自己的颜色。让标签和下面那一行融为一体的,是这个,而不是边框或阴影。
- 轨道是标签栏自己最下面那一行背景,不是 border-bottom。 子元素盖不住父元素的边框,而标签栏因为自己是横向滚动容器,会裁剪自身的溢出——所以活动 chip 永远没法盖住边框来打断自己下方的轨道。把轨道画成标签栏自己的背景,chip 就合成在它上面:活动 chip 的不透明填充遮住下方的轨道,而标签之间的空隙和非活动标签则保留轨道。标签栏也因此不带底部内边距,这样 chip 的盒子才能贴到轨道。
降级
| 对话形态 | 标签栏画在哪里 |
| 主列,其父级有 header 子节点 | 锚定行——该列的第一行 |
| 嵌入式出现(子代理预览——没有 header) | 什么都不做;在那里放一条 dock 条只会是噪音 |
| 无法识别的形态(没有 [data-conversation-content] 祖先) | 它注册时所在的 dock |
最后一行是安全网:未来的应用版本可以移动这条 dock 条,但不会丢失它——而且这些路径都不会让 web 启动失败。
为什么注册要等声明
conversation.input.dock 由 Conversation Factory 声明,而它的入口挂载发生在该插件的两个服务(slots、sessions)解析之后,所以注册放在 ctx.slots.inject("conversation.input.dock", …) 里:回调在座位被声明时运行,无论是现在还是以后;如果座位始终没被声明,回调就永远不运行。如果在 apply 时直接写 ctx.slots.register({ name: "conversation.input.dock" }, …),会抛出 slot "conversation.input.dock" is not declared (a parent entry's children table must declare it),而 apply 内部抛错会让插件的整个 fiber 失败——桌面外壳随后会用「could not start」对话框替换掉应用。
布局
package.json dsh.client declaration (platform: web) + local package metadata
lib/index.js node half — empty apply(), gives the Loader a host-side row lib/client.js browser half — the strip (inlined policy) + top-row anchoring lib/policy.js the recycle rule, imported by verify.mjs verify.mjs policy + bundle contract + rendered chips (node verify.mjs)
lib/client.js 不能 import lib/policy.js:客户端 bundle 是 window.__ModuleLoader__ bundle,只能 require 外壳种子 react 和 react/jsx-runtime,而浏览器那一半是原样发布的,没有打包器。因此策略被逐字内联,verify.mjs 则断言两份副本一致——每个常量一致,deriveTabs 推导在其全部 fixture 上一致,chipLabel、fitLabel、graphemes 在其全部用例上一致——所以只改一份而不改另一份会让校验失败。
标题与宽度
每个 chip 都是同样的宽度(TAB_WIDTH,190px,border-box),与标题无关:一条 dock 条上的标签页随文字变宽变窄,看起来就是噪音,而且一个很长的标题会把邻居挤到一边。在这个固定盒子里,标签占据状态点和预留关闭槽之外剩下的空间(flex: 1 1 auto,min-width: 0),并在那里省略——所以标题更长,或者外壳字体更大,都只会裁剪标签,不会改变标签页尺寸。
标签文字仍然先被裁剪,当整个词离上限不算太远时按词边界裁,这样 chip 读起来像短语,而不是被切碎的 token:
| 会话标题 | Chip |
| Add DeepSeek peak/off-peak session tabs | Add DeepSeek… |
| a_very_long_single_token_filename | a_very_long_single… |
| Fix the login bug | Fix the login bug |
完整标题仍可通过 chip 的 tooltip 查看,所以截断永远不会隐藏一个 chip 对应哪个会话。
标签页右键菜单
右键点击标签,弹出 shell 自带的菜单素材(MenuSurface + MenuItemButton,来自 dsh-client-ui-primitives,portal 到 body 并限制在视口内):
| 菜单项 | 作用 |
| Rename session | 就地编辑标题——标签的 label 槽位变成输入框,预填完整标题;Enter 提交,Escape 和失焦放弃 |
| Close all left | 关闭它左边的所有标签页 |
| Close all right | 关闭它右边的所有标签页 |
| Close all others | 关闭其他所有标签页 |
两条规则保证它不出错:
- 批量关闭只作用于标签页,绝不作用于会话。 它们只是往关闭集合(closeRangeIds)里加 id,和 × 用的是同一份记忆。
- 活动标签页永远不会被落下。 如果批量关闭关掉了正在阅读的会话,对话会跟着保留下来的那个标签页走——这样标签栏和对话内容不会对「当前是哪个会话」产生分歧。
- 被右键点击的标签页不会被这三项中的任何一项关闭,而没事可做的菜单项是禁用而不是隐藏,这样菜单能保持原有形状。
重命名走 sessions.rename(sessionId, title)——也就是侧边栏自己的重命名对话框最终调用的那个服务方法。那个对话框本身无法由第三方插件唤起(它的请求 store 是插件本地状态),所以标签栏自带内联编辑器。
交互
- 点击标签——聚焦该会话(ctx.uiWorkspace.openSession,每次点击时查找)。这不是 ctx.sessions.open:sessions 服务没有这个方法,所以早期构建的标签看起来是死的,而侧边栏导航却正常。
- 点击 ×——从标签栏移除该标签,并立即重新渲染:关闭集合是一个 ref(只在推导标签栏时读取),所以处理函数还会递增一个渲染 ticket——没有它,标签会一直滞留到某个无关的 store tick,× 看起来就是死的。会话本身不受影响;删除会话仍然是侧边栏的职责。关闭活动标签页时会先选中相邻标签,所以标签栏永远不会指向一个它已不再显示的会话。
- × 是预留槽位,不是插入。 可关闭的标签始终带着这个控件的 16px(加上间距),只切换可见性:活动标签上始终可见,其余标签悬停时可见。悬停时才追加它,会撑宽那个标签,并把后面所有标签在光标下横向推开。最后一个标签页和预览标签完全没有槽位——两者都无法关闭。悬停 × 会在它下面点亮一个小圆形背景(比标签自身的高亮更紧凑),而这个悬停只改变颜色:closeControlStyle 被断言在任何状态下都返回相同的几何尺寸。
- 最后一个标签页不可关闭,因为空标签栏没有退路。预览标签也不可关闭——它本来就是一次性的。
- 正在运行的会话会在它的标签上显示一个绿点。
验证
bash
node ~/.dsh/plugins/dsh-session-tabs/verify.mjs
覆盖回收规则(浏览后只留一个标签页;参与会话会把它提升;已参与的标签页在浏览后依然保留;顺序稳定;子代理排除在外;上限生效)、不动点记忆(一段重放的重渲染循环,标签条不得因此移动)、贴合式标签文字(像素级贴合、其词边界与截断规则、按字素安全切割、canvas 测量器的字体传递及其无 canvas 回退、布局前先顶上的字符预算)、滚轮转横向映射、打包契约与内联副本一致性、dock 注册(它声明的座位与列表 id,以及 apply 在声明等待之外无法触达 register)、放置决策(锚定行、嵌入时静默、回退到 dock),以及渲染出的标签,包括关闭控件的规则。
用变异测试验证,而不是靠信任:关掉预览规则会挂掉 19 条断言,破坏截断会挂掉 8 条,注册错误的座位会挂掉 1 条,恢复固定标签窗口会挂掉 2 条,滚轮映射失效或只支持纵向会分别挂掉 2 条和 1 条,标签条被裁剪(overflow: hidden)会挂掉 1 条,而每道放置守卫都会挂掉各自对应的断言——在已有行下方插入一行挂 1 条,宿主锚定在嵌入出现处挂 1 条,缺少 header 检查挂 1 条,某行能撑开对话列挂 1 条,无法识别的形状不再回退到 dock 挂 1 条,缺少锚点标记挂 1 条,不等待声明就注册(这个 bug 曾导致 web 启动失败)挂 2 条。
挂掉 2 条。
安装
插件装进 desktop 配置档,该配置档归 DeepSeek Harness 应用所有。dsh plugin 命令会拒绝这个配置档,所以直接改它的文件。
1. 退出 DeepSeek Harness。
2. 把包加进 ~/.dsh/profiles/desktop/package.json 的 dependencies:
json
"dsh-session-tabs": "github:rifkyazizf/dsh-session-tabs"
3. 在配置档目录里安装:
bash
cd ~/.dsh/profiles/desktop && pnpm install
4. 把加载器行追加到 ~/.dsh/profiles/desktop/cordis.patch.yml:
yaml
- insert:
- id: ui-session-tabs
name: 'dsh-session-tabs'
5. 启动 DeepSeek Harness。标签条会显示在对话面板顶部。
要固定版本,就在 spec 后面加上 tag 或 commit,例如 github:rifkyazizf/dsh-session-tabs#a8f106b。要更新,就在配置档目录里运行 pnpm update dsh-session-tabs。
本地开发时,克隆仓库,把依赖指向克隆目录,写成 "file:/path/to/dsh-session-tabs"。配置档是实时监听的(patchReload: live),DSH 的 client-HMR 行会轮询客户端 bundle,所以改动 lib/client.js 会在打开的标签页里热重载。新增插件行第一次需要刷新一次页面。
标签条注册在 conversation.input.dock(列表座位,id session-tabs,order 10),并绘制在对话 header 下方自己那一行里。
禁用或移除
要禁用标签条,就把 ui-session-tabs 这条 insert 注释掉,位置在 ~/.dsh/profiles/desktop/cordis.patch.yml。要卸载,还要移除依赖,并在配置档目录里运行 pnpm install。改动在下次刷新页面后生效。
已知缺口
- 标签条是滚动,不是循环复用。 参与的会话非常多时,这一行会保留每个标签并滚动;裁剪标签靠的是标签上的 ×。滚动过程中,shell 自带的 5px 主题横向滚动条会排进标签条最后一行,于是这一行高出 5px,活动标签的脚踩在滚动条上,而不是踩在轨道上。这一行收不回来——自定义 ::-webkit-scrollbar 会占用布局空间,而标签条必须保持可滚动,因为固定标签宽度正是标签契约的全部意义(见 标题与宽度)。
- 记忆存在 localStorage,不在宿主上。 关闭记录和参与顺序在刷新页面、重启应用后都还在,但清除站点数据——或者从另一个源打开 GUI——就会从头开始。参与集合本身在宿主上是持久的(靠它的 blank 位),所以只有你关掉了哪些标签、以及它们的顺序是本地数据。
- 一次关闭会持续到你再次访问那个会话。 当前会话是别的时,关掉标签会把它藏起来;导航回那个会话(包括从侧边栏)就会把它带回来——这是规则,不是 bug。
- 不支持拖动排序,也不支持中键关闭。右键只提供重命名和三种批量关闭——没有固定、分叉或归档(这些留在侧边栏里)。
- 菜单只有英文。 插件没有注册任何语言字典,这一点和随附的客户端插件不同,后者会把它们的行交给 ctx.locale 处理。
License
MIT预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
rifkyazizf/dsh-session-tabs
在对话顶部用标签页切换各个聊天会话
相关插件 · UI / 主题
破甲增强版JeffKeeping/dsh-purge-plus
dsh-purge 的非官方分支fork:适配 DeepSeek Harness 0.2.1-rc.2、破甲内核…
★ 387已汉化DeepSeek 客户端
71.9
无代码业务系统平台nocobase/nocobase
NocoBase 是什么
★ 24,804已汉化DSH
71.8
1Panel 服务器面板1Panel-dev/1Panel
全球 250 万+ 自托管用户共同喜爱。
★ 37,350已汉化DSH
71.5
工作状态行动态ccch1mneyyy/working-activity
作者:chimney@ccch1mneyyy。社区项目,非 DeepSeek 官方项目。
★ 664已汉化DSH
71.2
多模型配置管家farion1231/cc-switch
跨平台桌面一体化助手,支持 Claude Code、Codex、OpenCode、DeepSeek Harness 等工具,唯一官网 ccswitch.io
★ 151,506已汉化DSH
71.1
AI 矢量设计工具ZSeven-W/openpencil
全球首个 AI 原生开源矢量设计工具。
★ 7,027已汉化DSH
71.1
这是你的仓库?认领条目