- 桌面应用
- 开发者工具
- 人工智能
- AI 应用
- AI Agent
- 代码智能体
【免费下载链接】warp
Warp is an agentic development environment, born out of the terminal.
导读
本文基于 Warp 开源仓库中 QUALITY-1169 后续规格文档,完整讲解 Computer Use(AI 代理控制电脑)录屏烧录注解系统的三个缺陷修复方案:点击涟漪必须以捕获光标坐标为几何中心、拖拽轨迹必须能跨多个UseComputer调用保持连续、滚动条按下交互必须有明确且唯一的处理结果。文中所有结论均以仓库源码(overlay.rs、use_computer.rs、recording_controller.rs、lib.rs)为证据,读者读完后能掌握 ASS 矢量对齐、录制级指针会话持久化、录制级事件流分类的完整实现思路与验证方法。
背景:dogfooding 暴露的三个缺陷
在 PR #14173(commit59bb73fa3c6e04a1c67a5ca0a134abd165f67338)合并之后,内部试用(dogfooding)发现了三个可见问题,本规格文档(.agents/specs/QUALITY-1169-click-drag-follow-up.md)即为针对性的后续修复设计:
- 点击涟漪偏离光标:点击环状动画的几何中心没有落在捕获光标坐标上,而是偏移到左上方。
- 拖拽轨迹断裂:当一次拖拽的
MouseDown → MouseMove* → MouseUp序列被拆到多个UseComputer调用中执行时,渲染端只看到不完整的按下事件与零散的移动事件,无法还原出一条完整的拖拽轨迹。 - 滚动条交互行为不确定:在滚动条上按下时会出现一圈全圆闪光,但其行为是"误伤"还是"预期",此前没有明确的产品决策。
该修复完全发生在客户端(warpdotdev/warp),不需要修改服务端动作协议(action schema)或产物(artifact)API——从 overlay.rs 的文档注释可以看到,结构化客户端动作流已经携带所需的指针原语,而烧录(burn-in)本身由客户端拥有。
规格文档为这次修复定义了五条用户可见不变量(User-visible invariants),它们是后续所有设计决策与验收测试的基准:
- 点击环的几何中心 = 捕获光标坐标(经过捕获空间钳位与屏幕/窗口坐标映射后);拖拽起始锚点与按住指示器同样适用。
- 单个
UseComputer调用内的MouseDown → MouseMove* → MouseUp与拆分成多次调用的同一序列,必须产生同一条连续轨迹(含路径、按钮身份、时间戳、释放淡出),且不产生点击环。 - 无活跃按压的散落移动/释放、被取消/失败的调用、未完成的按压,都必须被确定性处理:不能附着到之后无关的点击上;未完成的按压在录制结束前只能渲染受限的按住/锚点状态。
- 既有的键盘/文本输入脱敏(redaction)、滚动标签、智能剪辑(smart-cut)计时、光标捕获、捕获尺寸钳位与尽力而为的原视频回退(best-effort original-video fallback)全部保持不变。
- 滚动条按下/滚动交互只有一个文档化的结果:保留滚动条按下时的点击环,并像其他任何点击环一样以光标为中心(不抑制)。动作流不标识 UI 元素,因此没有附近
MouseWheel的滚动条拖拽无法可靠区分,仍作为普通拖拽注解处理;不实现任何MouseWheel相邻抑制规则。
缺陷一:点击涟漪的几何中心偏移
问题根因:ASS 对齐语义与绘图原点
在渲染器层面,缺陷源于 ASS 对话(dialogue)的对齐标签与矢量路径绘图原点不匹配。修复前,点击环使用原点居中的圆形路径(ass_circle_path),配合\an5(中心对齐)与\pos(cx,cy)定位。\an5的语义是"以文本/绘图包围盒的中心对齐到\pos点",因此 libass 会把包围盒中心(即圆形路径的中心)对准\pos,但圆形路径本身以绘图原点 (0,0) 为中心,绘制时会额外偏移约一个半径的距离,最终渲染到光标左上方。
规格文档给出了可复现的实测数据:在 libass 中对合并后的 ASS 矢量在(640,360)处回放,\an5测得的中心约为(603.5,323.5),而改用\an7后为(639.5,359.5);128×128 的复现图在\an5下被左上角裁切,\an7下则围绕(64,64)居中。
修复方案:统一改用\an7顶左对齐
修复的核心思路是保留路径,改变对齐语义:ass_circle_path继续以绘图原点 (0,0) 为圆心生成路径,但所有圆环/圆形对话从\an5\pos(cx,cy)改为\an7\pos(cx,cy)。\an7是顶左对齐(top-left),libass 会把绘图原点精确放置在\pos坐标处,于是圆心正好落在捕获光标上,缩放也围绕光标对称展开。
从当前源码 overlay.rs 可以看到该修复已落地,append_click_ring生成的对话形如:
Dialogue: 1,{start},{end},Cursor,,0,0,0,,{\an7\pos({cx},{cy})\clip(0,0,{width},{height})\1a&HFF&\3c&H{POINTER_COLOR_BGR}&\3a&H00&\bord{CLICK_RING_THICKNESS}\fscx{start_scale}\fscy{start_scale}\t(0,{dur_ms},\fscx100\fscy100\3a&HFF&)\p1}{path}{\p0}其注释明确说明:\an5会把绘图原点偏移约一个半径的距离,导致环渲染在点击点左上方;\an7则让环围绕光标对称展开。
同样需要改动的还有拖拽锚点圆(append_drag中的ass_circle_path(DRAG_ANCHOR_RADIUS))与按住指示器圆(HELD_INDICATOR_RADIUS圆),而拖拽轨迹折线(trail polyline)保持原有的\an7\pos(0,0)不变——因为轨迹路径本身是绝对坐标折线,顶左对齐正是其语义。所有改动不得改变坐标缩放、智能剪辑重映射(remap_source_interval)、裁剪(\clip(0,0,width,height))、颜色与动画时长。
设计备选与取舍
规格文档记录了三种对齐方案的选择过程:
- (a) 选定方案:
\an7+ 原点居中路径。与 libass 的绘图原点语义一致,无需半径补偿。 - (b) 保留
\an5并在\pos上偏移半径:对缩放后的环、不同圆尺寸和移动中的指示器都脆弱。 - (c) 将所有路径重写为顶左原点:几何重复,动画计算更复杂。
最终选择 (a),因为它最小改动且语义最准确。
缺陷二:拖拽轨迹跨调用断裂
问题根因:每个调用独立的指针状态
拖拽断裂的根因有两层:
- 状态不持久:修复前 use_computer.rs 为每次
UseComputer调用创建全新的PointerSink与事件缓冲,调用结束后一次性排空为一个ActionLogEntry。Linux X11 执行路径(linux/x11/mod.rs)为每次调用维护独立的last_capture状态:record_down_move更新该状态,而record_up在当前调用没有先前点时直接省略释放事件。于是拆分的Down/Move/Up调用分别提交[Down]、[Move]、无释放事件,渲染端永远看不到一条完整的拖拽。 - 分类粒度错误:修复前 overlay.rs 的
classify_pointer_gestures只接收单个条目(一个调用)的事件,因此一个条目里的Down看不到另一个条目里的Move或Up。
修复方案一:录制级PointerSession持久化指针状态
核心设计是引入录制作用域(recording-scoped)的PointerSession,由ActiveRecording持有,并通过Arc共享给每次调用的PointerSink。它只存储两种状态:当前按下的按钮(active button)与最后一次解析出的捕获空间点(last point)。
从当前源码 lib.rs 可以看到该结构已经实现:
PointerSessionState仅含last_point: Option<Vector2I>与active_button: Option<MouseButton>,通过Arc<Mutex<...>>共享;record_press_or_move(kind, button, point):Down设置活跃按钮并更新最后点;Move/Scroll只更新最后点(指针物理上已移动到该处),不触碰按钮;活跃按钮存在时新的Down会替换旧按钮(先前未完成的按压由分类器关闭为一个按住拖拽);record_release(button):仅当释放按钮与活跃按压匹配时返回最后解析点并清除按钮;不匹配的释放(按钮不同,或没有先前按压)被忽略,不会产生携带过期坐标的事件;clear():清空全部状态,用于目标表面不匹配与失败/取消调用后的重置。
关键点:MouseUp动作本身不携带坐标,所以跨调用释放必须复用会话中的最后解析点。在 Linux 与 Windows 执行路径中,record_up都遵循"无匹配活跃按压即不产出释放事件"的规则(见 linux/x11/mod.rs 与 windows/mod.rs)。
录制级生命周期由 recording_controller.rs 提供:RecordingController持有ActiveRecording、PendingActionGroup与已提交的ActionLogEntry。finish_start创建录制时即构造pointer_session: computer_use::PointerSession::new(),并将其与PointerSink一并交给每次UseComputer调用(use_computer.rs)。
状态机语义(规格文档明确定义):
MouseDown:用既有屏幕/窗口映射解析坐标,确定性替换或拒绝已活跃的同按钮按压,产出Down;MouseMove:在解析点产出Move,保留活跃按钮与最后点;若移动落在与录制目标不匹配的表面上,则清空活跃指针状态,而不是复用过期坐标(对应record_positioned_event中resolve_capture_point返回None时调用session.clear());MouseUp:动作不带坐标时使用会话最后解析点,只为匹配的活跃按钮产出Up,随后清除该按钮——使新调用中的释放可在最后捕获空间点被观测到;- 失败/取消的
UseComputer:丢弃 pending group 并重置会话状态,使后续点击无法继承被放弃的按压(use_computer.rs 中UseComputerResult::Error(_) | Cancelled分支调用discard_action_group); - 成功完成但按压未完成:只在录制结束前保留受限的按住状态,不合成释放事件;
- 保留每个事件相对录制开始的
Instant偏移与既有的Target::Screen/匹配的Target::Window坐标规则;非 Linux 平台可如旧让 sink 保持未使用,但共享类型与 no-op 路径必须继续可编译。
修复方案二:录制级事件流统一分类
第二处改动是分类粒度。规格文档要求:
- 烧录时将所有已提交条目的
pointer_events拼接成一个录制级事件流,按事件偏移稳定排序(相同时间戳保持插入顺序),一次性分类,而不是逐条目独立分类。 - 扩展分类器以跟踪按钮身份:匹配的
Down+ 零或多个Move+ 匹配的Up构成一个手势;有按压无移动是点击,有移动是拖拽,无主按压的散落移动/释放被忽略;按钮活跃期间出现新的Down会确定性关闭先前的未完成手势再开启新手势。 - 用既有段重映射与有界动画时长渲染手势。拆分调用的拖拽因此得到一条轨迹对话、一个锚点、一个按住指示器,且没有环。
- 即使标签为空,纯指针动作组也要保留在智能剪辑时间线上;混合键盘/指针条目继续同时渲染脱敏胶囊与指针几何。
从当前源码 overlay.rs 可以看到flatten_pointer_events正是这样实现:拼接所有条目后stream.sort_by_key(|event| event.offset)(Rust 的sort_by_key稳定,相同偏移保持派发顺序,从而保证同偏移的Down先于Up、调用 A 的事件先于调用 B 的事件)。随后classify_pointer_gestures(overlay.rs)对整条流分类,遵循"拖拽与点击互斥"的不变式——拖拽永远不产出点击环:
- 有
Down起始且随后出现至少一个Move→PointerGesture::Drag(无论是否有Up); - 有
Down起始、无Move、有匹配Up→PointerGesture::Click(一个扩张环); - 有
Down起始、无Move、无Up→ 按住指示器(单点无释放的Drag); - 释放按钮不匹配或没有先前按压 → 忽略,继续扫描匹配释放;
Scroll样本仅记录位置,不参与分类,不改变按钮状态;- 活跃按钮期间的新
Down关闭先前的未完成手势(渲染为按住拖拽)并从新按压重新开始分类。
classify_pointer_gestures同时为每个事件返回一个CursorRole(Free/Drag(usize)/Skip),供合成光标的运动规划使用:拖拽路径上的事件让光标连续移动,拖拽内部的滚动样本与不匹配释放被跳过,其余为自由路点。
设计备选与取舍
- (a) 选定方案:录制级指针会话 + 扁平化分类。修复缺失的释放坐标,并在条目间保留精确的事件偏移。
- (b) 让 actor 跨调用存活:会把录制生命周期耦合到平台输入状态,并改变既有 actor 所有权结构。
- (c) 从摘要/光标位置推断拖拽:丢失按钮身份,且对窗口目标坐标不可靠。
缺陷三:滚动条交互的确定性决策
规格文档记录了该问题从"推荐默认"到"已确认结果"的演变:
- 最初的推荐默认是:与滚轮事件紧邻的点击抑制点击环,以避免误导性的全圆闪光,同时保留普通点击的环。
- 需求方在规格批准时确认了**保留(keep)**结果:滚动条上的点击渲染正常点击环,并通过与所有其他点击环相同的
\an7居中修复以光标为中心——不抑制。 - 由于客户端动作流不标识 UI 元素,无法从坐标稳定地推断滚动条元素(没有稳定的 UI 命中测试或元素元数据),因此不实现任何
MouseWheel相邻抑制规则,也没有抑制测试。
规格文档同时锁定了配套行为:
- 无邻近
MouseWheel的滚动条拖拽仍是普通拖拽轨迹(协议中无滚动条元素身份); MouseWheel本身保持无指针几何,既有滚动胶囊不变;- 选定的"保留并居中"结果写入规格与夹具(fixture)命名中,防止行为静默回归。
设计备选与取舍
- (a) 抑制紧邻滚轮事件的点击环:早期推荐默认,现已被取代。
- (b)选定方案:保留所有点击环(协议最简单),并借同一
\an7修复以光标居中——滚动条全圆不再偏移渲染到左上方。 - (c) 从坐标分类滚动条元素:被否决,客户端无稳定 UI 命中测试或元素元数据。
验证与验收:12 项必须全部通过
规格文档给出了严格的验证标准(合入前全部通过),这些标准同时是后续回归测试的清单:
- 对齐缺陷复现:用既有 ASS 矢量 + libass/ffmpeg 在已知坐标处验证——改动前
\an5输出可测量地偏左上,改动后\an7渲染几何在 1 像素(或一次栅格化容差)内以请求坐标为中心。 - 渲染器回归测试(如
click_ring_and_drag_circles_are_centered_under_libass):必须对合入前实现失败、合入后通过;须实际渲染环、拖拽锚点、按住圆并断言像素包围盒中心与请求点一致,而非仅搜索 ASS 子串。 - 纯 ASS 生成断言:所有原点居中圆形对话使用
\an7,轨迹保持\an7\pos(0,0),裁剪保持(0,0,width,height)。 - 同调用规范夹具:
Down → Move → Up断言一个拖拽、一条含所有非零段落的轨迹、一个锚点、一个按住指示器、正确的释放淡出、零点击环。 - 拆分条目夹具:条目 A 含
Down,B、C 含Move,D 含Up;断言与规范夹具相同的单手势/时序/路径,包括从录制级指针会话恢复的释放点。 - 按钮与边界夹具:右键/中键仍是点击;匹配按钮的释放只关闭自己的手势;不匹配释放与散落移动不渲染任何内容;按住期间第二次按压与失败/取消调用确定性重置状态;未完成按压永不产生后续点击环。
- 屏幕/窗口目标坐标夹具:拆分事件保持捕获空间映射、钳位与目标不匹配失效;混合键盘/指针条目仍脱敏可打印键/输入负载并保留滚动标签。
- 滚动条回放覆盖:在"推荐默认"下,点击后紧邻滚轮只抑制该点击环,而远距离/独立点击仍发出正常的 900 ms 环;在"保留"下,同一回放断言正常环的时序/淡出;无滚轮的滚动条拖拽仍是确定性拖拽轨迹。
- 智能剪辑重映射与动画边界不变:点击环与拖拽淡出保持在保留段内,纯指针条目仍保留其动作窗口,无注解输出到捕获尺寸之外。
- 失败与生命周期:失败/取消的指针调用丢弃 pending group 并重置会话状态;有效动作的终结仍上传处理后的产物;烧录失败仍上传原始录制并清理临时文件。
- 聚焦测试套件:运行
computer_useoverlay/录制行为与 app recording-controller/finalization 的 Rust 聚焦测试,再运行仓库文档化的./script/presubmit检查(Linux),且无需实时 MAA 登录即可通过。 - 代表性产物演练:覆盖 Linux 纯点击、规范拖拽、拆分调用拖拽、右键/中键点击、滚动条点击+滚动、混合键盘/指针录制;下载各产物,目视与逐帧检查中心/路径/时序,确认无可见输入负载,并在人为诱导烧录失败时确认原视频回退。
源码中的落地现状
本次修复的设计已基本在仓库中落地,关键实现路径如下,读者可直接对照阅读:
- ASS 矢量渲染与录制级分类:overlay.rs——
append_click_ring的\an7居中(L1169-L1195)、append_drag的锚点/按住指示器居中与轨迹对话、flatten_pointer_events(L766-L774)、classify_pointer_gestures(L657-L744)、build_overlay_ass中"先拼流再分类"的烧录入口(L575-L585),以及clamp_point(L1295-L1299)与remap_source_interval(L442-L467)。 - 录制级指针会话:lib.rs——
PointerSession/PointerSessionState的定义与record_press_or_move、record_release、clear语义;linux/x11/mod.rs 与 windows/mod.rs 的record_positioned_event/record_up/push_pointer_event执行路径。 - 录制控制器与调用执行:recording_controller.rs(
ActiveRecording持有PointerSession,PendingActionGroup的提交/丢弃)与 use_computer.rs(构建/排空PointerSink,成功提交、失败/取消丢弃)。 - 录制级终结(烧录入口):recording_finalize.rs 排空动作并调用
computer_use::post_process_recording(recording_post_process.rs),既有 stop 后烧录/剪辑路径是唯一的产物合成器。 - 测试证据:overlay_tests.rs 覆盖名义点击/拖拽 ASS 字符串(如
single_click_emits_one_expanding_ring断言\an7\pos(100,200)与\t(0,900,...)动画、drag_emits_trail_anchor_held_and_no_ring断言拖拽零环),pointer_session_tests.rs 覆盖会话状态语义,Windows 侧 windows/recording_tests.rs 在整actor调用中注入PointerSession::new()。
规格文档同时披露了可追溯性边界:给定的 MAA 跟踪 URL 因 staging 需要认证而无法直接观测,因此在当前环境下不应声称该跟踪包含拆分序列;实现必须同时支持同调用与拆分调用两种序列,并为各自添加夹具。
风险与缓解
规格文档明确了四类风险及其对策:
- 过期指针状态可能连接无关手势:在失败/取消调用时重置会话、拒绝不匹配释放,并用交错/放弃序列测试覆盖;
- 跨条目排序可能重排等时事件:使用稳定排序,并测试同偏移点击/拖拽边界;
- ASS 对齐修复可能回归裁剪或窗口目标缩放:保留坐标/钳位测试与 libass 图像级中心测试;
- 滚动条启发式可能漏掉滚动条拖拽或抑制附近的有意点击:保持阈值显式、文档化,并使产品选择易于切换。
结语
QUALITY-1169 的这次跟进修复是一次典型的"客户端渲染语义纠偏":用\an7顶左对齐统一了所有圆环的几何中心语义,用录制级PointerSession让无坐标的释放事件在调用边界间存活,用录制级扁平化事件流让分类器能看到完整手势,最终以明确的"保留并居中"产品决策终结了滚动条交互的不确定性。这一套"不变量 → 设计备选 → 落地实现 → 逐条验收"的规格结构,也为后续在 overlay.rs 与 recording_controller.rs 中扩展新的指针注解形态提供了可复用的范式。
- 桌面应用
- 开发者工具
- 人工智能
- AI 应用
- AI Agent
- 代码智能体
【免费下载链接】warp
Warp is an agentic development environment, born out of the terminal.
相关推荐
Warp computer-use 视频产物点击/拖拽标注(QUALITY-1169)设计与实现解析
Warp computer use 视频产物点击/拖拽标注(QUALITY 1169)设计与实现解析 Warp 的 computer use 录像在停止录制后会
桌面应用开发者工具人工智能AI 应用AI Agent代码智能体Listen gem与现代Web框架集成:Rails、Sinatra等项目的完整配置方案
Listen gem与现代Web框架集成:Rails、Sinatra等项目的完整配置方案 Listen gem是一款强大的文件监听工具,能够实时检测文件系统的变
开发工具tui-rs 鼠标交互完全指南:点击/拖拽/滚轮事件处理终极教程
tui rs 鼠标交互完全指南:点击/拖拽/滚轮事件处理终极教程 tui rs 是一个强大的 Rust 库,用于构建丰富的终端用户界面和仪表盘。虽然 tui r
CLIUI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考