Resolve 15 shared-control, overlay and responsive-layout issues plus the MCP editor being hidden behind settings. Restore theme contrast, danger and disabled states, visible keyboard focus, and consistent Continue fields. Keep the workbar add button reachable and model protocol values readable in narrow containers.
Make nested MCP and image dialogs usable above their parents with focus restoration, keep project Escape handling within the active modal, release native browser views for overlapping help, and restore keyboard access and Escape in sandboxed HTML previews without relaxing sandbox permissions. Add unit and real Electron regression coverage and update design and validation documentation.
Release note: 修复深浅主题下控件边框、危险操作、禁用状态和键盘焦点显示不一致的问题。MCP 编辑器与图片预览可在父弹窗上正常操作,HTML 全屏预览支持键盘进入和 Escape 返回;窄工作栏的添加按钮保持可用,模型连接协议文字完整可读。
Validation: the full suite recorded 5309 passed, 3 failed and 83 skipped. The ProjectSwitcher initial-focus regression was then fixed, and App, ProjectSwitcher and overlay Electron tests passed all 331 cases. The other two HeartbeatCenter failures were reproduced on isolated HEAD 11facf0 and remain pre-existing. Typecheck and lint passed after the fix; staged whitespace checks passed. The full suite was not rerun after the focused fix.
Limits: Electron evidence is from Windows development runs, with fixtures for unrelated services and native occlusion throttling disabled in some drivers. No production build, packaging, macOS/Linux, remote Agent host or external MCP connection validation; skipped tests and the documented scaling and native-select coverage gaps remain.
19 KiB
复发缺陷与防复发规则
本文档收录已经在本仓库重复出现过的缺陷类型。每条规则都来自至少一次真实修正,不是预设的 通用最佳实践。新增条目前,先确认该问题在提交历史中确实出现过一次以上,或一次修正中同时 出现在多处。
修正复发缺陷时,优先建立一条能覆盖整类问题的机制(共享选择器、共享函数、能发现全部实例 的测试),而不是逐个点名修补。逐个修补正是这些条目反复出现的原因。
普通结构升级再次触发历史回收
症状:历史已经转换完成,后续升级仍显示清理历史并执行耗时的全库回收。
原因:只修复启动检测不够;普通旧 schema 仍会进入升级 Worker,若共享升级方法 把任意空闲页当作回收依据,就会在以后每次结构升级时重现。
规则:共享启动决定分别描述内容转换与回收,普通结构升级默认不回收。只有具体 旧内容转换允许回收,同次启动重试沿用内存决定;下一次启动重新检测,不增加持久标记。 具体规则见迁移指南。
防复发:schema 45/46 的真实 SQLite fixture 保留正常写入产生的空闲页,断言
无历史转换、无 VACUUM、内容与完整性保留;真实 Worker 验证取消及重试,
启动调度测试分别覆盖允许和禁止回收的决定在重试中不变。
历史:8dce1d7 和 38d0923 先后修正启动检测,后者随 0.13.9 发布;
2026-09-26 检查 0.13.15 的 schema 46→47 路径发现共享 Worker 仍无条件按空闲页回收。
本次修复在共享操作层分离结构升级与回收,而非给 46→47 增加特例。
异步界面测试只等待部分状态
症状:本地通过的界面测试在完整 CI 中找不到预期控件,或在前一步更新后立即读取到旧状态。
规则:复用 Testing Library 的 act、waitFor 和现有 deferred Promise,
按请求完成和最终可见状态同步,不以另一个元素出现或消失代替完整操作结束。
测试请求重试时显式控制后一次响应,分别断言等待中和响应完成后的状态;
同一异步操作的多个最终断言放入同一个 waitFor。不要只延长全局超时或放宽断言。
Electron 原生拖拽测试应确认窗口焦点、拖拽状态建立、宽度更新及拖拽结束后再断言;
移动事件明确携带左键按下状态,不用固定睡眠代替输入处理完成。
历史:2026-09-26 检查 Desktop 0.13.14 标签 CI 36165805814 与
main CI 36164698647:原生能力重试用例在启动状态未稳定时打开选项,
浏览器关闭用例只等待页签消失便同步断言错误已清除。两处测试改用上述已有同步机制;
是否消除 CI 失败以修复提交的 Linux CI 结果为准。
同日 Windows 全量测试及独立复跑中,Magic Notes 原生拖拽分别停在索引列 168 和
AI 列 280。测试同步原生输入与实际状态后独立复跑通过,保留原有宽度和持久化断言。
整窗遮罩与原生拖动区域
症状:弹窗覆盖标题栏区域时,其关闭按钮无法悬停和点击。
原因:.brand 与 .topbar 声明了 -webkit-app-region: drag。整窗遮罩绘制在其上方
时,原生标题栏仍然截获指针。
规则:整窗遮罩必须 inset: 0 铺满窗口,并由 styles.css 中统一的遮罩选择器声明
-webkit-app-region: no-drag。新增遮罩时扩展该共享选择器;不要在各自规则里复制
no-drag,也不要用 inset: 38px 0 0 之类的偏移绕开标题栏。完整要求见
UI 设计系统。
防复发:WorkspacePrimitives.test.tsx 的
"releases the native drag region for every full-window overlay" 递归扫描 Renderer CSS,按
position: fixed 与 inset 声明发现遮罩,排除不接收指针的透明宿主,并校验共享 no-drag。
历史:e9679fe、ca4a58c 各修了一部分被人工点到的弹窗,同一批改动又新增了两个未覆盖
的遮罩;后续复查改为共享规则加自动发现测试。
2026-09-27 复查发现自动发现仍依赖 backdrop/modal 后缀,漏掉知识引用的
.knowledge-citation-dialog。补入共享规则并移除后缀限制;真实 App 的 Windows
WM_NCHITTEST 回归验证修复后为 HTCLIENT,临时清除该规则的对照为 HTCAPTION。
共享控件样式依赖页面祖先
症状:放弃笔记草稿弹窗中的危险按钮显示为原生按钮,其他页面的危险确认也缺少样式。
原因:唯一的 .danger-solid 定义限定在 .magic-notes-page 下;Portal 挂到 body
后失去该祖先,心跳、监督者、工作区文件删除和工具环境设置也无法命中。
规则:共享按钮在全局定义并复用基础控件规则,不向每个调用处补页面类。 具体状态和令牌见 UI 设计系统。
防复发:magic-notes-capture.electron.test.ts 在真实 App 中触发草稿确认,检查
按钮不属于页面祖先时的浅深主题 computed style、尺寸、字体、禁用及原生 Tab 焦点;
悬停通过 Chromium 调试协议强制伪状态检查,不作为原生鼠标命中验证。
共享令牌测试检查默认和悬停危险背景上的文字对比度。
历史:2026-09-27 同次检查确认草稿确认及上述四处调用均受同一作用域错误影响。
同日后续全局审查发现,控件已有全局定义仍会被容器与状态规则覆盖:深色通用输入规则替换
控件边框,普通图标 hover 覆盖危险图标,菜单按钮规则覆盖危险确认;.field input 还将
Radio 与 Switch 当成文本字段。共享规则现已排除二元控件,恢复全局焦点轮廓,降低菜单
默认样式特异性,并移除深色控件兜底覆盖。Continue 独立字段通过 .field-control 复用
输入规则。shared-controls.electron.test.ts 检查浅深主题、设置容器、body Portal、菜单
组合、默认/悬停/禁用/选中态及原生 Tab 焦点,并渲染实际 Continue 组件。令牌测试同时检查
主按钮 hover 白字与控件背景的对比度。审查修复记录
保留本次范围、验证结果和未测项。
网页浮层遗漏原生浏览器视图
症状:知识库实例、项目设置等 Modal 的右侧被浏览器网页覆盖,通知或菜单也可能 被压在原生视图下面。
原因:WebContentsView 不参与 Renderer 的 CSS 层叠。先前只在关闭终端确认时
显式隐藏 viewport,没有覆盖其他页面的 Portal 和浮层。
规则:BrowserViewport 共用 browser-viewport-occlusion.ts,按 Modal 语义和
浮层相交状态释放显示租约,不销毁网页会话。不得再增加逐个弹窗的特殊隐藏条件。
防复发:组件回归覆盖 body Modal、嵌套 Modal、已打开 Modal、隐藏或非相交浮层、 通知、终端确认和非活动页签;实际 Electron 验证同时检查原生视图可见性,不能只看 DOM 的遮罩或截图。
历史:先前浏览器多实例修正只覆盖了终端关闭确认;2026-09-13 用户在知识库实例 Modal 再次复现,同一检查确认其他应用级 Modal 均缺少处理。 2026-09-27 同行帮助的 tooltip 仍未进入选择器。现已纳入同一相交判定和尺寸观察; Electron 回归从运行记录页打开真实 InlineHelp,并通过工作栏分隔条形成相交与非相交布局, 检查实际 WebContentsView 可见性及恢复后的页面标记,确认网页没有重建或刷新。
Modal 内的菜单与通知被遮挡或隔离
症状:设置中的“测试解析”菜单落在遮罩后面;设置、应用中心等全局 Modal 打开时, 应用通知被遮挡,关闭按钮也无法点击或聚焦。
原因:菜单挂在 body,却使用低于 Modal 的层级;通知留在被 Modal 设为 inert
的应用壳层内,同时层级也低于 Modal。单独增大 z-index 无法解除模态隔离。
规则:菜单与通知共用 FloatingPortal。稳定的非模态 Popover 宿主挂入所属/当前
Modal,兼顾 top layer 绘制、辅助技术的模态归属及焦点循环;没有 Modal 时才挂在
body。保留背景隔离,不在页面各自加层级常量,不因移动通知重置其计时。
防复发:FloatingPortal.test.tsx 覆盖宿主移动和内容不重挂载;
overlay-layering.electron.test.ts 使用真实组件、Portal、鼠标命中及原生键盘,覆盖
解析菜单/二级结果、设置、应用中心、本机推理监控和原生 dialog,以及浅深主题和短窗口。
历史:2026-09-19 同次检查确认解析测试菜单和应用级通知两处挂载机制错误,后者影响 多个全局 Modal;统一宿主后补完整 App 的真实保存通知与 TXT 解析复验。
2026-09-27 在真实 App 的“设置 → 能力与工具 → MCP → 自定义 MCP → 添加 Server”
再次复现子层遮挡:MCP body Portal 沿用 z-index: 70,低于设置的 130,输入框中心
实际命中父设置页签。编辑器仅手动聚焦,父设置未设为 inert。添加与编辑统一接入
--z-dialog 和 activateModalFocus;遮罩关闭阻止默认鼠标焦点转移,Escape 停在子层。
新增 Electron 回归检查真实鼠标命中、输入、Tab 循环、各关闭入口、生产 IPC 保存及重读,
并检查浏览器在父子 Modal 期间隐藏、最后恢复同一未刷新的网页。子 Modal 应使用共享模态
机制;上述 FloatingPortal 仍用于非模态菜单与通知。
同次审查确认解析结果打开图片时也沿用低于父层的 z-index: 80,现复用 --z-dialog。
解析正文使用稳定的组件与图片回调,避免 App 打开大图时重建触发节点而丢失返回焦点。
窄侧栏打开项目 Modal 也已接入共享背景隔离,避免一次 Escape 同时关闭两层。
本阶段实现、键盘边界和 Electron 证据见浮层审查修复记录。
Flex 滚动内容压缩页签和分段控件
症状:知识库来源选择被长参数表单压成窄缝,标签和按钮文字被裁掉。
规则:PageTabs 与 SegmentedControl 在共享样式上固定 flex: 0 0 auto;
直接包装层也必须保留高度。不要按页面复制高度常量。
防复发:WorkspacePrimitives.test.tsx 检查共享分段控件不收缩,已有平台功能、
能力和 MCP 页签回归继续保留;Electron 复验必须包含长表单与短窗口。
历史:设置页签曾补过包装层不收缩规则;2026-09-13 知识库创建类型的共享
SegmentedControl 又出现同类问题。
逐行查询代替主键查询
症状:列表渲染或校验函数随数据量线性变慢,且慢在单次操作内重复发生。
原因:用 listAll().some(...) 或 listAll().find(...) 判断单行是否存在。表有主键或
唯一索引时,这是把 O(1) 查询写成了全表扫描加 JSON.parse。
规则:判断单行存在或取单行数据时,在 store 上提供按键查询方法(例如 hasBinding、
getBinding),不要在调用方过滤全量列表。确实需要按分组统计时,循环外取一次并构建
Map,不要在循环内反复查询。
历史:外部知识库绑定同时出现三处:requireLibrary(30+ 调用点,一处在循环内)、
getKnowledgeSnapshot(每个知识库两次全表扫描)、listInstances(每个实例过滤一次)。
将累计过程当作增量事件反复落库
症状:聊天正文很小,执行记录数据库却增长到几十 GiB;只增加几个字节的子任务输出, 又写入接近 1 MiB 的旧过程副本。
原因:本机与远程 OpenCode 共用完整进度快照,两个桌面落库入口都按流式频率新增 完整 JSON;正文流的缓冲器没有覆盖这种累计快照。
规则:界面快照不直接作为追加式日志的存储表示。子任务事件通过共享存储层记录变化, 保留逐条还原和远程来源去重;会话仍保存完整显示结果。旧用户数据通过同一编码器无损 转换后回收空间,不靠删聊天或单独修本机数据库解决。
防复发:使用含大工具输出的多次更新检查总存储量,并验证重启、重复远程事件、
消息事务回滚及旧库中断续跑。工具对象须包含生产路径中的 undefined 可选字段,
经过真实 JSON 往返后仍识别为无变化,不能按对象键存在性比较。真实模型验证须包含
子代理正常使用工具、修改文件及运行测试,不能只测禁用工具的文本流。
真实旧库需验证逐条等价和物理文件缩小,已升级 schema 的重复数据同样要覆盖。
历史:2026-09-13 检查到 305,551 条子任务事件占用 90.11 GiB;
本机 appendTaskEvent 与远程 insertRemoteTaskEventOnce 同时存在重复保存路径。
0.13.1 发布后的真实工具任务再次复现:同批 928 条事件有 7,787 次重复工具块写入;
只统一 JSON 空字段后,事件负载从 10,467,622 降为 452,391 字节,逐条还原一致。
生产路径复用“测试”函数
症状:正常用户操作触发了只应在配置校验时出现的错误码,或多发了一次网络请求。
原因:让生产路径直接调用为“测试连接”“保存前校验”准备的函数。这类函数通常带有额外的 能力预检和更严格的失败条件。
规则:生产路径与校验路径各自提供入口。校验入口可以在生产入口之上叠加预检,反向复用 不允许。共用的裁剪、归一化逻辑抽成独立函数由两者调用。
历史:对话检索曾调用 ExternalKnowledgeService.testRetrieval 并把用户问题放进
testQuery,导致每次提问多一次 detail 请求,且 EXTERNAL_KB_CAPABILITY_UNAVAILABLE
会出现在正常对话中。
事件迭代结束前释放底层订阅
症状:并行 OpenCode 会话已结束,但部分 SSE 响应流仍未关闭;只检查最终取消信号 会误判回收成功。
原因:OpenCode SDK 1.18.29 在迭代器 return() 时移除取消监听并释放 reader lock,
但不主动取消 reader。外层 for await 先结束 SDK 迭代器,外层 finally 再 abort 的
顺序过晚,带缓冲的响应流可能继续存活。聊天和原生 Compact 均曾使用该顺序。
规则:两条 SDK 消费路径共用 consumeEventSubscription,先 abort 对应订阅,再结束
底层迭代器;请求取消和 Server 生命周期仍保留各自作用域。不要修改依赖包、放宽回收
断言或关闭共享 Server 来代替单次订阅回收。
防复发:Runtime 回归检查正常结束和消费方提前返回时,底层迭代器清理前取消已经发生。
opencode-runtime-lifecycle.test.ts 使用真实二进制与 SDK,在 1、4、8 路并行、单请求
取消和原生 Compact 后检查打开的响应流归零,并验证并行请求不被误取消。
历史:b03c256 已增加请求级订阅取消;2026-09-13 候选检查仍复现 3 条响应流未关闭,
复查发现聊天与 Compact 都需要调整上述顺序。
快照比对代替取消
症状:为检测“操作期间配置被改动”而反复读回状态并 JSON.stringify 比较。
原因:把取消语义当成配置漂移检测来实现。桌面单用户场景下,改动配置的那次操作本身就能 取消在飞请求。
规则:用 AbortController 在配置变更点取消在飞请求,并让被取消的操作不写回任何结果。
不要引入前后快照比较、版本化内部凭据或多阶段状态机。唯一性约束交给数据库索引。
历史:外部知识库服务曾有 5 处快照比对与专用错误码 EXTERNAL_KB_CONFIG_CHANGED;全部
删除后行为反而更正确,因为旧实现会把“用户停用实例”记成 probeStatus: 'failed'。
删除疑似死代码时的验证顺序
症状:删除后类型检查通过,但运行时行为改变,或测试断言的是已不存在的行为。
规则:删除枚举取值、文案键、导出函数前,按此顺序确认:
rg全仓搜索引用,区分生产代码与测试 fixture。- 确认该取值是否可能由生产代码写入。仅测试 fixture 使用即为死代码。
- 检查是否属于已持久化数据的解析契约。持久化契约即使当前不写入也必须保留。
- 删除后跑受影响的定向测试,再跑
npm test。
历史:probeStatus 的 'healthy' 仅存在于测试 fixture,可以删除;而
externalKnowledgeLocatorSchema 的 sourceUrl 虽然同样从不写入,但它解析已持久化的引用
数据,必须保留。两者表面症状相同,处置相反。
渲染期派生值未记忆化
症状:流式响应期间界面卡顿,或大列表操作延迟。
原因:在组件体内直接构建 Map/Set 或链式 filter/flatMap,而该组件在流式增量期间
每个 token 都会重渲染。
规则:组件体内的派生集合必须包在 useMemo 中,依赖收窄到真正影响结果的值。紧邻的
useMemo 之后又裸建集合,会让记忆化收益减半。
注意:React 允许在渲染期用守卫调用 setState 来响应 props 变化,这是官方推荐做法,不要
把它改写成 useEffect。仓库 lint 规则 react-hooks/set-state-in-effect 会拒绝后者。
解析警告被当作提取正文
症状:OCR 没有识别文字,但缺图警告让图片附件切成文字发送,或让仅图片结果被判为 可发送正文。
规则:HTTP OCR、显式图片提取及结果完整程度共用 hasExtractedDocumentText。
图片引用和解析器生成的缺图警告不计入正文;预览中的警告保留,不靠删除诊断掩盖失败。
防复发:真实 loopback HTTP 回归同时检查有/无识别文字、有效图片与缺图并存、 显式提取结果及保存后的完整程度;无正文时必须保留原图发送方式。
历史:2026-09-19 同次复验确认 DocumentParsingService.extractImage 与
parsedCompleteness 两处误计警告,HTTP Provider 自身的检查却正确,故统一判定函数。
列表刷新重建历史消息并触发重复保存
症状:焦点刷新后,正文完全未变,却再次序列化并保存全部会话消息;历史越多, 前端和 Main 的短时内存、主线程工作量越大。
规则:会话消息的图片状态使用同一保留引用的合并函数,三个合并分支都必须在 语义未变时返回选定原对象。列表、活动跳转、任务创建刷新都使用摘要或按 ID 读取, 不以完整历史列表作为单条查询。搜索只保留命中 ID;视图淘汰同时释放已确认保存的 详情引用,不把摘要误当成完整模型上下文或替换式保存输入。
防复发:App 回归检查 1,600 条历史刷新后保存消息数为 0;真实 SQLite 回归覆盖 未加载正文搜索、较早会话、501 条完整消息及 header-only 保存。另覆盖后台审批 晚于摘要请求到达、未打开会话复制、加载失败重试及恢复请求的问题投影。
历史:2026-09-15 在 f8d0741 引入的三个图片合并分支同时确认消息引用重建;
同次检查还发现活动跳转与任务创建继续整包读取历史,均改为共用的摘要/详情读取。