Appearance
功能规格说明:消息渲染系统
创建日期:2026-03-24
用户场景与测试 (必填)
用户故事:消息历史显示(优先级:P1)
作为用户,我希望看到与 agent 的对话历史,以便我可以回顾之前的交互。
为什么是这个优先级:对于基于聊天的 agent 的核心用户体验至关重要。
独立测试:向 MessageList 组件提供消息列表并验证它们按时间顺序渲染。
验收场景:
- 假设有消息列表,当渲染时,则它们按时间顺序显示。
- 假设有较长的消息历史,当渲染时,则仅显示最近的消息以维护性能和可读性。
- 假设有历史消息,当渲染时,则它们使用 Ink 的
Static组件渲染以避免不必要的重新渲染。
用户故事:动态块渲染(优先级:P1)
作为用户,我希望看到活动工具执行或运行命令的实时更新,以便我知道 agent 正在工作。
为什么是这个优先级:在长时间运行的操作期间为用户提供即时反馈。
独立测试:将工具块的阶段从 running 更新为 end 并验证 UI 相应更新。
验收场景:
- 假设一个处于
running阶段的工具块,当渲染时,则它显示动态展示(如加载指示器或进度指示器)。 - 假设一个正在运行的 bash 模式命令(
!ls),当渲染时,则其输出实时更新。 - 假设任何其他块类型(如 text、error),当渲染时,则它被视为静态内容。
用户故事:多种块类型支持(优先级:P1)
作为用户,我希望不同类型的内容(文本、代码、错误、图片、工具调用)被适当地渲染,以便我可以轻松区分它们。
为什么是这个优先级:确保复杂 agent 响应的清晰度和可读性。
独立测试:渲染包含每种块类型之一的消息并验证它们都正确显示。
验收场景:
- 假设一个
stage === "streaming"的文本块,当在折叠模式下渲染时,则它显示最后 30 个字符,灰色并截断(与工具和推理块流式行为一致)。 - 假设一个
stage !== "streaming"(如end或未定义)的文本块,当渲染时,则它使用 Markdown 渲染显示。 - 假设用户消息中的文本块,当渲染时,则它显示完整内容,无论 stage 如何。
- 假设一个错误块,当渲染时,则它以红色显示并带有 "Error:" 前缀。
- 假设一个工具块,当渲染时,则它使用专门的
ToolDisplay组件。 - 假设一个工具块处于
start或streaming阶段,当渲染时,则状态点显示为灰色(执行中、结果未定);处于running阶段时显示黄色;处于end阶段时按结果显示——成功为绿色、失败为红色。 - 假设一个图片块,当渲染时,则它显示图片的占位符或摘要。
用户故事:IDE 插件 Edit 差异预览(优先级:P1)
前 3 个故事描述 CLI 终端渲染;本故事描述 IDE 插件侧的工具结果展示。作为 IDE 插件用户,我希望在 AI 修改文件后直接在对话中看到修改前后的差异对比,以便快速审阅 AI 的改动。
为什么是这个优先级:差异审阅是用户判断 AI 改动是否正确的主要依据,直接影响对改动的信任与采纳。
独立测试:让 AI 使用编辑工具修改文件,验证对话中出现红绿差异视图;在编辑工具的权限确认出现时,验证确认框中同样展示该差异预览。
验收场景:
- 假设 AI 完成了一次文件编辑,当工具结果渲染时,则显示差异对比:删除的内容以红色标识、新增的内容以绿色标识、未变更的上下文以灰色标识。
- 假设改动在文件中的位置较集中,当差异渲染时,则未变更的长上下文自动折叠,仅保留变更前后各约 3 行并以省略号标示省略部分。
- 假设改动内容较长,当差异渲染时,则预览区域限制最大高度并可滚动查看。
- 假设编辑工具正在流式输出参数,当结果尚未完成时,则不渲染差异预览,避免展示不完整内容。
- 假设编辑工具请求用户权限确认,当确认框出现时,则确认框内展示同样的差异预览,用户审阅后再决定是否批准。
- 假设编辑前后内容完全一致,当差异渲染时,则显示"No changes"空态。
用户故事:IDE 插件 Write 文件预览(优先级:P2)
作为 IDE 插件用户,我希望在 AI 写入文件后看到写入内容的折叠预览与结果摘要,并能一键在编辑器中打开该文件,以便快速核对与跳转。
为什么是这个优先级:写入预览提升可核对性,但写入结果摘要已能提供基本信息,故次于差异预览。
独立测试:让 AI 使用写入工具创建文件,验证对话中出现文件标题、结果摘要与内容预览;点击文件名或打开按钮,验证在编辑器中打开该文件。
验收场景:
- 假设 AI 完成了一次文件写入,当工具结果渲染时,则显示文件标题(工具名与相对路径)与结果摘要(如写入行数)。
- 假设写入成功,当预览渲染时,则显示写入内容的折叠预览,底部以渐变淡出提示内容被截断。
- 假设用户点击文件路径或打开按钮,当点击发生时,则在编辑器中打开对应文件。
- 假设写入工具正在流式输出参数,当结果尚未完成时,则仅显示文件标题,不渲染内容预览。
- 假设写入参数无法解析,当渲染时,则仅显示文件标题,不渲染内容预览。
用户故事:工具文件路径相对化显示(优先级:P1)
作为 WebView 用户(VS Code/JetBrains/桌面端),我希望 read、edit、write 工具标题中的文件路径在 agent 工作目录(workdir)内时显示为相对路径,不在 workdir 内时才显示绝对路径,以便标题简短易读,且与终端(CLI)中的显示保持一致。
为什么是这个优先级:文件工具是 agent 最频繁的操作之一,深目录下的绝对路径会占据大量标题空间,且与用户熟悉的 CLI 行为不一致(同一会话中 CLI 与 IDE 显示不同路径会让用户困惑)。现有实现仅对正斜杠路径生效,Windows 下(workdir 与文件路径为反斜杠)前缀匹配失败,实际显示为绝对路径,与 CLI 相悖。
独立测试:渲染包含 read/edit/write 工具块的消息,验证标题中的路径按 workdir 相对化显示(在 workdir 内为相对、之外为绝对),并验证点击路径仍以原始路径打开文件。
验收场景:
- 假设 文件位于 workdir 内(如 workdir=
/home/user/repo、文件=/home/user/repo/src/index.ts),当 工具标题渲染时,则 显示相对路径src/index.ts。 - 假设 文件位于 workdir 之外,当 工具标题渲染时,则 显示绝对路径。
- 假设 workdir 与文件路径的分隔符风格不一致(Windows 反斜杠与正斜杠混合),当 工具标题渲染时,则 仍正确显示相对路径,与 CLI 行为一致。
- 假设 文件路径与 workdir 相同,当 工具标题渲染时,则 显示
.,与 CLI 行为一致。 - 假设 workdir 未知(宿主尚未提供),当 工具标题渲染时,则 显示原始路径。
- 假设 用户点击工具标题中的路径,当 点击发生时,则 仍以原始(绝对)路径打开文件,相对化仅影响显示。
用户故事:跨会话测量高度缓存(优先级:P1)
作为 WebView 用户(VS Code/JetBrains/桌面端),我希望消息列表记住每条消息的实测高度,以便切换会话或刷新页面后总高度与视口位置立即准确,而不是从粗略默认值开始逐行测量收敛、滚动条跳动数秒。
为什么是这个优先级:长会话下高度收敛的逐行测量链(每次测量缩小 spacer → 重新锚定底部 → 挂载下一行)会让恢复后的滚动条持续跳动,直接损害会话恢复体验。
独立测试:渲染消息列表使行被测量后,将 wave-webview:measured-heights:v1 写入 localStorage;重新加载页面后验证相同消息 id 的行直接以缓存高度渲染,无收敛过程。
验收场景:
- 假设 行被测量过,当 测量发生时,则 该消息 id 的实测高度与折叠状态写入 localStorage(400ms 防抖批量写)。
- 假设 页面重新加载或切换会话,当 虚拟列表估算行高时,则 命中缓存的 id 直接使用缓存高度;未命中的行使用已测行的平均值。
- 假设 用户展开/折叠了推理或压缩块,当 行重新测量时,则 缓存条目被覆盖为新高度 + 新折叠状态;折叠状态与默认渲染状态不符的缓存条目(如展开后重载、默认折叠)不被信任,直到重新测量。
- 假设 缓存条目超过 2000 条,当 新测量写入时,则 按插入顺序淘汰最旧条目。
- 假设 存储不可用或数据损坏(隐私模式、配额超限、非法 JSON),当 读写缓存时,则 优雅回退到内存缓存,列表功能不受影响。
用户故事:首开会话钉底——测量增长方向(优先级:P1)
作为 WebView 用户(VS Code/JetBrains/桌面端),我希望首次打开一个从未缓存过的会话时,视口钉在真实底部,以便最后一条消息完整可见,而不是停在估计高度上留下数百到数千像素的空白。
为什么是这个优先级:初始估计高度(默认 200px)低于工具密集型会话的真实平均行高时,挂载窗口的实测会持续增长 spacer 总高度;若钉底追逐只在固定帧数内进行(8 帧 chase),测量增长尚未结束时追逐已放弃,视口停在旧总高上且无后续恢复机制(实测偏移 578~2198px)。测量缓存命中后该问题消失,但任何首次打开的会话(或全局平均值偏离较大的会话)仍会触发。现有合成测试只覆盖估计高于真实的收缩方向(浏览器 clamp 自动修正),增长方向无覆盖。修复机制对齐 OpenCode(生产验证):① scrollToFn 在滚动前先写 spacer 高度,滚动目标按当前总高 clamp(消除旧高度短钉);② spacer 尺寸变化事件驱动持续重钉(无帧预算上限),覆盖追逐退出后到达的增长波;③ 虚拟器内部尺寸补偿产生的伪滚动事件标记为程序滚动,不被误判为用户上滚。注:OpenCode 的 initialOffset=MAX_SAFE_INTEGER 方案经评估弃用——它使内部偏移超出末尾、wasAtEnd 补偿跟随总高,但会让收敛延迟约 200ms,迟到的测量修正(估算→实测的缩小)落在流式 baseline 之后,破坏"流式期间不向上回跳"不变量。
独立测试:加载行高显著高于估计值的合成会话(如 300 条约 700px 高的消息),初始加载完成后滚动容器 scrollHeight - scrollTop - clientHeight ≤ 2px,且钉底过程跨越 spacer 持续增长的阶段。
验收场景:
- 假设 会话从未被测量过(空缓存),当 初始加载完成时,则 视口钉在真实底部(
scrollHeight - scrollTop - clientHeight ≤ 2),即使挂载窗口测量导致总高度持续增长。 - 假设 钉底跟随期间(含流式/思考内容持续增长、每块更新都触发程序重钉)用户向上滚动,当 用户的滚轮/触控板向上输入发生时,则 立即停止重钉,不把用户拉回底部——上滚意图须在输入事件同步识别(不等浏览器合成
scroll事件:合成事件在后续渲染帧派发,密集的流式重钉可能先执行而把视口拉回),仅当用户主动滚回底部附近才恢复跟随。 - 假设 总高度因测量/流式持续变化,当 spacer 尺寸每次变化且用户未上滚时,则 事件驱动持续重钉(无帧预算上限,对齐 OpenCode),最后一次变化后视口保持在底部。
- 假设 行测出比估计矮(如 30px 用户行 vs 200px 默认估计),当 虚拟器补偿产生伪下滚事件时,则 该事件不被误判为用户上滚(不锁死
userScrolledUp),后续重钉照常进行。程序滚动产生的scroll事件在渲染帧之后才派发(实测约 70% 晚于下一帧 rAF),因此程序滚动标记须由滚动事件消费清除而非单帧复位,伪滚动事件不得参与用户意图判定。
边界情况
- 消息超过 10 条怎么办? 系统应该只渲染最后 10 条消息,以确保终端保持响应且回滚不会变得过多。
forceStatic为 true 或视图被展开怎么办? 所有块,包括最后一条消息中的块,都应该渲染为静态内容,禁用任何动态更新或加载指示器。这确保视图在展开时保持"冻结"和高性能。- 块类型未知怎么办? 系统应该优雅地处理,通过忽略它或显示通用占位符,以防止整个 UI 崩溃。
- IDE 插件差异预览中超大改动怎么办? 未变更上下文折叠展示,预览区域限制最大高度并可滚动,避免长 diff 撑开对话流。
- IDE 插件工具参数解析失败怎么办? 差异/写入预览优雅降级:差异预览不渲染,写入预览仅显示文件标题,不影响消息其余内容。
- 测量缓存与折叠状态不符怎么办? 缓存条目按折叠状态校验,状态不匹配(如展开后重载、默认折叠)时视为失效,回退到平均值直到该行重新测量。