角色指令
从页面任何角落,告诉同一个智能体的所有角色它正在做什么。
工作中
idle
两张登记表,一个 Provider
MarkStageProvider 已经维护一张按外观索引的登记表:跑多少个引擎、哪些角色镜像哪一个。指令登记表是它按智能体索引的另一半。它记录每个智能体正在做什么——这才是应用真正关心的事——订阅了该智能体的每个角色都会跟着变。名册行、聊天页头、悬浮卡片可以在树的不同角落谈论同一个智能体,既不必把状态提升到共享的 React state 里,也不会引来随之而来的重渲染风暴。
useMarkCommands() 返回写入端。没有 Provider 也能用:回退登记表只是没有本树之外的订阅者。
事实与钉住
两种写入,分量不同:
holdFacts(agentId, facts)发布已知的事实——running、composing、activity。状态是在读取时由characterStateForRoster从事实推导出来的,所以同一组事实在聊天里是composing,在头像上是idle。第二次调用会替换第一次——事实按值比较,重渲染传来的同样内容不会通知任何人。返回的释放函数只清掉自己设下的事实;releaseFacts则不管是谁设的都清掉。holdState(agentId, state, { priority, ttlMs })把一个状态钉在事实之上。优先级最高者胜出,平手时最近的钉住赢,ttlMs到时自动松开——正适合一场不该赖着不走的庆祝。返回一个释放函数;releaseState清掉该智能体的所有钉住。
读取走 useMarkCommand(agentId, surface),它让一个组件订阅一个智能体,返回 MarkCommandSnapshot:解析后的 state、胜出的 held 状态(状态来自事实时为 null)、原始 facts、emphasis 标志,以及每次 poke 都递增的 pokeSeq 计数——宿主响应它的变化,而不是它的值。
让动作诚实的几个 hook
组件内部用到的三个小 hook,导出给自己搭界面的宿主。
原始 working → 显示 working
戳我
入场中…
useMarkSettle(state, { settleMs })在输入停止变化满settleMs之前一直保持上一个状态(默认SETTLE_MS,1400 毫秒;0关闭)。事实抖动很常见——工具启动、80 毫秒后返回、又一个启动——每一次都渲染,就是一排脸在闪,看起来像出了故障。usePokeCycle(ref)返回一个函数,每次调用在句柄上跑下一个花样:转圈、跳一下、彩纸、再转圈。永远转圈到第二次点击就不好玩了。POKE_COOLDOWN_MS(340 毫秒)内的重复戳会被忽略;ref 上没有引擎时返回false——镜像的StagedMark与别人共享动画,不能独自动,调用方可以改为让外层容器跳一下。useDeadline(ms, deps)在依赖上次变化后满ms之前是false,之后是true。它省去了入场动画自带的setTimeout加 ref。
API
useMarkCommands
返回 MarkCommands:
useMarkCommand
useMarkCommand(agentId, surface = 'list')——订阅一个智能体,拿到它的 MarkCommandSnapshot。
useMarkSettle、usePokeCycle、useDeadline
见上文。createMarkCommandRegistry() 构造一张独立登记表,供测试或自己提供 MarkCommandContext 的宿主使用。
接下来
- MarkStageProvider——按外观共享引擎
- 桌面伙伴——活几个小时又不吵