dsh-turn-fold
> 简体中文(默认) | English
> DeepSeek Harness(DSH)原生折叠 + 扑克牌视觉 + 回合性能指标。
DSH 原生负责全部折叠语义(分组、成员归属、展开/收起状态、分页、搜索显隐、历史状态),
本插件在官方折叠引擎之上做纯 UI 增强:
- 增强回合栏(Enhanced Turn Process Bar):替换官方
turn-process渲染器,
提供比官方更丰富的回合栏——扑克牌前导图标、耗时、首字(TTFT)、token、tok/s、
缓存命中率、右对齐轮次「第x轮」、非正常结束状态词(已停止 / 运行失败 / 已中断)。
折叠状态完全来自官方(turnProcess.open / setOpen / foldable / hasContent),
点击回合栏只调用turnProcess.setOpen(),成员的隐藏/显示由官方 seat 完成; - 运行中状态条(Running Turn Bar):回合开始(
turn/start投影)即出现——
官方渲染器此时什么都不渲染,插件补上这根状态条:耗时从 0 秒起步实时走动,
token / tok/s / 缓存命中率随真实数据到达而更新。它只是状态表面:不维护任何
折叠状态、不隐藏任何成员;回合结束后平滑交接为完整的收起态回合栏; - Step 扑克牌皮(Poker Step Skin):官方步骤分组栏(
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 即归零); - 插件设置:回合栏字段显隐(耗时/首字/Token/tok/s/缓存命中)+ 统一折叠栏图标模式
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轮
- 字段槽位始终存在:设置里启用的字段从 0 秒起就占位(无真实数据显示
—),
真实数据到达只替换—、完成瞬间不新增字段段;关闭的字段整段消失(连—也不显示)。
统计数值全部来自官方真实数据,绝不伪造(没有估算、无数据 =—)。运行中显示的
token 数字是 presentation-only 的展示值:有官方数据时 = 真实值 + 受限视觉偏移,官方
usage 还没首次上报时 = 独立的视觉计数(详见下「真实数据层 / 展示层」);
读屏文本(sr-only)始终是官方真实值(真实值未到就是— token)。 - 指标来源(全部官方真实数据):
- 耗时:
TurnLocation.start.time → end.time(运行中用实时时钟补足); - 首字(TTFT):durable 优先——插件自己的官方 projection
turnFoldMetrics
(见下方「durable Turn 指标」)给出该回合稳定步的首字延迟
(firstTokenTime - stepStartTime,与官方统计同款语义);
其次客户端finalNode.timing(只在本次页面 live 流过时才有);运行中官方
timing 未就绪时给 provisional 可见首字延迟(data.time - stepStartTime,
页面内观察过就记住,settle 后 exact 缺失时继续显示、不退回—); - Token / 缓存命中:回合结束后优先 turn-tail 的官方聚合
tokenUsage
(deriveTurnTokenUsage折叠全部计费 attempt,含被重试请求;缓存分母 = prompt 侧
总量),运行中/缺失时按 step usage 累加(官方 step data store 与节点可见性无关,
隐藏纯工具步骤同样计入); - tok/s:decode-speed(与官方 StatsPills 同一定义)——
Σ outputTokens ÷ Σ(completedTime − firstTokenTime),只统计首字/完成时间/outputTokens
三项齐备的 step;TTFT、tool 执行、step 间等待、整个回合墙钟时长都不进分母。
decode 证据同样 durable 优先(projection)、客户端 step 聚合兜底。 - durable Turn 指标(
turnFoldMetricsprojection,为什么需要):官方客户端的
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 回合(超出后最旧回合降级为—)。 - 真实数据层 / 展示层(运行中 token):
canonicalTokens(真实数据层)=computeTurnMetrics的tokens(官方 usage / turn-tail 聚合),
是唯一权威值,参与 tok/s、缓存命中、TTFT 与 durable projection;官方 usage 未到时它
保持undefined,绝不由展示值回写;displayTokens(展示层),只用于运行中 UI,全部状态存在组件本地 ref
(不写 localStorage / 不写 projection / 不写任何 node data),不参与任何统计:- 有 canonical:
displayTokens = canonicalTokens + 视觉偏移; - 无 canonical(官方 usage 尚未首次上报):当 running 的 assistant-step 已经出现
可见 reasoning/text(官方blockIsVisible语义:tool-call / 空白文本不算)时,
token 槽启动 presentation-only 视觉计数——第 1 帧就是1,之后按同一节奏增长,
独立封顶 500;该数值不是统计值,首次真实 usage 到达后立即校准到官方 token;
读屏文本在真实值到达前仍显示— token; - 提示:刚发消息、模型还没有产出任何可见内容时不启动(不会自己从 0 开始跳);
- 只在模型真的在流式生成可见 assistant 输出时增长:工具执行、等待结果/审批/子代理、
等待期间该 step 已 settle(或尚无可见 block),数字冻结不动,也不会退回— token
(避免137 → — → 数字的跳变); - 节奏固定且确定性:每 200ms 一个 UI tick,步长序列
+1, +1, +10循环(无随机 jitter); - canonical 偏移上限
min(500, max(20, canonicalTokens × 5%))(例:1,000 → +50;
10,000 → +500;100,000 → +500),到顶即停,等下一次真实 usage; - 官方 usage 更新 → 立即校准:bootstrap/偏移全部清零,界面下一帧直接等于新真实值
(绝不从旧展示值慢慢滚过去),再从新基线继续缓慢增长。校准不改变订阅状态:
若该 assistant-step 仍在流式输出可见内容(running + 可见 block),ticker 保持订阅、
下一 tick 继续从新基线增长;若此刻已进入工具执行/等待阶段(没有 running 的可见
step),则只显示真实值并退订(工具阶段本来就停止增长)。Turn settle → 全部归零,
历史会话 / 刷新页面只显示官方真实值(无 canonical 则回到— token); - 性能:视觉 ticker 只在真正有动画(运行中 + 已有可见输出 + Token 字段开启 +
未开启减少动态效果)时订阅——历史回合、已结束回合、工具执行/等待阶段一律不订阅;
没有任何活动订阅者时 module 级 interval 彻底停止(100 个历史 Turn = 0 订阅者 + 0 定时器); - 系统「减少动态效果」开启时立即归位(丢弃已滚出的视觉偏移:有 canonical → 直接显示
canonical,无 canonical → 回到— token)并退订 ticker;
会话中途切换该设置会实时生效(媒体查询 change 事件驱动重渲染);正常模式下视觉数字滚动、
sr-only 文本保持 canonical(读屏永远拿到真实值)。 - 滚轮数字动画:运行中数值变化时逐位滚动(里程表效果,回弹缓动);完整文案有
sr-only 副本(读屏无障碍);系统「减少动态效果」时自动退化为静态数字。 - 回合栏下常驻分隔线(
--dsw-alias-line-secondarytoken 链式回退,随主题适配)。 - 无障碍:
aria-expanded/aria-label,键盘可操作(Enter / Space);不可折叠的
回合(如 Verbose 官方语义、中止/失败回合)呈现为静态栏。 - 四个官方 transcript 档位全兼容:Compact / Standard / Detailed / Verbose 下回合栏
都正常渲染;foldable=false(如 Verbose)与分页加载中(turn/start未在窗口内,
官方本就不渲染控制条)时优雅降级。
Step 扑克牌皮
官方步骤分组栏(含官方标题语义「正在读取…」「已读取 3 个文件」「正在思考…」、官方
shimmer、官方分页与搜索显隐)原样工作,插件只换装:
- 牌身透明的扑克卡(CSS mask,当前色描边 + 花色点,壁纸可透出)替换官方活动图标,
牌尺寸与 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); - completed 双态图标(Fold-state aware):收起 = 花色牌堆(closed = 牌收好未翻看),
展开 = 扇形(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 等官方交互不受影响); - 牌数从哪来(纯只读视觉桥,零分组重算):数据源 = 官方现成 API——session 作用域
条目都能拿到的标准 kituseConversation(官方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 张(固定牌面); - 桥的 leader 生命周期(per-session,本轮修复):宿主里每个回合都会挂一个桥实例,
但每个 session 只允许一个真正订阅——mounted registry 里首个挂载的实例立即成为 leader,
leader 卸载时从同一 session 仍挂载的实例里立即晋升一个(纯 React mount/unmount
registry;没有任何 DOM 查询或轮询,也不依赖"下次新组件 mount")。接棒期间该 session 的
规则不清空、不闪回 fallback;只有该 session 最后一个桥卸载才撤下它的规则块。
session 完全卸载时只清它的展示层分配(topFaces / bag),全局共享的变体资产与已注入的
样式元素保留复用(池 5 面 × 2 档位,天然有界);同一 session 重新打开时重新随机; - 规则按会话作用域隔离(本轮修复):最终 CSS selector 是
[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 的旧宿主则回落为不带作用域的选择器且只在唯一时输出; - 每组牌面从哪来(completed 组不再"牌数决定顶牌"):旧行为是固定序
♦ ♣ ♠ ♥ 🐋取前 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 个牌数档位; - 步骤文件清单(组头尾部"本步骤碰过哪些文件",纯 CSS 文本):数据同样只读官方 API——
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 的运行态回落); - 运行态牌面轮换 · 平面旋转 35.5°(不是回合栏运行中那套竖直对角线轴翻牌——
两者刻意不同):运行中的 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跟随主题: - 设计源 = 参考实现
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的
>>> step-running-poker-svg标记块。脚本断言的是接线与结构:参考稿第三行的动画本体与
平面旋转接线仍在(const ANIM_SVG/svg3dRotated/anim-root-rotation/
rotate(' + angle + ' 8 8))、第五行的 35.5 字面量仍在,旋转组真的包住全部相位、产物不含
第四/五行轴旋转时代的禁用标识,并把产物计数打到 stdout。5 组相位 / 72 个
animateTransform / 15 个 animate /0.8s×82 /4s×5 这些计数不变量由
tests/unit.css.test.mjs的「平面旋转契约」断言(即「只加旋转、不动关键帧」)——
脚本不是这些数字的 pre-commit 守卫; - 结构 = 五牌面轮换(每 0.8s 一组:两张完整平面牌对角轻微错开再合拢,唯一的 rotate 是
±3.1° 二维平面小角度;中途discrete在中点换层;四个动态 luminance 蒙版把下层牌与
上层牌重叠处的线条挖空;五组相位连成 4s 完整循环:
diamond → club → spade → heart → deepseek →(回到 diamond))
外面套一层 ``; - 边界(如实记录):本 SVG 是构建期烘焙进
client.js的,运行时 localStorage
图标包覆盖不改写它(覆盖只作用于回合栏翻牌与运行时生成的 completed 双态牌面)——
与改动前的实现同性质; - 运行态与牌数无关(本组只有一个工具也照轮换);回合结束 shimmer 消失 → 自动回落
completed 双态(按本组工具数的 3/5 张收起牌堆 / 展开扇形),全程纯 CSS cascade、
零 JS 运行态。系统「减少动态效果」开启时运行态直接显示静态花色牌; - 旋转角只由 SVG 的
transform属性决定:CSS 不得再写 transform 覆盖它
(CSS transform 会压掉同名属性,回合栏踩过这个坑)。步骤侧旋转组类名
tf-step-flat-rotation与回合栏ccg-axis-rest-rotation刻意分开,守卫见
tests/unit.css.test.mjs的「平面旋转契约」; - 软依赖:全部选择器挂在官方 DOM 钩子上,总闸 = 皮肤
` 元素的disabled属性(插件不写任何document.body全局状态)——DSH 改掉钩子时 **最坏退化 = 皮消失、官方图标原样显示**,官方折叠行为不受任何影响。 运行态识别依赖官方 shimmer 属性(两个版本任一存在即触发 Running Step Poker 动画;两者都不存在 → 静态 Poker fallback;即使视觉钩子失效,也不影响 Step Fold / Tool / Think / Turn Fold 与页面稳定性)。 逐组牌面规则额外消费官方会话锚点data-conversation-session(0.1.7-rc.2+; **官方自己的stop-shortcut就是用closest('[data-conversation-session]')解析会话**, 属官方既有契约):该属性缺失的宿主(0.1.7-rc.1)由 CSS 回退分支[data-conversation-content]:not([data-conversation-session])` 兜住;0.1.7-rc.1 没有
官方会话锚点、CSS 无法区分两棵会话树,多会话并存时插件自动撤下 session-specific
的 completed 牌面覆盖(回落基础 Step Poker 通用视觉),绝不让 Session A/B 串样式
(详见下方「会话作用域四态」);插件不写官方 DOM、不加任何官方属性。
Compatibility Architecture
现代化宿主的唯一 Fold owner 是 DSH 自己(官方 turn-process owner state + 官方
Process Group disclosure);插件只做 UI(Turn 栏 / Poker / 指标 / token 动画 / 设置)。
插件按能力选择 feature-level adapter,而不是比较版本号字符串:
宿 主 能 力 探 测(capability-first)
│
┌────────────────────────┴────────────────────────┐
Modern Adapter Legacy Adapter
(官方契约齐全) (官方契约缺失且被显式证明时才启用)
└────────────────────────┬────────────────────────┘
│
Shared UI(Turn 栏 / Poker / 指标 / token 动画 / 设置)
三态语义:UNKNOWN ≠ LEGACY
每个 feature 的 capability 都是三态(sessionScope 为四态),语义严格定义:
modern/reactive/official= 已确认官方拥有该能力;legacy/fallback/tree-only/none= 已确认官方没有该能力(只能由
显式证据定论:注册期官方 slot 表、或渲染期真实会话快照证明契约缺失);unknown= 当前阶段尚无法判断。
UNKNOWN 不是 LEGACY。 一个 feature 只有在宿主被显式证明缺少官方契约之后才允许
进入 Legacy 模式;注册期探不到的一律 unknown,等渲染期 runtime probe 定论,
绝不因 unknown 启动任何 Legacy 引擎。运行时 capability 状态一经定论不再翻转
(modern ↔ legacy 需要显式重新初始化)。
PROBE != COMMIT(探针读取与定论提交分离)
- React render body 只读取探针值:
StepCardRuleWriter读views.grouped契约
三态、EnhancedTurnProcessView读ChatNodeStore形状——都不修改全局状态; - capability resolution 只发生在 committed effect:render/并发渲染被 abort 也不
会改变全局兼容状态(未 flush effect 时hostCapabilityState保持不变,测试锁死); - Legacy activation 只绑定
unknown → legacy的那次 committed 迁移:
resolveHostFeature返回true(真迁移)才允许激活一次 legacy backend;
StrictMode effect replay / 重复渲染一律不再激活、不再记账、不再刷诊断
(StrictMode 下legacyStepEngineActivations === 1,测试锁死); - 注册期(Cordis registration phase)不是 React render:
adoptRegistrationCapabilities
对 turnFold 的同步定论不受此约束。
各 feature 的合法定论值按 feature 独立定义(HOST_FEATURE_VALUES):
turnFold/stepFold ∈ {modern, legacy};metrics ∈ {reactive, fallback};
sessionScope ∈ {official, tree-only, none}。跨域赋值(如 metrics = "tree-only")
一律拒绝且不写入状态(逐项测试锁死)。
注册期唯一可探面是官方 conversation.chat.node slot 表(有无 turn-process 条目)
→ 只有 turnFold 在注册期定论;stepFold / metrics / sessionScope 注册期一律
unknown,渲染期定论:
- Step(stepFold):会话快照就绪后探测
views.grouped契约(committed effect
里 resolve)——快照未就绪(undefined)保持 unknown("宿主 API 存在但数据未初始化"
与"宿主没有 grouped view contract"是两回事);快照就绪且views.grouped不是函数 →
显式legacy;契约在(函数存在)→modern(此时 grouped 读端可能因"无活动 Group
Definition"返回 undefined——那是"暂无数据",不是"没有能力")。 - Metrics(metrics):渲染期探测官方
ChatNodeStore.turnDataSource是否存在
(useChat具名形状探针,Hook 全部无条件调用,capability 只决定数据源选择,
resolve 在 committed effect):存在 →reactive;不存在 →fallback(逐 step
直读);未就绪 →unknown。Turn fold 能力 ≠ metrics 能力:0.1.2~0.1.6 有
turn-process(官方 owner state)但没有 turnDataSource——它们是
turnFold=modern + metrics=fallback,而不是 "metrics = modern" 也不是
"metrics = legacy fold engine"。metrics fallback 绝不触发 Legacy Fold Engine。
一个 hybrid 宿主的最终形态可以是
{ turnFold: modern, stepFold: legacy, metrics: fallback }—— 这正是 0.1.2~0.1.6。 - 会话作用域(sessionScope):
official(官方data-conversation-session锚点,
0.1.7-rc.2+)|tree-only(只有data-conversation-content,0.1.6-alpha.2 /
0.1.7-rc.1)|none(两个锚点都没有,≤0.1.5)|unknown(尚未判定)。
sessionScope 的唯一事实源是统一 capability controller(hostCapabilityState),
CSS 子系统不持有第二份结论:state 仍为 unknown 时做一次性官方 DOM 契约读取
(不是轮询:无观察器/定时器/逐帧回调)——看到 session 锚点 → resolveofficial;
"有 content 无 session"(rc.2+ 上两属性同一次 commit 渲染)→ resolvetree-only;
会话 DOM 尚未渲染 / document 不可用 → 保持unknown。none只能来自显式的宿主
能力证据(审计 fixtures / 未来 legacy probe),绝不因"当前 querySelector 没找到"
推导。
Legacy 入口已按 feature 拆分:activateLegacyTurnEngine() 只在
turnFold === "legacy" 时调用(注册期 slot 表证明);activateLegacyStepEngine()
只在 stepFold === "legacy" 时调用(渲染期快照证明 Process Group 契约缺失)。
两者各自有激活计数(getLegacyEngineActivations() → { turn, step }),Modern
宿主必须恒为 0(测试矩阵逐版本锁死)。
0.1.1 的历史事实(修正):dsh-v0.1.1-rc.2 无 turn-process
slot 条目、无 useChat、无 useConversation、无 turnDataSource;但它的
session-scope 标准 kit 提供 sessionId + useSession + useProjection
(SessionStandardProps),useChat 是 0.1.2 的 ui-chat 才合并进来的。此前
README/注释里"0.1.1 连 sessionId / useSession 都没有"的说法是错的,已按
tag 源码纠正。因此 0.1.1 上:
- 数据面可达:Legacy 面用 useSession(s => s.chat) 拿官方 ChatSnapshot
(与 0.1.2+ 的 useChat(selector) 同形:order / nodes / locations / timeline /
legacy),Step 分组算法与 Turn index 零改动复用;
- Step 缺失证据两条独立存在:kit 里没有 useConversation,或快照没有
views.grouped —— 但绝不做"turn legacy ⇒ step legacy"的版本相关性推断:
两条证据链各自探、结论各自 commit(resolveLegacyStepCapability({ nativeStepGroups }):
false = 显式缺失证据;undefined = 未就绪保持 unknown;返回 true 的那次
unknown → legacy 迁移才激活一次 legacy backend)。
Legacy adapter 在官方等价契约存在时永不运行:判据全部是能力探针,版本号只出现在
测试 fixtures、README 与 engines.dsh 里。
0.1.1 Legacy Turn 兼容层(Full Legacy Node Backend)
0.1.1 没有 turn-process / TurnProcessOwnerProps → 官方不拥有 Turn Fold →
插件在 0.1.1 上拥有 Turn 折叠语义(PLUGIN OWNS TURN FOLD ONLY ON LEGACY HOSTS)。
实现方式是一个物理 backend、两套语义:
| 宿主 | 物理 shadow 键(恰好这些,一个宿主同一时刻只有一个 backend) | Turn 层 | Step 层 |
|---|---|---|---|
| 0.1.1(turn legacy) | assistant-step + tool-call + context(3 键,full-legacy 模式) |
插件 Turn index + Turn Bar + 可见性 | logical attach(同一 backend 上启用,绝不第二次注册) |
| 0.1.2 ~ 0.1.6(turn modern / step legacy) | assistant-step + tool-call(2 键,step-only 模式,本轮语义一字未变) |
官方 owner(插件零参与) | 插件 Step 分组 + 折叠 |
| 0.1.7+(turn/step 全 modern) | 无 | 官方 | 官方 |
Turn membership(纯算法 + memo):legacyBuildTurnIndex(snapshot) 是纯函数(唯一
事实源,WeakMap 按 snapshot 身份 memo,一帧只算一次),语义取自旧 main@356db80 的
A 类算法:
- 作用域 = (最后一个 user 锚点 anchorSeq, turn] —— 用 locations.getTurn(turn) 取该
Turn 的全部节点(含官方隐藏节点);context 排在 user 之前(审批策略注入)时属于
作用域外,永不隐藏、也不作 header 锚点;
- finalAssistantKey = 关闭(legacy.turnEnds 有该 turn)后的最后一个
assistant-step —— 最终答案永不在折叠集合内;
- headerKey = 作用域内第一个 member(tool-call / assistant-step / context),
没有中间成员时 fallback 到 finalAssistantKey;hideKeys 为空 → canCollapse=false;
- reason 取自 timeline.turns.get(turn).end.data.reason.kind:aborted / error
→ alwaysOpen(永不折叠);max-tokens / blocked 按正常完成处理;running
(turnEnds 无该 turn)→ 恒展开、Turn Bar 是状态面(无 aria-expanded、点击不写状态);
- FAIL OPEN:finalization 竞态(closed 但还没有 assistant-step)、形状异常(无
turn / 无 user 锚点)→ 不隐藏任何东西,绝不 return null。
Turn 可见性:与 Step 完全同构的插件自有 wrapper(hidden="until-found" +
onbeforematch 自动展开,旧内核回退普通 hidden),官方内容经 priority-0 builtin
委托渲染(原 props、原 node,内容零复制);turn-tail / user / steering /
turn-process 永不 shadow、永不隐藏(物理键集合与成员集合双方锁死)。
Turn open store 与 Step open store 正交:legacyTurnOpenBySession
(sessionKey+turn)/ legacyStepOpenBySession(sessionKey+leaderKey)互相独立,Turn
折叠/展开绝不改写 Step 的展开态(反之亦然);会话状态由统一 session cleanup 在
两个 registry 都空时一次性清理。
Bootstrap(无轮询):注册期 turnFold=legacy → activateLegacyTurnEngine() 启动
事件驱动 readiness:官方 builtin 可能尚未注册完成,则挂 ctx.on("slots/changed")
等待(同步首查 + 事件补查),三个 builtin 全部可捕获时做一次原子安装;稳定冲突
(任一 cell 已被第三方负 priority 占用)→ 停止 bootstrap、当前生命周期不重抢(FAIL
OPEN:官方 owner 原样显示,只是不折叠)。安装事务期间 bootstrap 判定被屏蔽(事务自己
会同步 emit slots/changed,否则会把本插件刚注册的 -1 条目误判成第三方占位而永久
自伤为 blocked)。
Ownership / degraded 复用既有安全机制:atomic install(builtin 预捕获 → ownership
preflight → 逐个注册 → winner 身份验证 → monitor → COMMIT → 任一失败整体回滚)、
runtime monitor(slots/changed PRIMARY + onEntryError 同步 backstop + subscribe
次级)、运行期 ownership 丢失 → 整体 teardown + degraded="ownership-lost"、不自动
重抢——Step 与 Turn 共用同一套代码与同一份不变量(getLegacyNodeBackendState() 诊断
mode / keys / ownershipVerified / monitorInstalled / degraded / stepAttached /
bootstrapWaiting / turnBackendBlocked)。
能力独立:stepFold(kit 无 useConversation 且快照无 views.grouped)与
metrics=fallback(快照 nodes.turnDataSource 缺失)在各自的 committed effect
里独立定论,绝不从 turnFold 推导;0.1.1 上三者的最终值是
{ turnFold: legacy, stepFold: legacy, metrics: fallback }。
Turn owner contract evolution(owner 形状归一化)
官方 TurnProcessOwnerProps 有两代形状(逐 tag 审计),插件用
normalizeTurnProcessOwner(turnProcess, alwaysOpen)(纯函数、唯一的形状适配点)
归一化,视图从不感知契约代际:
- 旧 modern owner 契约(0.1.2-rc.1 ~ 0.1.6-alpha.2):
{ spec, foldable, open, setOpen }—— 没有 hasContent 字段。官方 0.1.2 的
TurnProcessNodeView 语义:foldable即足以决定可折叠,open = turnProcess.open、
点击setOpen(!open)。 - 新 modern owner 契约(0.1.7-rc.1+):增加
readonly hasContent: boolean,
可折叠 =foldable && hasContent。
归一化规则用 Object.prototype.hasOwnProperty.call(turnProcess, "hasContent") 区分:
- 字段不存在(旧契约):
canCollapse = foldable && !alwaysOpen; - 字段存在且 false(新契约):
canCollapse = false(静态栏、不调 setOpen); - 字段存在且 true:
canCollapse = foldable && !alwaysOpen(0.1.7+ 零回归)。
MISSING hasContent ≠ hasContent=false,更 ≠ LEGACY TURN——缺字段是契约代际
差异,不是缺官方 owner;turnFold 对 0.1.2~0.1.6 仍然判 modern(官方已拥有
foldable/open/setOpen,插件只消费官方 owner state)。能力矩阵里对应的是 raw 探针
turnOwnerHasContent(owner 契约形状能力,绝不参与 turnFold 计算、绝不触发
Legacy Turn Engine)。foldable=false(Verbose)与 alwaysOpen(aborted/error/
running)语义两代契约一致、零回归;所有切换仍只调用 turnProcess.setOpen(),
插件不自持 open 状态、不重算 membership、不做 DOM 隐藏。
能力矩阵(官方 release tag 逐版本源码审计)
来源见 tests/version-fixtures/host-matrix.mjs(每个探针字段独立审计、绝不互相推导):
| DSH 版本 | Turn backend | Owner 契约 | Step backend | Metrics backend | 会话作用域 |
| --- | --- | --- | --- | --- |
| 0.1.1-rc.2 | legacy:插件拥有 Turn Fold(Full Legacy Node Backend:assistant-step + tool-call + context;无 turn-process、无 useChat/useConversation;有 sessionId/useSession) | —(无 owner 契约) | legacy:同一 backend 上 logical attach(无官方 Process Group) | fallback(无 turnDataSource) | none(无任何锚点) |
| 0.1.2-rc.1 | modern(official owner state) | 旧形状(无 hasContent) | legacy:插件 Legacy Step Adapter(官方无分组契约) | fallback(无 turnDataSource,指标走逐 step 直读) | none(但插件 Legacy Header 自带 session scope) |
| 0.1.5-rc.3 | modern | 旧形状(无 hasContent) | legacy:插件 Legacy Step Adapter | fallback | none(插件自有 scope) |
| 0.1.6-alpha.2 | modern | 旧形状(无 hasContent) | legacy:插件 Legacy Step Adapter | fallback | tree-only(首次出现 data-conversation-content;插件 Legacy 面用自有 scope) |
| 0.1.7-rc.1 | modern | 新形状(hasContent 起) | modern | reactive(turnDataSource 首次出现) | tree-only(有 content、无 session 锚点) |
| 0.1.7-rc.2 | modern | 新形状 | modern | reactive | official(data-conversation-session,commit b7ac0ade10) |
| 0.2.0-rc.2 | modern | 新形状 | modern | reactive | official |
Live-host acceptance:当前支持区间(2026-10-06,RELEASE GATE PASSED)
对当前 engines.dsh 声明的三个版本做真机验收:官方 npm 发布包 + 隔离 DSH_HOME/profile +
官方 dsh plugin --profile web add 装载(profile dsh.profile.bundles + link),
真实 provider、真实模型会话(含工具调用的 Turn 与纯问答 Turn 各一条),并用插桩副本读取插件内部
状态(同一份 client.js + 只读 console/__test 桥,不改变插件行为)。
| 版本 | 宿主启动 | 插件装载 | capability 最终值 | Turn Bar 接管 | running/completed | Step Bar + Poker | 折叠切换 | 纯问答 | reload/历史 | React 警告 | 未捕获异常 | ownership loss | 证据等级 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.1.7-rc.1 | ✅ npm 发布包 | ✅ | modern / modern / reactive | ✅(running 实时指标 + completed 默认收起) | ✅ | ✅(Step Bar + 图标锚点,折叠正交) | ✅(展开 12→1 隐藏,再收起复原) | ✅(静态栏 + 真实指标) | ✅(重载后 3 根栏重建、零重复注册) | 0 | 0 | 0 | LIVE-HOST VERIFIED |
| 0.1.7-rc.2 | ✅ npm 发布包 | ✅ | modern / modern / reactive(sessionScope=official) | ✅ | ✅ | ✅(3 个 Step Bar) | ✅(16→3→16 隐藏成员) | ✅ | ✅(2 根栏重建) | 0 | 0 | 0 | LIVE-HOST VERIFIED |
| 0.2.0-rc.2 | ✅ npm 发布包 | ✅ | modern / modern / reactive | ✅ | ✅ | ✅(4 个 Step Bar + Poker 图标) | ✅(10→0→10 隐藏成员) | ✅ | ✅(3 根栏重建) | 0 | 0 | 0 | LIVE-HOST VERIFIED |
验收期间发现并修复的真机缺陷(bundle 装载时序竞态):本插件以 bundle 方式装载时只注入
slots,cordis 会在服务就绪时立即 apply——可能早于官方 conversation 层声明
conversation.chat.node。此时注册期探测把「没有 turn-process 条目」误判成 legacy →
在现代宿主上装出 3 键 legacy backend 且永不注册 Turn 渲染器(表现为官方 Turn Bar 保留、
Step 层退化)。用户自有 profile 经 patch-insert 层装载(apply 最后)恰好绕开该竞态。
修复(commit abc375c):hostCapabilitiesOf 的 turnFold 映射三态化
(undefined=unknown,绝不猜 legacy);注册期探测新增官方只读检查面 slots.snapshot(root)
判定槽位是否已声明;unknown 时经 ctx.on("slots/changed") 事件驱动等待,在同一同步突变批
(声明 + 官方 inject 回调注册条目)落地后的一个微任务里复判——有 turn-process → modern 并
注册 Turn 渲染器;声明且条目非空、无 turn-process → legacy(0.1.1 形态);零条目 → 继续等。
无轮询、无定时器;无 snapshot 检查面的极简宿主保持既有语义。
历史验收记录:首轮真机矩阵(2026-10-05,0.1.1 ~ 0.2.0-rc.2)
> 本节是那一轮的原始记录(行内结论按当时状态保留)。其中 0.1.7-rc.1 / 0.1.7-rc.2 /
> 0.2.0-rc.2 三行已被 2026-10-06 的「Live-host acceptance:当前支持区间」取代——三者现在
> 都是 LIVE-HOST VERIFIED,含 0.2.0-rc.2 的 Turn Bar 接管(下表那句 INCONCLUSIVE 只描述
> 那一轮的观测中断)。0.1.1 ~ 0.1.6 仍不在当前 engines.dsh 声明范围内,各自有独立阻塞项
> (详见下文)。
本轮把矩阵从"契约 harness"推进到真实宿主:用各 tag 的真实 runtime(官方 tag 源码构建,
或官方 npm 发布包)在隔离 DSH_HOME 里启动 Web 宿主、用官方插件机制装入本插件、跑真实
turn 并读插件自身的内部状态。结论(证据等级逐项标注):
| 版本 | 宿主启动 | 插件装载 | 关键实测 | 证据等级 |
|---|---|---|---|---|
| 0.1.1-rc.2 | ✅ 官方 tag 源码自建(git archive + 该 tag 的 pnpm 11.7.0/lockfile;npm 发布包无法启动 web,见下) |
✅(/plugins/@winteries/dsh-turn-fold/client.js 被真实加载) |
能力定论实测为 turn=legacy, step=legacy, metrics=fallback(真机 console 实证);3 键 backend 安装成功后自行 teardown(degraded=ownership-lost),折叠未生效 |
LIVE-HOST VERIFIED(失败) |
| 0.1.2-rc.1 / 0.1.5-rc.3 | ⛔ npm 发布包 app-boot 仍调用 hmr.registerConfig,而 npm 的 cordis-plugin-hmr@1.0.19 无此 API → dsh web 启动即崩(源码自建可绕开,未跑完整矩阵) |
— | 官方 tool-call 入口形态与 0.1.1 相同(children: {tool.call.toolview} + inject: hostDescription)→ 同一条 blocker 适用 |
HOST BOOT BLOCKED(npm)+ 源码审计 |
| 0.1.6-alpha.2 / 0.1.7-rc.1 / 0.1.7-rc.2 | 发布包 app-boot 已不再调用该 API(源码核实),未跑完整真机 |
— | 0.1.6 仍是 hybrid(Legacy Step → 同一 blocker);0.1.7+ 是纯现代路径 | SOURCE-AUDITED ONLY(本轮未真机) |
| 0.2.0-rc.2 | ✅ 官方 npm 发布包真机启动(隔离 home + 官方 dsh plugin add 装载) |
✅ 客户端 bundle 被加载、插件 apply 生效(三个插件样式表在 head) | 本轮未能完成 Turn Bar 接管观测:调试过程中 profile 状态被改动后宿主不再提供插件 client 模块(/plugins/... 404),未及在干净 profile 上复跑 → 不作通过结论 |
LIVE-HOST VERIFIED(启动+装载)/ Turn 接管 INCONCLUSIVE |
本轮发现的阻塞项(plugin bug,已在真机定位到具体 API 语义):
- 官方
tool-call渲染器不可被 shadow 委托:官方入口注册时带了
children: { 'tool.call.toolview': … }(子槽声明按 key 全局唯一,同一 key 二次声明直接抛错)
与inject: () => ({ hooks: { hostDescription } })(entry 级注入面)。插件 shadow 既无法
复制children(会与官方条目撞声明),也无法拿到该子槽的授权绑定
(renderSlot绑定按 entry 生成,调用未声明 key 抛SlotOwnershipError),于是委托渲染官方
ToolCallTree时官方组件拿不到useHostDescription/renderSlot→ 组件抛错 → 宿主把
本插件的 shadow 条目 abdicate → 插件的同步 backstop 按设计整体 teardown
(真机日志:slot entry crashed in 'conversation.chat.node': TypeError: useHostDescription is not a function
→[dsh-turn-fold] Legacy Step disabled: runtime shadow ownership was lost; backend torn down)。
该形态在 0.1.1 ~ 0.2.0 的官方源码里一致存在 → Legacy Step / Full Legacy 的"3 键委托"方案在
真实旧宿主上不成立,需要重新设计(详见下轮待办)。 - npm 发布包 0.1.1-rc.2 / 0.1.2-rc.1 / 0.1.5-rc.3 不能启动 web profile:官方
dsh-app-boot
在这三代里通过hmr.registerConfig监听用户 patch 层,而 npm 上@deepseek-ai/cordis-plugin-hmr@1.0.19
无该 API(tag 内是vendor/hmr工作区包,未随 npm 发布)→ 现象为
TypeError: hmr.registerConfig is not a function。这属于宿主自举阻塞,不是插件问题;
以真实 tag 源码自建(沿用该 tag 的 lockfile + pnpm 11.7.0)可正常运行。
因此本轮不放开 engines.dsh:0.1.1 的 live 结论是失败,0.1.2/0.1.5 连官方 runtime 都起不来,
0.1.6+ 未真机验证。当前区间保持 >=0.1.7-rc.1 <=0.2.0-rc.2。
诊断输出:probing → resolved 两行制
注册期不下最终结论(modern + unknown 不是 mixed):
[dsh-turn-fold] host capabilities: turn=modern, step=probing, metrics=probing
运行时 resolution 完成后输出一次最终 mode(每个有效变化最多输出一次,不逐 Turn 刷):
[dsh-turn-fold] host mode: modern (turn: modern, step: modern, metrics: reactive)
[dsh-turn-fold] host mode: mixed (turn: modern, step: legacy, metrics: fallback)
[dsh-turn-fold] host mode: legacy (turn: legacy, step: legacy, metrics: fallback)
turn=legacy 的宿主现代渲染器不注册;最终 mode 行只在三个 feature 全部定论后
输出——0.1.1 的 step/metrics 由 Full Legacy renderer 的 committed probe 定论,因此
这行会晚到(注释面完全就绪之后),注册期绝不提前下结论。getLegacyNodeBackendState()
可随时查询 backend/ownership/bootstrap 的实时事实。
会话作用域四态与 0.1.7-rc.1 安全降级
- official(0.1.7-rc.2+):规则前缀
[data-conversation-session=""]+ 完整组
条件,多棵会话树并存(主会话 + 子代理侧栏)时每块只命中自己的树,同名 groupKey
完全隔离;active session count 对它没有约束(第二个 session 刚 mount、还没有
chunk 时,A 的规则照常输出——official 作用域本身已安全限定)。 - tree-only(0.1.7-rc.1):CSS 层无法区分两棵会话树——单会话时允许输出
group-specific completed 牌面覆盖;多会话并存时自动撤下全部 session-specific
completed 牌面覆盖(回落基础 Step Poker 的通用 5 张双态与运行态平面旋转轮换)。
判断依据是当前已挂载的会话数(插件自己的 per-session bridge registry
stepCardBridgeSessions.size)——不是"已生成 CSS chunk 的会话数":第二个
session 一进 registry(哪怕它还没写出任何 chunk)就必须立即撤下,否则 mount 瞬间
存在 A 的 fallback 规则命中 B 树同名组的错误牌面闪现窗口(已用 registry 级测试与
harness 五阶段验收锁死)。不做任何 DOM 轮询。"少一点视觉个性"优于"Session A/B
串样式";降级只控制规则输出——牌面分配(bag)、completedFaceSets、变体样式资产
全部保留,会话数回落到 1 时立即恢复该会话的规则,无需刷新。不声称 rc.1 多会话
可完全隔离。 - none(≤0.1.5):没有会话内容锚点,无 sessionId 的规则块只在唯一会话时输出。
规则 selector 以"完整 host"为单位逐支生成(0.2.0-rc.2 真机验证):
[data-conversation-session="A"] [data-step-process][data-chat-group-key="X"]:not(:has([data-shimmer="true"])) … [data-step-process-icon]::before { … },
[data-conversation-content]:not([data-conversation-session]) [data-step-process][data-chat-group-key="X"]:not(:has([data-shimmer="true"])) … [data-step-process-icon]::before { … }
两支都是完整 selector:official 支在 rc.2+ 命中,回退支因 :not() 恒失败永不命中;
rc.1 上只有回退支命中。插件不写官方 DOM、不加任何官方属性。
Legacy Step Compatibility Layer(hybrid 宿主:官方 Turn + 插件 Step)
0.1.2-rc.1 ~ 0.1.6-alpha.2 的最终形态:
Official Turn Fold(官方 owner state:foldable / open / setOpen)
↓
Enhanced Turn UI(插件 Turn 栏 / Poker / 指标 fallback)
│
└── Legacy Step Adapter(官方没有 Process Group 契约)
↓
Plugin-owned LegacyStepHeader(挂官方 Poker skin 钩子)
↓
Shared Poker / Native / Settings(同一套视觉资产)
归属边界(硬不变量):Turn 的 Fold 引擎是 DSH 官方(Legacy 层绝不触碰
turnProcess.open / Turn membership / Turn hide / 不注册 turn-process shadow);
Step 的 Fold 引擎在 hybrid 宿主上由插件 Legacy Adapter 补位——只因为官方没有
Step 分组契约(没有 Process Group / ChatGroupSeat / ProcessGroupHeader /
views.grouped / groupSource / data-step-process 组条)。两组状态完全正交:
官方收起整回合时插件什么都不做,再展开时 Legacy Step 保留自己的 open 状态(测试锁死)。
Modern 宿主(0.1.7+)绝不运行 Legacy 层:stepFold 由渲染期契约探针定论为
modern → activateLegacyStepEngine 的安装点永不进入 → shadow 注册数 0、
legacy 组计算计数恒 0(测试锁死)。
数据层(纯函数,无 DOM/无文字匹配/无轮询):legacyBuildStepIndex(snapshot) 从
官方 useChat 快照(具名 selectLegacyChatSnapshot,仅 Legacy shadow 消费)单遍切段:
段 = 两个 text 之间的 tool-call + 含 reasoning 的 assistant-step 混排;think 不打断段、
text 是唯一闭合标记;todo_write 排除工具不入段;user / context / 纯 text 节点是边界
而不是成员(最终答案永远在 Step Fold 之外、user 消息永不可被隐藏)。索引按 snapshot
身份 WeakMap memo(每次快照发布最多构建一次,节点查询 O(1)——不是每 node O(N))。
组身份 = anchor 节点官方稳定 key(非数组下标/随机数),open/close、rerender、
分页 remount 期间稳定;回合已结束(legacy.turnEnds)强制段闭合(中断/出错不再永久
running)。Legacy 组模型:{ leaderKey, keys, memberKeys, toolCount, lastToolName,
turn, closed }。
视觉层共享:Legacy Header 是插件自己渲染的 DOM(data-tf-legacy-step /
data-tf-legacy-session 身份标记),它主动渲染与官方组条相同的视觉钩子
(data-step-process / data-chat-group-key / data-process-activity /
aria-expanded / data-step-process-icon)——Step Poker skin(含 running 平面旋转 35.5° 轮换:
running 时在插件自有按钮上置 data-shimmer="true",皮肤 :has() 直接命中)、3/5 张
threshold、per-session shuffle bag、completedFaceSets 变体资产、morph/knockout 全部
原样复用(逐组规则经 buildStepCardRulesCss(..., "legacy") 的 surface 适配:scope 前缀
换成 [data-tf-legacy-session=""],单分支、天然精确——不依赖官方
session DOM 锚点,0.1.2/0.1.5 的 sessionScope=none 也能做到真正的会话隔离)。
Native 模式:skin 总闸关闭时 Legacy Header 仍是功能载体(自带 chevron + 标题,
折叠语义不变,plugin 基础 CSS 提供布局壳)。Modern 官方 ProcessGroupHeader 与插件
LegacyStepHeader 是两种不同的 DOM,共用同一套 Poker 资产——0.1.2 没有官方组条。
成员可见性(FAIL OPEN):running 段成员恒可见(绝不隐藏正在生成的内容);completed
段默认收起,点击 Header 展开(legacyStepOpenBySession:sessionKey+leaderKey 隔离的
插件自有 open store,与 turnProcess.open 无关)。隐藏挂在插件自有的原地 wrapper 上
(hidden="until-found" + beforematch → 自动展开,浏览器搜索命中不外泄;旧内核安全
回退普通 hidden)——原官方内容经 priority-0 builtin 委托渲染(原 renderer 原样调用、
内容零复制零变形),think+text 成员只折叠 think 部分、text 正文段外恒可见。session 状态
(open store / 共享牌面分配)在该 session 最后一个 Legacy 面卸载后(微任务确认,
与 modern 桥共享同一清理门)才清理。
Installation safety(原子安装;FAIL OPEN 的准确含义):Legacy Step backend 的