wanted09527/dsh-progress-caption

wanted09527/dsh-progress-caption

在输入框上方加一行小字,告诉你 AI 现在第几步、在做什么、成没成,比如「第 3 步 · 正在执行命令 npm test · 琢磨中」。全部靠本机规则推断,不调模型、不联网、不改 DSH 的任何数据。

UI 強化deepseek-harnessdshplugin

dsh-progress-caption 思维链进度字幕

在 DSH 的输入框上方加一行小字,回答三个问题:现在第几步、在干什么、成没成。 给新手用:不用点开思考过程,也能看懂 AI 卡在哪一步。

第 3 步 · 正在执行命令 npm test · 琢磨中
第 2 步 · 正在读取配置文件 · 酝酿中

外观是一条圆角胶囊:DeepSeek 蓝的淡底 + 蓝点 + 蓝色步骤号,成功转绿、失败转红。 主文案始终是一整句(动作 + 具体命令/文件名),不会再被挤成两个字。 免责声明不再占着这一行——只留在鼠标悬停的提示(title)里。

怎么装

推荐:在 DSH 的插件市场里装。 打开插件市场(发现页)搜 dsh-progress-caption,点安装。 市场会从 npm 拉预构建的包,不用编译、不用装依赖,装完就能用。

命令行装也可以,让 agent 调 plugin_manager → install_bundle,target 填插件目录。

包也发在 npm 上:

npm i dsh-progress-caption

不过要注意:只用 `npm i` 不会自动注册成 DSH 插件,它只是把代码下载下来。想让 DSH 加载它,还是得走插件市场或 install_bundle。

手动装(从源码):

git clone https://github.com/wanted09527/dsh-progress-caption.git

然后在插件管理器里用克隆下来的目录安装。装完不需要重启:新 bundle 会通过热加载生效;替换已装包的代码才需要重启。

不做什么

  • 不调模型、不发任何外部网络请求、不做翻译。
  • 不改 DSH 的任何数据:只读界面已经推过来的快照;设置只存在浏览器本地 localStorage。
  • 不做历史归档、没有独立的设置页(设置就在字幕条自己的小面板里)、没有侧栏面板(留给第二版)。

说人话:三层「它到底在干嘛」

全部靠本机规则和数据推断,不联网、不调模型。按准确度排序,上一层说得出来就不用下一层:

层 · 数据来源 · 例子 ① 工具事实 · 正在跑/刚跑完的工具 + argsRaw · 正在执行命令 npm test、正在读取 client.js、跑完命令 npm test ② 规则表 · 思考文本末尾 400 字(17 条规则) · 正在理解你的需求、正在定位问题、正在对比方案、正在评估风险 ③ 阶段话术 · 本回合已完成工具的类型序列 · 看完了 3 个文件、刚改完 client.js、命令跑完了 ④ 空档事实 · 刚跑完的那条工具 + 它花了多久 · npm test 跑完了 · 1.2s;这一轮还没跑过工具就交给俏皮词 ⑤ 俏皮词 · 112 个词轮换 · 前面都说不出话时,酝酿中 / 琢磨中 / 翻箱倒柜 / 摸鱼中 顶上

工具动词分组(不再是「正在执行 读取文件」这种机翻腔):

工具 · 运行中 · 完成 bash / pwsh · 正在执行命令 · 跑完命令 read / read_image · 正在读取 · 读完了 glob / grep / search_context · 正在搜索 · 搜完了 write · 正在写入 · 写好了 edit · 正在编辑 · 改好了 web_search / web_fetch · 正在搜网络资料 · 查完资料 subagent / subagent_fork / send_message · 派了小助手 · 小助手回来了 compress / mnemon_* · 正在翻记忆 · 翻到相关记录 present · 正在整理交付 · 整理好了 todo_write · 正在列计划 · 计划列好了 job_output / job_kill / list_agents · 正在等后台任务 · 后台任务有结果了 ask_user_question · 正在等你回话 · 你回话了 skill · 正在调技能 · 技能调完了

对象取自 argsRaw,按下面的先后顺序取第一个有的:command / cmd、path / file_path / files / notebook_path(这四个都只留文件名)、description、pattern、query;以上都取不到时,用该组类 自带的兜底名词把动词补成一句完整的话(如「正在读取文件」「正在搜索代码」),不留「正在读取 」这种 半句,也绝不猜;太长会截断成 …,不会把字幕条撑爆。

