Skip to content
dsh-market Browse plugins GitHub 中文

qgx1992/dsh-ui-tools

Five web UI tools in one plugin: a provider + model two-button selector with reasoning-level control, collapse/expand-all for sidebar workspace groups, a session tab listing the files the session changed (needs kernel 0.1.2-alpha.1+, silently absent on older kernels), a workspace badge next to the session title, and a settings page that toggles them.

Stars ★ 0 Category UI Enhancements Listed 2026-09-13

Install

Inside DeepSeek Harness, with dsh-market

dsh plugin --profile web add dshmarket

Or from the command line

dsh plugin --profile web add github:qgx1992/dsh-ui-tools

Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.

README

This plugin publishes its README in Chinese only.

DSH Web 插件:五个 UI 工具合并成一个包,只动对应控件,不改 DSH 任何一行源码。

功能一:模型选择双按钮(原 dsh-model-select-style)

把输入框官方的「模型选择」控件替换为两个独立按钮,两级联动:

  1. 「供应商」按钮 —— 点击弹出供应商列表(如 DeepSeek / SiliconFlow / OpenRouter);
  2. 选中某供应商后,「模型」按钮亮起;
  3. 「模型」按钮 —— 点击只列出当前所选供应商的模型,点选即切换;
  4. 模型支持推理时,模型按钮显示 模型名 · 推理等级(如 deepseek-chat · 高),面板内带「推理等级」区可调节思考强度。

选择逻辑完全复用官方组件(同一份模型目录、同一套选择提交、错误 Toast),只是把入口从单个按钮拆成两个。

功能二:侧边栏工作区折叠/展开(原 dsh-workspace-collapse)

在侧边栏工作区列表下方渲染「折叠全部 / 展开全部」工具条,一键折叠/展开所有工作区分组(纯 slot 渲染 + CSS 对齐,不搬动 DOM)。

功能三:会话标题旁工作区徽章(v0.3.0 新增)

在会话页头部标题右侧显示一个圆角徽章,标出当前会话所属工作区名(与侧边栏分组标题同名),一眼看出当前对话跑在哪个工作区:

  • 数据:标准 kit 自带 useWorkspaces,从 workspaces.items 按 sessionIds 反查当前会话所属工作区,取 title;
  • 未归入任何工作区(未分组 / 空白会话)时徽章不渲染,不占位;
  • 徽章显示工作区显示名(改名后同步),title/aria-label 带可读文案。

实现原理:本功能注册进官方纯增量 list 槽 conversation.session.header.actions(session 作用域)——它是头部标题 cluster 内的动作行,位于面包屑标题右侧,正好是「标题旁」。list 槽是加性的(绝不替换标题、无占用冲突,当前该槽无其他插件占用),我们用负数 order: -100 让徽章排在其它交互动作之前、紧贴标题。纯 slot 渲染,不搬 DOM、不改源码。

功能四:插件设置页「DSH UI 工具」(v0.4.0 新增;v0.4.1 移除快捷命令条)

在官方设置中心新增一个「DSH UI 工具」页面,集中控制本包功能的偏好:

  • 布局偏好:工作区徽章开关、启动时默认折叠所有工作区分组、在「用时」旁显示输出速度计(tok/s);
  • 偏好经 localStorage 持久化(dsh-ui-tools:prefs:v1),刷新/重启后保持;
  • 能力感知(v0.4.2):速度计依赖的内核服务不可用时(0.1.1-rc.x),那一行开关灰显并在标签下提示「需内核 0.1.2-alpha.1+」,不会出现一个点了没用的开关。可用性由 alphaFeatures 子 fiber 实际激活来置位(比在 apply 期一次性探测更可靠:官方 provider 可能比本插件晚激活)。

存储说明:官方 settings 命名空间需要 host 侧 ctx.settings.register(ns, schema) 声明 schema 才能持久化(参考 ui-theme/src/index.ts);本插件刻意保持纯浏览器(lib/index.js 空入口、不引入 host 依赖与 schema 校验风险),故沿用浏览器插件社区惯例(同款 dsh-better-sidebar)的 localStorage。偏好仅当前浏览器生效;如需跨端/跨浏览器同步,可后续增加 host 半部迁移到 settings 文档。

实现原理:注册进官方 settings.section(root 作用域 list 槽),id: "dsh-ui-tools"、order: 100、label 走双语文案,设置导航自动出现该页。页面组件订阅同一个 localStorage 偏好仓库(useSyncExternalStore),所有开关写入即落盘;既有功能在 apply() 里注入同一偏好仓库:徽章/速度计组件渲染时读开关,侧边栏折叠条在插件加载时按「默认折叠」偏好执行一次。

功能五:输出速度计 tok/s(v0.4.6 新增)

在回合输出速度上补一个常驻读数,两个位置:

  • 回合结束后(精确值):在官方「用时 xx秒」pill 左侧紧邻处显示 ⚡42 tok/s,无需点开「本轮用时和速度」弹窗即可看到速度;
  • 生成中(估算值):回合进行时在消息流末尾显示 ⚡≈ 25 tok/s 生成中估算,每秒刷新。

数值口径(精确值)与内核「本轮用时和速度」弹窗里的 TPS 完全同源:先按 messageId 定位回合,再折叠该回合全部已固化 assistant 节点,只累计同时带 timing.firstTokenTime 与 usage.outputTokens 的 step,按 ΣoutputTokens ÷ Σ(completedTime − firstTokenTime) 求值(对应内核 deriveTurnMetrics / assistantStepReading);数据不足(无可计步、解码时长为 0)时不显示,绝不猜一个假值。

为什么生成中只能给估算值:内核在流式期间不上报 token 数 —— assistant/live-chunk 里的 usage chunk 被 publication: "none" 抑制,不推给客户端。所以生成中按已输出字符折算(CJK 1 字符 ≈ 1 token,其余 4 字符 ≈ 1 token),并始终带「≈」与「生成中估算」标注,与精确值明确区分;回合一旦结束即让位给精确 pill,不重复显示。

