Appearance
功能规格说明:会话内查找
本规格描述「在一条已打开的会话里找回某句话」的就地查找能力。作用域是当前会话已加载的内容;跨会话搜正文见 desktop-sessions.md「侧栏会话内容搜索」。文件面板里的查找条是同一形态的独立实现(见 desktop-file-panel.md「文件面板内查找与跳行」),两者同屏时按焦点归属决定谁响应快捷键。
用户场景与测试
用户故事:会话内查找(优先级:P2)
作为用户,我希望在一条长会话里按 Ctrl/Cmd+F 就地搜索并逐条跳转命中,以便在几百条消息里定位「那句话到底在第几轮说的」,不必用肉眼往回滚。
为什么是这个优先级:会话会越跑越长(工具调用、后台任务输出都在往里堆),而目前只有「往上滚」这一条找法。对齐 Claude 桌面端的 find-in-page:它同样不走 Electron 原生 webContents.findInPage,而是优先让页面在自己的数据树里查(原生查找只作兜底、5s 超时)——因为虚拟列表未渲染的行原生查找根本搜不到。形态与文件面板已有的查找条一致(大小写/整词/正则 + 计数 + 上一个/下一个),用户不需要学第二套控件。
独立测试:打开一条含几十条消息(含工具调用与输出)的会话,按 Ctrl/Cmd+F 验证查找条打开且焦点进输入框;输入一段只出现在早期消息里的文字,验证计数、高亮与自动滚动定位;按 Enter/Shift+Enter 在命中间移动并验证到末尾循环;切换大小写/整词/正则验证结果随之变化、输入非法正则验证可读提示;输入一个不存在的词验证「无匹配结果」且查找条不关闭;按 Esc 验证查找条关闭、高亮清除;在流式生成中打开查找并验证新内容到达时当前命中不被重置;把命中定位到虚拟列表尚未渲染的远端区段,验证能滚动过去并渲染出来。
验收场景:
- 假设会话内容区处于聚焦状态且消息列表已加载,当用户按
Ctrl/Cmd+F(macOS 为Cmd+F),则必须就地打开查找条并把焦点放进输入框——不打开新面板、不打开对话框、不改变会话的滚动位置;查找条以浮层形式贴在内容区右上角,不挤压消息列表的可用宽度。 - 假设焦点在会话各处(输入框、消息行、工具行),当用户按
Ctrl/Cmd+F,则同上打开会话内查找;例外:焦点在文件面板内时该快捷键归文件面板的查找条(不打开会话内查找),两个查找条同一时刻至多开一个。 - 假设查找条已打开且用户输入关键字,当停止输入(无需回车,无「搜索」按钮),则必须显示匹配计数(
当前第几个 / 共几个)并高亮全部命中,当前命中滚动到可视区并居中;匹配范围为会话已加载的全部消息内容:用户消息文本、助手消息文本(渲染后的纯文本,不是 markdown 源)、思考块正文、工具调用的名称与参数摘要、工具输出正文、计划与待确认卡片的文本。Diff 视图、文件面板、Artifact 卡片等独立面板的内容不参与会话内查找(各自有自己的查找入口)。 - 假设会话使用虚拟列表渲染(当前实现),当命中位于尚未渲染的区段,则仍必须能命中、能逐条跳过去并把目标行渲染出来——匹配必须取自消息数据(状态里的消息数组),不得只搜 DOM 中已挂载的节点;这条是本故事的核心判据,决定了实现不能退化成
webContents.findInPage。 - 假设查找条已打开且有命中,当用户按
Enter或点「下一个」,则必须在命中间前进;Shift+Enter或「上一个」后退;到最后一个再前进必须回绕到第一个(反之亦然)。 - 假设查找条已打开,当用户切换「区分大小写」「整词匹配」「正则表达式」,则结果、计数与高亮必须即时更新,并重新从第一个命中开始;正则语法非法时必须给出可读提示(不静默吞掉),且不匹配任何内容、不清空已输入的关键字。
- 假设查找条已打开且关键字无匹配,当查看查找条,则必须显示「无匹配结果」且计数为
0,查找条保持打开(不得自动关闭),消息列表滚动位置不变。 - 假设查找条已打开,当用户按
Esc或点关闭按钮,则必须关闭查找条、清除全部命中高亮、把焦点交回消息列表,滚动位置停留在最后一个当前命中处(不得弹回顶部或弹到底部)。 - 假设查找条已打开且当前有命中,当会话正在流式生成、新消息或工具输出追加进来,则当前命中索引与滚动位置必须保持不变(不因列表增长而跳动);新内容里的命中必须计入总数,计数随之更新。
- 假设查找条已打开,当用户切换会话(或该会话被关闭 / 该分屏换了会话),则查找条必须关闭并清空关键字与高亮——会话切换后残留上一会话的高亮属于错误行为。
- 假设查找条已打开,当用户用键盘在查找条内操作(Tab 在关键字输入框 / 三个开关 / 上一个 / 下一个 / 关闭按钮之间移动,Enter 与 Shift+Enter 的语义同场景 5),则控件必须可按 Tab 逐个到达、有可见焦点指示;查找条内的按键不得冒泡触发行级快捷键(裸键、方向键导航)或全局快捷键。
- 假设命中落在一段 markdown 渲染结果里(加粗、行内代码、列表项、标题),当高亮渲染,则必须在渲染后的文本上做命中标记且不破坏原有 markdown 结构(不得把原始 markdown 源文本贴进渲染结果);命中跨越多个渲染节点时(如加粗边界)按「同一段落内连续文本」匹配,允许在该段落内高亮相邻片段。
边界情况
- 超长单条消息:单条消息正文可达数万字符,匹配必须线性扫描且不阻塞输入(输入防抖 + 分片计算);不得因为一条长消息导致查找条输入卡顿。
- 正则回溯:用户可输入任意正则,必须具备超时或步数上限保护,超限时按「正则过于复杂」提示并停止本次匹配,不得让 UI 卡死。
- 非 ASCII 大小写:中文没有大小写,「区分大小写」对中文无影响;大小写折叠必须用完整 Unicode 折叠,不得只处理
a-z。 - 正在变化的行:命中落在正在流式增长的最后一条消息上时,高亮必须随文本增长而延伸,不得留下半截高亮。
- 屏幕阅读器:计数与「无匹配结果」状态必须通过
aria-live播报,查找条本体为role="search"。
非目标(明确排除)
- 替换:会话内容是只读的,不提供替换输入框(文件面板的编辑态才需要替换)。
- 跨会话搜索:本规格只作用于当前会话;跨会话搜正文见 desktop-sessions.md「侧栏会话内容搜索」。
- 按行号跳转:会话没有行号概念,不提供文件面板那样的跳行输入。
- 跳转到历史版本 / 编辑过的消息:不引入任何会话内容改写能力。