← Back to home@Yu-jianhai

dsh-session-progress

大鲸鱼在线写插件 · 让鲸鱼去监督鲸鱼:在 DSH 侧栏每条会话行上铺一条半透明进度带,每 30 秒用一个轻量模型读上下文估进度;可选监督模型、可调思考强度、可改颜色与样式,左侧栏还能看到判断模型的每一次原始输出。

Stars
2
Language
JavaScript
Created
Oct 24, 2026
Updated
Oct 26, 2026
GitHub repo

Introduction

大鲸鱼在线写插件

dsh-session-progress

你是不是还在为了不知道大肥鱼在吃多少白饭,到底干活干到哪了而烦恼?别担心!让我来助你!全新进度条让你实时了解大鲸鱼在干嘛!将进度条接入大模型注入上下文,让鲸鱼去监督鲸鱼!可选监督大模型、可调思考强度、可变进度条颜色、可变进度条样式,懂鱼更懂你!

侧栏实机:一条淡蓝色带从左往右爬过会话行

一条会自己爬的色带,长在 DSH 侧栏的每一行会话上。它不催你,也不打断任何东西——只是有个便宜的小模型每 30 秒瞄一眼鲸鱼在干嘛,然后把答案画成一行像素。

先说清楚它是什么

AI 干活最难受的地方不是慢,是不知道它在哪。

一轮跑了二十分钟,你只能盯着最后一个工具调用猜:是卡住了,是在跑测试,还是已经快好了?点进去翻日志,翻完发现它只是在等一个超时。

这个插件把那个答案变成侧栏上一条会动的东西。你刷手机回来,扫一眼色带的位置,就知道该继续等还是该进去看看。

它和普通的进度条不一样在三件事:

  • 色带铺在整行底下,不是另一根独立的条。标题、时间、悬停按钮照常可读可点——一个 DOM 节点都没往行里插,用的是 ::after 和两个 CSS 变量。
  • 判断的也是模型。真正干活的那个没空汇报,就派一个最轻的读一遍当前轮次的上下文,让它说「大概到 62% 了」。提示里带着它自己上一次给的数字和阶段名,所以卡住了它会往下调,不会为了好看一路抬到 95。
  • 它自己在花钱。每 30 秒一次小调用,约 1–2k 输入 token,默认最多同时盯 3 个会话。这笔钱单独记账,面板顶栏能看到判断模型累计吃掉了多少 token——你主对话的账单不受影响。

装

dsh plugin --profile desktop add github:Yu-jianhai/dsh-session-progress

然后完全退出 DSH 再打开。hmr 在我们这台机器上是关的,只刷新窗口不会加载新代码。

想锁版本就跟 commit:

dsh plugin --profile desktop add github:Yu-jianhai/dsh-session-progress#<commit-sha>

纯 JavaScript、lib/ 已提交,没有构建步骤,不需要给 pnpm 授权跑 prepare。卸载:

dsh plugin --profile desktop remove dsh-session-progress

用起来是什么样

位置你会看到什么
侧栏会话行整行宽的色带,宽度就是进度。默认 #90AFFF、60% 不透明、900ms 缓动——每采一次样,它往前跳一步
设置 → 插件 → 会话进度条模型、思考强度、采样间隔、同屏数量、颜色、不透明度、样式、动画、彩虹,改哪个下面那行预览就跟着变;「保存」后表单显示的是宿主实际存下的值,「恢复默认」把全部打回出厂
左侧栏「进度日志」判断模型的每一次采样,一个会话一列、从左到右并排;最左边是历史轮次列
设置页里那张同名卡片只是个入口——点按钮直接打开上面那块面板,不在这儿再复制一份数据

进度日志面板:两个会话并排,列头带进度曲线,一条采样一行

它是怎么想的

  1. 你发一条消息 → 这个会话开始采样,色带从 seedP(4%)起步,先给你一点"动了"的反馈;
  2. 之后每 30 秒一次。提示里带着上一次的数字 + 当时它写的阶段名(「当前进度:45%(当时阶段是「改接口」)」),请它对照这半小时的记录自己判断:又推进了多少、有没有停滞返工、上次是不是估高了。 往前往后都照单全收,宿主只封顶 95——方向是它的,不是宿主的。
  3. 你在一轮没跑完时又插嘴 → 剩余工作量变了,interjectDelayMs(2.5 秒)后补采一次,并明确告诉它"目标变了,按新要求重估",日志里那条会挂一个橙色「重估」。
  4. turn/end(不管什么原因结束)→ 立刻补满 100,停 700ms,再花 420ms 淡出。
  5. 采样失败 → 保留上一次的值。色带只在会话真的在跑的时候存在。