实现原理(不改源码的两条官方通道):

  1. 精确 pill 注册进官方 list 槽 conversation.chat.assistant-actions(session 作用域)。该槽的渲染位置就是 MessageIconActions 的 extraActions 位——与官方「用时」pill 同一条动作条,order: -10 使其排在官方 pill 之前(内核 list 槽按 order 升序渲染),即紧贴「用时」左侧。该槽当前亦被官方 dsh-client-ui-message-feedback 使用(order: 10),list 槽为加性,无占用冲突。节点数据经 uiConversation.binding(sessionId).target("chat") 读 chat target 快照(与官方 useChat 同源)。
  2. 生成中估算条 走官方 Definition 通道 ctx.uiConversation.events.register(...)(ui-goal、ui-workflow-run 同款机制)自建一个 token-speed-live chat 节点,并用 ctx.slots.inject("conversation.chat.node") 以该 kind 注册渲染器。为什么不用 conversation.chat.turnTail:该槽的数据源必须匹配到 turn/end 才产出节点(tailData 无 turn/end 即返回 null),生成期间根本不存在;assistant-actions 所在动作条同理。节点的排序锚点刻意取一个大于任何真实事件 seq 的常量,使其稳定落在本回合末尾(若锚在首个增量处会因 anchorSeq 比较跑到流式正文上方)。
  3. Definition 通道是内核对等能力中较新的一个:形状不符时只让生成中那段缺席(console.warn 记录),绝不抛出——它处在 alphaFeatures 子 fiber 内,抛错会让 capability.alphaApi 回滚、连设置页的可用性提示一起判定为不可用。精确 pill 不依赖该通道。

开关:设置页「在「用时」旁显示输出速度计(tok/s)」,默认开;偏好字段 tokenSpeedEnabled(同一个 localStorage key)。旧内核上该开关灰显。

历史回合为什么可能没有读数:firstTokenTime 来自流式增量(瞬态事件,不进持久化日志),因此重新加载页面后看到的历史回合通常缺该字段——此时精确值不可得,插件与内核「用时」弹窗都不显示 TPS(同一口径、同一结论)。在本回合刚跑完、未重载页面的情况下读数正常。

实现原理

  • 官方模型组件照常注册在 conversation.input.model(数据/提交逻辑原样保留),用 CSS 隐藏官方触发按钮;

  • 本插件通过 modelDirectories 服务读取同一份模型目录(groups = 供应商分组),注册到 conversation.input.right(list slot)追加双按钮;v0.4.4:该子作用域必须同时声明 remote 和 remote.session——内核对 directoryFor() 的实现会在调用方上下文里读 this.ctx.remote.session(cordis 的 Service tracker 会把 service 的 this.ctx 重绑到调用者),而 remote.session 是嵌套追踪服务,只声明 remote 仍会抛 cannot get property "remote.session" without inject;

  • 选择模型/推理等级时调用官方 directory.select(...),官方 store 同步更新,输入框状态与原生一致;

  • 折叠条注册到 sidebar.footer.action——官方渲染位置就是 footer 顶部、紧贴工作区列表正下方,用 CSS(对齐 --dsh-session-list-edge-inset 内边距)贴合列表即可,不搬动 DOM。搬动 slot 渲染出来的节点会与框架重渲染互相触发,导致渲染进程 100% CPU 卡死(v0.1.0 全局观察器、v0.1.2 收窄观察器均因此卡死;v0.1.3 起彻底不搬)。

  • 跨内核分层(v0.4.2;v0.4.9 收窄):uiConversation 是 0.1.2-alpha.1 才引入的 client target 体系服务,旧内核 store 里没有它的提供方。因此这个硬依赖不写在 loader entry 的 inject 上(写了会让整个 entry 停在 PENDING,被 boot 末尾只遍历 ctx.loader.entries() 的审计判为失败 → web boot: 1 entry did not activate + Failed to load plugins 横幅,其余功能一起丢),而是收进 ctx.plugin(alphaFeatures) 子 fiber:入口层只声明五个跨内核服务,功能五(速度计)整段注册(chat target 取数、conversation.chat.assistant-actions 注册、Definition 通道、TSP 文案)放在子 fiber 内,alphaFeatures.inject = ["uiConversation"]。旧内核上该子 fiber 停在 PENDING——cordis 不执行其函数体、不注册其 effect、也不计入审计,服务(哪怕延后出现)就绪时自动激活,并随 entry dispose 一并释放。渲染方式完全不变(纯 slot,无观察器/定时器)。

  • 工作区徽章注册进 conversation.session.header.actions(见功能三)。

  • 设置页注册进 settings.section(见功能四);所有偏好走 apply 期创建的一次性 localStorage 偏好仓库,组件经 useSyncExternalStore 订阅。

五个功能各自独立命名空间(locale / slot id / data-* 前缀),互不干扰。

v0.4.9 移除「修改的文件」选项卡:它原本注册进官方 conversation.view 槽(v0.2.0 引入、v0.4.9 整体删除)。移除原因:其取数是同一份 chat target 快照的又一遍解析,与速度计重复;维护成本集中在 run_code 内嵌路径提取这类易碎逻辑上(模板串、变量拼接、shell 级写操作都解析不了)。随之下线的还有 uiSession.provide 暴露的 useModifiedFiles hook、workspaces.openPath 打开链路、mfsCompact 偏好与 conversation.view 槽注册——alphaFeatures 子 fiber 的 inject 因此收窄为 ["uiConversation"],跨内核降级机制本身完全未变。

内核兼容矩阵

DSH 内核 入口 fiber alphaFeatures 子 fiber boot 审计 横幅 可用功能
0.1.2-alpha.1 及更新¹ active active pass 无 全部 5 项
0.1.1-rc.1 / 0.1.1-rc.2 active pending(静默) pass 无 1 / 2 / 3 / 4(功能五缺席,设置页速度计开关灰显)

¹ 「更新」指 同属 0.1.2-alpha.1 之后的发布线(0.1.2-alpha.1 / 0.1.2-* / 0.1.3-* / 0.1.5-* / 0.1.6-* 及更高)。DSH 的预发布版按 semver 只与同一 x.y.z 元组比较,所以字面区间 >=0.1.2-alpha.1 在真实 semver 下并不匹配 0.1.5-rc.2——本包因此按发布线逐条声明,而不是写一个连续区间,详见下节。

机器可读的内核约束(engines.dsh)

