Scrawl App UI 交接规范(给 Hex)¶
本规范描述 Scrawl 的 macOS 应用外壳(src/)中位于 scrawl-capture 与 scrawl-router 之上的用户可见流程和边界情况。纯几何计算、状态机与协议逻辑(capture::pure_shift_state、capture::InputSink、sketch::freehand_outline、routing::*)已经存在于各个 crate 中;Hex 需要实现下面的原生 Swift/Rust UI 流程。
1. 录制快捷键、胶囊与目标切换器¶
1.1 录制快捷键(默认 ⌥⇧S)¶
- 在松开修饰键时触发,而非按下时触发: 当处于空闲状态(
Activity::Free)时按下录制快捷键,Scrawl 会进入待定状态,并等待所有快捷键修饰键(⌥、⇧等)都松开后才开始录制(RecordScreen)。- 原因: 如果在
⇧仍按住时就开始录制,光标移动会立即被当作⇧墨水;而在目标应用上松开⌥⇧S会把修饰键抬起事件泄漏到录制中。 - 按住期间取消: 在快捷键修饰键松开前按下
Esc会取消待定的启动(Action::Cancel),且不会闪烁屏幕或创建 bundle。
- 原因: 如果在
- 双击保护(
DOUBLE_PRESS_MS = 400 ms): 录制开始后400 ms内再次按下录制快捷键会被忽略(Action::None),这样按键抖动或双击不会立刻结束一个 0 秒的录制。 - 录制中按下(
≥ 400 ms): 结束当前录制(Action::Finish)。 - 目标选择器打开时按下(
Activity::Picker): 开始录制选择器中当前高亮的目标(Action::RecordSelected)。 - 处理中按下(
Activity::Processing): 忽略(Action::None)。 - 默认快捷键别名: 当快捷键保持默认值(
⌥⇧S/⌥⇧V)时,capture::shortcut_matches也会接受在当前键盘布局的S/V键以及物理 ANSI 键码上的⌃⌥⇧、⌘⌥⇧和⌃⌥⌘。自定义快捷键则只匹配其确切的 Carbon 修饰键和键码。
1.2 目标切换器与显示标签(Area · Window · Screen · iPhone)¶
- 默认从光标所在显示器开始全屏录制(
Screen,自动裁剪): 按下并松开⌥⇧S会立即开始录制指针所指的显示器(Target::ActiveDisplay),并将录制区域定位在该显示器菜单栏和刘海下方(screens::capsule_origin_for_display)。 - 点按启动时显示标签: 通过快捷键启动屏幕录制后的前
3,500 ms(REVEAL_MS)内,浮动胶囊会展开以显示目标切换标签(Area · Window · Screen · iPhone):- 最短可见时间:即使你立刻点击或绘制,也会至少显示
900 ms(REVEAL_MIN_MS)。 - 操作后自动收起:一旦
900 ms过去,第一次被记录的用户操作(点击、滚动或笔画)就会把切换标签收回到紧凑的录制胶囊中。 - 悬停延迟:鼠标悬停在胶囊上会保持标签展开;指针离开胶囊后,它们会再保持打开
1,200 ms(REVEAL_LINGER_MS)然后收起。
- 最短可见时间:即使你立刻点击或绘制,也会至少显示
- 录制中途切换目标: 在切换标签展开时点击
Area、Window或iPhone会切换录制目标:- Window: 高亮悬停到的窗口(通过
capture::window_at_point从前往后做命中测试,排除 Scrawl/宿主窗口);点击某个窗口会将录制切换到CaptureMode::Window(或在该窗口上重启录制流)。 - Area: 让用户在单个显示器上拖出一个矩形(
screens::clamp_rect_to_display;通过screens::ensure_min_area_rect保证最小尺寸)。跨显示器的拖动会限制在拖动开始时的显示器内。 - iPhone: 连接已通过线缆接入的 iPhone(
helper::devices),打开镜像窗口,并录制设备原生视频 + 触摸遥测数据。如果手机断开连接或被锁定(Event::PhoneEnded),录制会干净地结束或提示用户解锁。
- Window: 高亮悬停到的窗口(通过
- 显式选择器(
Choose What to Record…): 从菜单栏打开选择器会在开始任何录制之前进入Activity::Picker;按Esc可关闭选择器。
2. 墨水覆盖层(⇧ 墨水、图钉、激光、标记与撤销)¶
墨水覆盖层是一个透明的、可点击穿透(ignoresMouseEvents = true)、不激活(NSWindowSharingNone)的窗口,覆盖目标显示器;通过 OwnWindows::excluded 排除在 ScreenCaptureKit 之外,并通过 OwnWindows::passthrough 在自点击过滤中被忽略。
2.1 纯 Shift 状态(capture::pure_shift_state)¶
- 只有当
⇧单独按住(没有⌘、⌥、⌃或fn),并且不是多修饰键组合(例如录制开始时的⌥⇧S,或录制中的⇧⌘4)遗留下来的状态时,⇧才会激活墨水/图钉。 - 如果在
⇧按下的同时又按下了另一个修饰键,await_shift_release会锁存为true,直到⇧被完全松开并再次单独按下。
2.2 绘制 ⇧ 墨水(InputSink::ink)¶
- 无需点击即可绘制: 按住纯
⇧并在录制目标内移动指针,会沿光标路径绘制实时红色墨水(#FF3B30)(sketch::freehand_outline),无需按下鼠标按钮。 - 最小跨度(
MIN_INK_SPAN_PT = 6.0 pt): 松开⇧时,仅当指针从起点至少移动了6 pt(≥ 2个点)才提交该笔画。原地不动的⇧点按(例如光标停在屏幕上时输入一个大写字母)会被丢弃,因此不会产生杂散的点。 - 点抽稀与上限: 距上一点小于
1.5 pt的点会被跳过,每笔最多2,400个点。 - 辅助功能 / DOM 探测: 当
agentContext开启时,松开一笔会在笔画目标点探测 Accessibility 树(以及已登录浏览器标签页的/web/context)(ax::stroke_probe_point:下划线正上方、圆的中心、箭头尖端)。
2.3 编号图钉(⇧ + 左键点击)¶
- 放下图钉: 按住纯
⇧并左键点击(button_down→button_up在PIN_MAX_PRESS_MS = 1,000 ms内且移动量< CLICK_SLOP_PT = 12.0 pt)会在按下点放下下一个顺序编号的图钉(1、2、3、…)。 - 绘制中途抑制: 如果在当前
⇧按住期间、点击之前已经绘制了≥ 6 pt的墨水,该点击会被视为绘制的一部分,不会放下图钉,也不会记录点击事件。 - 长按不动(
> 1,000 ms): 按住⇧和鼠标按钮,超过1 s不移动再松开,不会放下图钉。 - 视觉呈现: 在覆盖层上以短暂向外脉冲的方式渲染(
sketch::pin_pulse),并在关键帧上盖一个带白色边框的红色编号徽章。
2.4 激光笔¶
- 当指针在激光修饰键/按钮激活的状态下移动时触发(
InputSink::laser)。 - 在实时叠加层上渲染一条拖尾并逐渐淡化的红色光束(
sketch::slice_laser_window+sketch::smooth_laser_points),不会在后续关键帧上留下永久墨迹;激光结束时,其最终停留坐标被记录为InteractionEvent::Point。
2.5 标记与撤销¶
- 标记(Flag)(
InputSink::flag): 按下录制胶囊中的标记按钮(或使用快捷键)会盖上一个仅含时间戳的Shape::Flag标记(t_ms),即使屏幕静止,也会在该时刻强制生成一个优先关键帧。 - 撤销(Undo)(
⌘Z按住⇧,或InputSink::undo_stroke): 如果当前正在绘制实时墨迹,则清除正在进行中的笔划;否则弹出最近一次已提交的笔划或图钉。未按住⇧的普通⌘Z会作为普通按键组合传递给最前端的应用。 - 自身窗口过滤: 落在宿主自身胶囊或设置窗口(
OwnWindows,不包括可点击穿透的墨迹叠加层)上的点击和滚动永远不会被记录为捕获交互。
3. 粘贴、自动路由与成品捕获的交付¶
当捕获完成(SealedCapture::finish → Finished)时,应用决定如何交付它:
3.1 剪贴板与两步粘贴(⌥⇧V 或粘贴按钮)¶
- 剪贴板默认行为: 捕获一完成,
summary.md就会复制到系统剪贴板(SystemClipboard::copy_bundle),因此在任何地方手动按⌘V都会粘贴纯文本的 markdown 摘要,其中包含指向contact-sheet.png和scrawl.json的绝对路径。 - 两步合成粘贴(
paste_bundle_image_and_text/⌥⇧V):- 等待最多
350 ms,直到所有物理修饰键(⌥、⇧、⌘、⌃)都被释放,这样合成的⌘V不会变成⌥⇧⌘V。 - 激活目标外部应用(或者,如果用户将粘贴标签拖放到特定窗口/输入框上,则在该坐标处发送一次合成点击,并等待
65 ms获得焦点)。 - 第 1 步(图片):
- 如果最前端应用是终端(
Ghostty、iTerm2、Terminal、kitty、WezTerm、Alacritty、Warp),则将contact-sheet.png的绝对路径作为文本复制,并发送⌘V(终端会从文件路径附加[Image 1])。 - 如果最前端应用是 GUI 应用(OpenCode Desktop、Slack、Linear、浏览器),则将
contact-sheet.png作为NSPasteboardTypePNG字节复制,并发送⌘V。
- 如果最前端应用是终端(
- 等待
160 ms让目标编辑框(composer)接收图片附件。 - 第 2 步(文本): 将
summary.md作为NSPasteboardTypeString复制,等待20 ms,然后发送第二次⌘V。
- 等待最多
3.2 会话路由、倒计时、标签与撤销¶
- 路由决策(
scrawl-router): 在捆绑包组装的同时或紧接着运行(Router::decide),结合确定性线索(开始时的语音会话名称、routing.sites、routing.apps、git 检出中的开发构建二进制文件、/sdk.js签名页面、.opencode-session发布者标记、localhost 端口)以及 Jev(jevRouting)。 - 四种结果(
decision区间 +autoSend策略):- 立即发送: 确定性规则/地址(在
autoSend: "rules"或"confident"下)。立即投递(Router::deliver),并显示带有 撤销 按钮的“已发送”toast。 - 倒计时发送: 在
autoSend: "confident"下,经加权或 Jev 高置信度选定。显示一个简短倒计时胶囊,命名目标会话(Sending to <Title>…),并带有候补会话标签、粘贴 和 取消(Esc/✕)。- 边缘情况: 悬停在胶囊上会暂停倒计时;将指针移开则恢复倒计时。
- 修正记录: 按
Esc/✕记录Action::Cancel;点击Paste记录Action::Paste;点击候补标签会在该处投递并记录Action::Redirect(Router::correct)。
- 建议 / 询问(
Decision::Offer): 在autoSend: "rules"下,Jev/加权发送会变成一条建议(suggested: true),首先预选首选选项,随后显示最多 3 个候补标签(Jev 的次选、智能路由的最佳项、HOME),外加 粘贴。 - 保留在剪贴板(
Band::Keep): 保持在剪贴板上,带有一个 粘贴 按钮(可拖放到目标窗口上进行点击粘贴)。 - 规则学习提议(
OfferRule): 当用户针对同一站点源或非通用应用手动选择同一会话两次时(router/learned.json),胶囊会提供一个一键提示:“总是将发送到 ,并带有 总是(?” policy::add_rule)和 暂时不(policy::refuse_rule,后者会将键添加到routing.refusedRules,使其不再被提议)。 - 撤回已发送内容(
Router::undo): 在已发送的捕获上点击 撤销 会调用Router::undo(delivery_id, deadline)并显示结果(Report::text()):withdrawn:在会话收件箱中被读取之前取消。reverted:中断了该消息启动的回合,并将会话恢复到消息之前的状态(也包括文件更改,但仅限该回合的编辑工具所做的更改):先暂存(staged),再提交(committed),因此消息及其回合会被删除。disowned:会话已经处于回合中途或有更新的消息,因此发送了一条引导性后续消息,告知代理忽略先前的消息。在暂存回滚期间有新输入到达,或者提交失败,都会先清除暂存的回滚,因为任何提示词都会提交它。unfinished:暂存的回滚既无法提交也无法清除;不再对该会话发送任何内容,胶囊会将其显示为“太迟”。- 同时调用
Recents::taken_back,这样一次误发送就不会在下一次捕获时作为先例提供给 Jev。
- 离线队列与重试(
delivery::retry_due): 如果投递时 OpenCode 服务器不可达,会在捆绑包中写入一个.deliver.json标记(或写入router/outbox/<key>),并在后台以指数退避方式重试(15 s起翻倍,最大到30 m,24 h后过期)。
- 立即发送: 确定性规则/地址(在
4. 引导与权限¶
Scrawl 需要两项权限来录制,以及两项可选权限以获得更丰富的上下文(crates/capture/src/permissions.rs):
| 权限 | 作用 | 检测与授权流程 |
|---|---|---|
| 屏幕录制 | 用于 Mac 屏幕/窗口/区域捕获 | CGPreflightScreenCaptureAccess() / CGRequestScreenCaptureAccess()。在“系统设置”中授权时,如果进程已在运行,macOS 可能需要重新启动应用。 |
| 麦克风 | 用于语音旁白 | AVCaptureDevice.authorizationStatus(for: .audio)。首次请求时内联提示;若被拒绝,则深度链接到 x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone。 |
| 辅助功能 | 可选 — 启用模拟 ⌘V 粘贴、全局鼠标/键盘监听以及 UI 元素标签(agentContext) |
AXIsProcessTrusted()。通过 AXIsProcessTrustedWithOptions 提示;深度链接到 Privacy_Accessibility。若未获得此权限,Scrawl 将回退为轮询鼠标按键(NSEvent.pressedMouseButtons)和剪贴板复制,而非模拟粘贴。 |
| 输入监控 | 可选 — 在辅助功能关闭时记录全局组合键并触发快捷键 | CGPreflightListenEventAccess()。获得辅助功能权限后自动涵盖。 |
- 非阻塞权限面板: 当屏幕录制权限缺失时,如果用户按下
⌥⇧S,显示权限检查清单窗口,说明缺失项,并提供一键按钮直接打开对应的“系统设置”窗格;同时实时轮询授权状态(permissions::Grants::current()),一旦开关被打开,该行立即变绿。 - 签名二进制身份: 使用稳定的 Developer ID bundle identifier 对应用进行签名,以便 macOS
tccd在更新后保留屏幕录制、麦克风和辅助功能授权。
5. 设置界面¶
在 Hex 中,这些设置存放在 Hex 自己的配置存储中,并构造 scrawl_capture::EngineConfig 和 scrawl_router::RouterConfig(通过 scrawl_router::parse_scrawl_config(&str) 和 handoff::import_or_ensure_ingest_token_file 从 ~/.config/scrawl/config.json 一次性迁移):
- 快捷键:
- 录制快捷键: 默认为
⌥⇧S。不可取消设置(ShortcutKind::Record.can_unset() == false)。 - 粘贴上一次捕获快捷键: 默认为
⌥⇧V。可清除/禁用("paste": null)。 - 录制器边界情况: 在用户录制新的按键绑定时暂停全局快捷键监听器(
capture::pause_shortcuts(true)),这样在录制器字段中按下现有快捷键时会捕获按键,而不会开始录制。
- 录制快捷键: 默认为
- 路由与投递(
RouterConfig):- 自动发送策略(
routing.autoSend):Rules(默认):仅确定性匹配(语音会话名称、站点/应用规则、开发构建、/sdk.js、发布者标记、localhost 端口)会自动发送;Jev 和按词加权选中的候选项会预选一个建议 chip。Confident:Jev 的置信带(置信度≥ 0.80,边际≥ 0.50,拟合度≥ 0.65)也会在撤销倒计时结束后自动发送。Never:仅在明确说出目标地址时发送;规则和 Jev 始终显示 chip 选择器。
- 忙碌会话投递(
delivery):Steer(默认 — 立即加入当前活跃轮次)与Queue(等待当前轮次结束)。 - 撤销窗口(
routing.undoWindowSec): 默认为600 s(10 分钟)。 - 站点与应用规则(
routing.sites、routing.apps、routing.servers、routing.aliases): 可编辑的 URL 前缀和应用 bundle ID 列表,映射到项目文件夹或ses_…ID。将通用浏览器、终端和 OpenCode 排除在routing.apps之外(routing::policy::is_general_app)。
- 自动发送策略(
- 录制与转写(
EngineConfig):- 麦克风(
microphone): 跟随 macOS 系统默认(None),或固定到特定的 CoreAudio 设备 UID(MicChoice { id, name })。如果固定的麦克风被拔出,则本次录制回退到系统默认并显示提示(mic::Pick::note)。 - 转写器(
transcriber):hex(Hex 的本地模型服务)、apple(macOS 26+ 上的SpeechAnalyzer,带语言区域选择器appleLocale)或command(自定义 CLI 程序)。 - 自动裁剪(
autocrop): 默认对全屏捕获开启;将关键帧裁剪到活动窗口/区域,同时将完整帧保留在full/中。
- 麦克风(
- 存储与保留(
retention.rs):- 捕获文件夹(
capturesDir): 必须是绝对路径或~/…形式的可写目录;如果配置的外部磁盘被拔出或变为只读,则记录一次日志并回退到内置的 Application Support 目录,确保捕获内容不会丢失。 - 清理策略(
retention): 预设(Keep all、Balanced— 7 天后压缩/删除大体积视频,30 天后将 bundles 移入废纸篓;Compact— 2 天 / 14 天)。绝不触碰.pinnedbundles、.media-sendingbundles 或正在进行的录制。
- 捕获文件夹(
- 代理与 iPhone 配对:
- CLI 与 Skill: 将
scrawl安装到PATH(~/.local/bin/scrawl),并安装skills/scrawl/SKILL.md,以便编码代理可以运行scrawl zoom和scrawl inspect。 - ScrawlKit 配对(
ingestToken): 仅所有者可读(0600)的 bearer token,以及用于配对基于ScrawlKit构建的 iOS 应用的scrawl://setup?...深度链接 / QR 码。
- CLI 与 Skill: 将
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex