Uh oh!
There was an error while loading. Please reload this page.
feat(dictation): 落字失败时把话交还给用户,换行不再劈开聊天消息 - #943
Conversation
两件事,本质是同一个:说出来的话没落到该去的地方。 **一、换行会把聊天消息从中间劈开发出去。** macOS 把 U+000A 当 Return 键,而聊天框里 Return 就是「发送」。一条带空行 的两段话逐字上屏时,第一个换行直接把上半句发了出去,下半句留在输入框里。 改成发真实的 Shift+Return:聊天框里是软换行不发送,编辑器、终端、网页输入 框里就是普通换行。 做成设置而不是硬编码——风格市场里有靠换行把一段话拆成多条消息的风格包, 那种效果要的正是真回车。默认 Shift+Return,需要的人切回 Return。 **二、落字失败时,那段话会悄无声息地消失。** 密码框挡着(Secure Input)、辅助功能掉了权限、粘贴被拒——这些时候唯一的 兜底是把文本写进剪贴板。可它既依赖一个默认可关的开关,就算开着,屏幕上也 没有任何东西告诉你文本在那儿。用户看到的只是胶囊一闪而过,然后是个空输入框。 现在屏幕底部会弹一张卡片,里面是**完整**的那段话,一键复制。20 秒后自己 消失,鼠标停在上面就不走。 顺带修正一处数据失真:逐字上屏打到一半断掉时,此前一律报 InsertStatus:: Inserted(屏幕上明明只有半截),history 里的 insertStatus 因此是错的。
4c5158e to
fec1545Comparebigsongeth
commented
Aug 12, 2026
手打这么长也太牛了😂 三点都收到,PR 已经更新。 换行做成设置了 这个结论其实项目里早就有 —— Windows 侧的 有个差异想请你定:Windows 默认是 Enter,我这边默认给了 Shift+Return。 理由是 macOS 这次是真出了事故——一条带空行的两段话,上半句直接飞出去成了一条微信消息,只能撤回。默认值想站在「不误发」这边;靠换行拆多条消息的风格包,让用户去设置里切 Return,风格包说明里写一句就行。如果你觉得两边默认值该保持一致,我改成 Enter 也可以——那样默认用户还是会遇到误发。 焦点守卫整个撤掉了 你说的那种使用习惯确实会被它干扰。顺着想下去发现更根本的问题是:它在猜用户的意图。拿「app 变了」当「字会打错地方」的信号,可技术上分不清「不小心切走了」和「故意切过去让它落在那」——同一个动作,意图相反。 撤掉之后卡片不受影响。它靠的是「落字确实失败了」这个事实:Secure Input、辅助功能掉权限、粘贴被拒。这些没有猜测成分。 性能那条也就不用担心了,守卫没了。 你提的「没有光标才弹卡片」我觉得方向是对的——判断「这儿能不能打字」是事实,比猜意图靠谱。不过要动的东西比较多,这个 PR 先不碰。 |
Uh oh!
There was an error while loading. Please reload this page.
两件事,本质是同一个:说出来的话没落到该去的地方。
一、换行会把聊天消息从中间劈开发出去
说一段话,润色把它分成了两段。逐字上屏到微信的时候,中间那个换行直接把上半句当成一条消息发了出去,下半句留在输入框里。只能撤回。
原因是 macOS 把 U+000A 当 Return 键,而聊天框里 Return 就是「发送」。
改成发真实的 Shift+Return——聊天框里是软换行不发送,编辑器、终端、网页输入框里就是普通换行。
做成设置而不是硬编码:风格市场里有靠换行把一段话拆成多条消息的风格包,那种效果要的正是真回车。默认 Shift+Return,需要的人在设置里切回 Return(只在 macOS + 开了流式插入时显示,结构对齐已有的
WindowsSendInputNewlineMode)。二、落字失败时,那段话会悄无声息地消失
密码框挡着、辅助功能掉了权限、粘贴被拒——这些时候文本落不进去,而唯一的兜底是把它写进剪贴板。
问题是:这个兜底依赖一个默认可关的开关;而且就算开着,屏幕上也没有任何东西告诉你文本在剪贴板里。你看到的只是胶囊一闪而过,然后是个空输入框,话就这么没了。
现在屏幕底部会弹一张卡片,里面是完整的那段话,一键复制。20 秒后自己消失,鼠标停在上面就不走(你还在读的时候它不该跑)。
实现
技术细节
换行(
unicode_keystroke.rs):用kVK_Return虚拟键码 + shift flag,代替原来CGEventKeyboardSetUnicodeString送 U+000A 的做法。\r吞掉不发键(CRLF 里它只是 LF 的前缀,发出去会变成两个换行),但仍计入typed_chars——调用方拿它和 delta 的字符数比对,少一个就会判定「部分失败」并丢弃后续所有 delta。顺带把重复的 CGEvent 资源管理收口到一个post_key_event。卡片触发:
InsertStatus::CopiedFallback | Failed。PasteSent是 Windows / Linux 上的成功语义,不能算失败,否则每次正常听写都会弹卡片。逐字上屏打到一半断掉时,
run_streaming_polish把完整文本放进Inner::insert_fallback_text,收尾据此把 status 从Inserted纠正为CopiedFallback——此前那里一律报成功,history 里的insertStatus也跟着失真。卡片内容是完整文本,不是屏幕上那半截。
final_text遵守「所见即所存」的约定(#412 确立),但用户要拿回的是整段。卡片窗口复用词条建议卡片那一套(复用胶囊窗口、关穿透、缩尺寸、重定位),包括两个容易漏的缓存同步:
capsule_cursor_passthrough不同步,后续emit_capsule会以为「没变化」而跳过该调的那次,表现是胶囊之后一直挡着屏幕;capsule_layout不清,下次录音时胶囊会留在卡片挪过去的位置。时序:卡片在会话收尾那一刻弹出,而收尾自己安排了一次 idle 隐藏。照搬
selection_polish_capsule_active已有的让路机制,否则卡片刚出现就被自己这轮会话的尾巴收掉。复制走后端而不是
navigator.clipboard:卡片浮在别的 app 上,按钮刻意不抢焦点(抢了就把用户正在写的地方的光标弄没了),而未聚焦的文档调那个 API 会抛Document is not focused。测试
cargo test --lib1119 通过,tsc --noEmit通过。还没验证的