package.json 同时声明两处(生态两种写法都在用,值必须一致):

"engines":      { "dsh": "^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0 || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0 || >=0.3.0-0" },
"dsh": { "engines": { "dsh": "^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0 || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0 || >=0.3.0-0" } }
  • 谁在读:dshmarket 的 manifestFacts() 读顶层 engines.dsh,缺失时回落到 dsh.engines.dsh;市场会据此在 Discover 页展示宿主兼容性,并在安装/更新前直接拒绝判定为不兼容的版本(deriveHostCompatibility → incompatible → 400)。
  • 生效范围(请如实理解):市场是按 npm 包名向 registry 取这份事实的。本包目前以 github:qgx1992/dsh-ui-tools 分发,而该更新路径对 git 源不查 registry(usesNpmUpdateTarget = !restore && !isGit);且 dsh-ui-tools 这个 npm 名字属于他人(另一作者的插件,无此声明)。所以这条声明当下的作用是统一生态惯例、并为将来可能的 npm 发布备好,不构成本机 github 安装路径的运行时保险——本包的旧内核自动降级是靠 alphaFeatures 子 fiber 实现的(见上),与这条声明无关。
  • 为什么是逐条发布线而不是 >=0.1.1-rc.1:semver 的预发布规则要求预发布版本只匹配同 x.y.z 的区间,因此 >=0.1.1-rc.1 会漏掉 0.1.2-*、0.1.5-* 等线;^0.1.N-0 每项覆盖一条线,末项 >=0.3.0-0 兜住此后所有新线,且不硬顶未来内核(把未来内核误判为不兼容会让市场拒绝合法升级)。
  • -0 后缀的作用:^0.1.1-0 的下界取该线最早的预发布,否则区间会被预发布规则排除。
  • 该区间已对 npm 上全部 29 个已发布内核版本 + 3 个未来哨兵逐项断言,并保证普通 semver 语义与市场侧 includePrerelease 语义判决一致(见下「自检」)。
  • v0.4.8 修正:此前区间写作 … || >=0.1.6-0,其末项在普通语义下不覆盖任何 0.1.7-* / 0.2.0-* 预发布版(只在 includePrerelease 下才成立),实测有 6 条分流(0.1.7-alpha.1/2、0.1.7-rc.1/2、0.2.0-rc.1/2)。原因同上一节:预发布版只与同 x.y.z 的元组比较,而 >=0.1.6-0 的下界落在 0.1.6 线。更隐蔽的是——当时 0b 的版本表只列了 21 个,恰好漏掉整条 0.1.7/0.2.0 线,于是「两种语义判决一致」那条断言假绿。现改为逐条发布线 ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0 || >=0.3.0-0 并全量列表,令同类裂缝无处可藏。