被中断的轮次会把当时到几成记下来。下次同一个会话再开跑,色带从那个数字接着爬,而不是退回 0;判断模型也会被告知"上一轮被打断,已到 X%",免得它把接续的活当成新活估成个位数。

面板

一个会话一列,列头两行:色点 + 会话名,下面是「当前百分比 · 采样次数」、最近一次的时间,右边一条最近 24 次估计的曲线(最旧在左)。一条采样一行:时间 / 百分比 / 涨跌(+7 绿、-5 橙)/ 阶段名 / 耗时;最右边的小箭头表示能点开,点开才看模型、档位、token 拆分和 JSON 原文。

最左边是历史轮次列:一个结束(或被打断)的回合一小块,写着 #轮次、完成/中断徽章和最后一次的百分比。它跟着首屏一起加载——但走的是裁剪过的行,只带列要画的字段,不带每轮 2 KB 的模型原文。同一份数据实测 223 KB → 74.5 KB。

列本身贴着内容长,不会在屏幕下半截拖出三个空盒子;条目多了就在列内滚动,「还有 N 条」往下放。

配置

配置文件:~/.dsh/session-progress.config.json(UI 保存的值优先于 cordis.patch.yml)。

键默认范围说明
enabledtrue—总开关
providerdeepseek-account—模型提供方
modeldeepseek-flash—判断用的模型
reasoningEffortoff—思考强度;模型没有这档时按档位距离取最近的一档并打日志
intervalMs300005000–3600000采样间隔
interjectDelayMs2500500–30000插嘴后多久补采一次
firstDelayMs80000–3600000本轮首次采样前的等待,让模型先做点事
maxSessions31–10同屏显示的进度条数量,也是同时采样的上限
maxContextChars6000—送进模型的上下文上限(超出截尾)
timeoutMs30000—单次采样超时
seedP4—首次采样回来之前的起步宽度
color#90AFFF#RRGGBB色带颜色;非十六进制会被忽略并保留旧值
opacity0.60.05–1色带不透明度
styleband六种之一色带形状,见下节
flowtrue布尔色带里那道流动的光开不开
flowSpeedslowslow / normal / fast光走一圈:慢 10.5s、标准 5.9s、快 3s
flowQualitymediumrefract / high / medium / low / off表面那层的质感,见「流动的质感」
rainbowfalse布尔实验:色带换成会走的彩虹

思考强度以模型目录为准。 设置页的下拉、采样时传给 adapter 的值、启动时打印的 offered: …,都来自同一份 listModels();resolveModelInfo() 跟它对不上时以目录为准(deepseek-flash 的目录里就有 off)。

外观

六个选项:颜色、不透明度、样式、流动动画、动画速率、动画质量。下面那行预览是用当前数值真画出来的,拖颜色的时候侧栏的色带会跟着变。

预览是实时的,也是临时的:不保存就离开,会还原成已保存的值。宿主每 2 秒会广播一次自己保存的样式,插件把"未保存的预览"和"已保存的值"分开记,所以广播顶不回去。

六种形状(style):

值样子什么时候用
band渐变横幅,右端 10px 渐隐默认。最像"进度条",高对比但不抢标题
solid纯色实条,右端 1px 亮边最克制;侧栏窄、颜色又淡时比 band 更清楚
underline3px 细线贴行底不想压住标题和时间的读法
stripe45° 斜纹叠在底色上进度变化慢、但希望"在跑"这件事一直显眼
glow左淡右亮 + 内侧亮边 + 柔光视觉最重,适合只在少数会话上开
segments按 20% 切格想一眼看出"到第几格"

颜色要写成 #RRGGBB。取色器(包括那支吸管)偶尔会回 rgb(18, 171, 52) 这种写法,插件会先自己归一化;实在解析不出来就保留旧值,不会把配置写坏。

想先肉眼挑,打开 dev/styles.html(node dev/gen-styles.mjs 从真实源码生成:六种样式 × 三种进度 × 三种不透明度 × 三种颜色)。

流动的质感

