Files
navop/AGENTS.md
胡飞 4fbdb84cbd fix(windows): 后台子进程统一隐藏控制台,修掉启动闪黑框
Windows 上应用启动会闪一下控制台窗口,根因是启动期识别 WSL 发行版时直接
spawn 了控制台程序 wsl.exe 且未设置 CREATE_NO_WINDOW,系统为它新建了控制台
窗口(stdout 已重定向到管道也一样)。该路径由 #182 引入。

- terminal:WSL 识别改走 process_util::configure_background_child
- html-preview:Windows 分支拉起浏览器前设置 CREATE_NO_WINDOW,cmd /C start
  不再闪窗;纯逻辑 crate 内联 helper,不额外引入 tokio
- workspace_explorer:容器后端 docker exec 收敛到 process-util
- main:设置页安装 skill 的 npx/node 启动器同样处理
- 补 4 条结构 contract 测试钉住这些 spawn 点,防止回归
- AGENTS.md 沉淀该约定:GUI 进程 spawn 控制台程序必须隐藏控制台

验证:cargo test -p html-preview、cargo test -p terminal --lib
wsl_distributions、cargo test -p workspace_explorer --lib backend、
cargo check -p main --tests、cargo test -p main --bin navop mcp_skill、
cargo metadata --locked --offline 全部通过;改动文件无新增 clippy 告警。
Windows 实机「不再闪窗」需另行确认(macOS 上 cfg(windows) 不参与编译)。
2026-09-19 11:31:36 +08:00

104 KiB
Raw Permalink Blame History

全局 Agent 规则

本文件用于约束自动化代理在本机工作区中的默认工作方式,强调最短路径、风险分级、TDD 思想与可验证交付。

指令优先级

  • 指令优先级从高到低:
    1. 当前会话中用户的明确要求
    2. 仓库自身的规则、文档与约定
    3. AGENTS.md 的任务分流与流程规则
  • 本文件是自包含的工程工作流,不依赖特定外部流程体系。
  • 本文件明确标记为“个人硬门禁”的规则始终需要满足。
  • 对于仅涉及审查、分析、解释或方案讨论而不修改仓库文件的任务,可不进入完整实现流程,但仍应保持推理清晰、结论可追溯。
  • 如果用户明确要求 continue nonstop,则默认持续推进,直到满足验收标准或出现真实阻塞。

默认工作流原则

最短路径原则

  • 默认采用“满足质量要求的最短路径”。
  • 能直接完成并验证的,不升级为更重流程。
  • 能使用轻量版 planning 完成的小任务,不升级为重文档流程。
  • 流程的目标是降低返工与风险,而不是增加形式成本。

自包含工作流原则

  • 只读任务可直接分析并给出有依据的结论。
  • 实现类任务应先明确目标、边界、约束、验收标准和验证方式,再按风险决定计划与测试深度。
  • Debug 先收集证据、定位根因,再修改;Review 独立检查规格、正确性、回归与可维护性;完成前必须运行与风险相称的验证。
  • TDD 是新增或改变可观察行为、处理高回归风险逻辑时的工程方法,而不是外部工具或流程依赖。

任务分流模型

只读任务

以下任务可直接处理,不强制进入实现流程:

  • 分析
  • 解释
  • 架构说明
  • 代码阅读
  • 纯信息型问答
  • 不修改文件的只读审查

若任务属于真实问题排查,但尚未进入修改,应先收集日志、错误、复现条件和调用链证据,再形成可验证的根因假设。

实现类任务

以下任务原则上必须先完成需求澄清与计划拆分:

  • 新功能
  • bug 修复
  • 行为变更
  • 重构
  • 页面 / 组件 / API / 脚本实现
  • 数据处理逻辑改动

默认流程:

  1. 澄清目标、边界、约束、验收标准与验证方式
  2. 按任务规模形成轻量任务列表或详细实现计划
  3. 再进入具体实现
  4. 根据行为风险选择直接验证、回归测试或 TDD

轻量版 planning

  • 对小任务,允许采用轻量版 planning
  • 可在当前对话内完成,不强制产出长文档
  • 最小集合至少应明确:
    • 目标
    • 边界
    • 风险
    • 验证方式
  • 只有当任务规模、风险或不确定性明显上升时,才升级为更重的 planning 流程

Debug 类任务

适用:

  • traceback
  • error
  • exception
  • 数据异常
  • 运行时异常
  • 协议异常
  • UI 显示与数据不一致
  • 根因不明的问题

说明:

  • 对真实 bug / 异常排查,不直接猜测式修补
  • 先稳定复现或收集足够证据,再建立假设并逐项验证
  • 确认根因后再决定修复方案,并补最贴近故障表面的回归保护

Review 类任务

适用:

  • review
  • code review
  • reviewer output
  • spec compliance
  • 合并前审查
  • 阶段性交付审查

审查要求:

  • Review 应检查规格符合性、正确性、边界条件、错误路径、回归风险、测试充分性和可维护性。
  • 处理 review 反馈时,应先验证反馈是否成立,再修改;不要未经验证机械接受,也不要因表达方式而忽略有效问题。

完成前验证

在声称以下状态前,必须运行与改动直接相关的验证并检查实际输出:

  • 完成
  • 已修复
  • 可提交
  • 可合并
  • 测试通过
  • 验证通过

不得用过去的结果、推测或“理论上应该通过”代替当前验证证据。

前端任务

适用:

  • UI
  • UX
  • 页面布局
  • 组件视觉
  • 图表呈现
  • 交互设计

处理方式:

  • 明确布局、视觉层级、状态反馈、键盘操作、可访问性与不同窗口尺寸下的行为。
  • 修改后优先做结构测试、相关 UI 测试和必要的手工视觉验证。

流程升级规则

若在执行过程中发现以下任一情况,应升级到更重流程:

  • 影响文件、模块或系统边界超出初始判断
  • 出现公共 API、schema、持久化、并发或共享逻辑风险
  • 用户真实需求仍不清晰
  • 当前验证手段不足以覆盖风险
  • 任务已从局部修复演变为中大型实现或重构

流程降级规则

若任务满足以下条件,可降级到更轻流程:

  • 改动局部且边界清晰
  • 不涉及共享核心逻辑
  • 验证手段简单直接
  • 补长计划或补测试的成本显著高于风险收益
  • 问题已收敛为单点修复或局部调整

推进与验证

Step by Step Reasoning Workflow

  • 如果需求模糊,应先澄清目标、约束、验收标准与边界条件。
  • 为跟踪进度,请维护一个可见的任务列表。
    • 在其中列出先前任务的状态和待办事项,以及项目所需的计划行动(对于简单问答可以跳过)。
    • 对多步骤任务,任一时刻仅保留一个 in_progress 步骤。
    • 开始新步骤前及时标记已完成步骤,并避免重复输出冗长计划。
  • 回答时优先给出最相关结论,再补背景、依据与权衡。
  • 在任务推进过程中,遇到新信息时应主动修正先前判断中的错误或不一致。
  • 多步任务优先使用 update_plan 或同等方式维护高层进度。

Environment

  • 环境初始化优先遵循仓库文档与项目级 AGENTS。
  • 若无明确要求,则按当前任务所需执行最小准备,不做额外环境工程。
  • macOS 上 reqwest 默认系统代理探测可能在测试进程里触发 system-configuration 的 NULL object panic;应用内需要“无应用代理”的 HTTP client 时,优先使用 ReqwestClient::user_agent("onetcli") 这类显式 direct client 构造路径,并用相关 setting_tab/CLI 测试验证。

Command Verification Rules

  • 不得虚构已运行的命令、退出码或验证结果。
  • 如果一个关键验证命令无法执行,必须明确说明原因。
  • 在缺少验证证据时,不得声称“通过”“完成”“可提交”“可合并”。
  • 如果用户或仓库要求特定验证命令,应优先执行该命令。
  • 若关键验证被阻塞,应如实报告当前状态,并根据阻塞程度决定是否继续实现或先与用户确认。

Change Delivery Gate

在声明完成、准备 commit、准备 push、准备发起 PR 之前,应满足以下要求:

  1. 已完成与本次改动直接相关的验证,并如实报告结果
  2. 已按任务类型完成对应质量门禁:
  • 需要 review 的已 review
  • 需要 completion verification 的已验证
  • 需要测试保护的已按测试策略执行
  1. 若仓库要求更重验证,例如构建、集成测试、冒烟测试或特定脚本,应优先遵循仓库规则
  2. 若关键验证无法执行,必须明确说明原因,并降低完成度表述

测试策略判定(TDD 不是默认全量强制)

  • TDD 是一种实现思想:先用测试定义可观察行为,再以最小实现使测试通过,随后在测试保护下重构。
  • 是否采用严格 TDD,不按任务大小机械决定,而按“行为影响、共享范围、回归风险、测试价值”显式判定。
  • 如果采用 TDD,必须先观察到测试因预期原因失败;先写完实现再补一个立即通过的测试,不算 TDD。

A. 直接修改 + 定向验证

适用于:

  • 文案、样式、布局微调
  • 显然局部、低风险的小修复
  • 不涉及公共 API、数据库 schema、共享核心逻辑、复杂状态机或并发语义
  • 改动本身明显小于补测试成本

处理方式:

  • 可直接修改
  • 修改后必须执行与本次改动直接相关的定向验证
  • 若已有相关测试,则优先运行相关测试;没有则不强制新增测试

B. 修复后补回归测试

适用于:

  • 中小 bug 修复
  • 有局部行为变化,但范围有限
  • 容易补一个贴近问题表面的回归测试

处理方式:

  • 可先修复再补测试
  • 不强制严格 TDD
  • 但应尽量补最贴近问题表面的回归测试

C. 必须采用 TDD

适用于:

  • 新功能开发
  • 明确行为变更
  • 公共 API / contract 变更
  • 跨模块共享逻辑修改
  • 数据库 / 持久化 / 并发 / 状态机相关改动
  • 高风险或高回归风险任务

处理方式:

  • Red:先写一个最小、可读、能表达目标行为的测试,并运行确认它因缺少该行为而失败
  • Green:只写让测试通过所需的最小实现,避免顺手扩张范围
  • Refactor:在测试保持通过的前提下整理命名、结构与重复逻辑
  • Verify:重新运行定向测试,并按影响范围补充相关 crate、集成或端到端验证
  • 若受外部系统、硬件或现有架构限制,无法合理先写自动化测试,应说明原因,并先建立最接近行为边界的可执行 contract、fake、回归脚本或明确的手工验证步骤

质量门禁分层

Level 0:定向验证

  • 适用于局部、低风险、小改动
  • 直接修改后执行定向验证

Level 1:回归测试

  • 适用于中小修复或局部行为变化
  • 修复后补最贴近问题表面的回归测试

Level 2TDD

  • 适用于新功能、明确行为变更、共享逻辑或高风险改动
  • 按 Red → Green → Refactor → Verify 循环推进

Level 3Code Review

  • 适用于阶段收尾、中高风险改动、合并前审查
  • 对照需求、代码差异和验证证据进行独立审查

Level 4Completion Verification

  • 适用于所有准备声称完成、已修复、可提交、可合并的任务
  • 运行最终验证命令,阅读实际结果,并如实报告通过项、未执行项与阻塞项

工程实践

快速上手

  1. 阅读仓库上下文
  • 查看相关文件、文档、最近提交
  • 优先理解当前任务涉及的模块边界
  1. 如用户提供 plan2go=<path>
  • 将该文件视为当前执行来源
  • 执行过程中保持计划状态与进度同步
  1. 若需要理解代码架构、调用链、数据流、入口与依赖关系:
  • 优先使用 ace-toolmcp__ace-tool__search_context
  • rg / grep 只用于已知字符串的精确定位
  • 若用户明确要求“找出所有出现位置”,可以先用 ace-tool 缩小范围,再用 rg 做枚举;但架构结论必须以 ace-tool 结果为准

文档维护

  • 每当计划、目标、约束 / 假设、关键决策、经验教训、步骤或进度状态发生变化时,应同步更新相关计划文档。
  • 复杂任务在开始实现前,应先评估工作量并拆分为范围受限、因果有序的子任务。
  • 对项目中在开发、review、debug、验证过程中反复证明有价值的经验,应及时沉淀到项目级 AGENTS.md 或等效项目规则文档中,而不是只停留在当前对话。
  • 经验沉淀的对象包括但不限于:
    • 常见根因与排查顺序
    • 特定模块的实现 / review 注意点
    • 特定验证入口、命令、超时或环境约束
    • 易误判、易回归、易重复犯错的问题
  • 原则是让项目规则随着真实问题不断演进:一次有效经验,尽量避免团队或后续 agent 再次付出同样试错成本。
  • 若项目已有经验模板,应尽量按统一模板沉淀,避免写成只在当前语境下才能理解的零散描述。
  • 一个推荐的最小经验模板包括:
    • 标题:一句话概括问题 / 经验
    • 触发信号:什么现象说明又遇到了同类问题
    • 根因 / 约束:为什么会发生
    • 正确做法:以后应优先怎么处理
    • 验证方式:如何确认这次处理是对的
    • 适用范围:影响哪些模块、页面、链路或命令