分组是按真实会话里 6783 次工具调用的 name 分布补的:ops_bash 1580 次、pwsh 1465、
read 845、edit 696 之外,read_image 330、mnemon_* 392、compress 92、present 50
这些以前全掉进 other,只能显示「正在执行 read_image」这种把工具名糊上去的句子。

节奏:为什么字幕不逐字跟着思维链

v4.1-flash 这类模型思考极快,字是一串串来的。如果字幕逐字跟,会出现一秒跳三次的闪烁。这里的策略是:

机制 · 值 · 作用 常驻不闪没 · — · 思考中/跑工具/写回答,字幕一直在;整轮结束才收成齿轮。两步之间 partial 会短暂为空,这时说「刚才那件事 + 花了多久」(如 npm test 跑完了 · 1.2s),最长容忍 20 秒空档(TURN_GAP_MS),不会因此收起来;这一轮还没跑过工具就只显示俏皮词,不写「正在准备下一步」这类空话。步号整轮常驻:partial 空档时沿用上一个非空步号,跨轮才清空 事实优先 · — · 有工具在跑就用工具信息(准确),思考文本只作兜底 标签稳定窗口 · 520ms,快思考时 800ms · 候选文案要稳住这么久才真正换上去。前 3 步(SETTLE_STEPS)连淡入都不播,纯硬切换——那几步状态翻得最勤,淡入叠起来就是「闪」 最短停留 · 2600ms · 状态结束(开始写回答/工具结束)后,最后一条至少再挂 2.6 秒,想得再快也来得及读完 快思考判定 · 每秒新增 40 字 · 超过就把切换压到 800ms 一次,字再快也不会闪 计时刷新 · 无 · 条上那个每秒跳的耗时已整条移除,组件不再为它每秒重渲染(只剩俏皮词每 2.8 秒换一次)

同一个动作,换着说法说

字幕最容易犯的毛病是单调:每次读文件都说「正在读取配置文件」,看十次就麻木了。这里做了两层:

  1. 规则库带变体:17 条规则每条配 3 种说法,按「第几回合 + 第几步」确定性轮换

(翻一下配置文件 / 看看配置里怎么写的 / 对一下配置项)。不随机跳,同一个动作在同一回合里不会来回变。

  1. 副行挑一句有信息量的思考:把「思考原话」开关打开,主文案下面多一行模型自己的思考。

不是照抄第一句,而是从这段思考里挑一句——优先带判断/权衡/卡点的(「因为…」「问题是…」 「卡在…」「风险是…」),其次带具体东西的(路径、文件名、数字、命令)。 第一句几乎总是「Now let me…/让我先看看」这种过场话,所以会被跳过,句首的口语垫词也会被剥掉。 全程只截断、不改写、不润色,所以不会猜错;一段里实在挑不出一句像样的话,这行就不显示。

第 6 步  看看配置里怎么写的
         真正的问题是这条字幕没有固定宽度,每次换词都会把左边的字挤走        ◠  [盘算中]

关着就只有主文案,界面更紧凑。 (早期版本这里叫「行数 1 / 2 / 3」,但主文案是拼好的整句,几乎不折行,开到 2、3 看不出区别, 那三项实际只起到「要不要显示副行」这一个开关的作用,所以后来换成了这条说人话的开关。)

思考期间俏皮词一直轮换(每 2.8 秒换一个):有事实文案时它挂在最右端当陪衬, 前两层都说不出话时它才顶上当主文案——就是 Claude Code 等待时闪 Brewing / Pondering / Noodling 的那种效果。 模型开始写回答时显示「正在组织回答」,一样常驻不消失。

觉得还是太快/太慢就改 client.js 顶部的 MIN_SHOW_MS(默认 2600)和 OK_LINGER_MS(成功后停留,默认 5000)。

回合小结

每个回合结束时,在对话流末尾那条汇总行前面加一行:

本回合 · 3 次工具 · 2 成 1 败 · 用时 12.4s

只数事实(工具结果条数、成败、耗时,用 seq 卡住回合边界),没有工具调用就完全不显示。 这样滚动回看历史时,能一眼看出那一回合干了多少活。

小动画与俏皮词

