
DeepSeek Harness 中 Workflow 运行记录的状态驱动披露机制WorkflowRunPanel 展开、聚焦保护与确定性重建全解析【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness本文基于 DeepSeek Harness 仓库中已落地implemented的实现笔记 2026-08-11-workflow-run-status-driven-disclosure.md完整拆解 Web 客户端ui-workflow-run插件如何让一条 durable 的 workflow 运行记录在会话中原地生长从运行中前缀演进为终态记录同时既不反复覆盖用户主动收回对话空间的折叠决定又能对新工作、异常结果和正常完成保持足够的存在感。读完本文你将掌握WorkflowRunPanel的三态披露状态机、聚焦安全折叠、aria-disabled延迟停用等核心实现并理解它为何能仅凭既有事实即可在刷新与历史回放后重建出确定性的初始状态。背景为什么披露决策必须由状态驱动在 DeepSeek Harness 的 Web 客户端中一次顶层 workflow 运行例如一次多成员协作会以一条独立的 Chat 节点形式持久化地出现在会话里。这条节点不是静态快照它从一个只有run-start的运行中前缀开始随着成员逐个启动、逐个结束、最终整轮运行结束原地更新成一条包含完整成员与终态的记录。也就是说同一个 DOM 实体在生命周期内反复变更事实。于是渲染层面临一个矛盾关联笔记 的 Problem 一节新的工作、异常结果、正常完成都必须吸引用户注意——折叠得悄无声息会让用户错过关键生命周期变化但用户一旦主动折叠某个层级以收回对话空间后续普通更新不能反复把用户的折叠决定顶回去渲染器已经收到了 durable 生命周期中的每一条事实workflow Conversation Node 会消费全部相关事件因此披露选择disclosure choice理应归属于挂载层自身而不需要新增任何事件或持久化字段。同时还有一个生命周期约束外层 run 被隐藏时其内部的 phase 内容会被卸载unmount而卸载通常不会产生可靠的 blur 事件披露逻辑必须在这种场景下依然保住嵌套的 phase 选择并且永远不能移除仍持有键盘焦点的内容。这套问题催生了 WorkflowRunPanel.tsx 中的核心设计每个层级run 与每个 phase各自维护一份局部披露状态状态迁移完全由当前 durable 事实驱动仅在少数明确的边界上执行一次性自动开合。数据源头四条 durable 事件如何折叠成一条节点披露状态机的输入是 workflow 运行过程中沉淀下来的四条会话事件。它们由模型侧的 workflow 工具写回父会话packages/workflow/tool-workflow/src/types.ts事件类型含义关键字段tool-workflow/run-start打开一条顶层运行记录runId、nametool-workflow/agent-start记录一个已发布子会话的成员runId、seq、label、phase?、childIdtool-workflow/agent-end结算一个已启动成员runId、seq、outcomecompleted / cancelled / failedtool-workflow/run-end资源静默后关闭整条运行runId、stopReasoncompleted / cancelled / error浏览器端插件 index.ts 以 Cordis 副作用的形式注册三样东西一个ConversationNodeDefinition、一份workflowRun中英文词典、一个挂载在 keyed 槽位conversation.chat.node上的WorkflowRunPanel渲染器移除客户端入口时三者会一起被回收。测试 workflow-run.client.spec.tsxplugin lifecycle用例组专门验证了 Definition、slot 与 locale 的注册/销毁对称性。事件到数据模型的折叠逻辑位于 workflow-definition.ts匹配matchrun-start作为role: start其余三条事件以runId归并到同一条记录的update角色状态累加updateagent-start追加成员seq作为稳定标识agent-end按seq回填outcomerun-end记录stopReason投影buildViewNode → projectWorkflow把成员按 phase 分组成WorkflowRunChatData——只包含name、status、phases三个字段不带任何脚本、输出、日志或拓扑。这里有两个值得注意的细节1. phase 键的碰撞防御。分组键由 workflowPhaseKey 生成null字段缺失映射为字面量missing而空字符串被编码为value:0:。这样未携带 phase 字段与phase 为空字符串被视为两种不同的身份不会被 React 的 key 机制合并本地化文案上分别呈现为未分阶段与空阶段名见 locales.ts。2. interrupted 的推导。当一个成员没有outcome且其所属的 turn/step 位置已经关闭locationClosed该成员被投影为interrupted同理运行没有stopReason但位置已关闭时整条运行也是interrupted。这正是 workflow-run.client.spec.tsx 中 shows missing terminal facts as interrupted 与 folds same-phase cancellation and a turn-level interruption 两个用例验证的行为——异常状态不依赖新事件而是从既有事实推断。另外事件流以追加方式持续到来agent-start只追加不删除因此成员的顺序、分组乃至失败成员都会被完整保留结算只改变状态而不改变结构README 中的 settlement changes status without removing or reordering members。披露状态模型三态模式 追加式成员数WorkflowRunPanel在自身内部维护整份披露状态WorkflowRunPanel.tsxtype DisclosureMode clean | running | abnormal interface DisclosureFacts { readonly mode: DisclosureMode readonly activityCount: number } interface DisclosureState extends DisclosureFacts { readonly open: boolean readonly pendingCleanCollapse: boolean }DisclosureFacts是纯事实每个时刻从 durable 数据重新推导DisclosureState是事实加上用户/自动动作留下的选择痕迹open与待执行的折叠pendingCleanCollapse。状态推导规则在 phaseDisclosureFacts / runDisclosureFacts 中体现phase 三态判定任一成员failed / cancelled / interrupted→abnormal否则有任一成员running→running否则所有成员 completed→cleanrun 三态判定自身状态异常或任一 phase 异常 →abnormal自身running或任一 phase 为running→running只有当整条运行与所有 phase 都正常完成才为clean。activityCount是追加式成员数phase 为成员数run 为所有 phase 成员数之和它构成了一个轻量的活动纪元替代品只要计数变化而模式仍为 clean就说明一个完整活动周期在单次渲染内完成后文详述。初始状态由 initialDisclosureState 决定挂载时打开running与abnormal层级、关闭clean层级pendingCleanCollapse置空。因此初始事实初始行为run / phase 处于 running展开吸引对进行中工作的注意run / phase 处于 failed / cancelled / interrupted展开异常必须可见run / phase 全部 completed折叠正常完成归还对话空间零成员且 run 仍 runningrun 展开显示没有启动成员零成员且 run completedrun 折叠这一点在测试 derives the zero-member running and completed states from the current run status 中有直接断言空运行在 running 时展开、completed 后折叠点击头部可随时手动展开。状态机核心advanceDisclosureState 的转换规则每次 durable 事实更新advanceDisclosureState 都会基于当前状态 新事实 焦点位置算出下一个状态。它的规则可以拆成四条边界function advanceDisclosureState( current: DisclosureState, facts: DisclosureFacts, focusWithin: boolean, ): DisclosureState { const sameFacts current.mode facts.mode current.activityCount facts.activityCount if (sameFacts) { if (!current.pendingCleanCollapse || focusWithin) return current return { ...current, open: false, pendingCleanCollapse: false } } if (facts.mode clean) { const deferCollapse current.open focusWithin return { ...facts, open: deferCollapse, pendingCleanCollapse: deferCollapse } } if (current.mode clean || (facts.mode abnormal current.mode ! abnormal)) { return { ...facts, open: true, pendingCleanCollapse: false } } return { ...facts, open: current.open, pendingCleanCollapse: false } }规则一普通更新保持用户选择。只要模式与成员数都未变sameFacts状态原样返回——除非存在一个等待中的折叠且焦点不在内容内此时完成折叠。这意味着 running / abnormal 区间内的例行成员增删、状态微调永远不会触碰用户的展开/折叠决定。规则二进入 clean 只折叠一次。从任何状态进入clean时若当前是展开的则设置pendingCleanCollapse并视焦点位置决定是否立即折叠焦点在内容内部则延迟否则立刻关闭。而每次运行或 phase 正常完成正是进入 clean 的唯一路径从而保证了正常完成关闭一次的语义——不会在后续的 clean 更新中反复开关。规则三重新开始活动只打开一次。从clean进入running/abnormal或首次进入abnormal从 running 升级为 abnormal状态被强制打开一次。结合规则一之后的异常演进例如从 failed 追加 cancelled 成员不会再打扰用户。规则四同一 key 的新活动周期。在useLayoutEffect中WorkflowRunPanel.tsx若某个 phase 之前是 clean而新事实变成非 clean 或成员数增加phaseStartedCycle同时 run 层仍处于 active 且当前是折叠的则外层 run 也被打开一次。这保证了用户折叠了已完成 phase 后同一个 phase key 下又启动新成员时phase 与其外层 run 会一起再次展开以暴露新工作——测试 folds each normal completion once and opens a new same-key activity cycle 完整覆盖了这条链路reviewed完成后折叠、new成员到来后 run 与 phase 双双自动展开。边界特例单次渲染内到达的完整 clean 周期。如果一个 phase 保持 clean 而成员数增加activityCount变化代表一个完整活动周期在单次渲染内完成此时已打开的 phase 审阅视图被关闭一次同时若 run 仍 active 则外层 run 打开一次以便用户看到更新后的汇总折叠的 phase 头部的成员数与聚合状态。这不引入活动纪元activity epoch或任何 durable 字段纯粹靠追加式成员数完成。测试 refolds a phase when a complete activity cycle arrives as one clean update 断言run 折叠时收到一个新增 completed 成员run 自动展开而 phase 保持折叠用户能看到新汇总。自动动作之后鼠标、Enter 与 Space 恢复对层级的完全手动控制直到下一个明确的边界事件打开/关闭/异常升级再次触发一次性自动行为。聚焦保护pendingCleanCollapse 与安全折叠正常完成要折叠内容但如果焦点仍在内容内部直接卸载会让键盘用户凭空消失。为此 WorkflowRunPanel.tsx 实现了完整的延迟折叠协议焦点检测focusIsWithin 用element.contains(ownerDocument.activeElement)判断焦点是否位于内容子树内延迟折叠进入 clean 且内容持有焦点时层级保持展开、仅挂上pendingCleanCollapse标志失焦结算run 内容区与每个 phase 内容区都挂了onBlursettleRunBlur/settlePhaseBlur当relatedTarget落在内容之外时调用 collapsePending 完成折叠外层隐藏兜底外层 run 折叠会卸载 phase 内容而不产生可靠的 blur因此useLayoutEffect在每次渲染后统一处理所有 phase 的pendingCleanCollapse——这就是笔记中outer hiding unmounts Phase content without a dependable blur event, so this edge settles deferred closes注释对应的实现防止 header 抢焦点preventPendingHeaderFocus 在onMouseDownCapture阶段拦截指向 disclosure header 的鼠标按下避免一次指针失焦 点击 header被拆成两个动作后点击被折叠竞态吞掉。测试对这套协议覆盖得非常细defers normal completion collapse until focused member content loses focus——成员持有焦点时 rerender 为 completedrun 与 phase 保持展开成员按钮以aria-disabled形态留存焦点离开后才整体折叠handles a pointer blur and header click as one pending-completion close——mouseDown在保留焦点时返回true、指向待折叠 header 时被preventDefault返回false随后点击 header 只关闭该层settles pending completion when keyboard focus moves from content to its header——键盘焦点从内容移到 phase header 时先折叠 phase再从 phase header 移到 run header 时折叠 runsettles a deferred phase close when the user hides the outer run——焦点在内容内、phase 待折叠时点击外层 run header再次打开后 phase 已按计划折叠。MemberRow焦点保留与 aria-disabled 延迟停用成员行本身是一个可导航目标只有运行中的成员能打开子会话。当成员正在运行且满足全部条件时MemberRow渲染为可点击按钮而一旦成员变为终态导航资格随之消失。问题在于如果成员恰好在持有焦点时变成终态直接把按钮换成静态行会把焦点从用户手指下抽走。解决方式是延迟停用MemberRow成员终态后只要按钮仍持有焦点就保留同一个按钮 DOM但以aria-disabledtrue、tabIndex{-1}、无onClick的方式呈现直到blur触发setFocused(false)之后才渲染普通的非交互行。这既保护了活动 DOM 焦点目标又彻底杜绝了终态成员的可导航性还不需要引入一个焦点管理器。测试 defers normal completion collapse until focused member content loses focus 断言了aria-disabled与焦点保留随后失焦后打开 worker按钮消失、只剩静态行文本。可导航判定集中在 navigableMembers五个条件缺一不可成员自身状态为runningchildId存在于普通会话列表ordinary Session list会话行origin subagent会话行parentId等于当前会话会话行仍处于 running。远端行、仅寻址行、父会话不符、列表已终态、成员已终态等一律不渲染导航按钮。测试 does not navigate when %s 用五组参数逐一验证了这些拒绝路径而 opens only a running ordinary-list subagent proven to have this parent 与 promotes a running member when its ordinary Session row arrives 验证了正向路径会话行到位后按钮才出现。外层隐藏与重挂载选择如何被保存、又如何被重置关键架构决策是phase 的披露状态保存在WorkflowRunPanel的本地状态里useStateWorkflowDisclosureState而不是放在 phase 自身的披露内容中。因为外层 run 隐藏时会卸载 phase 内容若状态放在内容内部同一挂载记录内的独立 phase 选择就会被静默丢弃。现在折叠外层 run → phase 内容卸载但状态保留 → 重新打开 run 时每个 phase 的展开/折叠选择原样恢复测试 keeps clean sibling phases independent... 中先折叠 run 再展开两个 phase 各自的选择互不干扰地复原某个 phase 从事实中消失 → 其状态条目被删除渲染器重挂载remount→ 所有层级从当前 durable 事实重建初始状态而不是恢复旧选择——这保证了同一份持久记录在刷新、历史回放后总是重建出确定性初始状态测试 reinitializes manual choices from durable facts after a renderer remount 断言重挂载后运行中事实恢复为全展开。这种状态就地保留、重挂载即重置的边界是设计意图而非疏漏本地生命周期刻意不在重挂载后记忆用户选择跨刷新、跨设备、跨用户记住选择需要单独的持久化与陈旧选择决策不应被隐式地并入这份纯表现状态。验证组件测试与 Web 端到端回放披露状态机是纯粹的确定性函数因此组件测试可以穷举驱动workflow-run.client.spec.tsx 的WorkflowRunPanel用例组覆盖初始 running 控件、鼠标与键盘Enter/Space选择、普通 running 更新、外层隐藏与恢复、phase 完成、run 完成、clean 审阅、同 key 新活动、单渲染批处理的 clean 周期、全部异常状态、首次异常升级、后续异常更新、零成员完成、焦点成员完成、兄弟 phase 独立性、渲染器重挂载同时验证延迟焦点路径稳定后终态成员依然不可导航。面板层复用deepseek-ai/dsh-client-ui-primitives的DisclosureRowStatusDisclosure只是加上expandable渲染器本身不新增 DisclosureRow API。workflow-run.e2e.ts 是keyless shipped-Web 验收复用snapshots/session/workflow-run/下录制好的父子会话夹具在真实 workflow 工具、worker、子代理提供方、Session 日志、浏览器插件图与子会话导航全链路中回放。它折叠并重新展开 run 与 phase 控件、记录折叠后的状态汇总与 ARIA 状态、验证正常结算把两层都折叠、确认终态审阅无法导航成员、并在刷新后核对折叠历史快照比对见 snapshots/web/workflow-run/ 的ui-live.expected.md与ui.expected.md。被否决的备选方案关联笔记 记录了四条被明确否决的路径理解它们有助于把握当前设计的取舍备选方案否决理由把每个 running/abnormal 层级强制渲染为静态展开行注意力状态无法被用户消除且破坏了真实的鼠标、键盘与 ARIA 披露语义用首次渲染初始化一份单一手动状态后续活动、异常升级与正常完成将无法执行各自的一次性自动动作让每个 phase 在自己的披露内容内部持有状态外层 run 隐藏会卸载该内容同一挂载记录内的独立 phase 选择被丢弃持久化展开状态、确认回执或活动纪元当前 workflow 事实与追加式成员数已提供全部所需边界持久化会引入第二个 durable 属主和同步语义纯表现选择不需要后果与边界最终交付的行为README 同步描述workflow 记录对生命周期变化保持存在感、同时在任何状态下都可被用户消除正常完成自动归还对话空间当前焦点始终安全嵌套 phase 选择在外层隐藏后幸存同一份 durable 记录在刷新或历史回放后重建出确定性初始状态。渲染器不新增 Session 事件、store、设置、确认回执、计时器、自动滚动、持久活动身份或 DisclosureRow API也不改动 workflow 状态推导、phase 分组、成员顺序、导航资格、文案或视觉令牌。需要留意的是它的使用边界只有经由dsh-tool-workflow的顶层调用才会产生这些记录嵌套 PTC 模式调用与直接消费WorkflowEngine的路径不会导航刻意只对 live 成员开放节点只展示运行、phase、成员身份与状态脚本、输出、错误、日志、用量、静态拓扑与控件都在这层表面之外。若未来需要跨刷新记住用户的展开选择那将是独立的持久化与陈旧选择决策而不是这份表现状态的隐式扩展。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考