CodeWhale TUI 运动契约(Motion Contract)源码解析:Full / Reduced / Still 三态动画策略与流式显示时钟

CodeWhale TUI 运动契约(Motion Contract)源码解析:Full / Reduced / Still 三态动画策略与流式显示时钟 CodeWhale TUI 运动契约Motion Contract源码解析Full / Reduced / Still 三态动画策略与流式显示时钟【免费下载链接】CodewhaleOpen-source coding agent for your terminal, built in Rust and on a journey of continuous community improvement. Issues and PRs welcome.项目地址: https://gitcode.com/GitHub_Trending/de/Codewhale导读本文以 docs/MOTION_CONTRACT.md 为骨架深入 CodeWhale 终端 TUI 的动画与渲染节奏控制体系。CodeWhale 的水下主题终端包含大量装饰性动画环境光斑、鱼群游弋、Braille 状态转轮以及模型响应流式文本的展示节奏Motion Contract 正是约束这些运动的中央宪法。读完本文你将掌握三档运动模式Full/Reduced/Still的语义差异、MotionPolicy从设置派生的决策链、StreamDisplayClock如何把 Provider SSE 增量合并为 16ms 显示节拍以及仓库里追帧机制只搭建未接线这一诚实性约束背后的工程取舍。一、什么是 Motion Contract水下 TUI 的中央运动策略在 CodeWhale 的 TUI 中运动分为三类装饰性环境动画decorative ambient如水下光斑、鱼群、状态转轮status spinner工作进行时的占位标记与流式文本展示节奏streaming display cadence。Motion Contract 的中心策略代码位于 crates/tui/src/tui/motion/其模块注释明确写道All decorative motion, status spinners, and streaming display cadence should ask this module what is allowed. Ad-hoc shimmer/spinner timing outsideMotionMode/FrameRequesteris a regression.即所有装饰运动、状态转轮与流式显示节奏都必须向该模块请示什么被允许任何绕开MotionMode/FrameRequester的临时自写动画定时逻辑都被视为回归缺陷。这一设计把是否允许动策略与具体怎么动帧表、时钟解耦避免散落的thread::sleep、随机抖动或各自为政的刷新循环破坏整套终端的视觉纪律。二、三大运动模式语义总表MotionMode定义在 crates/tui/src/tui/motion/mode.rs契约对三种模式的语义划分如下模式装饰性环境动画状态转轮流式文本Full有动画 Braille 帧稳定的 ~60 FPS 显示时钟16 ms追赶catch-up是分阶段搭建的并非已生效行为——见下文的诚实性说明Reduced无静态平静字形同一个显示时钟——不是慢速打字机无追赶突发Still无静态箭头chevron仅状态变更时重绘流式文本仍在显示时钟上聚合输出三种模式共享两条关键原则Provider SSE 增量是输入永远不是动画定时器。模型推送的每个小块文本只决定有什么内容要显示绝不直接驱动渲染帧率。StreamDisplayClock位于tui/streaming负责聚合这些增量FrameRequester负责合并装饰性唤醒帧。而整个 TUI 中唯一允许发出terminal.draw的只有主ui轮询循环——契约明确禁止新增任何竞争性动画循环do not add a competing animation loop。三、策略派生MotionPolicy::from_settings3.1 决策树与源码MotionPolicy是运行时解析后的策略由三个布尔来源派生对应 crates/tui/src/tui/motion/mode.rs 的from_settingspub fn from_settings( low_motion: bool, fancy_animations: bool, constrained_frame_rate: bool, ) - Self { let mode if low_motion { MotionMode::Reduced } else if !fancy_animations { MotionMode::Still } else { MotionMode::Full }; ... }决策顺序为low_motion true→Reduced优先否则fancy_animations false→Still否则 →Full。注意第三个参数constrained_frame_rate终端兼容性帧率上限不参与模式选择。其语义在源码注释中写得很清楚终端兼容性限制只能降低重绘频率绝不能伪装成无障碍偏好也不得取消已被授权的运动Terminal compatibility limits may reduce redraw frequency, but must not masquerade as an accessibility preference or disable authored motion。这正是语义降级与性能降频分离的体现帧率压缩走min_frame_interval()/uses_constrained_frame_rate()通道而视觉语义由MotionMode单独承担。3.2 对应的设置项与默认值low_motion与fancy_animations是持久化设置键定义在 crates/tui/src/settings.rs默认值分别为false与true见 crates/tui/src/settings.rs。两者的字段文档doc comment本身即是契约的一部分low_motion减少装饰性运动。绝不允许合成模型文本速度两种模式下流式文本都跟随上游增量。fancy_animations启用富有表现力的实时状态运动。仅影响界面装饰与状态提示模型文本始终跟随上游流增量。在设置面板架构中这两个键归属同一 Motion 分组schema 登记在 crates/config/src/settings_schema.rsTypeScript/配置表面可通过low_motion别名motion与fancy_animations别名fancy/animations读写相关命令别名登记在 crates/tui/src/commands/groups/config/config.rs。此外还有运行时叠加层会强制low_motion从源码看tmux 等TERM_PROGRAM会话、SSH/Termius 会话以及传统 Windows 控制台宿主会被自动探测并叠加low_motion fancy_animationsfalse见 crates/tui/src/settings.rs 的detect_low_motion_override与 crates/tui/src/lib.rs 的会话说明。这类运行时叠加正是from_settings中force_reduced一类参数的来源场景。3.3 MotionPolicy 的核心判答MotionPolicy把模式解析成一组可供组件直接调用的方法crates/tui/src/tui/motion/mode.rsallows_decorative()/should_request_animation_frames()仅Full返回 true——决定环境动画与转轮动画帧是否允许存在allows_catch_up_bursts()仅Full返回 true——Reduced/Still永不允许追赶式突发刷新min_frame_interval()非Full或帧率受限时返回LOW_MOTION_MIN_FRAME_INTERVAL30 FPS ≈ 33.33 ms否则返回MIN_FRAME_INTERVAL120 FPS ≈ 8.33 ms常数定义在 crates/tui/src/tui/frame_rate_limiter.rsstream_commit_interval()无论何种模式一律返回DEFAULT_STREAM_COMMIT_INTERVAL——这正是Reduced 不是慢速打字机的代码级保证对应的单元测试reduced_is_semantic_not_slow_typewriter断言两种模式的流式提交间隔严格相等见 mode.rs 测试as_low_motion()历史兼容桥把Reduced/Still统一折算成旧的low_motion布尔量供 history/streaming 等尚未迁移的宿主使用。四、Spinner 的表态规则先赚取再动画4.1 共享帧表Braille 转轮的帧表统一收在 crates/tui/src/tui/spinner.rsBRAILLE_SPINNER_FRAMES是 8 帧上浮点阵[⠀,⢀,⣀,⣄,⣤,⣦,⣶,⣿]另有独立的验证勾选帧表VERIFY_TICK_FRAMES。这样做的目的是让转写工具卡片、侧栏以及任何运行中任务表面以同一节拍推进不会各自加速。帧率常数为 200 ms/帧即 5 Hz注释说明这是 v0.9.4 从 8 Hz125 ms降频校准的结果——5 Hz 下点阵填充仍可读作连续运动却消除了高频闪烁带来的烦躁感见 spinner.rs。4.2 赚取规则与静止字形Motion 的一个重要细节是运动标记只有在工作存活超过人眼快速事件窗口后才出现快速完成的工作应当直接落地为结果凭证receipt而不是闪一个转轮。LIVE_MARKER_DELAY_MS 400就是这条门槛spinner.rs。在 400 ms 之前界面只显示静态箭头LIVE_STATIC_MARKER ›超过后才开始推进 Braille 帧。三种模式对应的呈现由MotionPolicy::spinner_presentation(earned_live_marker)决策mode.rs条件呈现Full且已赚取标记Animate——使用共享 Braille 动画表Full但尚未赚取StaticChevron——保持 400ms 前的静态箭头›ReducedStaticCalm——固定平静字形⣤BRAILLE_SPINNER_STILL_FRAMEStillStaticChevron——静态箭头契约建议消费方优先使用MotionPolicy::spinner_glyph/spinner_presentation而非直接读low_motion布尔量mode.rs 中#[allow(dead_code)]标记即表明这些是给宿主 picker/部件预留的公开接口帧表本身则留在 crates/tui/src/tui/spinner.rs。测试还对每个活动帧做了unicode_width 1的断言确保转轮帧永不挤动相邻文本见 spinner.rs 测试。五、流式显示时钟Provider Delta 只是输入模型流式输出时Provider 会以几十个微小 SSE 分片到达。如果每个分片都直接改写可见转写记录界面会在几百毫秒内以数百帧每秒重绘。StreamDisplayClockcrates/tui/src/tui/streaming/mod.rs解决的就是廉价吸收 节拍提交DEFAULT_STREAM_COMMIT_INTERVAL 16 ms约 60 FPS这是把排队增量写入可见转写记录的标准节奏每次提交节拍commit beat将自上一拍以来收到的全部内容一次性冲刷进转写文本即StreamBuffer::take()mod.rs。没有逐字打字机也没有自适应排空策略flush_now()用于流结束时的最终冲刷StreamingState按内容块维护独立缓冲区thinking 与正文分开累积且accumulated_text/accumulated_thinking始终跟踪完整原始流与界面节奏无关——重试或构造 API 消息时看到的是模型真实输出。该模块的数据流如注释所示raw delta - StreamBuffer.push_delta - commit beat - take - transcript。契约特别强调流式文本必须仍在显示时钟上聚合输出stream still coalesces on the display clock。也就是说即使Still模式下没有动画帧文本内容也不能退化成一次性的整段闪出——它仍以相同的 16 ms 节拍被送入转写记录区别只在于没有动画特效。对应测试still_disables_spin_but_keeps_display_clock断言Still模式的stream_commit_interval()仍等于默认提交间隔。六、诚实性说明追赶机制是分阶段搭建尚未接线这是 Motion Contract 中最值得细读的工程诚实条款。6.1 追帧catch-up现状源码中确实存在完整的追帧能力阈值常数CATCH_UP_QUEUE_DEPTH 160排队增量数与CATCH_UP_OLDEST_AGE 1200 ms最老分片年龄定义于 streaming/mod.rsnote_delta_with_backlog(now, queued, oldest_age)会在 backlog 越过阈值且allow_catch_up为真时把下一拍提前到nowmod.rsset_allow_catch_up(...)由运动策略驱动事件循环中以stream_display_clock.set_allow_catch_up(motion_policy.allows_catch_up_bursts())接线crates/tui/src/tui/ui/event_loop.rs。但契约明说它从未真正触发。因为每个生产排空点目前调用的都是note_delta——而note_delta只是note_delta_with_backlog(now, 1, None)的简写即排队深度恒为 1mod.rs。深度 1 永远达不到 160 的阈值因此Full与Reduced实际上以完全相同的稳定时钟流式输出Full 模式的追帧从未发生过。契约对此的处理是明确给出边界在ui.rs排空点真正喂入队列深度 / 最老分片年龄度量之前不得把 catch-up 描述为已生效行为标记为 TUI-DOG-017 后续工作。换句话说文档、代码与注释三者共同维护了一条未完成功能不得被宣称可用的纪律。6.2 已被删除的自适应分块策略契约还记录了一段设计回溯一个与追帧无关的自适应分块策略streaming/chunking.rs及一个LineBuffer换行门控已在 v0.9.4 被删除。原因很干脆——它只可能决定排空一切可用内容且两个LineBuffer构造器都绕过了那道换行门控因此它保护的防线实际上不存在。替换方案是提交节拍无条件冲刷自上拍以来收到的全部内容而永远不显示半个未闭合代码围栏的换行边界安全改由下游增量式 Markdown 解析器负责——ParseState::commit_complete_lines见 crates/tui/src/tui/markdown_render.rs保留尾部未完成行不提交、并在每个节拍重新解析它。契约强调新行边界安全真正生效的地方就是那里which is where it is actually in force。这段历史是防御性抽象若从未真正生效就应删除而不是保留的典型样本也解释了为何StreamBuffer的职责被刻意收窄为纯粹的字符串聚合。七、唯一绘制出口主 poll 循环与 FrameRequester契约的架构约束之一是不允许出现第二个动画循环。所有terminal.draw都由主ui轮询循环发出装饰性动画唤醒则由FrameRequester合并。FrameRequestercrates/tui/src/tui/motion/frame_requester.rs是一个合并式帧请求调度器各部件调用request_frame/request_at表达未来想要一帧调度器把多次请求合并为至多一个唤醒截止时刻取最早到期者并统计request_count/emit_count供遥测take_due在到期后返回 true通知主循环设置needs_redraw仅用于动画不用于状态变更clamp_to_frame_cap用帧率限制器把动画唤醒钉在绘制上限之内。在Reduced/Still下request_frame直接返回不安排装饰帧状态变更重绘仍走needs_redraw通道。测试coalesces_multiple_requests_into_one_emit验证了 20 次请求只产生 1 次发出reduced_motion_drops_animation_frame_requests验证降级模式会丢弃动画帧请求。帧率上限本体在 frame_rate_limiter.rs默认 120 FPS约 8.33 ms低运动模式下切换到 30 FPS约 33.33 ms。注释给出了动机——当模型流式输出超长回复时每个 SSE 分片都会置位needs_redraw若不设上限主循环可能在几百毫秒内以每秒 300 帧全屏重绘而人眼根本无法感知超过约 60-120 FPS 的帧率ratatui 的 diff-and-flush 却要付出真实的换行、样式与 crossterm 排队成本。八、一次性阶段过渡One-shot Phase Transitions除了持续动画契约还规范了一类一次性过渡效果见 docs/MOTION_CONTRACT.md 的 One-shot phase transitions 一节回合成功凭证的沉降一轮成功的回合会记录该回合所拥有的第一个历史索引工具与 Agent 凭证receipt保持最终几何与顺序不变由一个有界的 70 ms 交错淡出短暂压暗每行后再落定。Reduced/Still跳过该处理、立即显示最终凭证——它们是语义即正确的降级而非删减内容。Ombre 深度随阶段ShellPhase变化working 阶段柔和加深verification 阶段偏向实时的表面墨色而 waiting、approval、failure 阶段精确回到静态基准渐变。鱼群惊散-回归弧线当空水 shell 进入 Working 时鱼群沿一条以turn_started_at为键的确定性 800 ms 惊散并回归弧线运动该动画永不循环且在 waiting、approval、stopped/error 以及降级运动状态下保持静止。这些效果共用三条不变量不增删转写行、不改变命中区域hitbox、不以 Provider 增量时序作为动画时钟。对 TUI 而言这三点防止了装饰动画破坏可用性——例如转写行数的变化会打乱用户正在阅读的位置命中区域漂移会破坏鼠标交互而用流增量驱动动画会让文本抵达节奏直接泄露为视觉噪声。九、集成清单写给动效代码的接线规则契约在 Integration 一节给出了所有需要接运动策略的宿主应当遵守的四条规则是修改 TUI 动效相关代码时的权威指引统一通过MotionPolicy::from_settings(low_motion, fancy_animations, force_reduced)派生策略禁止各自为政地读布尔设置Spinner 优先使用MotionPolicy::spinner_glyph/spinner_presentation解析字形共享帧表留在 crates/tui/src/tui/spinner.rs保证所有工作标记同节拍流式显示调用stream_display_clock.set_allow_catch_up(policy.allows_catch_up_bursts())让是否允许追帧完全由策略裁决输入框上方的 working/phase 装饰TUI-DOG-008 追踪在Reduced/Still下必须保持诚实——即平静重绘而不是套一个不转的假装饰。事实上第 4 条正是从属实现中大量#[allow(dead_code)] TUI-DOG-008 注释的来源策略 API 是为这些宿主预备的迁移目标在全面切换完成前保持编译期可见。十、测试佐证行为契约的四个钉点Motion Contract 不是一份愿望清单仓库中每个语义都以单元测试钉死策略层motion/mode.rs 测试reduced_is_semantic_not_slow_typewriter验证 Reduced 不慢化流时钟、不允许追帧与装饰动画、spinner 落在StaticCalmstill_disables_spin_but_keeps_display_clock验证 Still 关闭动画帧但保留显示时钟frame_cap_preserves_authored_motion_semantics验证帧率压缩不影响Full的语义授权。调度层motion/frame_requester.rs 测试验证多请求合并为单次发出、Reduced 丢弃动画帧请求、clamp 尊重低运动帧上限。帧表层spinner.rs 测试验证 400ms 前只显示静态箭头、活动帧按 200ms/5Hz 稳定推进、全部帧宽度为 1、低运动下固定为平静字形、验证勾选帧表与主 Braille 表互不相同。流式时钟层streaming/mod.rs 测试验证突发小增量被合并为一次历史变更、300 个逐毫秒增量在 33ms 间隔下提交数不超过 11、最终冲刷会消费挂起增量、backlog 越过阈值才触发追帧以及最关键的reduced_motion_keeps_steady_clock_without_catch_up_or_typewriter——禁用追帧后即使 backlog 两倍于阈值也不提前节拍提交间隔与全运动一致。总结给终端动效的一堂克制课CodeWhale 的 Motion Contract 表面上是动画风格约定实质上是一套运动语义与渲染时序的强类型约束用MotionMode把无障碍偏好减少运动与性能降级帧率受限分离用MotionPolicy统一回答能不能动、按什么节奏动用StreamDisplayClock保证流式文本始终以人眼舒适的稳定节拍出现、而绝不把 Provider 的网络到达节奏直接映射为屏幕闪烁最后用诚实性注释staged-not-wired防止未完成功能被误当作用户可见行为。对于需要维护大型 TUI 动画体系的开发者这份契约在架构分层、语义降级与删除从未生效的防御性代码三个层面都提供了可直接借鉴的范本。延伸阅读核心文档 docs/MOTION_CONTRACT.md策略实现 crates/tui/src/tui/motion/mode.rs 与 crates/tui/src/tui/motion/frame_requester.rs流式时钟 crates/tui/src/tui/streaming/mod.rs帧表与转轮 crates/tui/src/tui/spinner.rs帧率上限 crates/tui/src/tui/frame_rate_limiter.rs设置项 crates/tui/src/settings.rs 与 crates/config/src/settings_schema.rs。【免费下载链接】CodewhaleOpen-source coding agent for your terminal, built in Rust and on a journey of continuous community improvement. Issues and PRs welcome.项目地址: https://gitcode.com/GitHub_Trending/de/Codewhale创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考