两种动画(设置面板里选):

选项 · 表现 安静(默认) · 左侧三颗小圆点接力跳,周期 1.1s——一跳一落地,主文案完全静止 扫光 · 同一组点跳得快一档(0.9s)+ 一道光从左往右横扫整条字幕(不局限于第一行,整框一起扫),光带宽 = 整框宽,只动 transform,不推挤布局

老存档里如果还留着 anim: 'off',读设置时会被折回「安静」——.pgc-anim-off 那套全静止样式仍留在 CSS 里当兜底,只是面板上不再提供这个选项。

为什么默认是「安静」:思考时文字本来就在飞快地长,字幕条再跟着流光、再跟着跳,两个动的东西 叠在一起就是「一直在闪」。默认档只让左侧三颗点慢慢跳,主文案是静止的。 成功 / 失败时点继续跳、只换成绿 / 红(不冻住,理由见下文)。

三颗点仿的是 Claude 主题插件 dsh-claude-style 的 await loader(见 node_modules/dsh-claude-style/lib/client.js 的 .dsh-claude-chat-wait-dot 与 @keyframes dsh-claude-wait-dot):

  • 容器 .pgc-loader 写死 18×14,三颗 4px 的点绝对定位在 left: 0 / 7px / 14px;
  • 同一条 @keyframes pgc-dot-bounce(0% 贴地压扁 scale(1.7,.25),40% 起跳收回比例,

100% 到最高点 -8px)+ infinite alternate——一条动画既去又回,不用写两组关键帧;

  • 三颗的负延迟分别是 -1 周期 / -3/5 周期 / -2/5 周期:任何一帧都有一颗在最高点、

一颗在半空、一颗刚落地,读起来是「接力跳」而不是三颗一起蹦;

  • 只动 transform,容器总宽固定,所以跳起来也不会推动右侧文案。

状态标记:经典三点 / Claude 三态

左边那组点有两种,设置里用开关切换:关着是经典三点(上面讲的那种,一直原地接力跳), 打开是Claude 三态,它会每 6 秒(SHAPE_MS)换一种形态,三种轮着来:

形态 · 长什么样 · 来源 bounce 弹跳 · 三颗原地接力跳,周期 .5s,起跳前先压扁 · Claude 的 @keyframes dsh-claude-wait-dot line 斜线 · 三颗沿一条斜线往返,中途缩到 55%,周期 1.25s · @keyframes dsh-claude-wait-dot-line orbit 绕圈 · 四颗沿一个椭圆轨道转,相位各差 1/4,周期 2.4s 匀速 · @keyframes dsh-claude-wait-dot-orbit-1..4

实现上只有一套 DOM:` 里常驻四颗 .pgc-dot,形态名放在 data-pgc-shape 上,由 CSS 属性选择器决定挂哪条动画。 四颗是因为绕圈要四颗,弹跳 / 斜线只用前三颗,多的那颗由 .pgc-loader:not([data-pgc-shape="orbit"]) .pgc-dot:nth-child(4) { display: none }` 藏掉 ——切换形态时结构不变、不重排。

参数全部抄自 dsh-claude-style(--dsh-claude-distance-travel: 8px、 period-dot-bounce: .5s、period-dot-line: 1.25s、period-dot-orbit: 2.4s、 scale-dot-mid: .55),只是把变量名换成 --pgc-*。绕圈的四条轨道做了简化(每条 8 个采样点 而不是原版的 17 个),椭圆尺寸与相位保持一致。想关掉它就把那个开关关回去(经典三点)。

耗时的位置换成了一个绕圈的点组:原来字幕条右侧那个每秒跳一次的耗时数字已经撤掉(思维链区域 本来就有时间,重复了),位置留给 .pgc-slot——宽高写死 3.6em × 1em,和原来的耗时格一样宽, 所以俏皮词的位置一点没动。里面是 .pgc-arc:三个 2px 的点,各自跑同一条 0 → 720deg 的曲线 (.9s 那种匀速旋转已经被否掉了——一段硬边的弧按固定角速度转,看着就是个节拍器), 三个点只差 0.18s 的出发时间,靠 nth-child 的 animation-delay 错开:

  • 曲线用 cubic-bezier(.45,.05,.55,.95),两头慢、中间快。慢的地方三个点挤成一串、快的地方

