DSH Plugins Marketplace

DSH Plugins

Plugins

/

UI & Experience

/

dsh-model-in-use

d

dsh-model-in-use

Manifest valid

Marks models in use by other running sessions in the DSH model menu, and restores the remembered reasoning effort and group collapse state for each model.

UI (client)hasBundlePatch

dsh-model-in-use

让 DSH 的模型菜单看见会话占用、记住模型推理档位与折叠状态。

MIT license DSH cordis plugin web zero runtime dependencies localStorage


模型菜单:被其他会话占用的模型行尾带圆点,悬浮卡显示当前模型与推理档位

会话悬浮卡的「进行中」后追加当前模型与推理档位

它做什么

一个独立的 DSH 客户端(web)cordis 插件,只装饰宿主已有的模型菜单,不改宿主包、不引入运行时依赖。

功能说明
占用标记已被其他正在运行的会话使用的模型,行尾显示 #3cc400 圆点,悬停提示「其他会话正在使用中」
分组级圆点该分组内任一模型被占用时,标题前也点一个小圆点——分组折叠后仍能看出「这里面有模型在跑」
推理档位记忆为某个模型选过的推理档位被记住;切走再切回、或新开对话选同一模型,都会恢复上次的档位
折叠记忆每个服务商分组可折叠,状态记在 localStorage,重开菜单或刷新页面均保留
间距修正分组间距按视觉顺序排布,不再出现某两个分组贴在一起
吸顶不透明吸顶的分组标题底色钉为不透明,二级菜单文字不再从标题下方透出
悬浮详情会话悬浮卡的「进行中」后追加当前模型与推理档位,如 进行中 ● Deepseek-V4.1-Flash · x0.00 Low

关于占用标记:

  • 非互斥:只是提示,不阻止选择,多个会话可以选同一个模型。
  • 实时:对方开始 / 停止运行或切换模型,圆点立即出现 / 消失。
  • 当前会话自身那一行不标记(该行已有对勾)。
  • 被选中那一行同时有对勾与圆点时,圆点向左让位到对勾格前的间隙(right:27px),两者并排而非重叠。
  • 只统计主会话,subagent 会话不计入。
  • 只装饰 composer 的模型下拉;/model 斜杠命令的弹窗不受影响。

安装

# 从 GitHub 安装(推荐)
dsh plugin --profile desktop add github:Faide-cyber/dsh-model-in-use

也可以在 dshmarket 插件市场搜索 dsh-model-in-use 一键安装。本仓库已打 dsh-plugin topic,市场每 2 小时自动收录。

[!IMPORTANT] 装完必须完全退出 DSH 再重开。客户端 bundle 的字节在激活时被快照进 module table,没有 HMR watcher 时不会重读。

仓库已提交构建产物(lib/),安装过程不执行构建步骤。插件自带 cordis.patch.yml(dsh.bundle.patch),向 Web roster 插入一行 model-in-use;客户端半边只在 web 平台加载。

本地开发时可以直接 link:

// <profile>/package.json
"dependencies": { "dsh-model-in-use": "link:E:/dsh/plugin/dsh-model-in-use" },
"dsh": { "profile": { "bundles": [ /* …, */ "dsh-model-in-use" ] } }

使用

装好即生效,没有配置项。三个约定:

  • 推理档位按「模型」记忆,不按「服务商」。同一个模型常常能在多个 provider id 下取到(例如国际站与国内站都提供同一个模型),按模型记才能保证换站点也读得到。
  • 在档位菜单里选「跟随服务商默认」时,只对本次对话生效,不会删除已记住的档位——下次再点这个模型仍会恢复它自己的档位。
  • 分组默认全部展开。默认折叠会让用户误以为「模型列表没了」。

兼容性

宿主包用途
@deepseek-ai/dsh-api-session-controller会话与模型选择投影
@deepseek-ai/dsh-client-locale文案命名空间
@deepseek-ai/dsh-client-ui-model-selection模型目录与 ModelDirectory.select()
@deepseek-ai/dsh-client-ui-workspace侧边栏 Slot

这些包由宿主提供,插件不把它们放进 dependencies(那会遮蔽宿主实例),仅声明于 dsh.client.inject。插件只在 web 平台加载。

已知边界

  • 圆点的水平位置(未选中 right:14px、选中 right:27px)按当前行的 padding:6px 8px + gap:8px + 18px 对勾格量出。行距或内边距变化后需要重新量;彻底解耦需要上游组件开放行装饰 slot。
  • 折叠通过 CSS 隐藏行,键盘方向键仍会走过被隐藏的行。被隐藏的行不可见,可接受。
  • 推理档位记忆依赖宿主的 directory.select()、directory.store 与 modelDirectories.live.directories.values 形状不变。宿主若同时改掉这些入口或字段,记忆会被静默旁路——但只丢记忆,不会挡住选择本身。
  • 恢复的档位以「发请求那一刻能查到的 catalog」为准;菜单 UI 上的勾选落位依赖宿主 select() 成功后自身的 syncInputs(),插件不再二次刷新。
  • CSS 排版类修复无法在本装置的断言中自证(没有渲染引擎),只用断言钉住决定性 token。