已沉淀经验

  • 标题:扩展机制收敛时先做全仓 + 外部仓库死代码审计,extension-api 不是孤儿而是 WIT 契约宿主

  • 触发信号:试图删除某个 extension-* crate 或“统一扩展机制”时,凭 rg 在 workspace 内没找到 use extension_api 就判定它是死 crate;或看到 extension-host/src/runtime.rsIpcExtensionRuntime/ComponentExtensionRuntime/ExtensionRuntimeFactory 而以为它是统一运行时核心。

  • 根因 / 约束extension-api 的 Rust 代码确实无任何 crate 编译依赖,但它的 wit/ 目录是 extension-wasm 全部 component bindings 的 WIT 源(9 处 path: "../extension-api/wit"),删除即破 wasm 组件路径;用户明确保留 wasm 时不能删。extension-host/src/runtime.rs 才是真死机制:零消费(workspace + 外部 navop-extensions 都不引),且是被 extension-plugin-adapter::ActivationManager 取代的废弃“统一 IPC/Component 运行时抽象”,其中 ComponentExtensionRuntime 是 TODO 占位。crates/elasticsearch-provider 非 workspace member、无人引用,是悬挂目录,但不在构建里。

  • 正确做法:收敛前同时 grep workspace 与 ../navop-extensions(该仓库的 Cargo.toml 依赖面决定外部 ABI);区分“无 crate 编译依赖”和“被 fixture/bindings 以路径/WIT 引用”。对外部驱动在用符号(extension-host 的 client/process/transport/host_api/universal_plugin、extension-driverserve/Driver)绝不轻动。DB SQL 驱动的 IpcDriverRegistry(driver.json) 与 ExtensionRuntimeCatalog(extension.json) 是刻意分层:db crate 依赖 extension-runtime 会成环,且设计 Non-Goals 明确不发明统一 SQL 协议。

  • 验证方式:删除前后跑 cargo check -p extension-host -p extension-plugin-adapter -p db -p extension-runtime -p universal-plugins -p maincargo test -p extension-host -p extension-plugin-adapter -p extension-runtimecargo clippy -p extension-host --all-targets;并确认 navop-extensionsextension_host::runtime/runtime.rs 符号引用。

  • 适用范围crates/extension-*crates/universal-pluginscrates/db/src/ipc/registry.rscrates/extension-runtime/src/extension_db_gateway.rscrates/elasticsearch-provider,以及任何“统一/删除扩展机制”类改造。

  • 标题:GPUI UI 测试不要直接依赖真实 Tokio worker 的完成时序

  • 触发信号#[gpui::test] 覆盖 UI 加载时,代码路径内部调用 one_core::gpui_tokio::Tokio::spawn,测试出现非确定性 background thread / scheduler 活动,或需要等待真实多线程 Tokio worker 才能断言 UI 状态。

  • 根因 / 约束Tokio::spawn 使用全局 Tokio runtime,再通过 GPUI background_spawn 回到测试调度器;这会让本应确定性的 UI 测试混入真实多线程调度。

  • 正确做法:优先把异步任务完成后的 UI 状态变更提取为纯状态 contract,并用普通单元测试覆盖成功、失败、防重复加载、手动刷新等行为;网络解析和下载使用 fake HTTP client 覆盖。对纯 HTTP/IO 的 UI 加载,不要在 GPUI view 层额外包一层 Tokio::spawn,优先使用 cx.background_spawn,这样 gpuitest-support 能用 TestAppContext/condition 稳定驱动真实 view 测试。

  • 验证方式:运行对应状态 contract 测试、fake HTTP 网络测试、真实 GPUI view 测试,以及相关 crate 的 cargo check / cargo clippy -D warnings / cargo test

  • 适用范围main/src/settings/*、扩展市场加载、更新检查、数据库驱动安装等 GPUI UI 层异步加载路径。

  • 补充(拆分 init 的固定手法):当待测 init(cx) 同时做「注册 global」和「Tokio::spawn 启动后台任务」时,把注册部分抽成独立函数(如 register_application_owner),#[gpui::test] 只覆盖注册的纯状态契约(同一 owner、重复注册被拒),被 spawn 的后台任务改用普通 #[tokio::test] 直接覆盖(真实 runtime 里 start/stop 是确定性的,因为 RuntimeMonitor::stopselect! 抢在 sleep 前返回)。Tokio::spawn 完成时会在真实 worker 线程上唤醒 GPUI background executor,测试调度器直接以 “Detected activity on thread Some("tokio-rt-worker")” 失败——这类报错一律按「拆 init」处理,不要加 sleep 或重试。

  • 标题GPUI background_spawn 不得直接轮询依赖 Tokio runtime 的数据库 Future

  • 触发信号:macOS 上执行 SQL 转储、表导入或表导出时,在 tokio::time::timeout、数据库连接初始化或 Tokio socket/timer 路径出现“没有 reactor/runtime”类 panic,随后因 panic 穿过 GPUI 的 extern "C" 回调边界而触发 SIGABRT

  • 根因 / 约束GPUI background_spawn 使用 GPUI background executor,不会自动进入应用的 Tokio runtime;数据库 Future 即使表面只是 async,其连接驱动通常依赖 Tokio timer、reactor 或 socket。上述“纯 HTTP/IO 优先使用 cx.background_spawn”的经验不适用于这类 Tokio-bound Future。

  • 正确做法:由拥有数据库操作的状态层统一通过 one_core::gpui_tokio::Tokio::spawn_result 创建任务,并向 View 返回 GPUI Task;运行时绑定的核心方法保持私有,并在入口使用 tokio::runtime::Handle::try_current() 做防御性校验。View 只负责进度 channel、文件写入和 UI 更新,不自行选择数据库 Future 的 executor。

  • 验证方式:覆盖 Tokio runtime 内外的 contract 测试;用结构回归测试保证受影响 View 不出现 background_spawn 或危险的 direct/sync API;运行相关 dbdb_view 测试和 main 编译检查,并确认崩溃栈不再从 GPUI background executor 进入数据库连接初始化。

  • 适用范围crates/db/src/manager.rscrates/db_view/src/import_export/*,以及任何会调用 Tokio timer、socket、数据库驱动或 Tokio channel 的 GPUI 后台任务。

  • 标题GPUI foreground Future 创建 Tokio timer 前必须进入应用 Tokio runtime

  • 触发信号:从 cx.spawn / AsyncApp 启动 ACP、外部进程或其他异步流程时,在 tokio::time::timeout / sleep 创建处直接出现“there is no reactor running” panic,即使后续实际工作已经通过 Tokio handle spawn。

  • 根因 / 约束GPUI foreground executor 不是 Tokio runtime;只把子任务 spawn 到 Tokio 不会让包裹该子任务的 GPUI Future 自动拥有 Tokio reactor。Tokio timer 在创建时就要求当前线程已进入 runtime context。

  • 正确做法:优先把 timeout 放入 Tokio handle spawn 的 Future;若必须在 GPUI foreground Future 中等待 Tokio channel,则先用应用持有的 tokio::runtime::Handle::enter() 覆盖 timer 的创建与轮询范围。协议集成测试使用纯 Tokio 入口,不用 deterministic GPUI test scheduler 等待真实 Tokio worker。

  • 验证方式:从 GPUI AsyncApp 入口运行连接测试,确认不再出现 reactor panic;再用真实 stdio fake agent 覆盖连接 timeout、prompt timeout/cancel、进程退出和连接复用。

  • 适用范围crates/ai_chat_view/src/acp/connection/*,以及任何从 GPUI foreground executor 调用 Tokio timer、socket、process 或 channel 的路径。

  • 标题GPUI overflow_y_scrollbar() 不要直接承担父级 flex 裁剪职责

  • 触发信号:窗口或面板里已经调用 .overflow_y_scrollbar(),但列表/卡片区域仍无法上下滚动,尤其是该区域同时需要 .flex_1().min_h_0().min_w_0() 参与父级布局。

  • 根因 / 约束gpui_component::scroll::ScrollableElement::overflow_y_scrollbar() 会生成额外的 Scrollable 外层 wrapper;该 wrapper 渲染时主要继承原元素的 size,不能假设原元素上的 flex/min 尺寸约束会作为父级布局约束稳定作用到外层滚动盒。

  • 正确做法:用普通外层容器承担父级布局与裁剪,例如 .flex_1().h_full().min_h_0().min_w_0().overflow_hidden();把真正可滚动内容放到内层 .size_full().overflow_y_scrollbar() 中。若父级使用 h_flex(),注意它默认 items_center(),外层滚动边界通常必须显式 .h_full() 或其他明确高度,否则内层 size_full() 可能塌陷成白屏。参考 main/src/new_connection/connection_window.rs::render_card_area

  • 验证方式:补结构性回归测试,断言外层有 flex/h_full/min/overflow_hidden 边界、内层有 size_full/overflow_y_scrollbar;运行相关 UI 模块的定向 cargo test,必要时手工打开窗口验证滚轮。

  • 适用范围GPUI popup、dialog、tab 面板中需要滚动的列表、卡片网格、表单内容区域。

  • 标题:GPUI 可收缩侧边栏输入区不要让父子两层同时依赖 h_full()

  • 触发信号AI composer、表单或底部操作区仍存在于元素树中,但 debug_bounds 显示外层只剩 0–1px、内层高度为 0,界面表现为输入框完全消失;问题常在给可收缩输入区及其子节点同时增加 .h_full() 后出现。

  • 根因 / 约束:纵向 flex 中,父节点需要根据子节点的 intrinsic height 分配空间,而子节点的 h_full() 又依赖父节点先得到确定高度,容易形成无法提供有效 flex basis 的循环;若父节点还允许 flex_shrink,输入区会优先塌陷到 0。只断言元素“已渲染”、宽度相等或底边未越界无法发现该问题,因为零高度 bounds 仍满足这些条件。

  • 正确做法:输入组件根节点保留自然高度和 .flex_shrink_0();由直接外层输入区域承担 .min_h_0().flex_shrink(1.0),并在短窗口中使用有界的 overflow_y_scroll() 提供滚动,不要让父子两层都以 h_full() 建立高度。消息区继续使用 .flex_1().min_h_0(),把剩余空间交给可滚动内容。

  • 验证方式:真实 GPUI 布局测试必须同时覆盖正常高度和短侧边栏,明确断言 input area 与 input root 的高度大于 0、输入区位于 viewport 内,并验证长消息时输入区边界不越出宿主;不能只检查 debug_bounds(...).is_some()

  • 适用范围crates/ai_chat_view 的 sidebar composer,以及任何“可滚动主体 + 底部输入/操作区”的 GPUI 纵向 flex 布局。

  • 标题GPUI TabContainer 必须在 active view 的直接边界截断 intrinsic size

  • 触发信号:打开 RDP、图片、画布等 tab 后,tab 栏窗口控件、左右侧栏或中心区域被内容“挤压”、自动靠拢;只给 TabContainer 根节点或 tab-content 增加 .min_w_0() 后问题仍会复现。

  • 根因 / 约束flex shrink 约束必须覆盖从 active view 到窗口 chrome 的每一层直接布局边界。外层已有 .min_w_0() / .min_h_0() 并不能替代中间 AnyView wrapper、sidebar center 和图片根节点自身的约束;任一层保留自动最小尺寸时,RDP frame 的 intrinsic size 都可能继续向上传播。

  • 正确做法:所有 active tab 无论是否启用 sidebar,都先放入统一的 .size_full().min_w_0().min_h_0().overflow_hidden() wrappersidebar center 同样显式裁剪。图片/远程桌面类 view 的 root、content 和 frame 也应设置零最小尺寸,由父容器 bounds 决定最终大小,不允许 frame 反向参与 TabContainer 宽高计算。

  • 验证方式:先用 contract 测试确认 sidebar 与非 sidebar 两条路径都经过同一个 active-view boundary,并覆盖 sidebar center、RDP root/content/frame 的 shrink 约束;再运行 one-core、对应 view crate 的测试和 main 编译检查,手工切换普通 tab/RDP tab、缩放主窗口及展开侧栏,确认窗口 chrome 和侧栏位置不跳变。

  • 适用范围crates/core/src/tab_container.rscrates/remote_desktop_view/src/view/render.rs,以及任何在 tab 中渲染具有 intrinsic size 的图片、canvas、视频或远程桌面视图。

  • 标题Windows GPUI 原生标题栏按钮必须截断后方的 Drag hitbox

  • 触发信号:仅 Windows 在打开 RDP 等 tab 或操作最小化、最大化、关闭按钮时,窗口发生意外拖动、还原或标题栏控件位置跳变,但 GPUI debug_bounds 显示标题栏、侧栏和内容区域的 logical layout 始终稳定。

  • 根因 / 约束:自绘标题栏通常为可拖空白区注册较大的 WindowControlArea::DragGPUI 的 Windows hit-test 只有遇到 BlockMouse 才会截断后方 hitbox。原生 caption button 只声明 window_control_area 而不 occlude 时,后方 Drag 区可能抢占 Min/Max/Close 命中,表现得像布局被 RDP 内容挤动。

  • 正确做法Windows Min/Max/Close 按钮使用 .occlude().window_control_area(...),顺序与同版本 Zed 保持一致;保留空白标题栏 Drag 区及应用显式 start_window_move(),不要继续用堆叠 flex/min-size、缩小整个拖动区或调整 hitbox 注册顺序来规避。

  • 验证方式:用源码 contract 约束 Windows native controls 先 occlude 再声明 control area;用真实 GPUI 布局测试覆盖普通 tab → RDP tab、超长错误状态和大尺寸首帧,确认标题栏控件 bounds 不变;最后在 Windows 实机验证切换 tab、点击 Min/Max/Close、最大化/还原和拖动空白标题栏。

  • 适用范围crates/core/src/tab_container.rs 以及其他包含 broad Drag 区的 Windows GPUI 自绘标题栏。

  • 标题:RDP 自动重连必须保留最后呈现帧,瞬态状态不得替换 active tab 内容

  • 触发信号Windows 下 RDP 作为最后一个且当前激活的页签时,自动重连后窗口控件向内挤;打开其他页签后立即恢复;重连文案同时长期覆盖在远程桌面页面上。

  • 根因 / 约束RDP helper 会先发送 ConnectionFailure / Terminated,再经 backend signal 发送 Reconnecting。如果 View 在前一个终止事件里清掉 RenderedFrameLifecycle::current,或用 connected 门控当前图像,active view 会从大尺寸画面切换成状态文本/overlay,其 intrinsic layout 变化可继续影响 Windows window chrome。mailbox 丢弃旧 session 尚未呈现的 pending frame/delta 是正确隔离,不能与保留 View 已经呈现的最后一帧混为一谈。

  • 正确做法RDP 的 ConnectionFailureTerminatedReconnecting 都只重置输入、resize、remote size 和增量 framebuffer 等 session 瞬态,保留已经呈现的 current frame;新 session 的完整 frame 到达后再按正常 frame lifecycle 替换。渲染 current frame 不依赖瞬态 connected 标记。重连说明通过带稳定 ID、自动隐藏的窗口通知展示,并用 window.defer 避免 Render 期间重入 Root;不要把重连 badge/overlay 常驻到 tab 内容树。

  • 验证方式contract 测试覆盖 RDP 保帧而 VNC 终止仍清帧、current frame 不受 connected 门控、通知使用 stable ID + autohide 且页面不存在 reconnect overlay;真实 GPUI 布局测试显式保证 RDP 是最后一个 active tab,并比较首帧、重连、重连后新帧三个阶段的 window chrome bounds;最后在 Windows 实机触发自动连续重连。

  • 适用范围crates/remote_desktop/src/backends/rdp* 的事件顺序、crates/remote_desktop/src/output_mailbox.rscrates/remote_desktop_view/src/view/{output,render,frame_lifecycle}.rscrates/core/src/tab_container.rs

  • 标题:扩展管理器的 reload、安装和卸载刷新必须按 kind 且保持语言 WASM 惰性加载

  • 触发信号:重新加载、安装或卸载一个静态 composite、数据库驱动或 provider 时,UI 长时间无响应,日志出现大量 cranelift_codegenwasmtime 或 Tree-sitter 语言扩展编译记录。

  • 根因 / 约束:统一刷新路径如果丢失扩展 kind,或调用 load_language_extensions_from_root,会在 GPUI 线程同步读取并编译全部语言 WASM;非语言扩展实际只需要刷新 runtime catalog 和贡献点,语言扩展也只需要更新 manifest 与文件后缀映射,parser 应在调用方首次请求语言时惰性加载。

  • 正确做法:reload、安装和卸载完成后都必须把具体 ExtensionKind 传给 runtime 刷新;只有 LanguageLanguageBundle 才调用 register_language_extension_manifests_from_root,不得在刷新路径调用 eager 的 load_language_extensions_from_root。其他 kind 只调用 refresh_global_runtime_catalogrefresh_runtime_contributions;目录删除等文件 I/O 使用 cx.background_spawn,完成后回到前台更新 UI。

  • 验证方式:用 reload-scope contract 覆盖所有 ExtensionKind,用结构 contract 约束刷新只注册 manifest、卸载 I/O 使用 background executor;手工卸载静态 composite,确认 UI 不冻结且日志不再出现语言 Cranelift/Wasmtime 编译。

  • 适用范围crates/extension-runtime/src/extension_view_host.rscrates/extension_view/src/actions.rs、扩展管理页的重新加载、安装与卸载刷新路径。

  • 标题macOS 局域网 No route to host 先检查本地网络权限与 App Bundle 签名

  • 触发信号Navop 在 macOS 上可连接公网或正式服务器,但访问 10.*172.16-31.*192.168.* 等局域网地址时返回 No route to host (os error 65),而终端或旧版 OnetCli 可以连接。

  • 根因 / 约束:macOS 本地网络隐私会在 TCP 建连前拒绝未授权应用;若 Info.plist 缺少 NSLocalNetworkUsageDescription,或只签名 DMG、没有签名 .app,系统无法把授权稳定关联到 Bundle。未经 Bundle 签名的程序可能显示基于二进制哈希的临时 Identifier,且 Info.plist 不受签名保护。

  • 正确做法macOS App 声明 NSLocalNetworkUsageDescription,在创建 DMG 前先签名并严格验证 Navop.app;生产发布优先使用 Developer ID Application,缺少证书时仅将 ad-hoc 签名作为开发/临时回退。对私网 HostUnreachable 错误保留原始信息,并提示检查“系统设置 > 隐私与安全性 > 本地网络”、VPN、路由、代理和跳板机。

  • 验证方式:用 nc -vz <private-ip> 22route -n get <private-ip> 区分系统路由和应用权限;检查 codesign -dv --verbose=4 Navop.app 显示预期 Bundle Identifier、已绑定 Info.plist 和 sealed resources,并运行 macOS bundle 脚本测试及 SSH 错误提示测试。

  • 适用范围resources/macos/Info.plistscript/bundle-macos.sh、macOS Release/DMG 流程,以及 SSH、SFTP、数据库、远程桌面、端口转发等局域网 TCP 连接入口。

  • 标题:安装器文件关联变更必须覆盖只替换二进制的应用内更新

  • 触发信号:新版 MSI/DEB/RPM/.app 已声明新的文件类型,但旧用户通过应用内更新后,系统“打开方式”中仍没有 Navop,或双击文件仍无法交给新版本。

  • 根因 / 约束Windows/Linux 应用内更新只替换可执行文件,不会重新运行 MSI 注册表组件、复制 desktop/MIME 资源或刷新桌面缓存;macOS 虽替换整个 .app,同路径覆盖后 LaunchServices 也可能尚未重新扫描。现代系统还会保护用户已有的默认应用选择,不能依赖静默覆盖 UserChoice

  • 正确做法:把关联定义同时用于安装器和应用内启动迁移。新版本首次启动时在后台幂等执行:Windows 写入当前用户 Software\Classes 的 ProgID、OpenWithProgids 和绝对打开命令并发送 SHCNE_ASSOCCHANGEDLinux 将嵌入的 desktop/MIME 模板写入 XDG_DATA_HOME、刷新缓存,并只在当前 MIME 没有默认应用时设置 NavopmacOS 对当前 .app 执行 lsregister -f。用包含 schema 与 executable path 的 stamp 避免重复迁移,应用移动后应自动重跑。

  • 验证方式:用纯 contract 覆盖三种扩展、绝对路径/desktop Exec 转义、不写 UserChoice、已有默认应用不覆盖、stamp 幂等和 .app 推导;运行 main 测试与 check,并在 Windows CI 验证条件编译、在 Linux 包环境验证用户级 desktop/MIME 文件和缓存刷新。

  • 适用范围main/src/file_association.rsmain/src/main.rsmain/src/update/*resources/{macos,linux}、Windows WiX 和所有新增文件关联/URL scheme 的发布迁移。

  • 标题:macOS GUI 编辑器不要直接依赖 Bundle 内部 executable 处理重复打开

  • 触发信号:第一次能打开外部编辑器,但编辑器已运行时再次打开远程文件没有反应、产生第二个无效进程,或编辑器随 OnetCli 生命周期收到 SIGHUP

  • 根因 / 约束:部分 macOS 应用(例如 Notepad--)依赖 LaunchServices 的 QEvent::FileOpen 向已有实例交付文件;直接执行 .app/Contents/MacOS/* 会绕过该机制。编辑器安装检测仍应检查真实 executable,不能简单把 /usr/bin/open 当作可用性候选。

  • 正确做法manifest 用 launchMode: macos_open 声明 LaunchServices 模式,programCandidates 继续负责真实 executable 检测与首次确认;Host 从 executable 推导 .app Bundle,再以参数数组执行 /usr/bin/open -a <bundle> <file>,不经过 shell。Linux/Windows 和未声明该模式的编辑器保持 direct launch。

  • 验证方式:覆盖默认 direct、manifest/runtime mode 传递、.app 推导、非 Bundle 拒绝及完整 open argv;在编辑器已运行时连续打开两个文件,确认复用同一实例并正确收到文件。

  • 适用范围crates/remote_file_editorcontributes.remoteFileEditors manifest/runtime contract,以及所有 macOS .app 外部编辑器扩展。

  • 标题:外部编辑器自动上传必须以磁盘写盘为边界,并用轮询补偿 watcher 丢事件

  • 触发信号:编辑器已经保存本地临时文件,但 OnetCli 偶发没有上传;或编辑器采用原子替换、文件系统 watcher 丢事件,导致只依赖事件监听不稳定。

  • 根因 / 约束:OnetCli 无法访问第三方编辑器尚未写盘的内存 buffer,也不应修改编辑器配置或模拟保存快捷键;不同编辑器的文件事件语义不一致。

  • 正确做法:Host 同时使用精确文件事件和定时轮询,先比较本地内容指纹,未变化时禁止远端 I/O;成功上传或远端重载后更新指纹以去重。全局自动上传关闭时,新会话不创建 watcher、poller 或上传 controller。

  • 验证方式:覆盖默认设置、显式关闭、内容指纹变化/不变、远端重载指纹更新;手工验证 Zed 与 Notepad-- 保存后上传,以及关闭开关后远端不变。

  • 适用范围crates/remote_file_editor、远程文件编辑器设置及所有外部编辑器贡献。

  • 标题:远端写操作成功后统一刷新当前可见目录

  • 触发信号:外部编辑器上传或内置编辑器保存已经成功,但 SFTP 侧边栏仍显示旧的大小、时间或目录内容,需要手动刷新。

  • 根因 / 约束:远端编辑器与 SFTP 视图分属不同 crate,不能通过反向依赖直接刷新;同一个内置编辑器窗口还可能承载来自不同 SFTP 面板的 tab。

  • 正确做法:由调用方传入类型擦除且可克隆的远端变更成功回调,每个 tab/外部会话保存自己的回调;只有远端写成功后触发,失败、取消和只读操作不触发。回调刷新调用方当前可见目录,不改变当前路径;同路径 tab 被其他面板重新打开时更新为最新回调。

  • 验证方式:覆盖回调调用 contract;运行 remote_file_editorsftp_viewterminal_view 测试和 main check;手工确认外部上传与内置保存后侧边栏无需手动刷新。

  • 适用范围crates/remote_file_editorcrates/sftp_viewcrates/terminal_view/src/sidebar/file_manager_panel.rs

  • 标题:可见终端执行不能用 EOF 绑定 Agent 取消与命令完成

  • 触发信号terminal.exec 执行 command &npm run dev &nohup command & 后一直 pending;点击 Agent 的 × 后对话仍显示运行中;或取消 Agent 时误向终端发送 Ctrl+C、终止仍在运行的命令。

  • 根因 / 约束:后台进程会继承 PTY/stdout/stderrshell leader 退出不代表 reader 能收到 EOF。Agent turn、tool waiter 与终端命令若共用同一个 future,进程或 FD 清理就会反向阻塞对话终态。可见终端命令由用户终端拥有,Agent 取消无权终止它。

  • 正确做法:用 OSC 133 supervisor 独立管理 readiness、safe-replace、命令 epoch、observer 与 timeout。fresh InputStart 后将提示符标记为空;空 Ready 提示符直接提交 Agent 命令,只有 supervisor 已观测到用户或 insert-only 调用留下的未提交输入时,才发送一次 ETX 清理并等待新的 InputStart,避免每次执行都机械触发 Ctrl+C。提交动作必须立即把 readiness 悲观切到 SubmissionPending,即使 wait_for_output=false。命令完成以 CommandFinished 或新 prompt epoch 为边界,不依赖 EOF。取消前未开始的调用零写入;提交后的取消只 detach waiter,后台 supervisor 继续有界清理,并停止缓存无人消费的输出。Agent turn 同时立即发出 TurnCancelled,旧 turn 的迟到写入按 turn id 丢弃。诊断用户手动执行或超时后留在可见终端中的现场时,使用有界只读的 terminal.read(lines=N) 读取 live PTY/scrollback 尾部,不要为了重新拿输出而重复执行命令。

  • 验证方式:覆盖空提示符直接提交且零 ETX、半行输入与 insert-only 输入才清理、busy/unknown 零写入、fresh prompt 握手、wait_for_output=false 立即 busy、预取消不排队、取消后不发送控制字符、detached output 不增长、background/nohup 不等 EOF、terminal.read 行数/字符上限与滚屏 tail、旧 turn 不清理或污染新 turn;再运行 terminal/tool-runtime/agent-runtime/UI 的定向测试、check 与 clippy。

  • 适用范围crates/terminal/src/exec_supervisor/*、SSH terminal actor、terminal.exec Public MCP/Agent adapter、agent_runtime turn cancellation 与 ai_chat_view 终态处理。

  • 标题:显式终端 Ctrl+C 必须走 supervisor control,不能复用 Agent 取消或任意输入接口

  • 触发信号:AI 需要停止当前可见终端的前台任务;有人考虑把 Agent 的 × 映射成 Ctrl+C、把 "\\u0003" 当作 terminal.exec.command,或直接向 Agent 暴露任意 PTY 字节写入。

  • 根因 / 约束:Agent turn 取消只表达“停止当前对话等待”,不拥有终端进程;terminal.exec 的 safe-replace 只允许在可信 Ready prompt 上清理半行并提交命令,而真正需要 Ctrl+C 时通常处于 SubmissionPending / CommandRunning。任意字节输入会绕过 readiness、审批和自动化 lease,产生竞态或误中断。

  • 正确做法:使用独立高风险 terminal.control(action=interrupt)。由 terminal actor 内的 supervisor 原子检查 readiness,仅在明确的前台运行状态写入一次 ETX (0x03);其他状态全部 fail closed、零写入。control 不移除 exec observer、不伪造 exit code,真实完成仍由 OSC CommandFinished / prompt epoch 收口。

  • 验证方式:覆盖 running/submission-pending 允许、ready/awaiting-prompt not-running、busy/unknown/disconnected 零写入、预取消零入队、control 后 observer 仍由真实终态完成;验证 Agent prompt 区分 terminal_execterminal_control 与取消按钮。

  • 适用范围crates/terminal/src/exec_supervisor/*crates/terminal/src/ssh_backend.rscrates/terminal_view/src/public_mcp.rs、Public MCP terminal control 工具与 Agent prompt。

  • 标题:Agent Auto 模式不进行工具审批,High/Critical 也直接执行

  • 触发信号Auto 模式下出现 NeedUserInput 工具确认卡,或 Agent→tool_runtime 映射仍把 high-risk policy 设为 Ask

  • 根因 / 约束ToolExecutionMode::Auto 表达用户已授权 Agent 自主执行当前暴露工具;风险等级仍用于展示、审计和 Manual 模式审批,但不能在 Auto 模式再次暂停。ReadOnly 仍通过工具暴露过滤保证只读,不能用 Auto 的放行规则扩大其工具集合。

  • 正确做法requires_tool_approval 对 Auto 始终返回 falseAgent runtime adapter 保持 PermissionProfile::Auto 标识,同时将 high_risk_policy 覆盖为 Allow。Manual 继续确认所有非 Read 业务工具,ReadOnly 只暴露 Read 工具。

  • 验证方式:覆盖 Auto 的 High、Critical、同轮多个 High 直接执行且无 NeedUserInput;覆盖 Manual 非 Read 仍审批、ReadOnly 仍过滤写工具;验证 Agent Auto permission policy 的 mode=Autohigh_risk_policy=Allow

  • 适用范围crates/agent_runtime/src/tasks/agent.rscrates/agent_runtime/src/tools/runtime_adapter.rs、Agent 工具模式 UI 与相关审批测试。

  • 标题:ACP 安全确认保留 Dialog,并在权限卡解释二次审批

  • 触发信号:ACP 权限卡允许后又弹出 Public MCP 安全确认 Dialog,用户误以为发生了无意义的重复审批;或者自动执行模式仍显示二次审批提示、High/Critical 工具仍继续请求确认。

  • 根因 / 约束ACP request_permission 与实际 Public MCP tools/call 是两个独立安全边界。安全确认模式需要用强制可见的 Dialog 承担最终审批,但如果消息卡不解释模式来源,用户会把它理解为异常重复;Auto 模式则不应保留确认语义。

  • 正确做法:在“手动确认/安全确认”模式的 ACP 权限卡中明确说明:允许 ACP 后,实际工具执行还会弹出一次安全确认窗口;如不需要二次审批,可将 MCP 权限模式切换为“自动执行”。Public MCP Ask 统一进入全局 Dialog,不再通过 ACP 一次性路由替换成第二张可操作消息卡。自动执行模式不显示上述提示,且 High/Critical 一并直接执行。

  • 验证方式:覆盖手动确认卡片包含“安全确认、二次审批、自动执行”说明,自动执行卡片不含误导提示,ACP 后续 Public MCP 请求进入 Dialog 队列并展示完整脱敏参数,以及 Auto High/Critical 直接执行。

  • 适用范围crates/ai_chat_view/src/acp/*agent_transcript.rsagent_view.rscrates/public_mcp/src/permissions.rsmain/src/public_mcp_approval*

  • 标题:macOS 自定义标题栏中的可拖元素必须由应用显式接管标题栏拖动

  • 触发信号:透明标题栏或 tab 栏中,按钮、输入框或 tab 的拖动被解释为窗口移动;为了规避问题出现 allow_tab_drag = !is_macos 一类平台禁用逻辑。

  • 根因 / 约束GPUI 的 stop_propagation()prevent_default() 只影响 GPUI 事件传播,不能阻止 AppKit 把透明标题栏视为系统 window-move region。把 NSWindow.isMovable 设为 false 虽能规避抢事件,但会禁用 macOS Window 菜单的平铺与窗口管理快捷键。

  • 正确做法:使用包含 Zed/GPUI #60620 的提交或更新版本,主窗口保持 is_movable: true 并设置 app_owns_titlebar_drag: true;空白标题栏通过 Window::start_window_move() 显式拖窗,可交互子元素在 GPUI 层阻止冒泡并保留自身 on_drag。不要用 macOS 条件整体禁用 tab drag。

  • 验证方式:结构测试保证不存在 allow_tab_drag = !is_macos 且窗口启用 app_owns_titlebar_drag;macOS 手工验证空白区拖窗、tab 排序、终端 tab 拖动分屏、标题栏按钮点击和 Window 菜单 tiling 均可用。

  • 适用范围main/src/main.rscrates/core/src/tab_container.rs,以及任何放在 macOS 透明自定义标题栏内的可点击、可选择或可拖拽 GPUI 元素。

  • 标题GPUI 拖放解析源 Entity 时必须在 read 前排除当前更新 Entity

  • 触发信号:拖动 tab、pane 或其他实体到自身内容区时出现 cannot read <Entity> while it is already being updatedmacOS 上随后因 panic 穿过原生回调边界触发 SIGABRT

  • 根因 / 约束Context<T> 回调已经持有当前 Entity<T> 的可变更新权限;若源解析先 downcast 得到同一个 entity,再调用 read(cx) 检查其状态,就会形成 update 中读取自身的重入。事后比较源/目标是否相同已经太晚。

  • 正确做法:源解析函数显式接收目标 entity handledowncast 后先只比较 Entity/EntityId,相同立即返回,确认是外部 entity 后才允许 read(cx)。不要通过捕获 panic 或延迟通知掩盖重入。

  • 验证方式:补 contract 测试保证同实体 guard 在首次 read(cx) 之前;手工把当前 tab 拖回自己的内容区,确认无 drop overlay、无 panic,再验证拖到另一个 workspace 仍可正常转移。

  • 适用范围crates/terminal_view/src/workspace/tab_drag.rs,以及所有从 GPUI drag payload、AnyView downcast 或 registry handle 解析实体并读取状态的事件回调。

  • 标题Redis String 读取链路必须保留原始字节,不能默认按 UTF-8 解码

  • 触发信号:查看 Java 序列化、Protobuf、MessagePack、压缩内容等 Redis String 值时出现 Cannot convert from UTF-8,或为了消除错误而考虑使用 String::from_utf8_lossy

  • 根因 / 约束Redis 的 String/Bulk String 是二进制安全字节串,不保证 UTF-8;query_async::<String> / Option<String> 会在 redis-rs 转换层提前失败,而 lossy 转换会丢失原始字节并可能在保存时破坏数据。

  • 正确做法:值详情读取使用 Vec<u8> 保留原始内容;合法 UTF-8 继续按文本展示和编辑,非法 UTF-8 使用转义 Raw、Hex 或 Binary 展示,并在没有字节安全编辑/写回 contract 时保持只读。原始命令结果同样应映射为显式 Binary 类型。

  • 验证方式:用 Java 序列化头 AC ED 00 05 覆盖连接层原始字节保留、Raw/Hex/Binary 格式化和非 UTF-8 只读保护;同时验证普通中文/JSON 文本仍可正常显示编辑。

  • 适用范围crates/redis_view/src/connection.rskey_value_view.rstypes.rs,以及 Redis String、集合成员、Hash/Stream 字段等所有可能承载二进制 bulk string 的读取链路。

  • 标题:过滤树的自动展开必须与用户显式折叠分开建模

  • 触发信号:输入搜索词后匹配路径会自动展开,但点击展开箭头无法收起,或收起后下一次重建扁平列表又立即展开。

  • 根因 / 约束:搜索态通常需要派生“自动展开匹配路径”的可见性;若遍历逻辑在搜索时无条件忽略普通展开集合,就无法区分“尚未手动操作”和“用户明确要求折叠”。单一 expanded_nodes 集合不足以表达这两个来源。

  • 正确做法:保留普通 expanded_nodes 作为搜索结束后的持久状态,并增加仅在当前搜索词下有效的显式折叠 override;渲染箭头、点击切换和子树遍历统一读取同一个 effective expansion contract。搜索词变化时清空 override,用户在搜索中展开/折叠时同步更新普通状态以决定退出搜索后的结果。

  • 验证方式:纯 contract 覆盖搜索自动展开、显式折叠优先、非搜索态遵循普通展开状态;真实树测试确认搜索中箭头可反复收起/展开,修改搜索词后重新自动展示匹配路径,清空搜索后保留用户最后的普通展开选择。

  • 适用范围crates/redis_view/src/redis_tree_view.rs,以及数据库树、文件树、资源树等同时支持过滤和层级展开的 GPUI 树视图。

  • 标题:provider 自动重连必须先取新 activation lease 再释放旧 lease,判活依据是 generation 而不是事件类型

  • 触发信号provider 进程崩溃、宿主 supervisor 自动重启后,已打开的扩展连接 tabDocker 这类 headless 原生工作台)一直停在 Connecting 或持续报错;或接上自动重连后 provider 被反复重启,或 tab 在重启窗口内反复重建连接。

  • 根因 / 约束ActivationManager 的 activation 是引用计数租约(runtime.activations: BTreeSet<activation_id>),而 supervisor 重启只替换 session、推进 start_generation,不会动租约集合。若先 deactivate_activation(旧 handle)activate_runtime,当旧 handle 恰好是最后一个租约时会立刻拆除 runtime、shutdown 刚重启好的 session;两次异步任务并发时顺序不确定,因此“激活新 lease → 关闭旧 session → 释放旧 lease”必须放在同一个 Tokio 任务里顺序执行。另外 RuntimeMonitorEvent 只带 runtime id 时无法区分“瞬时 Degraded / 仍在退避的 Restarting”与“已换进程”,必须比对 service.runtime_generation() 与 activation 记录的 generation。

  • 正确做法:宿主监视桥接转发完整 RuntimeMonitorEvent(不要降级成 String)。tab 侧把“事件 → 动作”抽成纯决策函数(Ignore/Fail/Reconnect):仅当 generation 推进时重连;generation 未变且 session_closed && state ∈ {Failed, CrashLoop} 才判为不可自愈并置 FailedRuntimeRemoved 或宿主已无该 runtime 直接 Failed;已 closing、未连接(Connecting/Failed)或事件属于其他 runtime 一律忽略。shell view 侧只做失效、不做自动重连(重建 LoadedShellView/ShellMountSession 风险更高)。

  • 验证方式cargo test -p universal-plugins --features universal-plugins/shell-plugins(纯决策 contract 覆盖瞬时抖动、generation 推进、CrashLoop/RuntimeRemovedclosing 短路、其他 runtime),cargo clippy -p universal-plugins --features universal-plugins/shell-plugins --all-targetscargo check -p main --features main/shell-plugins。注意 monitor bridge 在测试构建下被 cfg(not(test)) 移除,runtime_changed 及其私有辅助方法需要 #[cfg_attr(test, allow(dead_code))]

  • 适用范围crates/universal-plugins/src/extension_connection_tab.rscrates/universal-plugins/src/shell_plugin_tab.rscrates/universal-plugins/src/shell_plugin_host/monitor.rscrates/extension-plugin-adapter/src/activation.rsrestart/generation 语义与日志),以及任何“进程重启后自动恢复已挂载资源”的链路。

  • 标题:旧 SSH 服务器的 DH 协商失败通常是超出 russh gex 位宽下限而非缺少算法

  • 触发信号legacy 兼容算法已开启且 No common Key/Mac algorithm 已消失,但连接仍失败;日志出现 russh::client::kex: DH prime size (2048 bits) not within requested range(1024 bits) not within requested range 后跟 Key exchange init failed。用 ssh -o KexAlgorithms=xxx 探测可拿到服务器真实 offer 列表。

  • 根因 / 约束russh GexParams 默认 min_group_size=3072,客户端配置校验也强制不低于 2048。2048 位组(老 Cisco/网管)可用 GexParams::new(2048, 2048, 8192) 放行;但更旧设备只提供 1024 位组时,GEX 路径无论如何都过不了 2048 下限。这类设备通常除 group-exchange-sha1 外还声明固定组 diffie-hellman-group1-sha1(固定 1024 位组不走 GEX 范围校验),而 russh 客户端按自身列表顺序选 kex,一旦 DH_GEX_SHA1 排在 DH_G1_SHA1 前就会优先走进 GEX 死路。Key exchange init failedError::KexInit 而非 NoCommonAlgoadd_legacy_algorithm_hint 不会附加提示。

  • 正确做法:在 build_russh_client_config 内按 allow_legacy_algorithmsgexlegacy 用 client::GexParams::new(2048, 2048, 8192),现代路径保持默认;同时把 legacy KEX 顺序固定为现代组 → DH_G14_SHA1DH_G1_SHA1DH_GEX_SHA1,让 1024 位设备优先走固定 group1 路径,GEX 只作为最后回退。用具名常量避免魔法数字;真实 offer 探测 ssh -o BatchMode=yes -o PreferredAuthentications=none -o KexAlgorithms=... 区分“无共同算法”与“只有 1024 位组”。

  • 验证方式cargo check -p sshcargo test -p ssh(覆盖 gex 参数开/关、固定 group1 排在 group-exchange 前、以及 fake server 只声明 DH_GEX_SHA1 + DH_G1_SHA1lookup_dh_gex_group 固定返回 DH_GROUP1 时 legacy 开启连接成功、关闭时报 No common Kex algorithm)、cargo clippy -p ssh --all-targets 无新增警告;再用真机 DMG 分别连接 2048 位组与 1024 位组设备确认 Key exchange init failed 消失。

  • 适用范围crates/ssh/src/ssh.rs::build_russh_client_config / legacy_gex_params / build_client_preferred_algorithms_with_legacy,以及任何直接构造 russh::client::Config 的 legacy 兼容链路。

  • 标题GPUI deferred 输入面板不要挂在会频繁重绘的业务 View 子树中

  • 触发信号Windows 上 Popover/List 搜索框输入时闪烁、字符无法持续输入或 IME/焦点丢失;日志或测试显示列表 cx.notify() 会让承载 trigger 的整棵业务 View 进入重绘。

  • 根因 / 约束GPUI 的脏标记沿 dispatch tree 祖先链传播;若 deferred/anchored overlay 的 ListState 与输入框仍是树行或大型业务 View 的后代,按键通知会重建该业务子树及 overlay。仅稳定 element id、受控 open 或对业务 View 使用 Entity::cached(...) 不能建立可靠失效边界,缓存还可能冻结动态内容。

  • 正确做法:把面板状态提取为独立 Entity,由更稳定的页面宿主作为业务 View 的 sibling 渲染;业务行只保留 trigger、静默锚点同步和 WeakEntity 调用。面板自己管理 deferred 注册、backdrop、Escape、焦点恢复和连接清理;筛选 ListState::notify() 不得直接通知业务树。

  • 验证方式:真实 GPUI 测试必须通过 Root 承载输入组件,覆盖搜索后面板保持打开、过滤结果更新、连接切换和 deferred 注册成对;结构 contract 保证行内不存在 Popover/ListState 状态且宿主 sibling 接线存在。不要把 sibling 的共同父级 render 次数误当成业务 View 自身被标脏;关键边界是输入状态不再处于业务 View 的 dispatch 子树中。

  • 适用范围crates/db_view 数据库树筛选,以及任何位于虚拟列表、树行或大型动态 View 中的 GPUI deferred 搜索/输入面板。

  • 标题MySQL 结果 collation 63 不等于字段一定是二进制

  • 触发信号:数据库名、表名、字符集、排序规则等文本同时显示为 0x...MySQL 列包的 character_set() 为 63,或会话 @@character_set_resultsbinary

  • 根因 / 约束MySQL 列包中的 character_set 实际是 collation id。63 既用于 BINARY / VARBINARY / BLOB,也用于 character_set_results=binary 下未转换的文本结果;BINARY_FLAG 还可能出现在 VARCHAR/TEXT ... BINARY 上,因此任意 SQL 结果仅靠 type、flag、id 或“字节看起来像 UTF-8”都不能无歧义恢复源字段语义与编码。

  • 正确做法:内置 MySQL 连接认证完成后显式设置 character_set_results,默认按服务器版本使用 utf8mb4 或旧版 utf8,但不要无配置时用 SET NAMES 改变 collation_connection;用户显式配置 charset/collation 时才执行 SET NAMES ... [COLLATE ...]。真实二进制按协议元数据保留 exact-byte sidecar;仍为 collation 63 的模糊结果不得猜编码,直接表查询交给 authoritative schema normalization 将 TEXT 与 BLOB 纠偏。

  • 验证方式:单测覆盖默认/旧版/显式 charset 初始化命令、63 的模糊结果保留字节、普通 UTF-8/GBK 结果解码及 BINARY/VARBINARY/BLOB sidecar;真实 MySQL 测试检查连接后的 @@character_set_results 非 binary,并覆盖 SET character_set_results=binary 下模糊文本保持无损、三类二进制列精确字节不变。

  • 适用范围crates/db/src/mysql/connection.rsquery_result_normalization.rs、MySQL 元数据查询、SQL 结果表格及导入导出/比较链路。

  • 标题:Go IPC 扩展不得按 Go 运行时类型判定 MySQL 协议文本/二进制

  • 触发信号OceanBase MySQL 模式等 IPC 扩展里,VARCHAR/TEXT/DECIMAL/DATETIME/JSON 查询结果全部显示为 0x... 二进制,而内置 MySQL 正常。

  • 根因 / 约束go-sql-driver/mysql / obconnector-go 把 MySQL 协议的所有字符串家族列扫描为 []byte;共享 toCell 若只看 Go 类型,会把全部文本编码成 CellValue::Bytes。驱动层 ColumnTypeDatabaseTypeName() 已按列 charset 区分 TEXT/CHAR/VARCHARBLOB/BINARY/VARBINARY,列声明类型才是权威。

  • 正确做法query/start 保存每列 typeKindcursorStatecursor/fetchtoCellForKind 按声明 kind 编码 []bytetext 家族(含 uuid/xml/interval 映射)→ textdecimal/date/time/datetime → 对应文本 kindjson → 解析失败回退 textbinary/unknown 及非 UTF-8 字节一律保留无损 base64 bytesJSON wire 会把非法 UTF-8 替换成 U+FFFD,必须回退而不是强转 string)。移除按内容猜测 JSON 的嗅探,避免内容恰为合法 JSON 的真二进制被误标。

  • 验证方式go vet ./internal/... && go test ./internal/...;用 streamingRows fake driver(可注入 typeNames)覆盖 VARCHAR/DECIMAL/DATETIME/JSON/VARBINARY/BLOB、非 UTF-8 文本、非法 JSON、NULL、UUIDbash scripts/install-local-drivers.sh oceanbase 本地安装后连接 OceanBase MySQL 租户验证文本列显示。

  • 适用范围navop-extensions/internal/dbipc/{query,server}.go 及所有共享 dbipc 的 Go IPC 驱动(oceanbase/dm/kingbase/oracle-go 等)。

  • 标题:终端标准控制键不得被无上下文的全局快捷键占用

  • 触发信号Ctrl+DCtrl+W 等按键的终端编码测试正常,但真实终端没有收到 EOT、删词等控制字符,或按键触发了关闭窗口等应用 action。

  • 根因 / 约束GPUI 的无上下文全局 KeyBinding 可能在终端 on_key_down 前分派 action;即使终端键码转换和 PTY 写入正确,冲突按键仍不会到达终端。Ctrl+DCtrl+WCtrl+CCtrl+Z 等是 shell/TTY 标准控制键,不适合作为终端聚焦时仍生效的全局默认快捷键。

  • 正确做法:窗口、页签和面板 action 优先使用不与终端控制字符冲突的组合,或绑定到排除 TerminalView 的明确 key context;运行时默认值、设置页展示和可刷新绑定必须使用同一默认来源。

  • 验证方式:回归测试同时断言全局默认绑定不包含目标控制键、设置页元数据与运行时一致,并运行 terminal_view 键码测试确认目标按键仍编码为预期控制字节。

  • 适用范围main/src/navop_app.rsmain/src/setting_tab.rscrates/terminal_view/src/view/keybindings.rs 与所有无 context 的 GPUI 全局快捷键。

  • 标题GPUI Keystroke.key 是键帽字符不是输入文本,凭据捕获必须用 key_char

  • 触发信号SSH/Telnet 连接时手动敲击输入密码一直提示认证失败,粘贴同样密码却正常;密码含大小写与特殊字符(issue #147)。

  • 根因 / 约束GPUI Keystroke.key 是不含 Shift 效果的键帽字符(macOS/Windows 上 shift+a 的 key 均为小写 "a"shift 保留在 modifiers 里;shift+2 等平台才换算为 "@"),key_char 才是应用修饰键后的实际输入字符。凭据捕获的 keydown 分支曾把 key 当文本追加,且消费按键后 prevent_default() 阻断平台文本系统(insertText→commit_text),使错误字符成为唯一来源:大写被输成小写。

  • 正确做法:keydown 里把按键当“文本输入”使用时(密码/MFA/内联捕获缓冲),优先取 keystroke.key_char,为空再回退 key;只当“命令键”匹配(enter/backspace/escape/快捷键)时才用 key。粘贴与 IME 提交天然带原文,无需处理。

  • 验证方式cargo test -p terminal_view credential_capture(覆盖 shift 字母/数字符号/无 key_char 回退、敲击与粘贴结果一致);结构 contract 断言捕获分支使用 keystroke_capture_text(&event.keystroke) 且被消费按键仍 prevent_default。

  • 适用范围crates/terminal_view/src/view/{terminal_events,credential_capture}.rs,以及任何把 KeyDownEvent 直接转成文本缓冲的 GPUI 组件(搜索框、内联输入、自绘表单)。

  • 标题:SFTP 大文件上传要同时配置写窗口、请求超时与单次远端同步

  • 触发信号:500 MB 级上传在临时文件写完后报 Timeout,错误集中于 remote flush/fsync;小文件稳定,或上传吞吐受 RTT 明显限制。

  • 根因 / 约束russh-sftp 高层 File 已通过 max_concurrent_writes 流水线发送 WRITE;默认只有 8 个并发写与 10 秒请求超时。AsyncWrite::flush 会等待全部 WRITE 确认,并在服务器支持时执行 fsync@openssh.com,其后再次调用 sync_all 会重复 fsync。

  • 正确做法:上传会话保留 256 KiB 包上限,将并发写窗口设为 64(最多约 16 MiB 在途),请求超时与 SSH inactivity timeout 对齐为 300 秒;完成阶段只调用一次 flush,随后校验远端大小并关闭句柄。瞬态 Timeout/断连只自动重试一次,权限、认证、空间不足等永久错误不重试。

  • 验证方式:配置 contract 断言 64 × 256 KiB 与 300 秒;重试测试覆盖成功重试、永久错误、最多一次和退避期间取消;运行 cargo test -p sftp -p sftp_transfer,并在真实 SFTP 上批量上传 500 MB 级文件观察吞吐与 finalize 阶段。

  • 适用范围crates/sftp/src/russh_impl.rscrates/sftp_transfer/src/operation.rs 及所有后台 SFTP 上传入口。

  • 标题:批量上传冲突要逐项决策,Apply all 只作用于同类且策略必须绑定到单项

  • 触发信号:同一批文件/目录存在多个重名项,需要逐个选择跳过、保留两者、合并或覆盖,并允许把当前决定应用到后续同类冲突。

  • 根因 / 约束:文件与目录支持的策略不同;若只保存最后一次选择并在末尾批量应用,前面目录的 Merge/Replace 决策会被覆盖,可能误删远端独有内容。重连期间旧弹窗的目录快照也不能继续提交到新连接。

  • 正确做法:使用有序冲突解析器保留原始顺序;Apply all 按 is_dir 区分同类;每个目录在决策时记录自己的 DirectoryConflictPolicy,全部冲突完成后再统一入队;Keep Both 以“远端现有名称 + 本批全部名称 + 已生成名称”分配唯一名;提交前校验连接 generation。

  • 验证方式:覆盖逐项顺序、Apply all 仅处理同类、两个目录依次选择 Overwrite/ Merge 后仍分别为 Replace/Merge、Keep Both 唯一命名;运行 sftp_transfersftp_viewterminal_view 相关测试和 cargo check -p main

  • 适用范围crates/sftp_transfer/src/conflict.rscrates/sftp_viewcrates/terminal_view/src/sidebar/file_manager_panel.rs

  • 标题:迁移后的 GPUI Dialog 配置按钮属性前必须显式启用默认 footer

  • 触发信号:Dialog 标题和内容正常显示,但确认、取消按钮全部消失;代码仍有 .button_props(...).on_ok(...)

  • 根因 / 约束:新版 gpui-componentDialog::new() 默认 default_footer = false.button_props(...) 只配置按钮文案和样式,不再创建 footer。自定义 .footer(...) 不受此规则影响。

  • 正确做法:需要确认和取消按钮的 builder 在 .button_props(...) 前调用 .confirm();只有一个确认按钮的提示调用 .alert();多动作弹窗继续使用自定义 .footer(...)

  • 验证方式:运行 cargo test -p one-ui --test dialog_footer_contract,保证所有实际 Dialog builder 的 .button_props(...) 前都存在 .confirm().alert();再运行受影响 crate 测试与 cargo check -p main

  • 适用范围:全仓所有使用 gpui_component::dialog::Dialog / AlertDialog 的页面、编辑器和确认操作。

  • 标题:嵌入自定义配色容器的 Markdown 不要依赖兼容层 TextViewStyle 覆盖语义色

  • 触发信号:外层容器已经设置正确的前景色和背景色,但 TextView::markdown 的正文、引用或链接仍接近背景,尤其发生在终端主题与应用主题不一致时。

  • 根因 / 约束gpui_component::text::TextView 兼容层会把局部样式叠加到全局组件主题,并且其 TextViewStyle 主要暴露代码块和表格 refinement;底层 TextView 会主动设置全局主题前景色,所以外层 .text_color(...) 无法覆盖正文、链接、选区等完整语义调色板。

  • 正确做法:需要独立调色板的富文本使用 gpui_base::TextView 和完整的 gpui_base::TextViewStyle,显式设置 foreground、muted foreground、link、selection、code background、border 与 light/dark 模式;同时保留代码块/表格圆角和全局 syntax highlighter fallback。

  • 验证方式:样式 contract 断言完整语义色映射;终端主题测试覆盖所有内置调色板与背景的基础明度差;运行 cargo test -p ai_chat_view、终端主题测试和 cargo check -p main

  • 适用范围crates/ai_chat_view,以及终端、远程桌面、编辑器等在应用全局主题之外渲染 Markdown/HTML 的嵌入式面板。

  • 标题:把 main 里互相依赖的插件 UI 模块迁入 crate 必须整簇搬移并下沉 app 级 Global

  • 触发信号:想把 main/src/shell_plugin_host + shell_plugin_tab 之类“插件机制”抽出到新 crate,却发现 host 依赖 UniversalPluginServiceGlobalTabContainer、headless ExtensionConnectionTab 等仍在 main 里的符号,而 Rust 库 crate 无法 use mainbin)。

  • 根因 / 约束:迁移对象只要引用了任何仍留在 main 的类型,就必须把它们一起搬出或先下沉到 crate,否则必然双向依赖。shell_plugc…具体到 Navopshell_plugin_host/shell_plugin_tab/universal_plugins/extension_connection_tab/extension_connection_form 是一整簇互相 crate:: 引用的单元,cx.global::<GlobalTabContainer>() 这类 app 级 tab 打开入口是所有 UI 共同的硬依赖,只能把 GlobalTabContainer(仅 Entity<TabContainer> 包装)下沉到 one_core::tab_container,不能反向注入。

  • 正确做法:整簇 5 个模块一次搬入新 crate(crates/universal-plugins),功能开关用 crate 自带 shell-plugins optional dep feature 表达(main 的 shell-plugins 改为 ["universal-plugins/shell-plugins"])。该 feature 自 2026-09-15 起列入 maindefault,所以默认构建就带 Shell 页;crates/universal-plugins 自身仍保持 default = [](给它也设 default 反而会让 main --no-default-features 漏进 Shell 页,因为该开关只能关掉当前被选中包的 default)。feature off 时 crate 内这簇为空。搬移时把 pub(crate)pub 只改 main 真实消费的边界(global 类型、load/service/open_connection/resource_connection/register_headless_tab 与 ConnectionShellOpen 字段),簇内引用 crate:: 路径在新 crate 里解析位置不变,多数文件零改动。main 侧只改 import 路径。

  • 验证方式:双态验证默认构建 cargo check -p main(含 shell-plugins)与关闭态 cargo check -p main --no-default-features --features wasm-components,embedded-webview,windows-native-rdp,各带 --testscargo clippy -p universal-plugins --features shell-plugins --all-targetscargo test -p universal-plugins --features shell-plugins。改共享契约(如 BindingContext 加字段)后两态都要跑:漏编发生在关闭态一侧(feature 未开时整个 shell_plugin_host 不参与编译),只跑默认构建会漏掉它。

  • 适用范围crates/universal-pluginsmain/src/{home_strategy,home_tab/connection_forms,new_connection/form_page,navop_app,file_open,extension_update,home/home_tabs}crates/core/src/tab_container.rs,以及任何计划从 main 抽 UI 逻辑到新 crate 的后续重构。

  • 标题:View 内联渲染自己的引用方会造成 GPUI 实体租约重入 panic

  • 触发信号:运行时在 entity_map.rscannot read X while it is already being updated,场景为 A::render 中直接 b.update(cx, |b, cx| b.render_something(cx)),且该路径内部再 this.a.read(cx) / this.a.update(cx)

  • 根因 / 约束:GPUI 渲染 View 时持有其实体租约;同一帧渲染路径中任何代码再 read/update 该实体即 panic。把整段子视图手动塞回某个方法,等于把对方的 render 搬进了自己的租约。

  • 正确做法:需要渲染“会反向读取当前实体”的子视图时,把子视图作为实体子节点渲染(view.clone() 作 child / AnyView),子视图在自己的 Render::render 租约里读取父实体;模式切换用显式字段(如 home_embedded)控制子视图输出。事件回调里的 read/update 不受此约束。

  • 验证方式:结构 contract 断言宿主 render 只引用实体不内联调用其 render 方法(含 set_* 模式同步),真实窗口切换布局确认不再 panic。

  • 适用范围main/src/home_tab/content.rsHomePage 嵌入 persistent_connection_sidebar Tree)、任何 View 互相持有 Entity 并嵌入渲染的场景。

  • 标题:延迟打开标签时不能重新租用即将失活的 View

  • 触发信号:从主页新建本地终端时,虽然用了 window.defer,仍报 cannot update HomePage while it is already being updated

  • 根因 / 约束add_and_activate_tab_with_focus 同步调用旧标签的 on_deactivate,其动态派发会执行旧 View 的 Entity::update;若 defer 内又包一层 home.update,主页仍被租用。cx.defer_in 同样会重新租用当前 View,不能替代这个边界。

  • 正确做法:在原更新内准备配置、标签编号和目标容器;使用 window.defer,在回调的 App 上创建新 View 并更新标签容器,不再更新原 View。保留焦点和标签生命周期回调,不通过吞 panic 绕过。

  • 验证方式:运行 home_tab::tests::local_terminal 结构契约和 cargo check -p main --bin navop;真实窗口回归从活动主页通过快捷键、按钮和自定义 profile 打开本地终端。

  • 适用范围main/src/home/home_tabs.rs::add_local_terminal_tab,以及任何从当前标签 View 发起的同步标签切换。

  • 标题:主页批量选择状态由 HomePage 持有,三布局共享;嵌入树不再有自带搜索框

  • 触发信号:给主页卡片/列表/树加批量操作时发现入口缺失或状态分叉;或主页 Tree 布局同时出现工具栏大搜索框和树内“搜索连接或分组”两个搜索框。

  • 根因 / 约束:树原本把 ConnectionSelectionselected_filter/树内搜索框留在 PersistentConnectionSidebar,嵌入主页时树头部被隐藏,批量入口随之消失,树内搜索框又与主页工具栏重复。

  • 正确做法ConnectionSelectionset_batch_mode / select_connection_in_batch / visible_manageable_connection_ids 位于 home_tab/connection_selection.rs,侧栏树经 home_page 读写同一状态;卡片/列表批量条由 home_tab/batch_bar.rs 渲染,Tree 布局的批量条仍由侧栏树内渲染。树入口仅在 !home_embedded 时渲染 render_tree_search,嵌入时 tree_rows 直接读 home.search_queryhome.selected_filter。数据加载完成后调用 prune_connection_selection 裁剪失效选择。

  • 验证方式cargo test -p mainhome_tab::connection_selection 单元测试、batch_bar/batch_toolbar 契约、embedded_tree_reuses_home_search_and_filter_without_own_search_boxhome_batch_mode_is_shared_across_card_list_and_tree_layouts)。

  • 适用范围main/src/home_tab/{connection_selection,batch_bar,toolbar,home_layout,connection_card,connection_list,data}.rsmain/src/persistent_connection_sidebar/{selection,batch_toolbar,rows,tree,mod}.rs

  • 标题:后台任务入口图标等应用级自定义 SVG 通过 AssetSource 内嵌并按路径引用

  • 触发信号:需要替换 tabs 栏后台任务入口等 IconName 覆盖不了的图标;gpui-componentIconName 由其 assets 宏生成,本仓库无法添加变体。

  • 正确做法SVG 放入 resources/icons/,路径常量定义在 one_core::storage(如 NAVOP_BACKGROUND_TASK_ICON),main::navop_brand_iconinclude_bytes! 注册;使用处 Icon::default().path(常量)Button::icon / Toggle::icon 接受 impl Into<Icon>,可直接传 Icon

  • 验证方式cargo test -p one-core background_task(入口与徽标契约)+ cargo check -p main

  • 适用范围crates/core/src/background_task_panel.rsmain/src/main.rscrates/core/src/storage/models.rsresources/icons/

  • 标题:原生资源工作台渲染必须走 gpui-component 设计系统,禁止手搓 div 表 + 硬编码色值

  • 触发信号:手写 div().bg(rgb(0x...)) 拼表格 / 卡片 / 工具栏,界面与主题(Navop Light/Dark)不一致、无斑马纹、无 hover、无列宽拖拽、状态用裸文字;被要求“写出好看的界面”。

  • 根因 / 约束gpui-component(工作区既有依赖)已提供 DataTable/TableDelegateTagButtonSpinnerIcon 及完整的 ActiveTheme 主题令牌(background/foreground/border/sidebar*/table*/list_hover/radius*/mono_font_family 等)。硬编码 gpui::rgb(...) 会绕过主题切换,且丢失组件自带的交互与可访问性。托盘/工作台类“公共页面”必须用 Rust 原生 + 该设计系统渲染;仅服务特定扩展的页面(如容器日志)才用 gpui-shell。

  • 正确做法:集合类数据用 TableState::new(delegate, window, cx).row_selectable(false).col_selectable(false).col_movable(false).sortable(true) + DataTable::new(&state).stripe(true).bordered(false).scrollbar_visible(true,true).with_size(Size::Small);委托实现 TableDelegatecolumns_count/rows_count/column/render_th/render_tr/render_td/perform_sort/render_empty/loading)。列样式由 manifest style 字段驱动(badge/mono/muted)。状态/徽标用 TagTag::success/danger/warning/secondary + 描边圆点),操作按钮用 Button::new(id).with_size(Size::XSmall).ghost()/...icon(...).tooltip(...).loading(...),加载用 Spinner,图标用 IconName。颜色一律取 cx.theme(),禁止字面量。工具链细节:font_medium/font_semiboldStyledExtopacityColorExtStateful 上的 on_clickStatefulInteractiveElementcx.newAppContextDataTableloading_view 优先生效(loading() 为真即显示骨架,即使行数为 0),empty_view 仅在 rows_count==0 && !loading 时渲染——为避免行操作时骨架闪烁,缓存重建条件应为 !loading && revision changed

  • 验证方式cargo check -p resource_viewcargo check -p main --features main/shell-plugins;启动 app 肉眼核对浅/深色主题下的表头、斑马纹、hover、状态徽标、按钮 loading 与空/加载态。

  • 适用范围crates/resource_view/src/{lib.rs,collection_table.rs}navop-extensions/extensions/composite/*/extension.json 的列 style 定义,以及任何用 Rust 原生渲染资源工作台/管理页面的场景。

  • 标题:GPUI 纯布局 / 拖拽交互回归用 debug_selector + debug_bounds + simulate_mouse_* 在测试内断言,不靠肉眼

  • 触发信号:改了表头、列宽、滚动容器这类纯布局代码,无法用返回值和状态断言覆盖;担心“列被压缩”“宽容器下表格留空白”“分隔条拖不动”这类回归只能靠启动 GUI 肉眼确认。

  • 根因 / 约束gpui 的 debug_selector(|| "id".to_string())(需先 .id(...),非 test 构建为空实现)+ VisualTestContext::debug_bounds("id") 能拿到元素真实布局矩形;VisualTestContext::simulate_window_resize(*handle, size(..)) 可改窗口尺寸(open_window 返回的 WindowHandle<Root> 需解引用成 AnyWindowHandle);simulate_mouse_down/move/up 能驱动 on_drag / on_drag_move 的真实事件链路。坑点:这类断言极易“空跑通过”,例如窗口恰好比表格宽时,压缩类断言永远成立。

  • 正确做法:给被测容器/表头加稳定 .id() + .debug_selector(...);用 Bounds::centered(None, size(px(w), px(h)), cx) 开窄窗口,断言 scroll.size.width < 期望内容宽度(自证前提)+ header.size.width >= 列宽之和 + header.right() > scroll.right();再 simulate_window_resize 到宽窗口断言 scroll.right() == header.right()(有空间时必须铺满)。拖拽类交互用 simulate_mouse_down → 至少两次 simulate_mouse_move(跨过拖拽起始阈值)→ simulate_mouse_up,并断言变化幅度(如 width_after >= width_before + 40.0)而非仅“变了”。写完后做一次变异验证:临时删掉生效那一行,确认测试确实转红。

  • 验证方式cargo test -p redis_view --lib <测试名>(含变异验证一次),cargo clippy -p redis_view --all-targets 无新增告警。

  • 适用范围crates/redis_view/src/{key_value_view.rs,value_table_columns.rs},以及任何 GPUI 表格 / 滚动 / 拖拽布局改动。

  • 标题:懒加载树的搜索:本地过滤不能叠在服务端结果上,过滤态必须保留结构锚点

  • 触发信号:用户报「redis 搜不到 key」「搜索结果为空」,但服务端确实存在匹配的键;搜索时左侧树面板整个变空,连连接 / 数据库节点都消失。

  • 根因 / 约束crates/redis_view/src/redis_tree_view.rs 的搜索是两层——输入时按单个树节点名做本地子串过滤,回车 / 搜索按钮才走服务端 SCAN。三个坑:① 本地匹配的粒度是节点名、服务端匹配的是整键,所以 *we* 命中 flow:east:1we 跨过 :)这类键会被服务端返回、又被本地二次过滤藏掉;② 过滤态下 local_search_visibility 会把「自身与子树都不匹配」的节点全剔除,连接 / 数据库一起消失,用户看到全空面板;而键列表是按需加载且上限 SCAN_TARGET_KEYS = 500,「本地没命中」是常态而非异常;③ 「搜索态自动展开」(issue #9)原来挂在「本地过滤生效」上,一旦关掉本地过滤,搜索结果会折叠成不可见。

  • 正确做法:把两个状态拆开——server_search_keyword(最近一次交给服务端 SCAN 的关键词)与 search_keyword;两者相等时列表即服务端权威结果,local_filter_keyword() 直接返回 None 不做二次过滤。「自动展开」改用 is_search_active()(关键词非空),与「是否本地过滤」解耦。过滤态把 Connection / Database 当结构锚点无条件保留(LocalSearchInput.is_structural),并在「关键词非空但没有任何可见键」时区分提示「本地没匹配 → 引导回车扫描服务端」与「服务端也没匹配」。入口收敛到 trigger_search(回车与按钮共用,避免漂移),目标库用 resolve_search_targets(选中库优先,否则回退所有已连接库),避免未选中节点时回车静默无反应。

  • 验证方式cargo test -p redis_view;纯函数测试覆盖 search_filter_keyword / resolve_search_targets / local_search_visibility,另有一个用 set_node_children 搭真实节点树的 gpui 测试(服务端命中跨命名空间键时可见 + 无服务端扫描记录时被隐藏的对照分支,防空跑),并跑一遍 render 覆盖新提示分支。改动做变异验证:分别去掉「免二次过滤」「结构锚点保留」「搜索态展开」,测试都转红。

  • 适用范围crates/redis_view/src/redis_tree_view.rs,以及任何「先本地过滤已加载子集、再服务端扫描」的懒加载树(其他 tree_view 同类结构)。

  • 标题GPUI 输入防抖:替换 Option<Task> 就等于取消定时器,别凭空加代次守卫;但 emit 是延迟 effect

  • 触发信号:要给输入框 / 编辑器做「停止输入一小段时间后再发请求」的防抖;或者视图自己依赖「事件订阅方回写的状态」做去重时出现重复请求;也适用于怀疑「drop 掉 Task 是否真的取消定时器」而准备额外加 generation 代次守卫时。

  • 根因 / 约束:本仓 gpui fork 里,Task 被 drop 后其 cx.background_executor().timer(..) 未到期部分不会再执行(已用对照测试验证:不 drop 时必然执行,drop 后不执行)。所以防抖只需「Option<Task<()>> 存字段 + 每次输入覆盖」,额外加的 generation 代次守卫是死代码(变异验证删掉它测试并不转红)。另一侧的坑更隐蔽:Context::emit 只是往 pending_effects 里塞一条 Effect::Emit,订阅者(如 handle_search_keyssearch_keys)要等 flush 才跑,于是订阅方回写的状态标记会滞后于 emit;视图在 emit 之后立刻读这个标记做去重,会读到旧值。测试驱动输入用 cx.simulate_input("we") + cx.executor().advance_clock(DEBOUNCE) + cx.run_until_parked();先 cx.update(|window, cx| window.focus(&handle, cx))handle 用 search_state.read(cx).focus_handle(cx) 取(Focusable 的同名方法带 cx 参数会遮蔽零参版本);断言事件用 cx.subscribe(&entity, |entity, event, cx| ..),回调里可 entity.update(cx, ..)emit 已 deferred,不会重入)。

  • 正确做法:防抖状态机 = Option<Task<()>> + 纯函数判定 should_scan_after_input(keyword, server_search_keyword)(空关键词或已扫过 → 不发);「立刻搜」入口(回车 / 搜索按钮)先 self.search_debounce = None,并在同一函数内同步写入「已交给服务端」的标记,不要指望事件兜一圈回来由订阅方补写;每次输入都替换 Task 字段即完成取消。

  • 验证方式cargo test -p redis_view;端到端用 simulate_input + advance_clock 断言「停顿前 0 次、停顿后 1 次、回车后不再补发」,并单独加一个对照分支测试证明 drop(Task) 会取消定时器——这样「多写一个代次守卫」这类死代码才会在变异验证里暴露。

  • 适用范围crates/redis_view/src/redis_tree_view.rs,以及任何 GPUI 视图中的输入防抖、定时器 + 事件订阅组合。

  • 标题:gpui 里「图标整块空白」先查 icon source 与 color mode 的组合,不要先怀疑布局或尺寸

  • 触发信号:某个图标(尤其外部扩展 / 驱动 / 自定义 SVG)突然不显示,且是「什么都没有」而不是「颜色不对 / 太小」;全量替换为 Icon::default().data(bytes) 之后开始出现。

  • 根因 / 约束gpui_component::Icon 有两个正交维度:IconSource::{Path, Data} × IconColorMode::{Mono, Color},其中 Data + Color 是坏的组合color_icon_content 对 Data 走 svg().data(data).size_full(),而 gpui-pre 的 Svg::paint 把 data 分支写成 if let Some(color) = style.text.color { paint_svg(..) },同时 Interactivity::compute_style_internal 只做 Style::default().refine(base_style)不继承父级 text colorNone → 整段 paint 被跳过(不是画错颜色,是根本没画)。Mono 之所以正常,是因为 into_svg 会显式 .text_color(..)。另一个独立坑:gpui::img(path_str)url::Url::from_str(..).is_ok() 判 URIdriver://x/y.svg 会被判成 URIdriver:// 是合法 scheme),于是走 Resource::Uri 发 HTTP GET 必然失败;同理 Windows 绝对路径 C:\... 也会被判成 scheme = cmacOS 的 /abs/path 反而安全)。只有 Resource::Embedded(无冒号的路径串)或 Resource::Path(传 PathBufIcon 不暴露)才会进 AssetSource。此外 svg() 在 gpui 里本质是 alpha mask 单色着色,想保留品牌原色只能走 img()

  • 正确做法:图标不显示时先确认实际走了哪条来源分支(本仓 db::ipc::display::preferred_icon_*main/src/connection_visuals.rs::external_driver_icon_sourcefile 优先,且 icon_file_path_for 不校验文件是否存在,所以 manifest 里有 ui.icon 就一定走 file)。修的时候让所有磁盘图标统一走 img(),即把「路径」表达成无 scheme 的资产路径再由 AssetSource 读盘:驱动包图标用 db::ipc::DRIVER_ICON_ASSET_PREFIXdriver-icons/{id}/{resource}{ext},经 IpcDriverRegistry 解析到 manifest_dir.join(ui.icon)),任意本地文件用 db::ipc::LOCAL_ICON_ASSET_PREFIXlocal-icon/{path},跨平台都不会被 is_uri 命中),调用侧一律 Icon::default().path(..).color()(即 img())。禁止 Icon::data(bytes) + .color(),也禁止 xxx:// 形式与裸绝对路径。

  • 验证方式cargo test -p db --lib ipc::(新增 driver_icon_asset_paths_are_never_urislocal_icon_asset_paths_are_never_uris_and_round_tripdriver_asset_source_serves_local_icon_files_and_driver_icons 三组回归,用与 gpui 同一个 url::Url::parse(..).is_ok() 谓词钉住这个坑)+ cargo test -p db_viewcargo test -p main --lib new_connection;快速判定某串是否被当 URI,直接用 target/debug/deps/liburl-*.rlibrustc --extern url=... probe.rs 实测,比读 gpui 源码猜快。

  • 适用范围crates/db/src/ipc/display.rs / resources.rsmain/src/connection_visuals.rsmain/src/new_connection/connection_kind.rscrates/db_view/{database_tab,db_tree_view}.rs,以及任何「外部扩展声明的自定义图标」「SSH 自定义图标文件」渲染路径。

  • 标题cargo fmt 不要带文件路径参数——本仓会重写整个 workspace,污染未提交改动

  • 触发信号:想「只格式化我改的几个文件」而运行 cargo fmt -- <paths>-- 之后的路径不是过滤器,会被当成 rustfmt 附加参数作用于每个 workspace 成员的每个 target);或想用 rustfmt --config-path <repo>/rustfmt.toml /tmp/x.rs 复现 rustfmt 行为、据此判断「某文件是否 rustfmt-clean」。

  • 根因 / 约束:前者一次性改写了 172 个文件(其中 140+15 个当时是已提交、干净的文件)。后者不可靠:文件里若有 mod x;rustfmt 在 /tmp 找不到同级模块会报错并跳过格式化,于是「rustfmt(副本) == 当前内容」的判据把这些文件误判成「含真实改动」。

  • 正确做法:只格式化改动文件时用 rustfmt --edition <edition> <file>(直接调 rustfmt)或 cargo fmt -p <crate>。判断「某文件是否只是被格式化」要在同一 crate 上下文里做:git worktree add --detach /tmp/wt HEAD → 在 worktree 内 rustfmt <file> → 与工作区文件 diff;相等 ⇒ 该文件原本与 HEAD 只差格式化,可安全 git checkout -- <file> 精确回滚;不相等 ⇒ 含真实未提交改动,不要回滚,交还用户(RustRover Local History 里有「External change」版本可逐文件还原)。

  • 验证方式:回滚后 git diff --name-only 只应剩下自己的改动 + 用户原有 WIP;再跑 cargo check -p <受影响 crate> 确认未破坏编译。

  • 适用范围:任何 Rust 仓库的批量格式化;rustfmt.tomledition/style_edition 时,务必用与 cargo fmt 相同的 --edition 复现行为。

  • 标题:GPUI 文字选择失灵先分「选区色不透明盖字」与「容器 stop_propagation 拦截窗口级选择」两类

  • 触发信号:侧边栏/面板里的 TextView 富文本(AI 输出等)①可以拖出选区但选中文本被实心色块盖住;②拖动完全没有选区高亮。两类症状共用 gpui-component 的窗口级文字选择机制(TextSelectionLayergpui_component::Root 里绘制,选区在 mouse-down bubble 阶段begin_in_window)。

  • 根因 / 约束:① gpui-base text/inline.rsInline::paint 先画字形、后画选区 quads(同仓 SelectableText 顺序是对的),不透明选区色直接遮字;应用主题的 selection 在组件 ThemeColor::apply_config 里被 clamp 到 alpha≤0.3 所以无感,但终端主题选区色(TerminalTheme.selection,预设均为不透明 rgb(..))经 agent_theme_from_terminal_theme 原样传入就中招。② tab_container.rs 贡献式侧边栏浮动 docka5f9837d4 引入 absolute 覆盖布局时)在容器上 on_mouse_down → stop_propagation 防点击穿透,连带把 bubble 阶段的窗口级选择 handler 一并拦掉,dock 内所有 TextView 选区无法开始(终端侧边栏在标签内容区内渲染,不经此容器,故不受影响)。数据库/MongoDB 等侧边栏都是贡献式 dock。

  • 正确做法:①给 AgentChatTheme.text_selection 传色时保证半透明(暗 0.3 / 亮 0.4,用 Hsla::alpha 设绝对值而非 opacity 乘法),不要信任上游主题源不透明;根治要改 fork 的 Inline::paint 绘制顺序(quads 画到 styled_text.paint 之前)。②容器要防穿透用 .occlude()(把下方内容挡出命中栈,元素级 handler 不触发),不要用 stop_propagation——后者会连带拦截所有窗口级 mouse handlerstop_propagation 只该用在确实要独占事件的窄交互元素(如 resize handle)。

  • 验证方式cargo check -p one-core -p terminal_viewcargo test -p terminal_view --lib sidebar::tests(含选区色 alpha 回归断言)、cargo test -p one-core --lib tab_container sidebarcargo clippy -p one-core -p terminal_view --all-targets;手工验证需分别开终端侧边栏 AI 与数据库侧边栏 AI 拖选文字。

  • 适用范围crates/terminal_view/src/sidebar/mod.rs(终端主题 → AgentChatTheme 映射)、crates/core/src/tab_container.rs(贡献式侧边栏 dock)、crates/ai_chat_view/src/theme.rstext_selection 消费点),以及任何向 gpui-component TextViewStyle::with_selection 传色、或在浮动覆盖容器上处理 mouse-down 的路径。

  • 标题:独立 MSTSC 的凭据 target 是 TERMSRV/<主机>,不能带端口

  • 触发信号:从 Navop 打开 RDPWindowsNative / 独立窗口)后系统 mstsc.exe 仍弹「Windows 安全中心 / 输入你的凭据」要求手输密码,且用户名已预填;或反过来想把「同一主机不同端口各存一份 RDP 凭据」做成功能。

  • 根因 / 约束MSTSC 在 NLA 阶段查 Windows 凭据管理器时只按主机名匹配,/v:<主机>:<端口> 里的端口不参与 target,所以 TERMSRV/192.168.111.245:3389 永远查不到——默认端口 3389 与自定义端口都一样。截图里预填的用户名并非来自我们写入的凭据,而是 MSTSC 自己的注册表键 HKCU\Software\Microsoft\Terminal Server Client\Servers\<主机>\UsernameHint,因此「用户名有、密码没有」不能当凭据写对了的证据。另经对照实验确认:CRED_TYPE_GENERIC + CRED_PERSIST_SESSION + 注释标记 的写入方式本身能被 MSTSC 命中,问题只在 target 名。

  • 正确做法mstsc_credentials() 只拼 TERMSRV/<host>/v: 参数继续带端口,连接目标不变;MstscCredentialInput 不需要 port 字段。副作用是同一主机不同端口无法保存不同凭据,这属 MSTSC 自身限制,不是本仓能解的。

  • 验证方式:真机 MSTSC 无法单测,用本机 loopback + 假 RDP 服务端做对照实验:假服务端只完成 X.224 协商并声明 PROTOCOL_HYBRID = 0x02不是 0x0B,写错会被客户端回退成传统安全模式、根本不出凭据框),按不同 target 写临时凭据后启动 mstsc /v:127.0.0.1:<端口>,以是否出现窗口类 Credential Dialog Xaml Host / 标题「Windows 安全中心」为判据:端口 3389 与 13389 下 TERMSRV/host:port 都弹框,TERMSRV/host 都不弹。单元测试用 cargo test -p main --bin navop home::remote_desktop_window 锁住 target 不含端口。

  • 适用范围main/src/home/remote_desktop_window*(独立 MSTSC 唤起路径),以及任何「替 MSTSC / 系统凭据管理器预写密码」的改造。

  • 标题:长期分支合入主干用「反向合并」——在分支侧解冲突,主干 ff 快进

  • 触发信号:把一个落后主干几十个提交的长期分支合入 dev,冲突文件多(含 Cargo.toml / Cargo.lock、共享 UI 文件),且主干上还有未提交的活跃改动;或已在主干 git merge --no-commit 后想撤退。

  • 根因 / 约束:在主干上直接解冲突会让主干经历冲突中间态,解错的试错成本回灌主干;主干上用户的未提交改动也会被中间态牵连(要整体 merge --abort 才还原)。反向合并把风险全留在分支 worktree,主干全程干净。

  • 正确做法:①预检 git merge-tree --write-tree --name-only <trunk> <branch> 拿冲突清单,再用 comm -12 求「主干未提交改动文件 ∩ 合并将引入文件」,非空则先停下确认;②在分支 worktree 里 git merge <trunk>(主干若残留中间态先 git -C <trunk> merge --abort);③解冲突排序:语义差异(两侧实现同一功能的不同方案)先问用户 → 一侧为超集取超集 → import/重复块按符号实际是否被使用收口,不盲目取并集;④依赖 rev 冲突用 git -C <dep-repo> merge-base --is-ancestor <a> <b> 判定并取后代那个(而非日期更新的),Cargo.lock 里同一 rev 多处出现用一次 replace_all;⑤cargo check --workspace --all-targets 通过后,主干 git merge --ff-only <branch>,快进后再跑一次主干 check(叠加用户未提交改动,文件不重叠不代表 API 不冲突)。

  • 验证方式cargo metadata --format-version 1 --locked --offlineexit=0 即 toml/lock 一致)、cargo check --workspace --all-targets(分支侧、主干各一次)、核心 crate --lib 测试;回退用 git reset --mixed <原 SHA>(保留工作区改动)。

  • 适用范围:任何「长期分支 → 主干」的合并。两个易踩的坑:①worktree 的 MERGE_HEAD / ORIG_HEAD<主仓>/.git/worktrees/<name>/ 下,不在 worktree 自己的 .git(那是 gitdir: 指针文件),查后者会误判「合并已结束」;②分支名与 worktree 目录名常不同(如 impl/ftp-support-172navop-ftp-support-172)。

  • 标题release profile 设 panic = "abort" 后,catch_unwind 既不能做契约断言也不能做隔离,契约要用返回值表达

  • 触发信号:把 release profile 改成 panic = "abort" 后,用 catch_unwind 断言「重复注册 / 非法输入必须 panic」的测试在 cargo test --release 下把整个测试进程 abort;或看到 catch_unwind 包着调用方回调、就以为 release 里仍能隔离该回调的 panic。

  • 根因 / 约束panic = "abort" 下 panic 直接终止进程,没有展开可捕获,catch_unwind 照常编译但永不返回 Err。CI 只跑 dev profileunwind),所以这类断言在 CI 里照常通过,只有 cargo test --release 或发布二进制才暴露;用 #[cfg(panic = "unwind")] 跳过用例会静默丢掉 release 模式的覆盖。尺寸收益是量出来的:__eh_frame 16.0MiB + __gcc_except_tab 8.6MiB ≈ 24.6MiB,所以改回 unwind 不是零成本。

  • 正确做法:可表达为契约的重复注册 / 非法状态不用 assert! + panic,改为返回 Option / Result,调用方(如插件服务的 init)用 expect 保留「这是 bug」的强语义,测试直接断言 is_none() / is_err(),两个 profile 都能跑。跨 FFI / C ABI 边界或调用方回调处的 catch_unwind 在 abort 下是死代码,应确认被包住的主体本身 panic-free(如只做 poison-safe 锁、channel send、原子交换),并在注释里写明该守卫只对 unwind 构建生效。

  • 验证方式dev 与 release 两个 profile 跑同一用例:cargo test -p universal-plugins --libcargo test --release -p universal-plugins --librelease 想省时间只覆盖 CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16panic 仍取 profile 值),再用 cargo test --release -p universal-plugins --lib -- --list 确认用例不再被 cfg 掉。

  • 适用范围crates/universal-plugins/src/universal_plugins.rscrates/terminal/src/recording/runtime.rscrates/windows_rdp_host/src/event.rs,以及 profile 设了 panic = "abort" 后所有用 catch_unwind#[should_panic]#[cfg(panic = "unwind")] 表达契约或隔离的测试与调用点。

  • 标题Windows GUI 进程 spawn 控制台子程序必须走 process-util 隐藏控制台,否则启动/操作时闪黑框

  • 触发信号:Windows 上启动应用闪一下控制台窗口(本次是启动期 wsl.exe --list --verbose),或打开某个功能(HTML 预览、容器文件树、设置页装 skill)时闪一个 cmd 黑框;也适用于新增任何 std::process::Command::new / tokio::process::Command::new 后台调用点时。

  • 根因 / 约束Navop 是 windows 子系统 GUI 进程,自身没有控制台。spawn 控制台子系统程序(wsl.execmd.exedocker.exenpx.cmdgit.exereg.exe…)时若未设 CREATE_NO_WINDOW0x0800_0000),Windows 会为子进程新建一个控制台窗口并显示——即使 stdout/stderr 都已重定向到管道也一样,所以「反正输出走管道」不能当作不闪的理由。GUI 子系统程序(mstsc.exe、自身 exe)不需要处理。

  • 正确做法:统一用 crates/process-utilprocess_util::configure_background_child(&mut std_command)std)或 configure_tokio_background_child(&mut tokio_command)tokio)。写法必须是「先 let mut command = Command::new(..)、配好 args/env/stdio,再 configure,最后 spawn() / output()」,不能保留链式 .spawn(),否则没有可变绑定可配。仅当纯逻辑小 crate 不想为 process-util(它硬依赖 tokio/process)拉进 tokio 时,按 main/src/file_association.rs 先例内联 #[cfg(windows)]creation_flags(CREATE_NO_WINDOW) helper,并配 #[cfg(not(windows))] 空实现避免 unused 告警。

  • 验证方式:四条结构 contractinclude_str! 断言源码里存在对应的 configure_background_child / creation_flags,沿用 workspace_explorer/src/git/tests.rs 风格)——cargo test -p terminal --lib wsl_distributionscargo test -p html-previewcargo test -p workspace_explorer --lib backendcargo test -p main --bin navop mcp_skill;再跑受影响 crate 的 cargo check --testscargo clippy --all-targets(只比对改动文件是否有新增告警)。macOS 上 cfg(windows) 分支根本不参与编译,本机无法验证「不闪窗」,最终必须 Windows 实机启动一次确认。

  • 适用范围crates/terminal/src/wsl_distributions.rs(启动期 WSL 识别,commit 57e31731b 引入的遗漏)、crates/html-preview/src/browser.rsWindows 走 cmd /C start)、crates/workspace_explorer/src/backend.rs(容器后端 docker exec)、main/src/settings/mcp_skill_install.rsnpx/node 启动器),以及所有新增后台外部命令调用点;workspace_explorer/src/git.rscore/cloud_sync/personal/git_store.rsextension-host/src/process.rsremote_desktop/src/backends/rdp/transport.rsremote_file_editor/src/external_launcher.rsmain/src/file_association.rs 是已按此约定收口的正确样例。

执行原则

  1. 先澄清,再实现;先缩小边界,再扩展范围。
  2. 优先局部修改与最小充分实现,避免无关扩张。
  3. 若任务复杂度上升,应及时升级流程,而不是硬撑轻流程。
  4. 若任务收敛为局部改动,应及时降级流程,避免形式成本。

How to Report Bugs

  • 清晰描述 bug 的现象、触发条件、预期行为与实际行为。
  • 给出尽量稳定可复现的步骤。
  • 说明真实影响范围与严重程度。
  • 清晰解释真实世界中的后果,并关联影响严重程度和修复优先级。
  • 收集有助于诊断 bug 的上下文信息,例如使用模式、错误信息、堆栈跟踪、日志、环境配置与版本。

Bug Fixing

  • 不直接猜测式修补,先确认根因,再决定修复策略
  • 优先通过复现、日志、调用链、最小实验或二分缩小问题范围
  • 修复后按测试策略与验证门禁完成收口

Testing Standards

  • 测试优先覆盖关键路径、边界情况和错误路径。
  • 测试应具体、可读、稳定,避免脆弱测试。
  • assertEqual 类断言,优先遵循“expected 在前,actual 在后”。
  • 是否需要 TDD,按全局测试策略判定,不在本节重复规定。

How to Write Code 如何编写代码

  • 遵循 SOLID、DRY、关注点分离与 YAGNI。
  • 命名应清晰、抽象应务实。
  • 仅在关键或不直观逻辑处添加简短注释,避免注释噪音。
  • 修改行为时,优先移除死代码和明显过时的兼容路径,除非用户明确要求保留。
  • 明确处理边界条件,不要隐藏失败。
  • 关注时间复杂度和空间复杂度,尤其在高 IO 或高内存路径上。
  • 新增文档字符串时,保持简洁,说明目的、关键假设与实现理由。
  • 除非没有更合理方案,不主动添加大范围 linter 抑制注释。

代码指标(硬性上限)

  • 函数长度:≤ 50 行(不含空行)
  • 文件大小:≤ 300 行
  • 嵌套深度:≤ 3 层
  • 参数数量:位置参数 ≤ 3
  • 圈复杂度:每函数 ≤ 10
  • 禁止魔法数字:提取为具名常量

Refactoring Standards

  • 默认优先保持行为不变,再提升结构质量。
  • 重构应在测试或验证保护下进行,必要时先补测试再重构。
  • 如果检测到循环导入,请将共享逻辑提取到新工具模块或现有模块中,以保持依赖图无环。
  • 对较大的重构,优先先拆分计划,再按步骤推进。
  • 重构完成后,仍需进行独立 review 与完成前验证。

Safety Rules 安全规则

  • 不要运行破坏性命令,例如 git reset,除非用户明确要求。
  • 不要使用非 Git 工具操作 .git 目录。
  • 避免危险删除命令,除非其作用范围被明确限制在临时产物。
  • 不要将密钥、凭证、API Key 硬编码进源码文件。
  • 数据库访问应使用参数化查询。
  • 不要通过拼接不可信输入来构造 shell 命令或 SQL。
  • 在系统边界校验并清理外部输入。
  • 除非用户明确要求,否则不要终止非当前任务启动的进程。

沟通与协作

沟通风格

语言约定

  • 默认使用简体中文回答,可混用英文技术术语。
  • 代码标识符使用英文。
  • 代码注释优先简体中文,保持简洁清晰。

混合输出模式

根据任务类型选择合适的输出风格:

  • 执行类任务:强调进度、当前动作、下一步
  • 分析类任务:强调结论、依据、权衡
模式 A:执行进度式

适用场景:代码修改、重构、bug 修复、多步任务、文件操作

推荐结构:

🎯 任务:一句话描述当前任务

📋 执行计划:

  • 已完成
  • 🔄 进行中
  • ⏸ 待执行

🛠️ 当前进度: 详细描述当前正在做什么,已完成什么

⚠️ 风险/阻塞: 潜在问题、注意点、阻塞因素

📎 参考:file:line

模式 B:分析回答式

适用场景:问答、代码解释、方案对比、架构分析、问题诊断

推荐结构:

结论:1-2 句直接回答核心问题

🧠 关键分析:

  1. 核心观点
  2. 依据
  3. 权衡

🔍 深入剖析:(可选) 📊 方案对比:(可选) 🛠️ 实施建议:(可选) ⚠️ 风险与权衡:(可选)

技术内容规范

  • 多行代码、配置、日志,优先使用带语言标识的 Markdown 代码块。
  • 示例代码聚焦核心逻辑,省略无关部分。
  • 需要强调变更时,可使用 + / - 辅助表达差异。
  • 仅在确有必要时使用表格。

输出结尾建议

  • 复杂内容后附简短总结,重申核心要点。
  • 结尾给出实用建议、行动指南或鼓励进一步提问。

子代理派发策略

  • 仅在任务可明确拆分、并行收益真实存在或需要独立审查时派发子代理。
  • 子代理不限制模型或推理等级,按当前环境可用能力、任务特点与成本选择即可。
  • 派发时明确目标、范围、输入、预期输出、验证方式和禁止触碰的区域。
  • 避免多个代理同时修改同一文件或同一逻辑边界;若无法避免,由主代理负责协调顺序与合并。
  • 主代理必须复核子代理的结论、代码和验证证据,并对最终交付负责。

@RTK.md