packages/dsh-task-board
zhu1090093659/dsh-web侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
更好的折叠栏:将工具调用与 Think 段自动折叠为步骤折叠栏,回合结束后整回合收拢为带实时指标(耗时/首字/token/tok/s/缓存命中)的回合折叠栏,只保留最终总结正文;前导图标可自定义,内置动态扑克牌(运行中花色循环卡牌动画、收起牌堆、展开扇形),提供官方折叠方式设置行,适配 DSH 新旧版本。零侵入 DSH 源码。
简体中文(默认) | English
DeepSeek Harness(DSH)原生折叠 + 扑克牌视觉 + 回合性能指标。
DSH 原生负责全部折叠语义(分组、成员归属、展开/收起状态、分页、搜索显隐、历史状态), 本插件在官方折叠引擎之上做纯 UI 增强:
turn-process 渲染器,提供比官方更丰富的回合栏——扑克牌前导图标、耗时、首字(TTFT)、token、tok/s、 缓存命中率、右对齐轮次「第x轮」、非正常结束状态词(已停止 / 运行失败 / 已中断)。 折叠状态完全来自官方(turnProcess.open / setOpen / foldable / hasContent), 点击回合栏只调用 turnProcess.setOpen(),成员的隐藏/显示由官方 seat 完成;
turn/start 投影)即出现——官方渲染器此时什么都不渲染,插件补上这根状态条:耗时从 0 秒起步实时走动, token / tok/s / 缓存命中率随真实数据到达而更新。它只是状态表面:不维护任何 折叠状态、不隐藏任何成员;回合结束后平滑交接为完整的收起态回合栏;
ChatGroupSeat /ProcessGroupHeader)原样保留,插件通过官方 DOM 钩子 (data-step-process-icon / data-process-activity)做一层纯 CSS 的扑克牌换装, 花色按官方活动类型语义映射(♥ 思考/提问 · ♠ 读取/搜索 · ♦ 编辑/写入 · ♣ 命令/代码 · 🐋鲸鱼 编排/计划/子代理)。软依赖:钩子失效时皮消失、官方图标与折叠原样保留;
TurnLocation.start/end、step 的usage 与 finalNode.timing、turn-tail 的官方聚合 tokenUsage)。运行中的 token 字段 额外提供 presentation-only 的视觉计数,用来填补官方 usage 离散上报之间的静止间隙: 该值不参与 tok/s、缓存命中、TTFT、durable projection,也不被持久化;真实 usage 更新时 立即校准(界面直接跳到新真实值再继续缓慢增长),Turn 完成后最终值始终等于官方真实 token(settle 即归零);
iconStyle(动态扑克牌 / 官方图标,一个设置同时管 Turn 与 Step),持久化到 localStorage['dsh-turn-fold:settings']。与官方 transcriptView 完全解耦—— 官方 Compact / Standard / Detailed / Verbose 四档照常工作,插件只增强其 UI。
**不修改任何 @deepseek-ai/dsh-* 源码。**
归属 · 内容 DSH owns · grouping · membership · disclosure(何时折叠/是否展开)· paging · search reveal · history state · Step 分组栏与其标题(正在读取…/已读取… 等官方语义) dsh-turn-fold owns · 增强回合栏 · 运行中回合状态条 · 扑克牌视觉 · 指标呈现 · 动画 · 插件设置
运行中(turn/start → turn/end):
[翻牌动画] 耗时0秒 · 首字— · — token · — tok/s · 缓存— ← 0 秒起字段槽位完整
────────────────────────────────────────
[官方过程内容逐条加载…(官方 liveProcess 语义,始终展开)]
[翻牌动画] 耗时13秒 · 首字0.8s · 3,214 token · 247tok/s · 缓存—
──────────────────────────────────────── ← 真实值到达只替换 —
回合结束(默认收起,点击展开):
[牌堆/扇形] 耗时22分34秒 · 首字4.9s · 370,202 token · 2.4tok/s · 缓存93.99% 第13轮
────────────────────────────────────────
[最终总结正文(官方渲染)]
失败/中止:
[牌堆] 运行失败 · 48秒 · 首字— · 7,812 token · 28tok/s · 缓存— 第13轮—),真实数据到达只替换 —、完成瞬间不新增字段段;关闭的字段整段消失(连 — 也不显示)。 统计数值全部来自官方真实数据,绝不伪造(没有估算、无数据 = —)。运行中显示的 token 数字是 presentation-only 的展示值:有官方数据时 = 真实值 + 受限视觉偏移,官方 usage 还没首次上报时 = 独立的视觉计数(详见下「真实数据层 / 展示层」); 读屏文本(sr-only)始终是官方真实值(真实值未到就是 — token)。
TurnLocation.start.time → end.time(运行中用实时时钟补足);turnFoldMetrics(见下方「durable Turn 指标」)给出该回合稳定步的首字延迟 (firstTokenTime - stepStartTime,与官方统计同款语义); 其次客户端 finalNode.timing(只在本次页面 live 流过时才有);运行中官方 timing 未就绪时给 provisional 可见首字延迟(data.time - stepStartTime, 页面内观察过就记住,settle 后 exact 缺失时继续显示、不退回 —);
tokenUsage(deriveTurnTokenUsage 折叠全部计费 attempt,含被重试请求;缓存分母 = prompt 侧 总量),运行中/缺失时按 step usage 累加(官方 step data store 与节点可见性无关, 隐藏纯工具步骤同样计入);
Σ outputTokens ÷ Σ(completedTime − firstTokenTime),只统计首字/完成时间/outputTokens 三项齐备的 step;TTFT、tool 执行、step 间等待、整个回合墙钟时长都不进分母。 decode 证据同样 durable 优先(projection)、客户端 step 聚合兜底。
finalNode.timing.firstTokenTime 只在收到 assistant/live-chunk(客户端瞬态事件、 不落盘)时记录(ui-chat/.../assistant.ts:146,154),settleMessage() 与历史重建用的 fallbackState() 都不从落盘的 assistant/message.stream 恢复它 —— 于是刷新页面或打开 历史会话后首字与 tok/s 同时消失(显示 首字— / — tok/s),而官方底部 sessionStats 仍有这两个值(它用 assistantStreamFirstTokenTime(event.data.stream) 做 durable 折叠)。 插件因此在宿主半边注册自己的 projection(官方公开扩展面 ctx.sessionProjections.register,unit 归插件所有、只读、返回同一引用不产生多余广播), 用与官方 sessionStats 逐条相同的事件语义折叠出 per-turn 指标,由 host 经 projection wire 推给客户端,前端用 useProjection('turnFoldMetrics') 读取 —— LIVE / SETTLED / RELOAD / HISTORY 四条路径数据来源一致。旧宿主没有该服务时整个注册跳过,前端自动回落到 客户端 step 数据(退化 = 重载后这两个字段为 —,不伪造)。每回合只存 3 个数字, 上限 2000 回合(超出后最旧回合降级为 —)。
canonicalTokens(真实数据层)= computeTurnMetrics 的 tokens(官方 usage / turn-tail 聚合),是唯一权威值,参与 tok/s、缓存命中、TTFT 与 durable projection;官方 usage 未到时它 保持 `undefined`,绝不由展示值回写;
displayTokens(展示层),只用于运行中 UI,全部状态存在组件本地 ref(不写 localStorage / 不写 projection / 不写任何 node data),不参与任何统计:
displayTokens = canonicalTokens + 视觉偏移;可见 reasoning/text(官方 `blockIsVisible` 语义:tool-call / 空白文本不算)时, token 槽启动 presentation-only 视觉计数——第 1 帧就是 `1`,之后按同一节奏增长, 独立封顶 500;该数值不是统计值,首次真实 usage 到达后立即校准**到官方 token; 读屏文本在真实值到达前仍显示 — token;
等待期间该 step 已 settle(或尚无可见 block),数字冻结不动,也不会退回 — token (避免 137 → — → 数字 的跳变);
+1, +1, +10 循环(无随机 jitter);min(500, max(20, canonicalTokens × 5%))(例:1,000 → +50;10,000 → +500;100,000 → +500),到顶即停,等下一次真实 usage;
(绝不从旧展示值慢慢滚过去),再从新基线继续缓慢增长。校准不改变订阅状态: 若该 assistant-step 仍在流式输出可见内容(running + 可见 block),ticker 保持订阅、 下一 tick 继续从新基线增长;若此刻已进入工具执行/等待阶段(没有 running 的可见 step),则只显示真实值并退订(工具阶段本来就停止增长)。Turn settle → 全部归零, 历史会话 / 刷新页面只显示官方真实值(无 canonical 则回到 — token);
未开启减少动态效果)时订阅——历史回合、已结束回合、工具执行/等待阶段一律不订阅; 没有任何活动订阅者时 module 级 interval 彻底停止(100 个历史 Turn = 0 订阅者 + 0 定时器);
canonical,无 canonical → 回到 — token)并退订 ticker; 会话中途切换该设置会实时生效(媒体查询 change 事件驱动重渲染);正常模式下视觉数字滚动、 sr-only 文本保持 canonical(读屏永远拿到真实值)。
sr-only 副本(读屏无障碍);系统「减少动态效果」时自动退化为静态数字。
--dsw-alias-line-secondary token 链式回退,随主题适配)。aria-expanded / aria-label,键盘可操作(Enter / Space);不可折叠的回合(如 Verbose 官方语义、中止/失败回合)呈现为静态栏。
都正常渲染;foldable=false(如 Verbose)与分页加载中(turn/start 未在窗口内, 官方本就不渲染控制条)时优雅降级。
官方步骤分组栏(含官方标题语义「正在读取…」「已读取 3 个文件」「正在思考…」、官方 shimmer、官方分页与搜索显隐)原样工作,插件只换装:
牌尺寸与 Turn 栏扑克图标同一设计语言:图案画布 24×24、16 视箱 mask 按 24px 渲染 (1.5px/单位)→ 牌外缘 ≈9.62×13.05px、描边 1.05px;两侧都把这张 24px 画布放进官方 16×16 的 leading 盒子里居中溢出(Turn 栏的 .ccg-poker-icon 就是 16×16,与官方 .leading 同尺寸)⇒ 墨迹左缩进 1.3px、与文字间隙 6(gap)+1.3 = 7.3px,两条栏在默认内容 字号下逐像素同款。边界(如实记录):官方 .leading 是 calc(16px + var(--dsh-content-font-delta, 0px)),内容字号被放大时它会跟着变宽;插件的 .ccg-poker-icon 与栏文字都是定值(16px / 14px),不随字号放大,此时两侧不再逐像素相等 (曾用 24px 盒子:墨迹居中后左右各空 5.3px,Turn 栏字距比 Step 栏宽 4px、牌也右移 4px);
展开 = 扇形(open = 牌已翻看)——与运行态牌面同序同构,几何直接复用 Turn 栏的 stack3/stack5 · fan3/fan5 变换表(icons/default.json 数据源);牌身份连续: 同一 card id 在 stack 与 fan 里是同一张牌、层级不变——stack3 的顶牌 card3 在 fan3 里是最右那张(与 fan5 同构:居中那张不旋转、两侧对称、id 越大越靠右),展开时 它从左上顶牌位置一路向右、始终压在最上层;张数按本 Process Group 的工具调用数决定:官方 summary.counts[].count 求和 ≤3 → 3 张、≥4 → 5 张 (旧宿主 / 数据取不到 → 5 张安全回落,绝不猜 3 张); 开合有 morph 过渡:预采样 stack↔fan 插值帧(cubic-bezier(.22,1,.36,1) 采样、 400ms、16 帧采样 = 17 关键帧、每帧静态 SVG)经 CSS keyframes 逐帧换 mask-image—— animation 每次状态变化都从头重放(同 URL mask 图像的 SMIL 时间线在 Chromium 全页共享、 重应用不 restart,帧序列绕开该限制),帧内 occluder 逐帧同步 knockout; easing 只作用于"几何进度"、开合互为镜像:帧时间进度 u=0→1 先过同一 easing 得到几何进度(展开 progress = E(u)、收起 progress = 1 - E(u)),几何 helper 只做 线性插值——两个方向因此都是"起手最快、后段减速、柔和停住"(实测前 100ms 走掉 ≈74%、 后 100ms ≈0.25%),与 Turn 栏 transition: transform .45s cubic-bezier(.22,1,.36,1) 同一手感。不是时间反转 E(1-u):那会让收起起手速度为 0、收尾最陡(前 100ms 仅 ≈0.25%、 后 100ms ≈74%),观感"前慢后急",与展开相反;重叠区用真 knockout 遮挡:每张下层牌一个内嵌 luminance mask,z 更高层牌以实心黑牌面 (fill=black occluder)把覆盖区从下层整体挖掉——牌身保持透明(壁纸/图片背景透出), 下层 stroke 与 pip 绝不穿透上层牌(occluder 必须直接内联图形——Chromium 不渲染 ` 内容里的 ` 引用);官方 chevron 在 Poker skin 下恒隐 (normal/hover/focus/active 一致显示 Poker,hover 变色与 focus ring 等官方交互不受影响);
条目都能拿到的标准 kit useConversation(官方 SessionStandardProps,ui-conversation 经 ctx.uiSession.provide 下发)→ ConversationSnapshot.views.grouped('chat') → groupSource(key) → GroupSnapshot.data.summary.counts(与官方 ChatViewInjected.keyedHooks.chatGroup 是同一个源);出口 = 官方组根上的稳定 DOM 事实 data-chat-group-key,插件只在自己的样式表里按它生成覆盖规则。官方分组算法、成员判定、 open/closed、折叠交互、隐藏与搜索展开全部不重算、不接管;不写官方 DOM、不加官方属性。 官方未提供这些 API 的旧宿主上不产出任何规则 → 行为回落为 5 张(固定牌面);
但每个 session 只允许一个真正订阅——mounted registry 里首个挂载的实例立即成为 leader, leader 卸载时从同一 session 仍挂载的实例里立即晋升一个(纯 React mount/unmount registry;没有任何 DOM 查询或轮询,也不依赖"下次新组件 mount")。接棒期间该 session 的 规则不清空、不闪回 fallback;只有该 session 最后一个桥卸载才撤下它的规则块。 session 完全卸载时只清它的展示层分配(topFaces / bag),全局共享的变体资产与已注入的 样式元素保留复用(池 5 面 × 2 档位,天然有界);同一 session 重新打开时重新随机;
[data-conversation-session=""] [data-step-process][data-chat-group-key=""] —— 官方 GroupKey 是"one Session 内的 identity"(跨会话会重复),官方 UI 又允许两棵会话树 并存(主会话 + 子代理右侧栏会话),只按 groupKey 匹配会让同名组串样式。作用域锚点用的是 官方既有 DOM 契约:ui-conversation 的 ConversationContent 在会话内容根上渲染 data-conversation-session={sessionId}(官方自己的 stop-shortcut 就是 closest('[data-conversation-session]') 解析会话),该元素包含该会话的全部 chat 节点。 插件只消费这个属性,不写官方 DOM、不加属性;多个会话的规则块可同时输出(各自只作用于 自己的会话树),无 sessionId 的旧宿主则回落为不带作用域的选择器且只在唯一时输出;
♦ ♣ ♠ ♥ 🐋 取前 N 张 → 3 张永远 ♠、5 张永远 🐋。现在每个 session 一个 top-face shuffle bag(五牌面池;与 Turn 顶层 Poker 共用 shufflePokerFaces 纯函数、bag 状态 完全独立):一批内五种顶牌各出现一次、重洗后首张不与上一批末张相同;每组的整套排列 = completedFacesArrangement(count, topFace)(cardN 必为该组分配到的顶牌,其余由 top face 播种的确定性洗牌给出,组内不重复;5 张恰好用满牌面池,池只有四花色时把重复牌面放在 最下层 card1)。身份 = sessionId + 官方 groupKey,只有已完成(官方 `ProcessGroupData.closed === true`)的组参与分配——running 组走运行态轮换动画 (展开态才平面旋转 35.5°),既不消耗 bag 也不产出覆盖规则;分配只在组第一次渲染(CSS 规则生成)时发生,之后重渲染 / 开合 / 快照更新都命中缓存(随机不藏在 CSS builder 里);不持久化,F5 / 插件重载后重新随机。 为控制样式体积,逐组牌面资产按 (count, topFace) 懒生成、永久缓存、每个变体一个 `` 元素(只写一次):逐组规则只有 ~0.5KB(引用变量),整页资产上限 = 池大小 × 2 个牌数档位;
GroupSnapshot.members(本组每个成员节点的 key)→ 官方 ChatNodeStore.get(key) 读工具 节点(ToolChatData.root = name + argsRaw)→ JSON.parse(argsRaw) 抽 file_path/filePath/file/target/path;出口是官方组头按钮 button[data-process-activity]::after{content:"…"}——不写官方 DOM、不加官方属性、 不做 DOM 观察(架构红线见 tests/unit.compat.test.mjs 的禁用标识符与 tests/unit.step-cards.test.mjs 的桥守卫)。显示规则:basename 再去重,然后按整名预算 取舍——最多 3 个;预算按内容区宽度自适应(stepFilesBudgetPx() = clamp(内容宽 − 300px, 150px, 380px),那 300px 是给标签/图标/齿轮留的位置)。内容宽是 实测出来的:取官方内容列里一行的 clientWidth([data-chat-group-key] 组根,有会话 锚点时按会话限定,宽度 0 的隐藏/未布局行跳过)——官方 --dsh-chat-content-width 声明在 会话内容元素上(不是 :root,自定义属性也不会向上继承),而且它的计算值是 var()/clamp() 文本、parseFloat 读不出数值,所以不作数值源;一次也量不到才回落到视口估算 (window.innerWidth − 312,下限 320,最后 680);窗口 resize 与组条目列表变化时重量一次。 名字再按等效 ASCII 字符折算(ASCII 记 1、中文/全角记 2.4 ≈ 1em/6px)⇒ 宽窗口约 63 字符 (吃满 380px),窄窗口夹在 STEP_FILES_MIN_PX(150px ≈ 25 个等效 ASCII 字符;旧版的固定 回落常量 STEP_FILES_BUDGET(32 字符 ≈ 192px)已删除)。放不下的名字整条折成 +N, +N 自己也要挤进预算; 只有第一个名字例外:为了"至少留一个名字,否则这段就没信息了",它无条件显示,所以它自身 可能超过预算,这时若宽于 max-width 就会被省略号截断——这是已知边界。写进 CSS 的 max-width 与数据层用的是同一个像素值(早期用固定 44ch 兜底:口径不同,而且在宽窗口下 白白浪费一半空间——实测 680px 的行里旧规则只用掉 130~167px、右侧空着 231~468px);正常 (名字都放得下)不触发 CSS 截断,也不会出现早先那种 ChatGroup… 残片; glob/grep/find/ls 这类目录型工具的 path 是搜索根、不当文件名 (file_path 恒采信);目录(以分隔符结尾)与 url 不算文件;运行中与已完成都显示 (运行中随官方工具数据变化累积,turnDataSource(turn,'tool-call') 只作刷新触发器), 条目消失/会话卸载即撤下规则。独立样式表 style-step-files,不随 iconStyle 启停 (信息不是皮肤,native 模式同样显示);旧宿主(0.1.2~0.1.6 legacy 面)不产出;已知边界 (如实记录):只读组的顶层成员节点——PTC 子调用(ToolCallBlock.subCalls)里才出现的 文件不进清单;
data-process-activity 值映射(ACTIVITY_SUIT 单一数据源生成 CSS):thinking/questions → ♥,read/readImage/search/webSearch/webFetch → ♠, edit/write → ♦,commands/code → ♣,subagents/plan/tools → 🐋鲸鱼(DeepSeek Logo); 未登记活动回退 ♥(该映射现在主要作为 reduced-motion 的运行态回落);
两者刻意不同):运行中的 Step(官方标题带 shimmer。Soft running-state dependencies——DSH 0.1.7 输出 data-text-shimmer、DSH 0.2.0+ 输出 data-shimmer, 两者都是官方真实历史契约,插件同时兼容)保持五牌面轮换本体 (♠ ♥ ♦ ♣ + DeepSeek 鲸鱼五张地位相同的牌面,无正/背面)。倾斜只在「运行中 + 展开」: 收起态是正立轮换(不加旋转包装),展开态才把整套可见动画(卡牌、花色、以及它们 引用的 mask)绕图标中心整体旋转 35.5°。自运行 SVG(SMIL)作为卡牌的 CSS mask, 牌线颜色仍由 currentColor 跟随主题:
aria-expanded="true"(官方 ChatGroupSeat 恒渲染aria-expanded={open},插件 Legacy Header 也主动渲染同一钩子;与 completed 展开扇形 同一个官方事实)。运行中收起 → 正立;运行中展开 → 倾斜。插件零 JS 判定,纯 CSS cascade;
最外层 `` 包装**派生的正立 SVG。两者动画本体逐字节 相同(守卫断言「只差最外层包装」),不存在第二套关键帧。代价:两份 data URI 各自一条 SMIL 时间线,开合切换会从相位 0 重新开始(切换本身是用户操作,可接受);
docs/扑克牌轮换_动态蒙版遮挡_文件图标加强版.html:动画本体取自第三行(── 第三行:五牌面轮换动画 ──)的 const ANIM_SVG,再加上 svg3dRotated() 那一步(在 ` 之后插入 , 参考稿注释原文:「在不改动动画关键帧的前提下,把整套可见动画绕 16×16 画布中心旋转。 defs 保持原坐标;外层 transform 会让卡牌、花色以及引用的 mask 一起旋转。」);而 **35.5° 这个目标角与设计标签本身来自第五行**——── 第五行:两个"点击切换型"动画 ── 的点击切换 变体(「牌面轮换 · 平面旋转 35.5°」/ const target = toward35 ? 35.5 : 0;第三行自己的 预览按 ANIM_ANGLES = [0, 30, 45, 60, 90]` 渲染,不带 35.5 字面量);
icons/default.json → pokerSpin.restAngle(≈35.5377° = atan(w/h),与回合栏轴角同一数据源);生成脚本断言它四舍五入到 1 位小数 == 参考稿第五行那条变体的 字面量 35.5,设计值与数据源任一边漂移即生成失败;
npm run sync:step-anim 写入 client.js 的