色带里有一道光在走。判断模型半分钟才给一次新数字,中间这段时间靠它让人知道"还在动"。质感分五档:

值效果代价
refract 实验 · 流光两道低对比的光带反向平移(一亮一暗,各自一个周期刚好接上),填充层带玻璃浮雕边——上沿提亮、下沿压暗,看起来像表面自己在流多一层 blur(1.5px) 合成;卡就往 medium 退。它永远在文字下面,不糊字
high 高级水波 + 光学:feTurbulence + feDisplacementMap 让光的边缘随水波摆,再叠 backdrop-filter 把底下颜色晕开、提亮GPU 吃最多
medium 中等(默认)只有水波扭曲,边缘像水面裂纹那样左右摆中等
low 低扁平高光,形状不动(0.10 及以前的样子)几乎为零
off 关没有光零

那几张 SVG 滤镜由插件挂在 <body> 里,启动时建、卸载时删,规则里只是 filter: url(#…) 引用。不联网、不依赖外部资源。万一壳子造不出滤镜,会静默退回纯色高光——引用一个不存在的 filter 会让整个元素都不画,这种失败必须挡住。

想看五档差别,打开 dev/water.html(node dev/gen-water.mjs):上面等比例、下面放大 2.6 倍,底部写着每档 filter / backdrop-filter 的实际取值。

实验 · 彩虹走马灯

颜色那栏底下的开关。打开后色带不是涂单色,而是一段会走的彩虹:

  • 七段色等距排开、交给浏览器插值(红橙黄绿青蓝紫再回红),所以是化开的,不是六条硬边。循环 168px,用一个周期 170.592px(168 / sin(100°))走出去,首尾同相,多宽都看不到接缝;
  • 速度跟着「动画速率」:慢 7s、标准 4.4s、快 2.6s;
  • 表面那层同时被借来当光晕:同一份光谱 blur(6px) 之后用 mix-blend-mode: screen 叠上去,是加光不是加颜料——用普通透明度整条会糊成粉彩;
  • 光晕被裁在色带里,不漏到旁边的行;彩虹层上压 12% 黑纱,否则白字走到黄绿段上会看不清;
  • 关掉「流动动画」它就变成静止的彩虹——那个开关管的是"动不动",不是"有没有颜色"。

换一台机器 / 别的 provider

provider / model 的两个默认值只是偏好,不是硬依赖。宿主第一次读到模型目录时会核对:

  1. 配置的模型在 → 直接用;
  2. 不在 → 按名字挑个便宜的顶上(mini/lite/small/nano/haiku 优先,其次 flash/fast/instant,最后 turbo,都没有取目录第一个),实际生效的路线写进 /config 的 effectiveRoute,设置页显示一行「原配置的模型在本机不存在,已自动改用 X」;
  3. 目录是空的(这台机器还没配任何 provider)→ 一次模型都不调,/config 回 catalogEmpty: true,界面直接告诉你「先去配一个 provider」。

所以 cordis.patch.yml 里不写死 provider/model:别人 clone 下来装上,第一次运行就知道该用什么。想锁定,在设置卡里选一次,值会落到本机配置文件。

原理(为什么不用 React 组件)

DSH 给每个会话行渲染了稳定属性 data-row-key="session:<id>",但没有一个常显的行内插槽:running 时 sidebar.session.row.leading 被状态点占着,行尾按钮只在悬停时出现。

所以本插件一个 DOM 节点都不往行里插,只做两件事:

  1. 注入一张固定样式表:给行加 position: relative; isolation: isolate,用 ::after 画色带(宽度取 --dsp-p、不透明度取 --dsp-o,z-index: -1 待在行背景之上、文字之下),用 ::before 画表面那层;
  2. 每次进度变化时重写另一张很小的样式表,按 id 写 [data-row-key="session:<id>"]{--dsp-p:42%;--dsp-o:.6}。

--dsp-p 用 @property 注册成 <percentage>,于是"改变量"就等于"CSS 原生过渡",不需要 React 重绘,也不受会话行重渲染影响。(inherits 必须是 true:变量写在行上、由伪元素读取,否则伪元素拿到初始值 0%,宽度永远是 0。)

运行态不轮询,只听宿主事件:

ctx.on('session/event',      (session, event) => event.type === 'turn/start' ? run() : event.type === 'turn/end' ? idle() : null)
ctx.on('api-session/status', (sessionId, running) => …)
ctx.on('agent/status',       ({ agent, status }) => …)

动画也不是散的:色带的动作全部由一张表决定(SURFACES:动作、图案、有没有滤镜、浮雕),周期 264px、位移 268.073px 只算一次,两段 @keyframes 由这个数生成后写进样式表。加一档只改表,改一下周期也只改一个数。

端点(只监听回环地址)

路径用途
/plugins/dsh-session-progress/eventsSSE,首帧全量,之后每次变化一帧(内容没变就不发)
/plugins/dsh-session-progress/snapshot一次性 JSON 快照
/plugins/dsh-session-progress/log采样历史(环形缓冲 300 条 + 关闭的 600 条),支持 since 增量
/plugins/dsh-session-progress/config读 / 写配置
/plugins/dsh-session-progress/debug运行态、采样器、实际档位、模型目录、客户端版本

这些端点拒绝非本机 Host 的请求,不会对外网暴露。

已知取舍

  • 依赖 data-row-key 这个内部属性。 宿主改渲染方式时色带会静默消失——不崩溃、不错位、不污染别人的界面。
  • 列头用会话名,不用 id。 取值顺序:session/title 的标题 → 该会话第一条用户消息(截 40 字)→ 短 id。新会话在自动命名之前,会先用你发的那句话当名字。
  • 自动换模型只看名字。 名字里没有便宜档线索时取目录第一个,可能正好是贵的那个;设置页改一次即可锁定。
  • 拖拽排序时会闪一下。 宿主用 ::before/::after 画插入指示线,跟我们的伪元素撞一瞬。
  • 颜色是字面色,不走主题 token。 #90AFFF 是需求指定的值,暗色主题下偏亮;只有下拉菜单这类原生控件交给 --dsw-alias-*。
  • 会真的花 token。 每会话每 30 秒一次真实调用(约 1–2k 输入 token),默认最多 3 个会话。把 intervalMs 调大或 enabled: false 随时停。
  • 采样不并发。 同一会话上一拍没回来,这一拍就跳过。

开发

node scripts/snapshot.cjs pre-<你要干什么>   # 开工先拍快照,崩了一键回滚
npm test                                     # 十套
npm run check
  • 改代码前先读 AGENTS.md(60 秒开工流程)和 ARCHITECTURE.md(符号索引),报错先查 PROBLEMS.md(每条都真炸过一次:现象 → 原因 → 修法 → 哪条测试守着)。
  • node scripts/snapshot.cjs <label> 把源码、测试、探针页、文档打包到 backups/(不进 git,带 git rev 与逐文件 sha256);node scripts/restore.cjs latest 原地恢复,并删掉快照之后新增的散文件。
  • test/host-smoke.mjs:宿主半侧冒烟,用临时 state 文件,不碰你真实的 ~/.dsh/*.json。
  • test/direction.mjs:nextProgress 的方向规则——上下都收、封顶 95、解析失败保留原值。
  • test/client-boot.mjs:桩 DOM 把浏览器半侧整个启动,喂真实 SSE 帧,断言样式表真的写出来、清理真的收干净。纯函数测试看不到这类错——render() 里一个未定义标识符会让色带整条消失,控制台只有一行 bad frame。
  • test/render.mjs:四个 slot(main / sidebar.panellist / plugins.bundle.config / settings.plugins.tab)用空 payload 与满 payload 各渲染一次。0.11.2 那次白屏就是 data.active 空指针——Number(null) === 0 骗过了守卫,只有真渲染才发现。
  • test/route-fallback.mjs:模拟别人的机器——目录里没有默认模型时自动改用便宜模型、目录为空时一次模型都不调。
  • test/perf-contract.mjs:空转成本三条线(增量 /log、SSE 去重、落盘瘦身)、客户端自报版本、历史行裁剪。
  • dev/release.cjs:发版用,「大鲸鱼在线写插件」那句由它加到正文开头,别再手拼 curl。
  • dev/harness.html、dev/flow.html、dev/water.html、dev/panel.html、dev/styles.html:把真实行结构、真实 CSS、真实采样数据拉进独立页面,没有宿主也能比对观感。
  • 浏览器侧没有 JSX、没有构建:lib/client.js 是 window.__ModuleLoader__.load({ id, factory }) 形式,React 从 require('react') 拿。

License

MIT © 2026 Ne_Rn