一下甩开,这就是 Windows 开机转圈的手感——不是匀速转盘;

  • 首尾各留 10% 的淡入淡出(opacity 0 → 1 → 0)。缓动在两端速度本来就是 0,接缝处不会有

「咯噔」一下,循环是无缝的;

  • 只动 transform / opacity,走合成层;pointer-events: none,不挡右边的状态胶囊;
  • 安静档放慢到 3.2s,成功 / 失败时停住并换成状态色(三个点常亮成一圈);
  • 系统开了「减少动态效果」时 animation: none,此时必须补一句 .pgc-arc > span { opacity: .6 },

否则点的可见度本来只由关键帧提供,会整组消失。

为什么不是三个原地跳动的点:条上左侧已经有一组状态点(.pgc-loader),再在右边放一组原地跳的, 同一行两组点跳相同的舞,看着吵;绕圈的形态和「原地跳」分得开,左边说「这一步是什么状态」, 右边说「还在转」。它也是「思考全程不闪」的一部分:宽高、周期、幅度都是定死常量,不随内容变化。

宽度与不闪的三条硬规矩(client.js 的 CSS 段):

  • 宽度跟输入框卡片对齐,但不是写 width:100%。官方输入卡是两层:父容器

.uV2eYG_root 先左右各留 --dsh-composer-side-clearance,再由卡片用 max-width: var(--dsh-composer-card-max-width) 卡上限;而字幕条所在的 .wSkVaW_composerStack 没有内边距,所以它必须自己减掉两侧留白: width: calc(100% - 2 * var(--dsh-composer-side-clearance, 16px)) + max-width: var(--dsh-composer-card-max-width, 952px) + margin: 0 auto。 全屏时与输入框等宽居中,不会把整个容器撑满。

  • 动画只碰 opacity / transform / background-position / box-shadow,绝不改宽高,不会引起重排。
  • 俏皮词那一格宽度写死(width: 6.6em)。词最短 3 字最长 5 字,如果不写死,每 2.8 秒换词时

右边一伸缩,左边的状态文字就被推来推去——这也是「闪」的一个来源。

  • 系统开「减少动态效果」时,@media (prefers-reduced-motion: reduce) 里把动画和过渡全部关掉。
  • .pgc-band 必须裁剪(overflow: hidden),否则收起途中内容会从 0 高度的框里溢出去,

压到上面那条排队消息上(就是「指令堆叠时显示异常」)。但裁剪框贴着内容高度时,换文案那 3px 的 translateY 又会把俏皮词胶囊的底边切掉一条(只有一行时看得见)。所以给自己加 padding-block: 6px; margin-block: -6px——把裁剪框上下各撑开 6px,再用负外边距抵回去, 外层占位高度一分不差,位移就切不到了。收起时这两项一起归零,收得干净。

  • 扫光要挂在整条胶囊上,不能挂在 `.pgc-band` 上。.pgc-band 只是「文字那一格」,

右侧放齿轮的 .pgc-settings 是它的兄弟节点、不在它里面;光挂在 band 上,扫到 band 右边界 就断了,右边齿轮那一格永远是暗的,看着就是「光效没占满」。所以根节点里加了一个纯装饰的 `,position: absolute; inset: 0; border-radius: inherit; overflow: hidden; pointer-events: none——光的边界就是整条胶囊的圆角轮廓(齿轮那一格也扫得到), 又不挡点击。.pgc-strip 因此必须有 position: relative 当它的定位参照。 光带本身 width: 100% + translateX(-100%) → translateX(100%)`,是「从左边外面整道扫到右边外面」。

出现与收起怎么过渡:

  • 整条永远是同一个 DOM,不会在「完整条 / 小齿轮」两套结构间切换(切换会让 React 卸载重建,

看起来就是闪一下)。收放交给 .pgc-band 的 max-height 7em → 0 + opacity 1 → 0 (.18s cubic-bezier(.23,1,.32,1) / .16s ease)。

  • 收起时内容不会被立刻抽走,而是记住最后画出来的那一帧(paintedRef)让它跟着淡出,

所以「字消失」和「条收缩」是同一个动作,不是先后两段。

  • 入场动画只挂在内容层 `.pgc-live` 上(pgc-enter,.22s,opacity 0→1 + translateY(2px) + scale(.984)),