dsh-breather
Take a breath mid-task: the current step finishes, then it holds. Resume sends no new request and spends no tokens; the timer freezes. Current session only.
Install
dsh plugin --profile web add dsh-breather
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-breather
给 DSH 的对话加一个「暂停 / 继续」状态——介于「正在运行」和「终止」之间。
零成本暂停:当前步骤做完后挂起,继续时不重发请求、不重复花 token;只有点继续或发新消息才接着跑,子代理结算与定时提醒唤不醒。侧栏显示暂停图标,计时器与动画冻结、不计暂停时段,恢复后从冻结值接着走。按钮区分「暂停中」与「已暂停」,暂停只作用于当前会话。
七点说明
1. 零成本暂停:暂停本身不发出新请求。当前步骤做完后停住,只有手动点按钮或发送新提示词才会继续;后台行为(子代理结算、定时提醒等)唤不醒它。
2. 侧栏与计时:左侧会话列表加入暂停图标;暂停期间计时器文字与动画冻结,计时器不计暂停时段,恢复后从暂停前的时间接着算。
3. 两种状态:按钮区分「暂停中」(请求已发、当前步骤还在跑)与「已暂停」(闸门真的挂住)。
4. 作用域:单次暂停只作用于当前会话。
5. 回合总结统计的是总时间:那行「深度思考了 X」按 turn.end - turn.start 算,包含暂停时段。这是底层架构限制;为了兼容更多版本,插件不去改它——你可以自行修改,但改过的文件会随 DSH 版本更新失效。
6. 重启不保留暂停:暂停后重启 DSH,再点「继续」本质上是终止对话并花 token 发了一条「继续」提示词。目前不存在关闭 DSH 后仍保持暂停的路径。
7. 计时模块坏了也不影响暂停:如果 DSH 更新导致计时接管(核心 DOM 钩子)出错,只会退回核心原生显示,暂停 / 继续照常工作,不会因此多消耗 token。
它做什么
- 会话运行时,输入框右侧(模型选择器左边、停止按钮同一行)多一个 暂停 按钮。
- 点暂停:当前这一步先跑完(正在输出的回复、正在跑的工具都不打断),然后在
发起下一次模型请求之前把这一轮挂住。按钮变成 继续。
- 点继续:闸门放行,模型从停住的地方接着跑。不重发请求、不重复工作、不多花 token。
- 挂住之前按钮显示「暂停中…」,此时再点一次等于取消这次暂停请求。
- 挂住之后,对话下方那行「深度求索中,用时 X」会冻住并变成「已暂停 · X」,流光动画
一起停;暂停的那段时间不计入——继续之后从冻住的数字往下走,不会一下子跳掉。
- 左侧会话列表里,被暂停的那一行会把转圈圈换成蓝色的暂停符号;其他会话照常转。
- 那一行的悬停卡片里会多一行「已暂停 · 点输入框旁的「继续」从停住的地方接着跑」——
这一处走的是核心本来就有的 sidebar.session.row.hover 插槽,不碰 DOM。
- 暂停期间按停止(或 Esc Esc)照常终止:闸门会跟着回合取消一起松开,不会卡住。
- 没有超时:暂停之后你不点继续,它就一直是暂停状态,放一小时也一样。
- 暂停期间你自己又发了消息会自动恢复(当成「我要继续」);子代理结算、定时提醒
这类后台唤醒不会解除暂停。
它做不到什么
- 不能打断正在流式输出的那一条回复。 暂停是「下一次请求前」生效,所以长工具
调用 / 长回复期间点暂停,要等这一步结束才真正停住。想立刻停,用原来的停止按钮。
- 暂停只作用于这一轮对话(turn);不会暂停子代理、后台任务和定时提醒。
- 同屏有子代理内嵌会话时,只接管主对话那个计时器;如果核心的结构让插件认不出
哪一个是主对话的(比如两个都在聊天节点外),它一个都不改并登记失效——宁可让
数字继续走,也不把子代理的数字改成主对话的。
- 核心如果把「深度求索中,用时 1分23秒」拆成两个文本节点,插件只冻结里面的数字,
不动标签;那种情况下不会显示「已暂停」几个字(数字停住本身就是信号)。
自检:接管失效时会怎样
有两处效果是靠核心的 DOM 钩子实现的(运行计时器冻结、侧栏行首图标)。核心改版把
这些 data 属性改名时,客户端会在失效发生的几秒内发现,而不是默默退化成原样:
- 计时器:见过
[data-chat-running]之后又连续 6 次(约 4.2 秒)找不到 → 登记timer
失效;一次都没见过(刚刷新、长会话还没渲染出来)则给 20 次(约 14 秒)宽限,避免把
「还没渲染」误判成「钩子没了」。
- 侧栏行:有
[role="treeitem"]行,但整页一个data-row-key都没有时,才登记
rows 失效——要求整页都没有,是为了不把文件树等别的树形控件误判成会话行改名。
- 计时器作用域:页面上有多个
[data-chat-running],但其中恰好没有一个能确定是主对话
(不在聊天节点外、或不止一个在节点外)时,登记 timer 失效并一个都不改。这种失效
不停掉 700ms 的重挂载循环,局面一清楚就自动恢复。
登记之后:控制台 console.warn 一次(带一句「其余功能正常」),暂停按钮的 title
追加「(部分接管失效:核心页面结构变了,详见控制台)」,并且停掉对应的接管
(停止 700ms 的重挂载循环),不再空转。功能本身(宿主闸门、按钮三态、继续零成本)
不受影响。
失效标记会自愈:钩子后来又出现时,连续两次轮询/重挂载都看到它,就撤销标记并
console.info 一句「xx 接管已恢复」——不会一次抖动就翻案,也不会一直挂着红字到刷新。
界面文案跟随核心的 <html lang>(读不到时看 navigator.language):中文一套,**其他
语言一律英文兜底**(日语、法语用户看英文,而不是中文);暂停 / 继续 / 暂停中…、按钮
title、行首图标与悬停卡文字都有中英两套。计时文案里的时长单位不是按界面语言猜的,而是
从核心那句真实文案里认——「已暂停」这三个字在没有证据时(只抓到数字)才回退到界面语言。
可选:把行首图标改由核心插槽渲染
原版核心的侧栏行首是二选一:运行中画状态点,空闲才渲染
sidebar.session.row.leading 插槽——所以暂停图标只能靠 DOM + mask 补。想让图标也走
上游插槽,可以打一个本地补丁(改的是安装包 app.asar,不是插件):
# 只检查(默认,什么都不改;预览写系统临时目录) node tools/patch-core-row-slot.mjs --check # 想在仓库里留下打过补丁的副本来看 diff node tools/patch-core-row-slot.mjs --check --preview # 完全退出 DSH 后,用有写权限的终端打补丁 node tools/patch-core-row-slot.mjs --apply # 还原 node tools/patch-core-row-slot.mjs --restore
它做的事:把 showStatus ? 状态点 : 插槽 改成「插槽先渲染、状态点照旧」,新代码多出
的 77 字节从紧邻的文档注释里扣回来,因此文件字节数不变;再把该文件新的 SHA256
写回 asar 头的 integrity.hash / integrity.blocks[0]。asar 总大小与其余条目都不动,
备份留在 tools/.backup/。
补丁生效后,插件对“行”的 DOM 依赖会再降一层:不再给行打标记,藏核心状态点改成
CSS 按「这一行里有没有我们的图标」自己算([data-row-key]:has([data-dshp-row-icon]) 和
*:has(> [data-dshp-row-icon]) 两条规则,兼容插槽输出被包一层的情况)。所以补丁模式
下就算行键改名,最坏也只是“暂停符号和转圈并排”,不会整个图标消失。
风险和边界,先说清楚:
- 脚本带版本与哈希双锚点(核心
0.2.1-rc.3,文件 SHA256 固定)。DSH 升级后两者对不上,
它会直接拒绝,不会瞎改。
- 改的是安装包,升级/重装 DSH 会被覆盖,需要重新打;不打也完全能用(回到 mask 方案)。
- 打了补丁之后,插件渲染图标时会自动给
<html>加data-dshp-rowslot,CSS 里的 mask
规则随即让位,不会画两遍。
安装
dsh plugin --profile desktop add dsh-breather
装了之后完全退出 DSH 再打开(宿主半只在启动时加载);浏览器半随页面刷新生效。
包声明了 dsh.bundle,安装时会把 cordis.patch.yml 这一层追加进 profile,宿主半作为一个
普通插件行被注册;浏览器半由 Web 端按 dsh.client.platform 自动加载。engines.dsh 写的是
>=0.2.1-rc.2 <0.3.0-0——下界必须带 -0,否则 semver 不匹配 0.2.1-rc.x 这类预发布版本,
rc 版 DSH 会直接拒绝安装。卸载:
dsh plugin --profile desktop remove dsh-breather
旧安装怎么办(只和开发机有关)
老包名是 dsh-session-pause,老装法是「Junction + 手写 patch row」。不用迁移:Node 按
目录名解析,旧 row 照样能加载,包名对不上只影响 dsh plugin list 的观感。硬要换装的唯一
红线是:先删 profile 里手写的那一行,再 remove 旧依赖——反过来(先删依赖、留着 row)
DSH 下次启动会直接报找不到模块,那才是真的起不来。
但换装会顺手弄坏别的东西:pnpm add/install/remove 会把没写进 dependencies 的手装
插件目录挪进 node_modules\.ignored\(dsh-session-folder、dsh-session-delete 这类)。
所以验证"发布出去的包"别动主 profile,用一次性的独立 profile:
dsh --profile breathe-test --from-default-profile desktop # 建一个干净 profile dsh plugin --profile breathe-test add dsh-breather # 从 npm 装(未发布时可 add <插件目录>) dsh --profile breathe-test # 起来点一遍,验完关掉、删掉这个 profile
本地开发
源码目录沿用了历史名字 <workspace>/.dsh-plugins/dsh-session-pause,主 profile 里仍以
旧包名挂着(Junction),改源码立刻生效。这条路径只适合开发机;正常用户走上面的
dsh plugin add。
配置(cordis.patch.yml 的 config)
字段 · 默认 · 说明
enabled · true · 总开关
engagedTimeoutMs · 0 · 可选的挂住超时;0 = 永不自动放行(默认)。只有自己明确填毫秒数才会兜底放行
路由不可配置:固定是 /api/session-pause,浏览器半把这条路径写死了。以前留过一个
path 配置项,结果只有宿主听那个开关——一改客户端就永远读不到状态,是最难查的一类故障。
填了也不会生效(0.3.0 起移除)。
实现
宿主半(index.js):
ctx.on('agent/pre-step', …):dsh-agent-loop在每次发起模型请求前用
dispatch.waterfall 调用这个钩子,且会 await 监听器返回的 Promise。所以在这里
await 一个闸门就等于把这一轮挂住;松开后 return next() 继续。
- 中止信号来自钩子载荷里的
signal(本回合的 AbortSignal):停止 / 取消时它 abort,
闸门立刻松开并返回 { kind: 'reject' },回合按取消路径干净结束。
ctx.connection.fetch.register({ path, methods: ['GET','POST'] }):状态查询与
暂停 / 继续操作;精确路由只有 owner peer(本机浏览器)能访问。
- 回合结束(
agent/status非 running)清掉暂停状态,避免残留到下一轮。 - 用户消息(
source.kind === 'user')进入 inbox 时自动恢复;其他来源的唤醒不解除暂停。 - 默认没有超时定时器,闸门只由「继续 / 用户发言 / 回合被终止 / 插件卸载」松开。
- 计时基准:浏览器在暂停挂住时把「这一轮已经跑了多久」上报(
action: 'baseline'),
宿主存在会话状态里并随快照返回;继续时记下「继续时刻」,继续之后这一轮没结束前都以
clocks 的形式留在快
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::5,最后更新 2026-10-31