验证口径:node tools/compat-check.mjs 跑在本机已装内核副本的真实 cordis 上(当前实测 0.2.1-alpha.2,98/98 全绿;断言计数随内核枚举动态变化,门槛是零 FAIL 而非某个具体数字,因此别把它当固定指标读),且场景 B3 会逐内核遍历本机全部可发现内核(kernels/* 各副本 + app.asar 内桌面版自带内核),断言五个功能的槽位注册、子 fiber 状态与 boot 审计口径;0.1.7-rc.2 / 0.1.5-rc.2 / 0.1.2-alpha.3 为历史验证点(这些内核副本已不在本机,本机现存副本为 0.2.1-alpha.1 与 0.2.1-alpha.2,B3 会跳过缺席者)。真机(浏览器)行为另由 tools/live-smoke.mjs 覆盖,需自备 URL+token(0.2.1-alpha.2 已实测:入口无横幅、五功能标记在位、设置页三行开关均正常)。

自检

node --check lib/client.js       # 语法
node tools/check-model-seat-hug.mjs  # 功能一「座位紧贴听写按钮」的选择器回归钉(手写 DOM 骨架 + 求值真实 CSS)
node tools/compat-check.mjs      # 内核自适应回归(真实 cordis,场景 0/0b/A/B/B2/B3/C/D + 速度计组件/口径断言)
node tools/live-smoke.mjs <URL+token>   # 真机冒烟(可选;自动探测本机 Chrome/Edge)
# 或一次跑完:npm run check

tools/check-model-seat-hug.mjs 专治一类静态阅读看不出来的样式 bug:它把手写 DOM 骨架建成内核 InputBar 右侧动作区的真实层次(座位在 div.standardControls 内、听写按钮嵌在兄弟容器 div.activity 的 span.triggerAnchor 里),求值 MSS_CSS 模板串(而不是只读源码文本)后用自写选择器匹配器逐条核对:座位父容器被推到最右、横缝收到 0、中英两套 aria-label 都命中、无座位/听写缺席时不误伤、CSS 里不出现内核哈希类名、官方模型按钮仍被隐藏。它也守住「模板串被反引号提前闭合」这类事故——那种情况下求值会失败或留下未展开的 ${...}。本仓库零依赖,不引入 jsdom。

tools/compat-check.mjs 不开浏览器也不重启服务:它从本机已装内核副本里加载 cordis,搭一个最小宿主把 lib/client.js 的 entry 挂上去,在「旧内核 / 新内核 / 服务后到 / 内核不提供 remote」四种主场景(外加声明层 0/0b、速度计 B2、真内核装配器 B3)下复刻 boot 的审计口径(只看 loader entry fiber 是否停在 PENDING),断言入口激活、子 fiber 状态、各槽位注册、功能一目录冷解析、设置页能力位(含三条「conversation.view / uiSession.provide / workspaces.openPath 永不被调用」的逆向钉),以及功能五的数值口径、Definition 状态机与两个组件的 DOM/开关门控(后三项直接截取 bundle 源码段执行,测的就是被测 bundle)。

另含声明层断言(场景 0b):用内核自带的真实 semver 校验 engines.dsh 覆盖全部已发布内核版本、不误伤未来内核,并保证普通语义与市场侧 includePrerelease 语义判决一致——手搓匹配器会把「被测语义」偷换成「我以为的语义」,故不用。可用 DSH_SEMVER_ENTRY=<...>/semver/index.js 指定匹配器。

tools/live-smoke.mjs 用 puppeteer-core 驱动本机 Chromium 内核浏览器(按 DSH_SMOKE_CHROME → ms-playwright 缓存 → 系统 Chrome/Edge → 常见类 Unix 路径依次探测),打开正在运行的 DSH Web 断言无加载横幅、各功能 DOM 标记在位、并截图。拿不到 URL/token 或找不到浏览器时 SKIP(exit 0)。

变更记录

  • v0.4.11(本次):把功能一(模型双按钮)里两个主按钮的背景色显式指定为透明,并做一次全仓库口径扫尾。

    • 透明背景(用户要求「两个按钮的背景色指定为透明框」):MSS_CSS 里 [data-mss-btn] 的常态 background 由 color-mix(in srgb, var(--dsw-alias-brand-primary, #4176e6) 8%, transparent) 改为 transparent——不再有那层 8% 品牌色底,视觉上是输入框里两个「纯文字 + 箭头」的透明框,与官方 composer 其它无底控件连成一片。保留交互反馈::hover 与 [aria-expanded="true"] 仍是 16% 半透明淡色(按钮若无任何按压/展开反馈就失去可点性提示)。若后续要求连悬停也全透明,把那两条的 background 一并改成 transparent 即可。
    • 新增 5 条守护断言(tools/check-model-seat-hug.mjs,14 → 19 条):这条需求的失败模式是「改完刷新页面看不出差别」,只读源码文本抓不到(写回 color-mix 照样是合法 CSS),所以断言沿用它:求值后的 CSS 按 cascade 取实际生效声明。钉住四面:① 常态背景必须是 transparent;② 没有另设不透明的 background-color 把它盖掉;③ :hover 与 [aria-expanded="true"] 各自仍保留半透明反馈(防「改一处、静默坏另一处」);④ 该规则不得牵连面板内的下拉项([data-mss-row] 不命中)。
    • 扫尾 ① 验证口径数字漂移:README 与 AGENT.md 里的 compat-check 计数/内核路径由 93/93 + 0.2.1-alpha.1 更新为 98/98 + 0.2.1-alpha.2(本机现存内核副本为 0.2.1-alpha.1 / 0.2.1-alpha.2)。
    • 扫尾 ② 已删功能说明残留:删掉「明确不覆盖的范围」里那条仍在讲「修改的文件」tab 取数逻辑的说明(该功能 v0.4.9 已整体移除),改为一句指向变更记录的口径说明。
    • 扫尾 ③ tools/live-smoke.mjs 残留清理:清掉 16 处已删功能痕迹——MARKERS.modifiedFiles([data-mfs-root])、pageerror 过滤里的 mfs、整段「2b 功能三取数」断言、设置页「四个偏好行」与「修改的文件行」断言、demo 段误读的 row-mfs/mfs-toggle/mfs-hint(真实 demo 里这三个 id 早已改为 row-alpha/alpha-toggle/alpha-hint,原样跑会直接抛异常),功能编号由「功能六」校正为「功能五」;文件头过期注释(v0.4.2 验证用、内核 0.1.2-alpha.3)一并更新,并补上 DSH_SMOKE_URL 环境变量入口与 SKIP 口径说明。
    • 扫尾 ④:issue #1(v0.4.7「撤回异常」归因)技术上已在 v0.4.8 闭环(现象真实、归因证伪),本次核对后关闭。
    • 回归:npm run check = 座位钉 19/19 + compat-check 98/98 全绿;live-smoke.mjs 语法与 SKIP 分支实跑通过(无 URL+token 时 exit 0,不阻塞 npm run check)。package.json 版本 0.4.10 → 0.4.11。只改 lib/client.js 的 CSS 段与文档/工具(客户端 bundle,无构建步骤),刷新页面即生效。
  • v0.4.10(历史):把输入框的模型双按钮往右推、紧贴右侧的「听写」按钮,并顺带修掉一个潜伏的真实 bug(该 bug 会让官方模型按钮不再被隐藏、与插件座位同时出现)。

    • 紧贴的实现(lib/client.js 的 MSS_CSS,纯 CSS、不动注册逻辑):官方 composer 右侧动作区的结构是 div.trailing(gap:12px; margin-left:auto)下并列 div.standardControls(座位所在的槽的父容器)、div.activity(听写按钮的槽)、button.primary(发送)。座位在 .standardControls 内,听写按钮在兄弟容器 .activity 内 —— 纯 CSS 没有「叔侄选择器」,:has() 也只能向下或向兄弟,因此无法直接互相定位。可行做法是把座位的父容器连同座位一起推到最右、并把两个容器之间那段来自 trailing 的 gap 收掉:
      div:has(> [data-mss-seat]) { margin-left: auto; }
      div:has(> div > [data-mss-seat]):has(+ div [aria-label="开始录音"]),
      div:has(> div > [data-mss-seat]):has(+ div [aria-label="Start recording"]) { column-gap: 0; }
      div:has(> div > [data-mss-seat]) + div:has([aria-label="开始录音"]),
      div:has(> div > [data-mss-seat]) + div:has([aria-label="Start recording"]) { margin-left: 0; }
      
    • 为什么用 :has() 而不写类名:内核用 CSS Module,类名带哈希(当前是 uV2eYG_trailing / uV2eYG_activity),随每次构建变化;aria-label 是官方文案契约(中文 开始录音 / 英文 Start recording,见 dsh-experimental-client-ui-voice-input),data-mss-* 是本插件自己的,两者都稳定。降级:浏览器不支持 :has() 时整段规则被丢弃,座位回到默认 gap:12px,只是「不够贴近」,不会错位或消失 —— 故不需 @supports 兜底。
    • 途中修掉的真实 bug(真机可见):隐藏官方模型按钮的规则写作 ${OFFICIAL_TRIG} 内联进模板串,而该模板串前面的说明注释里出现了反引号,把 ECMAScript 模板串提前闭合,使 ${OFFICIAL_TRIG} 原样留在生成的 CSS 里 → 选择器里出现一个不存在的 $ 元素、隐藏规则整条失效,插件座位与官方控件会同时出现在输入框上。修法:说明注释里不再出现反引号;同时把含引号的选择器常量改为「拼接出来的两半」(OFFICIAL_TRIG_A/OFFICIAL_TRIG_B),避免模板串里出现裸引号触发的同类解析歧义。(OFFICIAL_TRIG 本身保留原样,供 querySelectorAll 使用。)
    • 新增守护断言 tools/check-model-seat-hug.mjs(14 条,已并入 npm run check):手写 DOM 骨架忠实复制内核 InputBar 层次(含听写按钮被 span.triggerAnchor 包裹这一实情),求值(而非只读取源码文本)MSS_CSS 模板串后用自写选择器匹配器核对:座位父容器拿到 margin-left:auto、横缝收到 0、activity 左边距清 0、中英两套文案都命中、无座位/听写缺席时不误伤、CSS 里无内核哈希类名、且官方模型按钮仍被隐藏。本仓库零依赖,不引入 jsdom。
    • 该断言的价值已被证明:它先是抓出产出 CSS 里的一处真实 bug —— 规则写成 :has(+ div > [aria-label=…])(直接子元素)而真实听写按钮嵌在 span 内,导致贴合规则永远不命中;修正为后代匹配 :has(+ div [aria-label=…])。随后又通过「必须求值模板串」这一改动抓出上面那条 ${OFFICIAL_TRIG} 未求值的真 bug(只读源码文本的写法会让该断言恒红,而求值写法能同时覆盖「模板串提前闭合」这类事故)。
    • 回归:npm run check = 新钉 14/14 + compat-check 93/93,全绿。package.json 版本 0.4.9 → 0.4.10,check 脚本加入新钉。只改 lib/client.js(客户端 bundle,无构建步骤),刷新页面即生效。
  • v0.4.9(历史):移除功能三「修改的文件」会话选项卡(conversation.view / ui-tools-modified-files)。移除原因:① 它的取数是同一份 chat target 快照的又一遍解析,与功能五(速度计)读的是同一个 target,属于重复建设;② 维护成本集中在自己实现的 run_code 内嵌路径提取上——这套静态字符扫描只认字面量字符串路径,模板串、变量拼接、shell 级写操作(pwsh Set-Content/重定向、gitbash)全都解析不了,每次内核调整都要跟着修,收益与风险不成比例。

    • 删除范围:MFS_ZH/MFS_EN 文案与 MFS_NS 命名空间、MFS_MUTATIONS 工具白名单、RUN_CODE_TOOL/RUN_CODE_CALL_RE、全部 mfs* 解析函数(mfsReadQuoted/mfsMatchParen/mfsExtractKeyValues/mfsExtractRunCodePaths/mfsNormalizePath/mfsRecordMutation/collectModifiedFiles/mfsRelativeTo)、ModifiedFilesView 组件与 MFS_CSS、mfsSources WeakMap 与 mfsChatSource、ctx.uiSession.provide({ hooks: ["modifiedFiles"] }) 整块、ctx.slots.inject("conversation.view", ...) 整块注册(含 workspaces.openPath 打开链路)、偏好字段 mfsCompact 与设置页对应开关。
    • alphaFeatures 子 fiber 保留、但收窄:inject 由 ["uiConversation", "uiSession"] 改为 ["uiConversation"]——功能三移除后 uiSession 已无消费者(它只被 provide 用),继续声明只会让子 fiber 在「有 target 体系但没 uiSession」的内核上白白停在 PENDING。跨内核降级机制本身完全未变:旧内核上子 fiber 照旧静默 PENDING、入口 entry 照旧 ACTIVE、boot 审计照旧 0 失败、无横幅。共享常量 MFS_CHAT_TARGET/MFS_EMPTY_CHAT 保留(tspChatSource 仍在用)。
    • 偏好数据无需迁移:normalizePrefs 是逐字段白名单,旧 localStorage 里残留的 mfsCompact 会被自然丢弃,不写迁移代码、不升 key 版本(dsh-ui-tools:prefs:v1 不变)。
    • 回归:compat-check 同步删除功能三的 8 条断言,并把「功能三在旧内核缺席」这类单向断言换成三条逆向钉——「任何内核都不得注册 conversation.view」「uiSession.provide 永不被调用」「workspaces.openPath 永不被调用」——防止残留代码悄悄复活。合计 115 → 113 断言全绿(断言数因删除而减少,覆盖却更严)。负向对照有效:把 alphaFeatures.inject 改回 ["uiConversation", "uiSession"] → 场景 C 出现 2 条 FAIL(子 fiber 停在 PENDING、功能五 pill 未到位),111/113、exit 1。package.json 版本 0.4.8 → 0.4.9;插件由六功能变为五功能。
  • v0.4.8(历史):针对 issue #1(「v0.4.7 未修复撤回异常:DSH 0.2.0-rc.1 上 4 个 loader entry 被级联击落」)做三项加固。核实结论:issue 的现象描述真实(确有撤回异常与同 bundle 级联击落),但归因不成立 —— 其 crash log 采集于我安装 v0.4.7 之前(shell pid 14260 启动于 17:57,我的安装完成于 18:26),两份日志记录的都是 v0.4.6 期间的运行;19:08 之后(已装 v0.4.7)的两次启动未再产生 crash log。我进一步从 app.asar 提取 0.2.0-rc.1 的真实 dsh-client-ui-conversation,用它的真装配器驱动插件 Definition:context.current 在该版本存在且语义未变,v0.4.7 通过。故 issue 假设的「context.current 在 0.2.0 上失效」被证伪。

    • ① 修正矛盾注释:tspLiveDefinition 的 doc 仍写「turn/end 置 ended,buildViewNode 随即返回 null」,与 v0.4.7 新不变量(返回同 key + hidden)直接打架,易误导后续维护者。已改为说明正确行为并标注旧写法为何危险。
    • ② 拆分防御分支(issue #1 的核心洞见,实为真问题):原写法把「内核未提供 context.current」与「确认从未物化」合并成同一个 previous = null —— 一旦内核换了字段形态,防御会静默失效、异常照旧复现而不留痕迹。现拆为 canReadCurrent(能否读到)与 previous(已知时上一帧节点)两个判据:读不到时不敢断言「未物化」,一律保守返回同 key + hidden(撤回异常的代价远大于多挂一帧隐藏节点),并只告警一次。
    • ③ 把 0.2.0-rc.1 纳入验证矩阵:0b 的版本表原只列 21 个,漏掉整条 0.1.7/0.2.0 线,使「两种 semver 语义判决一致」断言假绿(旧区间在该 6 个版本上确有分流);现补全为 29 个已发布版本 + 3 个哨兵。同时 B3 由「只测第一个内核」改为逐内核遍历,并新增 app.asar 来源 —— 桌面版大版本升级后真正生效的 client 包在 asar 内,kernels/ 下可能没有对应版本,不读 asar 就等于让最需要验证的那版内核缺席(0.2.0-rc.1 正是如此)。
    • engines.dsh 同步收紧:… || >=0.1.6-0 → … || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0 || >=0.3.0-0(两处值一致)。旧写法对 0.1.7-*/0.2.0-* 预发布版在普通语义下全部漏判。
    • 回归:compat-check 90 → 110 断言全绿(B3 现在对 5 个内核各跑 5 条:0.1.7-rc.2/0.1.7-alpha.1/0.1.6-alpha.2/0.1.5-rc.2/app.asar);新增「读不到 context.current 时保守返回 hidden」断言。负向对照有效:把防御分支退回合并写法 → 该断言 FAIL(109/110)。package.json 版本 0.4.7 → 0.4.8。
  • v0.4.7(历史):修复功能六导致回合结束后界面不再刷新、后续发送的提问不显示(真实内核回归,非渲染样式问题)。

    • 根因:功能六的「生成中估算条」是自建 chat 节点 Definition(token-speed-live)。内核 ConversationNodeAssembler.buildTargetUpserts() 的判定是「上一帧 context.current.get(target) 非 null、本帧 buildViewNode 返回 null」→ 抛 conversation Definition "token-speed-live" withdrew materialized target "chat"; return the same key with hidden visibility instead。该异常从 flush() 冒出后整条 chat 视图快照链停更——界面表现为回合结束后不再刷新、之后每次提问都没有新消息出现(0.1.6-alpha.2 与 0.1.7-alpha.1 均已复现;0.1.6 起该校验即已存在,并非新内核新增行为,而是功能六自 v0.4.6 引入以来一直存在、在「本回合确实产生过可见输出」时触发)。
    • 关键难点(只看 state 修不干净的那一半):真机的流式增量是 transient 记录;回合收尾内核走 settleAssistant(),先 retire 全部 transient、再 replayContext() 重放 —— 状态被倒推回 firstTokenAt=null / estTokens=0(并不是 ended=true)。因此「state.ended / 无输出 → 返回 null」这种写法照样触发撤回异常。
    • 修复:判据改用内核给的 context.current(上一帧已物化的节点)——只要它非空就返回同 key 节点、仅切 visibility;仅「从未物化且当前无数据」才返回 null(安全路径)。
    • 为什么此前没测出来:① 真机流式的 transient + settleAssistant 路径未被任何测试覆盖;② 原有断言只调 live.buildViewNode(...) 纯函数、从不传 context.current,等于绕过了内核唯一的撤回判定依据;③ compat-check 用的是假内核,根本没实现该校验。
    • 回归加固:新增 场景 B3「功能六 × 真内核 ConversationNodeAssembler」——从本机内核副本载入真内核装配器 + 真 bundle 截取的 Definition,按真机事件序列(durable 起止 + transient 增量 + settleAssistant)逐帧驱动,断言全程不抛 withdrew、节点 visible↔hidden 正确切换、多回合连续会话(后续提问)依旧刷新;另补 4 条纯函数断言显式传 context.current。compat-check 78 → 89 断言全绿;负向对照有效:把 buildViewNode 换回只看 state 的写法 → 8 条 FAIL(含真内核 settleAssistant 抛 withdrew、后续提问全程抛错、visible 帧数 3→1)。package.json 版本 0.4.6 → 0.4.7。
  • v0.4.6(历史):新增功能六「输出速度计 tok/s」——回合结束后在官方「用时 xx秒」pill 左侧常驻 ⚡42 tok/s(无需点开「本轮用时和速度」弹窗),生成中在消息流末尾显示 ⚡≈ 25 tok/s 生成中估算。精确值与内核同源同口径(ΣoutputTokens ÷ Σ(firstTokenTime→completedTime),只计 timing 齐备的 step),生成中因内核流式期间不上报 token 数(usage chunk 被 publication:"none" 抑制)而按已输出字符折算并明确标注「≈ / 生成中估算」。位置实现:精确 pill 走 list 槽 conversation.chat.assistant-actions(order: -10 → 紧贴「用时」左侧,渲染位即 MessageIconActions.extraActions);生成中走官方 Definition 通道 uiConversation.events.register 自建 token-speed-live chat 节点 + conversation.chat.node 渲染器(turnTail 槽在生成期间不渲染 —— 其数据源必须匹配到 turn/end,故不能用于实时)。排序锚点用大于任何真实 seq 的常量,避免节点跑到流式正文上方。设置页新增开关 tokenSpeedEnabled(默认开,旧内核下灰显)。实现过程中被自测抓到并修复一处真实缺陷:Definition 通道形状不符时原先直接抛出,会让整个 alphaFeatures 子 fiber FAIL、把「修改的文件」一起拖下水 —— 现改为形状探测 + 只让生成中那段缺席。回归:compat-check 49 → 77 断言全绿(v0.4.7 起 89/89)(新增数值口径、除零/空入参边界、Definition 状态机全流程、两组件 DOM 与开关门控),负向对照有效(改坏锚点 / 去掉形状守卫各触发 FAIL);真机 live-smoke 在 0.1.6-alpha.2 上通过(入口无横幅、功能标记在位、设置页开关正常,新增功能六断言)。历史回合可能无读数属预期:firstTokenTime 来自瞬态流式事件、不进持久化日志,重载页面后历史回合的 TPS 内核自身也不显示(同一口径)。package.json 版本 0.4.5 → 0.4.6。

  • v0.4.5(历史):新增机器可读的内核版本约束 engines.dsh(同时写顶层与 dsh.engines.dsh,两者值一致),并修正 README 里「0.1.2-alpha.1 及更新」这一在真实 semver 下不成立的口径。背景:此前本包只在 README 与 package.json description 里用自然语言描述内核要求,没有任何字段可供工具读取——而生态实际消费的字段是 engines.dsh:dshmarket 的 manifestFacts() 读它(缺失时回落 dsh.engines.dsh),在 Discover 页展示宿主兼容性,并在安装/更新前拒绝判定为不兼容的版本。区间写成 ^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0 || >=0.1.6-0:不能写成 >=0.1.1-rc.1 或 >=0.1.2-alpha.1——semver 要求预发布版只匹配同 x.y.z 区间,实测 >=0.1.2-alpha.1 不匹配 0.1.5-rc.2(本仓库当前实测内核),>=0.1.1-rc.1 也会漏掉 0.1.2-* / 0.1.5-* 各线;逐条 ^0.1.N-0 覆盖每条已发布线,末项 >=0.1.6-0 兜住后续新线且不硬顶未来内核(误判未来内核会让市场拒绝合法升级)。-0 后缀用于把下界取到该线最早的预发布。compat-check 新增场景 0b(6 条断言,合计 49/49):用内核自带真实 semver 对 npm 上全部 21 个已发布内核版本 + 2 个未来哨兵逐项断言,并要求普通语义与市场侧 includePrerelease 语义判决一致(偏差即失败)。负向对照有效:把区间换回 >=0.1.2-alpha.1 触发 4 条 FAIL(含 0.1.5-rc.2 未被自身声明覆盖)。未改动任何运行期逻辑(lib/client.js 与 v0.4.4 逐字一致);内核兼容行为与 v0.4.4 相同,当前实测内核 0.1.5-rc.2。

  • v0.4.4(历史):修复功能一在 0.1.5-rc.1 内核上每次挂载会话抛一次 cannot get property "remote.session" without inject。根因:内核 ModelDirectoryResolver.directoryFor() 内部要读 this.ctx.remote.session 构造 ModelDirectory,而 cordis 的 Service tracker 会把 service 的 this.ctx 重绑到调用方上下文——所以调用方 fiber 必须自己声明 remote。本插件功能一的 ctx.inject([...]) 只声明了 slots/modelDirectories/sessions,于是每次冷解析(新会话、会话切换)都抛未捕获错误;只因 directoryFor 开头有「已缓存则早返回」的短路,多数时候命中内核自建目录才没炸出可见故障(竞态相关)。修复:① 在该子作用域补声明 remote + remote.session(两者都要:后者是嵌套追踪服务,只补前者实测错误照旧;刻意都不进 loader entry 的 inject,否则会重新引入 v0.4.2 修掉的旧内核 entry 停 PENDING / 五功能全丢);② 加 mssResolveDirectory() 兜底,解析失败不再冒泡成 pageerror,而是降级为「本座位不渲染 + 经 html[data-ui-tools-model-seat="fallback"] 把官方控件放回来」,避免「官方被 CSS 隐藏 + 插件座位缺席」两头皆空。同类语义对等插件 dsh-vision-router 已有防护代码。回归:compat-check 扩到四场景 43 断言(新增场景 D 用「内核不提供 remote」夹住依赖位置;功能一冷解析正向断言),live-smoke 新增 remote.session 与座位真的渲染两条真机断言(只断言「不报错」不够——兜底会把异常吞成 warn 并静默降级),并修复浏览器探测(原先只认 ms-playwright 写死路径,本机没装 Playwright 时静默 SKIP)。

  • v0.4.3(历史,已被 v0.4.9 移除):「修改的文件」支持 run_code 内嵌工具调用路径提取——当前 DSH agent 环境的文件操作全部包在 run_code 里(工具名恒为 run_code,路径藏在 code 字符串),旧逻辑按工具名白名单匹配导致这类会话一律显示「0 个文件」。新增 mfsExtractRunCodePaths:从 code 静态提取 tools.edit/write/mkdir/move/copy/delete… 内嵌调用的字面量路径(含字符串数组参数),按内嵌工具映射回白名单 ops,复用去重/徽标/打开链路。字符扫描解析(无正则拼接),反斜杠转义原样保留(Windows 路径不因 \t/\n 语义被改写);变量拼接、模板串、shell 级写操作(pwsh Set-Content/重定向、gitbash)不计入。compat-check 新增对应断言(当时 33/33 全绿)。v0.4.9 已随功能三整体删除——正是这类易碎逻辑促成了移除决定。

  • v0.4.2(历史):内核版本自适应(软依赖 + 子 fiber 隔离),修复「切到 0.1.1-rc.x 旧内核后 Web UI 顶部 Failed to load plugins / web boot: 1 entry did not activate 横幅、桌面端反复重载」——根因是入口 inject 硬声明了 0.1.2-alpha.1 才有的 uiConversation / uiSession,旧内核无提供方 → 整个 entry 停在 PENDING 被 boot 审计判失败,其余功能一起丢。现在入口只声明五个跨内核服务,功能五(速度计)的整段注册搬进 ctx.plugin(alphaFeatures) 子 fiber(alphaFeatures.inject 承载该硬依赖;v0.4.9 前为 uiConversation + uiSession,功能三移除后收窄为 uiConversation),旧内核上子 fiber 静默停在 PENDING、其余四项照常;另加能力探测(子 fiber 激活即置位 capability.alphaApi)+ 设置页灰显提示「需内核 0.1.2-alpha.1+」+ 关键调用点形状探测与 try/catch 降级;新增 tools/compat-check.mjs 无头回归(真实 cordis,三场景)与 npm run check。行为与验收见内核兼容矩阵。

  • v0.4.1(历史):按反馈移除 v0.4.0 的「composer 快捷命令条」——删除 conversation.composer.dock 注册、QCB 命名空间/CSS、偏好字段 quickbarEnabled/quickbarItems 及设置页快捷条编辑器;「DSH UI 工具」设置页保留(仅布局偏好:徽章开关 / 启动默认折叠 / 修改文件紧凑显示——后两项里「修改文件紧凑显示」已于 v0.4.9 随功能三移除),插件回归五功能。

  • v0.4.0(历史):新增「composer 快捷命令条」——注册进官方 conversation.composer.dock(list 加性,order 10),输入框下方一排常用命令 chip,点击 = inputActions.setDraft + submit 填入并发送,命令列表可在设置页自定义;新增「DSH UI 工具」设置页——注册进官方 settings.section(root list 槽,id dsh-ui-tools、order 100),集中开关四个既有功能 + 快捷命令条,「修改的文件」新增紧凑单行模式、侧边栏新增「启动默认折叠」偏好;全部偏好经 localStorage 持久化(dsh-ui-tools:prefs:v1,沿用 dsh-better-sidebar 同款社区惯例;官方 settings 命名空间需 host 侧注册 schema,本插件保持纯浏览器故不采用)。v0.4.1 起快捷命令条已移除。

  • v0.3.4(历史):修复 DSH 0.1.2-alpha.1 上「折叠/展开」工具条错位(与余额挤在一行、「展开全部」被裁剪)——旧 CSS 里硬编码的侧边栏哈希类名 .hHd-Xa_footerActions 已失效,且 renderer 给 slot 套的 div[data-slot] 是 display:contents(不参与布局),原 :has 换行规则命中也无效。改用哈希无关的 [class*="footerActions"]{flex-wrap:wrap !important} 命中真实布局容器,内核升级不再失效。

  • v0.3.3:真正修复 DSH 0.1.2-alpha.1 上的加载失败(Failed to load plugins / client-modules: require(...) missed the module table)——client bundle 不再 require @deepseek-ai/dsh-client-runtime/client:resolveWorkspacePath 按官方 dsh-client-ui-chat 的做法内联进 bundle,空快照 MFS_EMPTY_CHAT 代替 EMPTY_CHAT_SNAPSHOT(MFS_EMPTY_CHAT 在 v0.4.9 功能三移除后由速度计继续使用)。该内核的 client 模块表严格校验,runtime 未必是 profile 的 graph 行,v0.3.2 用 dsh.client.inject 登记的方案实测不生效(已回退)。

  • v0.3.2:尝试用 package.json 的 dsh.client.inject 把 @deepseek-ai/dsh-client-runtime 登记进 client 模块表以解决加载失败,实测在 0.1.2-alpha.1 上不生效(runtime 非 graph 行时 inject 是 no-op),已被 v0.3.3 取代。

  • v0.3.1:兼容 DSH 0.1.2-alpha.1 内核——该内核把会话内容迁到 target 体系,useSession 不再提供 nodes/runningCalls;功能三「修改的文件」(v0.4.9 已移除)当时改经 ctx.uiConversation.binding(...).target("chat") 读 ChatSnapshot.legacy 取数,并注册 useModifiedFiles 标准 hook,写法与官方 dsh-client-ui-chat 同构。其余功能所用槽位/服务在内核中未变。

  • v0.3.0:新增功能三「会话标题旁工作区徽章」——注册进官方 conversation.session.header.actions 增量 list 槽,在会话页标题右侧显示所属工作区名,纯 slot 渲染不改源码。(该功能在 v0.3.0~v0.4.8 期间编号为「功能四」,v0.4.9 功能三移除后前移为功能三。)

  • v0.2.0:新增「修改的文件」选项卡——注册进官方 conversation.view 开放槽,会话头部主选项卡区在「对话 / 轨迹」之后多出「修改的文件」,列出本会话改过的所有文件,点击经 Host 打开。v0.4.9 已整体移除(该功能在 v0.2.0~v0.4.8 期间编号为「功能三」)。

  • v0.1.4(2026-08-27):解决与余额插件(dsh-cost-meter)共用 sidebar.footer.action 时的同行冲突——纯 CSS 让容器换行、工具条占满整行,自动排到余额下方自己的行。

  • v0.1.3(2026-08-27):彻底放弃搬动 DOM——v0.1.2 的收窄观察器方案实测仍卡死(搬动 slot 节点与框架互搏)。改为纯 slot 渲染(工具条即官方 sidebar.footer.action 位置,紧贴工作区列表下方)+ CSS 对齐列表内边距。

  • v0.1.2(2026-08-27):尝试用收窄的 footer 观察器恢复 v0.1.0 的位置,实测仍卡死,回退。

  • v0.1.1(2026-08-27):修复 v0.1.0 卡死——移除全局 document.body MutationObserver 与 40ms 定时搬节点(工具条当时因此回到 footer 位置)。

  • v0.1.0:合并 dsh-model-select-style(模型选择双按钮)+ dsh-workspace-collapse(侧边栏折叠/展开)。

安装

dsh plugin --profile web add github:qgx1992/dsh-ui-tools

或手动在 ~/.dsh/profiles/web/package.json:

  • dependencies 加 "dsh-ui-tools": "github:qgx1992/dsh-ui-tools"
  • dsh.profile.bundles 加 "dsh-ui-tools"
  • 在 profiles/web 目录执行 pnpm install,然后重启 DSH。

注意:首次登记进 profile 后需要重启一次 DSH;之后调整 lib/client.js 只需刷新页面。

从旧插件迁移

本包合并了 dsh-model-select-style 与 dsh-workspace-collapse。迁移时从 ~/.dsh/profiles/web/package.json 移除这两个旧条目(dependencies 与 bundles),只保留 dsh-ui-tools,然后 pnpm install 并重启。

停用 / 卸载

  • 临时停用(推荐,无需卸载):在 ~/.dsh/profiles/web/cordis.patch.yml 追加下面两行,重启 DSH 即可(要恢复就删掉这两行再重启):
    - id: ui-tools
      disabled: true
    
  • 设置 → 插件 中的停用同样有效(五个功能同时关闭)。
  • 彻底卸载:从 bundles 与 dependencies 移除条目、删除 node_modules 内链接与本地插件目录。

明确不覆盖的范围

  • 输入框里输 /model 弹出的命令面板选择器走的是另一套 popupSelect 组件,本插件未涉及。
  • 侧边栏折叠条只影响工作区分组展开状态,不改变其他侧边栏 UI。
  • 偏好只存当前浏览器 localStorage,不会跨浏览器/跨设备同步。
  • 插件只覆盖上面列出的五个功能;此前的「修改的文件」会话选项卡已在 v0.4.9 整体移除,其取数与速度计重复且维护成本过高(详见变更记录)。

Content from the project README on GitHub ↗

Comments

Comments live in GitHub Discussions. Sign in with GitHub to post or react.