测试

node verify.mjs          # 行为断言,直接加载 lib/client.js,不复制不重写
node negative-proof.mjs  # 反例:故意写坏的实现必须全部被装置抓住
node scope-proof.mjs     # 装置自检:菜单不是 body 子节点、观察者尊重 subtree、不支持的 selector 抛错

三个脚本共用 fixture.mjs,不需要安装任何依赖。

  • 装置按真实形状搭建:mountComposer() 先挂出闭合状态的 composer,openCodexMenu() 再往其子树里插菜单——菜单永远不是 <body> 的直接子节点。FixtureMutationObserver 尊重 observe() 的 options,只给「watched 是变更目标、或带 subtree 的祖先」投递记录。modelDirectories 由 traceable face 包住:字符串赋值会像真实 Cordis shadow 一样消失,只有通过 cordis.original 改到 origin 才能拦住稍后才创建的目录。
  • 反例用单点替换:negative-proof.mjs 每个变体以 from 字符串锚定 shipped 源码,并要求该串在源码里恰好出现一次——同时出现在注释里的锚点会生成「真实代码未被改动」的假变体,这类装置 bug 直接判 FAIL。

实现要点

以下记录几个关键行为「为什么这么做」。修改本插件前建议先读这一节。

分组间距:flex gap 而非 margin-top

上游规则是 section + section { margin-top: 4px },按 DOM 相邻生效。而 freecodego 用 CSS order 给分组排序、从不移动节点,于是这个 margin 落在「DOM 里相邻的那一对」而非「用户看到的那一对」,表现为某两个分组贴在一起。

修法是把容器改为纵向 flex + gap: 4px:gap 按视觉顺序排布,与 order 取值无关。同时将原 margin-top 归零,避免叠加两次。

吸顶标题:相对颜色语法中的 / 1 不可省略

分组标题为 position: sticky; top: 0,会覆盖滚过的行,底色取自 --dsw-alias-bg-layer-2。玻璃皮肤把该 token 调成了半透明,于是标题下方的行文字透上来,两层文字重叠。

修法是在同一条规则里把同一颜色的 alpha 钉为 1:

background: var(--dsw-alias-bg-layer-2, #2c2c2e);
background: rgb(from var(--dsw-alias-bg-layer-2, #2c2c2e) r g b / 1);

rgb(from …) 保留皮肤色相,/ 1 钉死 alpha,因此换任何皮肤都是正确的实色。第一条声明是给不支持相对颜色语法的引擎兜底。

[!WARNING] / 1 不能省。相对颜色语法中省略 alpha 不等于 1,它会继承源颜色的 alpha(见 MDN: Using relative colors:"If the alpha channel value is not explicitly specified, it defaults to the alpha channel value of the origin-color (not 100%)")。写成 rgb(from … r g b) 会原样复现半透明,文字照样透出。

verify.mjs 场景 14 与 negative-proof.mjs 变体 12 均钉住这一条,后者就是删掉 / 1 的单点改写。

为什么走 DOM 装饰

composer 的模型下拉落在 conversation.input.model 这个 single 槽上,实际生效的 occupant 是 dsh-codex-subscription 的 CodexModelSelect(priority: -10,把随包发行的 ModelSelect 压为 active: false),而它没有行装饰扩展点。因此插件:

  1. 给命中的模型行 <button class="codexModelSelectOption" role="menuitemradio"> 打 data-dsh-model-in-use 属性;
  2. 给分组标题 <div class="codexModelSelectGroupTitle"> 打 data-dsh-group-toggle,给 <section> 打 data-dsh-group-collapsed;
  3. 注入样式,用 ::after 伪元素绘制圆点与折角箭头,用一条 display:none 收起整组。

伪元素不在 React 的 reconciliation 范围内,React 重渲染行或标题时不会触碰它;反过来插件也只增删属性,不挪动 React 管理的子节点——折叠完全依靠 section 上的状态属性与 CSS,因此不存在 React 重排子节点导致菜单异常的风险。

行 DOM 内没有 provider / model id,因此行是靠组标题 + 模型名这两个组件实际渲染的字符串,与共享目录 modelDirectories.catalog 对齐还原出 (provider, model)。Codex 版渲染的组标题是 group.name 原文(而非本地化的 provider.account),所以不需要 model 命名空间的翻译。

为什么 title 打在名字 span 上

title(悬停说明)刻意加在行的 span.codexModelSelectOptionName 上,不加在 button 上。freecodego 的菜单装饰器以 group.querySelectorAll('button[role="menuitemradio"][title]').length > 0 判定一个分组是否归它管;一旦行按钮带 title,它就会认领同一批分组,导致同一个标题上挂两套折角与折叠状态、互相冲突。title 留在名字 span 上可让那道闸门始终关闭,该装饰器独享这些分组。

verify.mjs 的 the tooltip is not on the row button 钉住这一条。

分组级圆点与折叠记忆

  • 分组内任一模型被占用时,给 <section> 打 data-dsh-group-in-use,标题前用 ::before 点一个同样的 #3cc400 圆点,标题文字颜色不变。折叠会把行藏起来,标题是折叠状态下唯一还能表达「有会话在跑」的位置。
  • ::before 与折叠箭头 ::after 同挂标题,标题为 flex 行,两者分居两端。箭头用 margin-left:auto 顶到右侧,不用 justify-content:space-between——标题文字是裸文本节点(匿名 flex item),加上圆点后成为三个 item,space-between 会把文字挤到中间。
  • 分组身份取服务商 id,从 section 的 aria-labelledby(组件写的是 ${reactId}-${group.id},菜单 id 是 ${reactId}-menu,前缀可推)切出,不用标题文字。标题文字是本地化显示名(内置账号路由在中文下是「DeepSeek 账号」),以文字为 key 时用户切换界面语言会丢失全部折叠记忆。
  • 存储键 dsh-model-in-use:collapsed-groups,值形如 { [providerId]: true }。localStorage 被禁用或写满时全部读写吞掉异常:折叠当次仍生效,只是不持久化,菜单不会因此失效。

占用判定

一个模型被标记,当且仅当存在另一个会话同时满足:

  • 是主会话(origin !== 'subagent' 且没有 parentId);
  • 此刻正在运行(running);
  • 其当前模型即该模型 —— projectionValues.modelSelection.next ?? catalog.default。

其中 next 为 pending ?? lastUsed,即「它现在真正在用哪个模型」。当前会话自身那一行永不标记。

推理档位记忆

为模型选过的档位记在 localStorage,键 dsh-model-in-use:model-efforts,条目键名即模型 id,只按模型、不带 provider。

为什么按模型而非按服务商:同一个模型常常能在多个 provider id 下取到(例如 workbuddyai 国际站与 workbuddyai-cn 国内站都提供 deepseek-v4.1-flash)。真实会话日志中同一模型正是在这两个 id 之间来回出现;若键里带 provider,换一个站点就读不到自己刚写的记忆,切走再切回、新开对话都会退回 Default。

写侧。模型切换通常汇入 directory.select(selection),插件包装了它。但 Codex 在点击「已经是当前模型」的行时只关菜单、不调用 select(),因此插件还会遍历宿主正式的 modelDirectories.live.directories.values,订阅每个已在使用的目录 store,把已就绪的官方默认档位校正为该模型记住的档位。会话目录按需创建,而 sessions.list 的通知可能早于该会话 scope 可用,插件通过 Symbol.for('cordis.original') 取得 Cordis Service 的真实对象,在真实 directoryFor 上挂创建拦截。不能给 Service face 直接赋值——那只会写入一次性的 traceable shadow,后续选择路径看不到。

包装同时覆盖原型与实例 own 属性上的 select。freecodego 的 installModelSelectionEcho 会把 select 装成 directory 的 own 属性并遮蔽原型补丁,且 bind 的是补丁前的方法;只包原型会导致首次 RPC 不带档位、插件被迫补发第二条修正请求(真实日志中表现为 2–5ms 成对的 undefined → level)。插件用 hookedInstances 追踪已包装实例,在多个时机重复检查,dispose 时还原 own 属性。

判别规则由上游调用点决定,不能靠猜:

  • chooseModel(模型行)只在模型真的换了才调用 select();其 selection 通常只带 { provider, model },不带档位。
  • chooseEffort(档位子菜单)总是带当前模型:真实选档位时带 reasoningEffort,「跟随服务商默认」那项不带。若点击的档位已经生效,Codex 会只关菜单、不调用 select(),插件通过菜单与会话目录的映射补记这次显式点击。

由此得到三条规则:

  1. 同模型且 reasoningEffort 是字符串并改变当前有效档位 ⇒ 用户明确选了档位,包括它刚好等于官方默认档位,记下它。
  2. 同模型且完全不带档位 ⇒ 选了「服务商默认」,只对本次对话生效,不删记忆。
  3. 换模型 ⇒ 查目标模型已有记忆,仅在它自己的 reasoning.efforts 仍支持该档位时覆盖。

[!NOTE] 不能用 selection.reasoningEffort === defaultEffort 否定同模型的真实档位点击。那样会让用户选 Low 后没有绑定,随后 A→B→A 回到 Default。

其余细节:

  • ModelDirectory.select() 先把目标写入 store.pending,再等待 Host projection;判别当前模型时取 pending ?? current,避免快速 A→B→A 拿到尚未结算的旧值而误判为同模型。
  • 新对话若已默认落在同一模型、因 Codex 的 close-only 分支根本没有发出选择,由该 live directory 的 store 校正。校正按 (目录, 目标档位) 只尝试一次:宿主拒绝该档位时会更新 store 并再次通知订阅,缺少这道闸门会对同一次被拒的选择无限重试。
  • 每个主会话首次出现时,其 durable modelSelection.next ?? lastUsed 若带受支持的明确档位,会为尚未记忆的模型补种一次;因此插件启动前已有的绑定同样有效。每个会话只参与补种一次,避免用户清除绑定后又被后续 projection 更新写回。
  • 读路径每次都重新读 localStorage(内存副本仅作读不到时的回退),多个窗口之间互相可见。存储完全不可用时只丢持久化,不影响选择。
  • 读侧替换:组合框标签只读 store.current,而 select() 的同步阶段只写 status / pending、不碰 current,因此首帧会先显示默认档位。插件在 directoryFor 渲染路径上同步改写 store.current 的档位使首帧即正确,用 substitutingDirectories WeakSet 防重入回声、projectedSelections WeakMap 存宿主原值供写穿读取。

悬浮详情与菜单挂载

  • 悬浮详情使用 workspace 正式提供的 sidebar.session.row.hover Slot,由 Slot 传入稳定的 sessionId,不按对话标题猜是哪张卡。renderer 会先包一层 div[data-slot="sidebar.session.row.hover"],因此隐藏 anchor 必须先 closest() 回到这层 Slot wrapper,再找 wrapper 后面的「进行中」行;随后用 React portal 把文字作为该行最后一项渲染进去——既满足同行展示,也不直接改写 React 管理的 DOM。模型名原样使用 catalog 的写法(其自带的 · 保留自己的空格),档位与模型名之间只加一个普通空格,不再追加 ·。
  • CodexModelSelect 把菜单内联渲染在 composer 子树深处(源码中没有 createPortal),打开菜单只会往 <body> 的深层子孙插入节点,<body> 自身的 childList 不变。因此 <body> 的观察者必须带 subtree: true,否则菜单打开对插件完全不可见;但裸的 subtree 观察会在每个流式 token 上触发,所以 mutation records 先经 paneTouched() 过滤,只有「增删了 codexModelSelectGroups」才唤醒一次 scan()。

verify.mjs 场景 4 与 negative-proof.mjs 变体 6 钉住这一条。


License

MIT

MIT © 2026 Faide-cyber

Comments

Loading…

Similar plugins

DSH-model-controls

by Hu9956

切换供应商、模型与思考强度,每个模型各自记住档位。

UI & ExperienceDevelopment & InfrastructureManifest valid

★ 1

MIT

JavaScript

Oct 14, 2026

dsh plugin --profile web add @kewen/dsh-model-controls

by Mutx163

Reasoning-effort tier manager for custom API models plus cross-session preference memory: inline low/medium/high/max toggles inside Settings → Models, with atomic settings writes.

Development & InfrastructureModels & ProvidersManifest valid

★ 2

↓ 160/wk

MIT

TypeScript

Sep 2, 2026

dsh plugin --profile web add dsh-model-memory

Stops compaction summaries and session titles from burning their output budget on reasoning, by lowering the thinking level and raising the token floor for auxiliary calls only.

Models & ProvidersManifest valid

★ 0

dsh plugin --profile web add dsh-aux-degrade

Adds routing groups to the session model picker and fails over to the next usable model once the current one exhausts every retry policy.

Models & ProvidersManifest valid

★ 0

dsh plugin --profile web add model-failover-manager

Governs multi-model access, monthly credits, manual/quality/credit/auto routing, failure rerouting, and usage auditing for DSH.

Models & ProvidersManifest valid

★ 0

dsh plugin --profile web add dsh-llm-governor

by Marquez807

Cross-session experience memory for DSH: a lesson reaches the model only with a verifiable source, the relevant ones are injected each turn, and what nobody uses is retired.

Tools & CapabilitiesMemory & ContextManifest valid

★ 1

↓ 60/wk

MIT

TypeScript

Sep 22, 2026

dsh plugin --profile web add dsh-experience-memory