diff --git a/.agents/notes/archived/feature/2026-10-22-database-multi-schema-tree.md b/.agents/notes/archived/feature/2026-10-22-database-multi-schema-tree.md new file mode 100644 index 0000000000..f5b7944542 --- /dev/null +++ b/.agents/notes/archived/feature/2026-10-22-database-multi-schema-tree.md @@ -0,0 +1,119 @@ +# Agent 笔记:MySQL 连接下的多数据库树 + +状态:已实现 +归档日期:2026-10-24 + +## 归档原因 + +macOS 内置数据库模块已删除,数据库改由官方插件 `dev.lithe.plugin.database` 提供,多数据库树和“显示哪些库”的选择在插件网页里重新实现。本 Note 描述的 SwiftUI 侧栏、`DatabaseFeatureModel` 和对应测试都已不存在。当前决策见[数据库拆成双端共用的独立插件](../../implemented/architecture/2026-10-24-database-provider-plugin.md)。 + +## 先说结论 + +macOS 数据库侧栏保留混合类型的分组文件夹,在每个 MySQL/MariaDB 连接下展示账号可见的数据库,并允许同时展开多个库。展开目录只加载目录,不切换编辑区;打开表、查询或执行右键动作时才选择该节点所属的数据库。各库的加载状态相互独立,不能再用当前编辑区的表列表绘制所有节点。 + +## 问题 + +原侧栏只绘制当前连接的当前数据库。切换连接或数据库时,旧库的表列表被清空,因此即使保留展开箭头也不能真正同时显示多个库。同名表的右键操作如果只携带连接编号,还会使用编辑区当前的库。 + +## 决策 + +数据库功能模型 `DatabaseFeatureModel` 为每个连接保存账号返回的数据库名称,并为每个数据库保存独立的表、对象、加载状态和请求编号。只在展开时读取该库的元数据,重复展开使用内存结果,刷新才重新获取;不预先扫描所有库,也不缓存表数据或密码。连接和数据库目录各自持有后台请求;折叠、断开、强制刷新或模块释放时取消相应 worker,让取消信号到达 macOS 进程组终止路径,同时用请求编号阻止迟到结果恢复已清理的节点。数据库列表刷新后删除不再可见的缓存条目,失败保留其他数据库内容并提供重试。 + +文件夹、连接、数据库(含非 MySQL 的默认库)和 Tables 等对象分组统一采用名称单击选择、名称双击切换展开和收缩、箭头单击独立切换。分组原先每次单击都切换,双击会切换两次回到原状;数据库原先点击总是展开,无法双击收缩。因此不能只修连接层,必须沿各层容器的入口一起修正。单击数据库目录不切换编辑器,打开表或执行操作才选择实际数据库;单击当前连接也不重复刷新。未放入文件夹的连接直接显示在树根层,删除额外的“未分组连接”分区标题;原标题承载的根层拖放入口改为树尾空白落点,全部连接都已分组时也能拖回根层,原“移回根目录”菜单继续保留。 + +表行也区分选择、展开和打开:单击表名只选择,展开箭头先更新展开集合并显示加载动画,双击表名才打开数据编辑器。树展开只取列、索引、外键,不读第一页数据,也不切换活动数据库。每个“连接+库+表”保存结构缓存和唯一的后台请求;编辑器需要同一结构时复用该请求,折叠不取消其他消费者正在等待的加载,迟到结果不能重新展开节点。列、索引、外键的并行读取使用结构化子任务,模块释放取消共享 worker 时不会留下独立的 sidecar。配置变更、断开、删除和模块释放会使请求失效;显式刷新清理已完成的结构缓存,导航则复用缓存。 + +刷新按钮以树的选中节点为范围:表节点只读取该表的列、索引和外键;库或对象分组刷新所属库已加载的结构;连接刷新可见数据库名称和已加载的库、表结构,不重新查询表格记录,也不切换编辑器。连接右键的 Diagnostics → Force Refresh 先清除该连接的结构缓存和旧请求,再重新读取先前已加载的范围;不扫描所有可见库。普通刷新保留旧内容直至结果返回,移除已删除库、表的缓存。当前接口没有结构版本差异,因此不宣称实现 IDEA 的增量检测。 + +数据编辑区和结构页未选择表时复用 `LitheToolWindowStatusText`,只显示操作提示,删除大图标与重复标题。依据 JetBrains 官方 [Empty State](https://plugins.jetbrains.com/docs/intellij/empty-state.html) 的详情区说明及既有 Community StatusText 共享样式;不把专有数据库插件的界面猜作源码基线。 + +同步的钥匙串读取和数据库元数据请求都放在后台,先向界面发布 loading 状态。展开后的加载提示使用现有 `ProgressView` 与树行 token;单个表失败在原节点显示 Retry,不覆盖另一张表的数据编辑器错误。 + +这种拆分依据 Community `platform/ui.tree/src/com/intellij/ui/tree/AsyncTreeModel.java`:`promiseChildren` 先使用缓存,没有缓存时放置 `LoadingNode`,后台读取后再更新界面,并用节点队列和过期检查保护结果。官方表编辑器说明将双击表名定义为打开数据编辑器;通用 `LoadingNodeRenderer` 提供加载文字占位,Lithe 按产品需求使用已有加载动画,并不复制专有数据库插件实现。 + +节点操作必须先通过 `selectDatabaseContext` 选择节点所属的数据库,再进入已有编辑区、导入导出和危险操作确认流程。例如 alpha/shared 与 beta/shared 是不同节点:点击 beta/shared 后的查询、导出或删除必须使用 beta,不能继续使用 alpha。展开 beta 的箭头则不应改变当前查询目标。 + +布局、行高、缩进、图标尺寸、明暗主题、悬停与活动/非活动选中状态复用 `LitheTheme.Tree`、`litheTreeRow`、`LitheIDEAIcon`,右键菜单复用 `litheContextMenu`。不增加独立颜色或另一套树组件。树的单选节点由侧栏独立保存,不能从活动连接、活动数据库和活动表分别推导高亮;点击数据库或表后,其祖先连接不得同时高亮。展开连接和数据库的箭头只改变展开状态。 + +交互依据是 JetBrains 官方 [数据源分组说明](https://www.jetbrains.com/help/idea/managing-data-sources.html) 和 [数据库工具窗口说明](https://www.jetbrains.com/help/idea/database-tool-window.html)。通用树行为核对 Community revision `d8c6714c7a3c110c56994d6b0f10814f902100e7` 的 `platform/platform-impl/src/com/intellij/ide/ui/laf/darcula/ui/DarculaTreeUI.java` 与 `platform/util/ui/src/com/intellij/util/ui/tree/WideSelectionTreeUI.java`;后者处理整行选中及展开控制的选中/焦点状态。数据库插件的专有实现不作为可复制源码;外观继续采用仓库已经批准的共享树 token 和已有 SVG。 + +连接和文件夹的右键使用 Move → To Folder… / To Top Level。Move 用于组织连接配置与分组,不能对 MySQL 实体库执行本地分组移动。没有分组时 To Folder 直接打开名称输入;已有分组时展示目标路径列表,也可新建后移入。名称支持 `/` 嵌套,已有中间层复用,末级重名、空路径段、过期目标及文件夹自身/后代循环均在功能模型拒绝;失败不发布半成品目录。创建后移动连接时,存储先完成两份 JSON 编码,再写原有组织配置键,不改凭据键。 + +新建与重命名分组共用项目区的 `LitheNamePopup`:从现有新建文件/目录内容提取,保留无标题栏、plain 名称框、Enter 提交和 Esc 取消,不新增独立表单样式。目的地列表通过 `LitheDropdownPopover` 的居中菜单入口复用 `LitheContextMenuPresenter`,包含原有键盘导航、选中、滚动和屏幕边界处理;路径仅用于本地组织,不发送给数据库服务。 + +连接旁的 `N of M` 是已选可见库数与账号可访问库总数,不是展开数、序号或已加载元数据数。计数必须原样显示,不能使用全局 `%lld of %lld` 本地化键;该键在中文中表示“第几个,共几个”,语义不同。2026-10-23 直接核对本机 IDEA 2026.2.1(262.9437.185)后,选择器改为无标题、无 Apply/Cancel 的复选列表,底部提示按 Enter 或点击列表外应用;Esc 丢弃草稿。搜索在输入时出现,继续使用原生输入法和共享弹层焦点路由。范围提交仍由功能模型验证,不在页面另存状态。官方语义见[数据库选择说明](https://www.jetbrains.com/help/idea/introspection.html#select-schemas-and-databases-for-introspection)。 + +徽标绘制依据 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade` 的 `platform/platform-api/src/com/intellij/ui/SimpleColoredComponent.java`(`drawClickableFrag`)以及同 revision 的 `UIUtil.java`、`ManyIslandsDark/Light.theme.json`:24pt 行内上下内缩 2pt、左右内缩 1pt,圆角直径 6pt,普通描边,悬停且非活动选中行时填充;small 字号为 max(普通字号−2, 11)。颜色复用已存在的 `toolbarHoverBackground`,其深浅值分别为白色 23/255 和黑色 18/255,文字使用共享 Tree 次要文字。连接名称后保留 en space 的间隔。徽标显式使用共享 `lithePointer` 呈现可点击的小手,但没有悬停文字;退出和卸载时由同一 modifier 恢复光标,不修改整棵树的光标策略。选择弹框通过局部环境关闭小手,树行与工具栏均保持普通箭头。弹层外框、搜索框和工具栏优先使用 Lithe 共享控件。范围列表是复选树,不是普通菜单,不能用 `LitheSettingsCheckbox` 加 `LitheDropdownRowStyle` 替代。原 Git 树复选框提升为 `LitheTreeCheckbox`,Git 原调用与数据库共用同一套 24pt SVG(内部绘制 16pt 方框),资源已包含深浅、焦点、禁用及三态变体,不重画勾选符号。列表使用 `litheTreeRow` 的 13pt 文字、24pt 行高、树选择/悬停色;保留 12pt 树边距与 19pt 根节点控制占位,复选框标题旁单行省略,行内空白也能点击。 + +同 revision 的 `CheckboxTreeBase.kt` 通过 `ThreeStateCheckBox` 与 `ColoredTreeCellRenderer` 组成行;`CheckboxTreeHelper.kt` 隐藏根节点但保留根控制,`PopupChooserBuilder.java` 不对树套用普通列表边距。`ManyIslandsDark/Light.theme.json` 的 Tree 继承通用工具窗口背景,故正文使用已有 `sidebar` 色,只有底部说明区使用 `popupBackground`。说明文字采用 Popup.Advertiser 的 UI 字号减 1pt(12pt)、垂直 8pt/水平 12pt 边距;列表采用已有 480pt 高列表视口,少量条目也保留空白,弹层宿主负责屏幕边界。外观依据是公开的通用控件及实际界面,不复制专有数据库插件代码;模式匹配、动态默认 schema 仍不属于当前范围存储模型。 + +连接行的名称与徽标右侧空白必须保持相同的单击选择、双击展开/折叠行为。引入独立范围徽标时不能把名称按钮缩短后留下不响应事件的 Spacer;空白命中区域复用同一连接动作,徽标与展开箭头仍独立处理,避免整行手势使它们重复切换。零选库不等于连接无法展开:展开后使用共享树的次要文字色与 11pt small 字号显示“未选择架构”,缩进对齐下一层图标,不放省略号按钮。 + +显示范围保存在现有连接配置的可选字段 `visibleDatabases`:缺省或 null 表示全部可访问库(新出现的库自动加入),空数组表示一个也不显示,非空数组是固定选择。旧配置没有这个字段时保持之前全部显示的行为。这里不能复用 `database` 字段,因为它属于编辑器当前查询目标;隐藏 beta 后仍可继续编辑 beta 中已打开的表。切换范围只写配置,不读取密码、不测试连接、不查询记录;数据库选择器保存前校验账号当前可见的名称,连接或权限发生变化时拒绝过期选择。连接级刷新跳过隐藏的库,即使该库曾被编辑器加载过。隐藏操作清理相应结构缓存和请求标记,迟到结果不能恢复隐藏节点。元数据仍按展开延迟读取,没有增加全库预扫描。 + +工具栏“+”与侧栏右键 New 共用 `LitheMenu` / `LitheContextMenuItem` 的新建菜单:查询控制台、数据源类型子菜单、分组、DBX 导入。类型来自已有 `DatabaseKind.allCases`,选择后将默认引擎传入原连接编辑器;新建初始化与切换引擎复用同一默认值函数,编辑现有连接不重置参数。云数据源、模板、驱动管理等未实现能力不添加空动作。右下角角标来自 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade` 的 `platform/icons/src/expui/general/dropdown{,_dark}.svg`(16pt 画布右下角 4pt 直角三角形);`ActionButton.java` 判断 popup group 后由 `ActionButtonLook.paintDownArrow` 将角标相对主图标偏移 (1, 1)。只补齐现有 IDEAIcons 源资源,不引入下载脚本或运行时写入。菜单外框、字体、选中颜色、子菜单及键盘导航继续由 Lithe 共享菜单拥有。连接编辑器保留共享弹窗尺寸支持,最新分页布局使用稳定内容尺寸,避免切页跳动。新建菜单的数据源入口按用户后续明确要求,复用右侧数据库工具窗口的 `database/expui/dbms@20x20` 圆柱图标,以现有相同资源统一显示;其 DatabaseTools 来源和 Apache-2.0 声明保留在 IDEAIcons/NOTICE,不声称来自 Community。其余菜单图标使用 Community 资源,七种数据库保留现有同 revision 的 `providers` 原图。查询、数据源、分组三行连续使用共享 24pt 行高,仅导入前保留分隔组,避免每项后叠加 11pt 分组留白。Community 无 Nacos 专属图标,因此共享品牌控件改用该 revision 的通用 `expui/general/settings` 原图(本地命名 nacos),删除原 DBX PNG;缺失资源时的备用图标也使用 Community dataTables,取消 SF Symbols 分支。 + +侧栏空白区(包含尚未建立连接的空状态)复用 `litheContextMenu`,提供 New → 连接/分组、DBX 导入、按所选节点刷新和全部折叠。根层空白拖放区域填满视口剩余高度,因此右键不再只有树末的一行能命中;节点菜单保持独立。 + +## 考虑过的备选方案 + +- 每展开一个数据库就切换当前连接配置:代码更短,但多个库仍共享同一份内容,展开也会打断查询,因此不采用。 +- 为每个数据库复制一份连接:会重复保存服务器地址与凭据,改变现有连接和分组语义,因此不采用。 +- 展开连接时读取所有库的所有元数据:首次加载开销随服务器规模增加,且一个无权限对象可能拖累整棵树,因此按库延迟加载。 + +## 后果 + +同一连接的多个库及多个连接能独立展开,分组与混合类型连接继续保留。代价是元数据在模块内存中保存到刷新、配置变更、断开或模块释放;外部数据库结构变化需要刷新,已有进程端口的超时仍负责限制单次数据库调用。当前编辑区仍只有一个活动数据库,树的并行展开不表示为每个库新增独立编辑会话。Windows 的树没有在此次 macOS 界面修改中同步。 + +## 连接编辑器排版与草稿检测 + +连接编辑器保留原有字段和保存流程,名称固定在顶部;常规页承载引擎及连接参数,SSH / TLS 页承载原有网络选项,安全与标记页承载分组、颜色、只读和保护选项。使用现有输入框、选择器、工具窗口分页及操作按钮样式,固定 760×580 内容区,长内容在页内滚动。 + +用户另行授权测试连接:操作栏左侧复用 `LitheBannerActionStyle`,可选颜色仅覆盖本按钮为主题 accent,默认调用者不变;文字及 hover 下划线同色。检测只调用已有 sidecar 接口,不保存连接或密码、不改当前工作区和连接状态。修改有效草稿、密码或关闭弹窗取消 UI 任务,迟到结果不能覆盖新草稿。macOS 同步进程运行器会观察任务取消并沿现有进程组终止路径清理测试 sidecar;30 秒请求超时仍是兜底。编辑连接的空密码沿用已保存密码;新连接为空则使用空密码。成功后自动打开位于测试链接上方的共享 `LitheDropdownPopover`。纠正结果卡片误用菜单表面:仅该调用选择 contentProvidesSurface,复用宿主定位、关闭和事件生命周期,内容自行提供结果气泡表面。底栏保留 Community greenCheckmark 与引擎名称,可重开/收起结果;浮层使用绿色成功标题、主题色复制链接和可选择正文。现有 sidecar 仅返回 connected,因此只展示已知引擎与单调时钟测得的端到端检测耗时,不能把它称为网络 Ping,也不虚构服务端/驱动版本、大小写规则或实际协商的 TLS 状态。 + +## 验证 + +- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh` +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'DatabaseWorkspaceLifecycleTests/databaseTree|LitheTreeRowStyleTests'` +- `./scripts/verify-service-boundaries.sh` +- `./scripts/verify-runtime-bundle-immutability.sh` +- `./scripts/verify-platform-feature-matrix.sh` +- `./scripts/verify-agent-notes.sh` + +回归使用现有数据库进程端口的可控替身,覆盖 MySQL/MariaDB、混合分组、无默认库、同名表上下文、权限失败重试、刷新移除库以及数据库和表结构在断开/删除/释放后的迟到结果。另用有本地期限的真实 sidecar 子进程验证连接或数据库目录在任务取消、折叠、断开、强制刷新和模块释放后退出;测试不手动放行阻塞请求。受控慢请求还验证结构加载期间立即展开并显示进度、折叠不重开、重复展开命中缓存,编辑器与树共享结构请求且展开不查询表数据。深浅主题宿主测试通过原生辅助功能操作展开和折叠,并验证两个库的表同时出现;这些检查不替代完整工作区的真实连接验收。另用临时 MySQL 8.4.11 实例验证数据库列表的权限过滤,以及两个库中同名表的查询结果和全部对象分组查询;受限账号看不到未授权的第三个库。临时实例及数据在检查后清理。 + +## 适用范围 + +- `macos/Sources/LitheDatabaseModule/Application/DatabaseFeatureModel.swift` +- `macos/Sources/Lithe/Views/Database/DatabaseSidebarView.swift` +- `macos/Tests/LitheTests/DatabaseWorkspaceLifecycleTests.swift` +- `shared/platform-feature-matrix/features/database-connections.json` + +## 成功气泡源码核对(2026-10-23) + +上游统一固定为 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade`: + +- `platform/platform-impl/src/com/intellij/ide/ui/laf/LafManagerImpl.kt`:`INTER_SIZE=13`、`Font.PLAIN`;`shouldFallbackToSystemFont` 对 zh/ja/ko 为真,macOS `installMacOSXFonts` 使用 Helvetica Neue 13。结果正文和 Copy 随 locale 使用该分支,成功标题按用户截图单独加粗,不再让菜单动作样式覆盖字体。 +- `platform/platform-impl/src/com/intellij/ui/BalloonImpl.java`:非 dialog 模式 ARC=4,上下方向 pointer width=14、length=10。移植四角和连续底边指针路径,1pt 描边使用 Shaper 的整数坐标和 size - 1 远端边缘(原先错误内收半点已纠正),不能先画完整矩形再叠加三角形而留下内部横线。 +- `platform/platform-resources/src/themes/islands/ManyIslandsDark.theme.json` 及 Light:沿 parent 和颜色别名解析 dialog-bg、popup-border、text-default、Label.successForeground。深色 #191A1C/#33353B/#D1D3D9/#6DB083;浅色 #F7F8F9/#DDDFE4/#000000/#338555。结果卡片采用 dialog surface,不继承菜单 popup-bg,也不直接使用 ToolTip 的反馈灰底。 +- Community plugins 目录无 Database Tools 实现,公开 `DataSourceTestConnectionManager` 堆栈也不能证明私有结果面板的构造参数。因此上述字体、气泡默认几何和主题 token 是可核对的 Community 规则;选择非 dialog 气泡、dialog-bg/popup-border 组合、标题粗体、16pt 内容留白、12pt 标题间距以及跟随本地 760pt 表单减去两侧留白的宽度,是按用户截图进行的独立布局适配,不声称拿到了私有面板源码或其全部规则。 + +验证应覆盖真实资源深浅主题、中文字体回退、正文常规/标题粗体、边框与底部透明区域及指针、重新打开和取消清理;保持其他共享菜单表面不变。不增加服务器版本或驱动元数据请求。 + +### 输入框与原生浮层外沿纠正 + +连接编辑器改用项目已有 `litheSettingsTextField`(共享 28pt 高度、4pt 圆角),Commit 对话框输入样式也按用户后续要求复用共享 4pt 圆角 chrome;所有使用该 modifier 的 Git 和数据库表单一并生效,保留原焦点绑定与 1/2pt 边框宽度。用户名和密码保留与 Host 相同的端口区占位,端口标签按当前语言自然测量,隐藏占位不进入辅助功能树。检测耗时与按钮统一取视图 locale,避免 AppLanguage 英文与系统中文混用。自带表面的结果气泡关闭 AppKit 原生窗口阴影附带的外沿,保留既有独立描边;普通菜单仍使用原生阴影。 + +### 成功浮层淡入 + +Community 同一 revision 的 `BalloonImpl.runAnimation` 按帧提高 alpha,`BalloonPopupBuilderImpl.myAnimationCycle` 默认 500ms;Database Tools 私有调用是否覆盖时长不可确认。结果浮层采用原生 NSWindow 500ms 线性淡入,仅首次展示/重新打开执行,布局更新不重新播放;关闭仍立即移除,不留下延迟回调重开窗口。尊重 macOS 减少动态效果设置,其他菜单默认无淡入。 + +### 两种密码保存范围与复制结果 + +按用户确认仅支持 Until restart / Forever,不展示未实现的 Never / For session 或 IDE 密码设置入口。`DatabaseProfile.passwordPersistence` 为可选解码字段,旧数据回退 Forever。前者由 DatabaseConnectionStore 进程级加锁内存字典持有,跨模块重载仍有效、退出进程自然释放,不写偏好或凭据存储;后者沿用已有安全存储。切换到临时模式先成功删除持久化凭据再发布内存值,切换永久模式先成功写入再清内存;保存配置失败时恢复原密码及原范围。复制连接保留保存范围,删除连接清理两种存储。下次启动临时密码为空,仍通过已有编辑连接入口重新填写,不增加自动凭据弹窗。 + +新建连接用户名留空,不再默认 root/postgres/sa。成功浮层 Copy 使用手形光标,复制后收起,点击结果入口可重新打开;测试恢复用户完整剪贴板内容。 + +### 新连接默认名称 + +按用户提供的 IDEA 界面形式,Name 默认显示 `数据库@主机`,未填写数据库为 `@主机`。新草稿在未手动改名前随 Host/数据库/引擎更新;手动改名或编辑已有连接后保持用户名称。SQLite 使用文件名,未选择文件时使用引擎名称。MongoDB URI 仅提取 host,禁止把账号、密码或 query 拼入名称。当前默认 Host 仍为 127.0.0.1,不为显示名称改变网络连接地址。公开文档不提供完整私有自动命名实现,此处是与截图命名形式一致的本地适配。 diff --git a/.agents/notes/implemented/architecture/2026-09-12-git-execution-and-project-console.md b/.agents/notes/implemented/architecture/2026-09-12-git-execution-and-project-console.md index 137dbcec26..3694927485 100644 --- a/.agents/notes/implemented/architecture/2026-09-12-git-execution-and-project-console.md +++ b/.agents/notes/implemented/architecture/2026-09-12-git-execution-and-project-console.md @@ -253,6 +253,12 @@ JDT 通道曾触发其 15 秒测试期限,同一提交在 SharedRust 通道用 所有测试的时限来隐藏这类问题。计时工具用可控时钟验证普通、集成和总截止 时间,并验证 HTML 与 JUnit 不会把合法的集成耗时误判为失败。 +checkout/clone 完整流程用例 `git_write_executes_checkout_preflight_clone_and_validation` +在 Windows CI 39935852248 中全部断言通过,Rust 内部计时 15.22 秒、测试进程计时 +18.529 秒,超过默认十五秒后触发清缓存重编,继而耗尽原套件期限。该用例创建分叉 +分支,覆盖干净、脏和未跟踪文件预检、智能/强制切换以及本地 clone,因此只为它在 +SharedRust 和真实 JDT 两个入口设置三十秒预算;不延长请求或整套期限,不重试失败断言。 + 取消模块的确定性回归核对 mutation 后正常读取保留原截止时间和取消能力, 已取消或超时请求的独立读取仍受清理期限约束,错误和异常退出后恢复原状态。 真实仓库 rebase 回归核对 edit、conflict、continue、skip、abort 和过期 amendment; diff --git a/.agents/notes/implemented/architecture/2026-09-13-module-runtime-boundaries-and-lifecycle.md b/.agents/notes/implemented/architecture/2026-09-13-module-runtime-boundaries-and-lifecycle.md index 1502f95f28..c0a4201b7c 100644 --- a/.agents/notes/implemented/architecture/2026-09-13-module-runtime-boundaries-and-lifecycle.md +++ b/.agents/notes/implemented/architecture/2026-09-13-module-runtime-boundaries-and-lifecycle.md @@ -89,6 +89,13 @@ MacServiceContainer 错误,不能把残留资源伪装成成功禁用。上述任务仍在主执行器上使用异步等待, 不阻塞线程、不新增插件代码加载能力。 +使用真实 AppModel 的测试也必须遵守项目打开的清理顺序。`openProjectDirectly` 会启动 +模块图关闭;项目文件快照已经发布不代表关闭完成。Safe Mode 测试要手动注入能力时, +先通过有截止的 `WorkbenchTestScope.run` 等待 `awaitModuleRuntimeShutdown()`,再放入 +测试能力。否则迟到的 shutdown 事件可能移除刚注入的能力,让前几次搜索通过、随后 +查询全部为空。不要用睡眠或反复重新注入能力掩盖这个顺序;搜索排除回归与插件页面 +生命周期测试都使用同一个明确的完成边界。 + ### 数据库视图在模块释放后的刷新 退出应用或休眠数据库模块时,SwiftUI(macOS 的声明式界面框架)可能先刷新 diff --git a/.agents/notes/implemented/architecture/2026-09-13-repository-ownership-and-sharing-boundaries.md b/.agents/notes/implemented/architecture/2026-09-13-repository-ownership-and-sharing-boundaries.md index b437606cd9..1b835157eb 100644 --- a/.agents/notes/implemented/architecture/2026-09-13-repository-ownership-and-sharing-boundaries.md +++ b/.agents/notes/implemented/architecture/2026-09-13-repository-ownership-and-sharing-boundaries.md @@ -39,7 +39,6 @@ Swift 源码,也不得依赖 macOS 类型。 | `Plugins/win/` | Windows 所有的插件包 | | `frontend/editor/` | 两端共同依赖的 Monaco 表现层、分词与编辑器模型;不调用平台 API | | `shared/` | 跨平台契约和夹具,不放编译实现 | -| `infra/` | 仓库级开发和验证基础设施 | | `third_party/` | 固定版本的上游清单和必要的局部源码补丁 | 应用层依赖方向如下: @@ -272,5 +271,4 @@ Core 操作会随协议演进持续增加,逐个新增 Tauri command 会在 - `shared/` - `frontend/editor/` - `scripts/` -- `infra/` - `third_party/` diff --git a/.agents/notes/implemented/architecture/2026-09-25-shared-acp-agent-conversation.md b/.agents/notes/implemented/architecture/2026-09-25-shared-acp-agent-conversation.md index 19607a5f84..7666c12be0 100644 --- a/.agents/notes/implemented/architecture/2026-09-25-shared-acp-agent-conversation.md +++ b/.agents/notes/implemented/architecture/2026-09-25-shared-acp-agent-conversation.md @@ -4,7 +4,7 @@ ## 先说结论 -Agent 对话默认关闭,打开某个项目的 Agent 面板时才启动本机 Agent。每个项目的每种 Agent 只有一个对话连接,一个连接里可以有多个会话;会话历史由 Agent 自己保存;Lithe 负责显示,并单独保存收藏、自定义标题和可恢复的隐藏状态。供应商管理允许 Codex 在自定义 API Key 和本机 ChatGPT 订阅间切换;Claude 仍只支持 API Key。订阅额度放在输入框上下文的右侧,不新增页面。Agent 需要的 Node.js 由用户自己安装,Lithe 只负责检测;ACP 适配器由 Lithe 提供一键安装,安装时用的是用户本机的 npm。ACP(Agent Client Protocol,编辑器与 Agent 之间的对话协议)连接和进程管理写在同一个 Rust crate 里:Mac 通过 C ABI 调用,Windows 通过 `windows/tauri/src-tauri/src/agent.rs` 的连接桥调用,两端只各自实现界面。 +Agent 对话默认关闭,打开某个项目的 Agent 面板时才启动本机 Agent。每个项目的每种 Agent 只有一个对话连接,一个连接里可以有多个会话;会话历史由 Agent 自己保存;Lithe 负责显示,并单独保存收藏、自定义标题和可恢复的隐藏状态。供应商管理允许 Codex 在自定义 API Key 和本机 ChatGPT 订阅间切换,Claude 在自定义 API Key 和本机 Claude Code 已登录的 claude.ai 订阅间切换。订阅额度放在输入框上下文的右侧,不新增页面。Agent 需要的 Node.js 由用户自己安装,Lithe 只负责检测;ACP 适配器由 Lithe 提供一键安装,安装时用的是用户本机的 npm。ACP(Agent Client Protocol,编辑器与 Agent 之间的对话协议)连接和进程管理写在同一个 Rust crate 里:Mac 通过 C ABI 调用,Windows 通过 `windows/tauri/src-tauri/src/agent.rs` 的连接桥调用,两端只各自实现界面。 ## 问题 @@ -30,14 +30,15 @@ Agent 对话默认关闭,打开某个项目的 Agent 面板时才启动本机 - **Codex 兼容入口只补配置与错误信息**:固定适配器覆盖自定义网关配置,`CODEX_CONFIG` 中的重试字段会丢失;混用点号路径和整个表又会受到原生 HashMap 应用顺序影响,不能靠偶尔成功认为已生效。因此单次连接创建一方标准输入/输出转发 helper(只传输公开 App Server 协议),在创建、恢复和分叉会话时补齐原生预算,其他配置照传。AIR 重连警告会丢掉原生错误信息,helper 将结构化 `codexErrorInfo`、原始说明、当前轮次和通知序号保存在失败标题内的 JSON 信封;Host 只在协商后的失败元数据中解码,正常消息原样传递,不扫描模型文本或私有日志。公开通知序号去重、原生当前轮次判断拒绝迟到错误,界面只显示原始说明。`/lithe-codex-retry-/` 由该连接持有,结束进程树或启动失败后删除,无密钥内容;临时目录由平台解析,不写安装包、用户配置或第三方文件,不影响签名与 Sparkle delta。资源注册表将它排除,不能跨工作树复用。适配器未来直接保留这些原生配置并转发错误信息后,应移除此兼容 helper,保留预算和契约测试。 - **重连状态由实际事件驱动**:Host 发出带唯一轮次标识和尝试次数的 `turnRetrying`,macOS 在尚无工作时显示“正在重连 2/5…”并继续累计耗时;已有工作后的原生恢复省略 `maxAttempts`,只显示“正在重连”,不能编造调用上限。首次恢复警告后五分钟显示等待较久的提醒,继续等待只关闭提醒,不重发;后续警告不能推迟提醒。实际输出恢复后切换回复或推理状态;有效计划更新(包括清空计划)也证明重连已恢复,界面退出重连并清空次数,保留轮次计时和忙碌,不覆盖已有回复、推理、权限或停止状态。无法解析的计划不触发此清理。完成、失败、取消、断连清空次数;迟到通知不能复活已结束轮次,取消优先于重连,取消确认前不解锁。Claude 的下一次尝试必须等前次失败且无进度;Codex 的流恢复仍由原生执行。后续接入其他 ACP,应先核对它是否提供结构化失败、明确重连事件及可配置预算;只支持 ACP 不代表可以控制它的内部重试。未经适配的 Agent 保留原生策略,正常轮次不设绝对上限,不能对所有 Agent 盲目套五次 prompt 重放。 - **思考强度档位控件**:按用户给定的 Effort 参考设计,模型菜单底部把纵向强度选项收成一行:左侧按用户要求统一显示 `Effort (当前档位)`,右侧用胶囊轨道、圆点和选中圆块表示上游实际档位。模型滚动区只包含完整模型目录,目录为空或长列表滚动到底时仍能访问强度。档位数量、顺序、ID 与名称都由 Agent 提供,不固定成参考图的四档,也不手工补齐 Low、Medium、High、Xhigh、Max、Ultra。点击圆点、方向键及辅助功能调整均提交同一配置动作;按用户要求,调整 Effort 后保持同一个模型弹窗,等待上游确认期间只禁用控件,确认或失败后恢复操作,不能因提交请求或进入配置等待而收起弹窗。选择模型及其他配置仍关闭菜单,点击外部、按 Esc 和会话切换继续使用共享关闭逻辑;拖动仅在控件内预览,松开后提交一次,不能把每次移动都变成上游请求。空闲时仍等上游确认才改变当前值,下一轮预选继续由既有状态机负责;禁用态不发请求,未知当前值不伪装成第一档。弹窗继续复用 `litheDropdown` 和共享外框,按用户要求,模型弹窗最小宽度设为 300pt,给模型列表与 Effort 行留下更宽松的空间;长标签与轨道仍参与内容测量,最大宽度遵守共享上限,不改成固定宽度;标题与括号按原文渲染,保证中文界面不会因翻译成更宽的字符而超出测量宽度、出现 `思考强…edium)` 截断。控件使用现有主题色和字体,不新增另一套菜单背景。代价是短模型目录的弹窗也保留 300pt 最小宽度,收益是多档强度不再占用多行并且始终可见。只新增视图内手势与焦点状态,不新增缓存、下载或发行包写入。 -- **权限模式呈现**:菜单和底栏保留 Agent 上游名称,例如 Claude 的 Manual、Accept edits、Plan、Auto、Bypass permissions;模型和思考强度也使用上游名称。名称不能进入全局翻译表,否则 `Plan` 会碰到其他界面的“计划”翻译,导致个别选项与其余名称混用语言。模式入口采用纯文本与向下箭头,菜单复用 LitheMenu 的紧凑单行选项与左侧勾选;删除卡片式逐项图标、双行说明及独立滚动/尺寸测量。当前模式说明留在入口 tooltip,Claude 继续显示上游实际提供的编辑、计划等额外选项。依据 [Claude 权限模式说明](https://code.claude.com/docs/en/permissions),说明可按界面语言呈现,但保留实际 Agent 的审批与环境约束,不把 Codex 的沙箱说明直接套到 Claude。只改变显示,不修改选项 ID、数量、顺序或权限命令;上游确认继续决定当前值。未知选项保留其原文,不按列表位置猜测权限,也不把 Claude 的五项折叠成三项。不新增状态、资源或运行时写入。 +- **权限模式呈现**:2026-10-24 用户要求权限模式跟随界面语言。已固定版本适配器的模式按“上游 ID + 上游英文名”同时匹配后显示产品译名:Claude Agent 的 `default`/`acceptEdits`/`plan`/`auto`/`bypassPermissions`(Manual、Accept edits、Plan、Auto、Bypass permissions),Codex 的 `read-only`/`agent`/`agent-full-access`(Ask for approval、Approve for me、Full access);两项都匹配才替换,因此其他 Agent 同样叫 `default` 或 `plan` 的模式、上游改名后的模式及未知模式仍显示 Agent 原名,不会被误译。同一适配器的已知模式全部翻译,不会出现个别选项与其余名称混用语言的情况。模型和思考强度仍使用上游名称。模式入口采用纯文本与向下箭头,菜单复用 LitheMenu 的紧凑单行选项与左侧勾选;删除卡片式逐项图标、双行说明及独立滚动/尺寸测量。当前模式说明留在入口 tooltip,Claude 继续显示上游实际提供的编辑、计划等额外选项。依据 [Claude 权限模式说明](https://code.claude.com/docs/en/permissions),说明可按界面语言呈现,但保留实际 Agent 的审批与环境约束,不把 Codex 的沙箱说明直接套到 Claude。只改变显示,不修改选项 ID、数量、顺序或权限命令;上游确认继续决定当前值。未知选项保留其原文,不按列表位置猜测权限,也不把 Claude 的五项折叠成三项。不新增状态、资源或运行时写入。 - **Agent 菜单标志与勾选**:输入框的 Agent 下拉菜单为每个选项显示 16 点品牌图标,复用底栏同一套只读资源与内存染色。原生单选控件(Picker)将当前选项的勾选状态与品牌图像分别呈现,避免选中项用勾选图标替换品牌。控件直接绑定面板已有的选择值与切换动作,不在 View 里再存一份当前 Agent;图标和勾选以实际选择为准,不改变连接、会话或权限语义。 - **侧栏入口图标**:Agent 使用 Lithe 原创的“对话气泡 + 星光”线性 SVG,由工作台 renderer 显式绑定,避免落入通用模块图标而让用户误认成插件管理。图标作为固定源资源随应用打包,复用现有模板着色与选中态;不下载、不生成运行时文件,不影响安装包的只读边界。面板内 Codex/Claude 标志仍表示具体供应商。 - **安装包图标解析**:Agent 品牌图标从应用的 `Contents/Resources/Lithe_Lithe.bundle` 只读加载;资源包或图标缺失时使用默认标志。不要在已安装的 `.app` 中直接调用 SwiftPM(Swift 包管理器)生成的 `Bundle.module`:它只查应用旁的资源包和编译目录,找不到会直接终止进程,开发机器残留的构建资源还会掩盖问题。开发和测试入口保留 SwiftPM 回退;安装版不使用编译目录,也不通过启动时复制资源来修补发行包,避免改变代码签名和 Sparkle 增量更新基线。 - **底栏品牌标志**:参考 [CC GUI 的紧凑入口](https://github.com/zhukunpenglinyutong/jetbrains-cc-gui/blob/7bcd6c3803ec44944cef90f5bff0aa666cab99a2/webview/src/components/ChatInputBox/selectors/ProviderSelect.tsx) 与[品牌组件](https://github.com/zhukunpenglinyutong/jetbrains-cc-gui/blob/7bcd6c3803ec44944cef90f5bff0aa666cab99a2/webview/src/components/shared/ProviderModelIcon.tsx),Claude 使用彩色星芒,Codex 使用主题单色结形标志。上游 Web 入口为 16 像素;Lithe 原生工具栏使用 18 点视觉尺寸、28 点点击区域,模型入口保留较小的同品牌标志。Claude 的 `#D97757` 与既有 `@lobehub/icons 5.8.0` 彩色组件一致。轮廓复用既有 SVG,欢迎区和历史页继续使用模板着色。SwiftUI 的前景色在原生 Menu 转换图像时可能丢失,因此底栏通过只读 SVG 的透明度蒙版在内存中生成带颜色的原始图像,不依赖原生控件替模板补色。图像缓存身份包含资源包、尺寸和颜色,深浅主题切换不会拿到旧色,也不修改其他视图共用的模板。正确做法是按品牌和主题选择颜色;不要复制 SVG 路径、改写源图或把染色图写进 bundle。该派生图像只在应用内存中存活,不新增磁盘缓存、下载或跨工作树复用资源,不影响代码签名和 Sparkle delta。 - **共享实现**:`rust/lithe-agent-host` 使用官方 `agent-client-protocol` SDK。一个 `AgentHandle` 对应一个项目的 Agent 进程和 ACP 连接,负责初始化、网关登录、会话新建/列出/加载、消息、权限、取消和进程树清理。Mac 通过 Rust Core C ABI(`lithe_agent_open_json`、`lithe_agent_send_json`、`lithe_agent_close`)调用;Windows 通过 `windows/tauri/src-tauri/src/agent.rs` 直接依赖同一个 crate。命令和事件的 JSON 形状由 `shared/fixtures/agent/acp-events-v1.json` 固定。 - **按需启动,跟着项目走**:`LitheAgentConversationModule` 是内置可选模块,默认禁用。每个项目有自己的模块运行时,所以会话天然属于项目。切换标签或窗口不会结束任何会话,后台项目的这一轮会继续跑完;只有关闭项目、关闭功能或退出应用时才停止 Agent。后台项目的会话在等待权限时,项目标签上会显示提醒点。 -- **API Key 按上游公开接口传递**:Lithe 通过 ACP 的标准输入输出(stdio)通道把凭据传给适配器,不进入 Lithe 启动适配器的命令行参数、环境变量或文件,也不设置 `APP_SERVER_LOGS`。Codex 使用 Responses,在初始化时声明 `auth._meta.gateway = true`,通过 `gateway` 登录传服务商地址和 `Authorization: Bearer `;没有该登录能力就报错。Claude 使用 Anthropic Messages,通过上游公开的 `_meta.claudeCode.options` 在 `session/new` 与 `session/load` 传入地址和 API Key,仅新会话传入供应商默认模型;由上游适配器与 Claude SDK 管理其内部原生凭据转换。SDK 的环境配置和程序化 settings 两层都显式指定同一路由,并清除旧 Bearer、OAuth、自定义请求头、云平台路由与 `apiKeyHelper`,防止用户或项目设置覆盖所选服务商。缺少凭据时直接报错,二者都不静默回退到账户订阅。 +- **Claude 订阅由用户自己的 Claude Code 负责**:维护者决定 Claude 与 Codex 一样支持「API Key / 官方订阅」两种模式(`AgentAuthentication.claudeSubscription`)。Lithe 接入的是上游 ACP 适配器 `claude-agent-acp`,它运行用户本机安装并已登录的官方 Claude Code CLI。订阅模式下 Host(`claude_subscription.rs`)只做三件事:启动已安装的适配器,并用 `CLAUDE_CODE_EXECUTABLE` 指向检测到的 CLI;不传 `gateway`、不注入 `_meta.claudeCode.options`,因此不覆盖 CLI 自己的登录;从 Lithe 继承的环境里移除 `ANTHROPIC_API_KEY`、`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_BASE_URL`、`ANTHROPIC_MODEL`、`CLAUDE_CODE_OAUTH_TOKEN` 和继承的 `CLAUDE_CODE_EXECUTABLE`,避免它们悄悄改变计费来源。初始化后读取适配器推送的 `_auth/status_update`(适配器在 initialize 时运行 `claude auth status --json`):`kind == "none"` 时以固定文案失败,提示在终端运行 `claude` 并 `/login`;8 秒内没有报告视为未知,继续连接,由会话请求暴露真实认证失败。Lithe 不发起浏览器登录,不读取、复制或刷新任何令牌,也不传上游的 `--hide-claude-auth`。订阅只属于各自的适配器:Claude 订阅不能用于 Codex,反之亦然,解析时直接拒绝;订阅模式不启用模型调用统计代理(没有可观察的推理地址),也不显示 Codex 的额度。Anthropic 对第三方产品提供 claude.ai 登录有授权要求,是否对外宣传此模式由维护者决定。 +- **API Key 按上游公开接口传递**:Lithe 通过 ACP 的标准输入输出(stdio)通道把凭据传给适配器,不进入 Lithe 启动适配器的命令行参数、环境变量或文件,也不设置 `APP_SERVER_LOGS`。Codex 使用 Responses,在初始化时声明 `auth._meta.gateway = true`,通过 `gateway` 登录传服务商地址和 `Authorization: Bearer `;没有该登录能力就报错。Claude 使用 Anthropic Messages,通过上游公开的 `_meta.claudeCode.options` 在 `session/new` 与 `session/load` 传入地址和 API Key,仅新会话传入供应商默认模型;由上游适配器与 Claude SDK 管理其内部原生凭据转换。SDK 的环境配置和程序化 settings 两层都显式指定同一路由,并清除旧 Bearer、OAuth、自定义请求头、云平台路由与 `apiKeyHelper`,防止用户或项目设置覆盖所选服务商。缺少凭据时直接报错,二者都不静默回退到账户订阅;只有用户在供应商管理中显式启用订阅才走账号登录。 - **Claude SDK 子进程的凭据边界**:上面的保证只覆盖 Lithe 启动 ACP 适配器这一层,不覆盖整个子进程树。当前固定版本的 Claude SDK 会把对象形式的 `options.settings` 序列化为 Claude Code CLI 的 `--settings` 命令行参数,因此 `settings.env.ANTHROPIC_API_KEY` 会出现在该 CLI 子进程的参数里;开启 SDK debug 还可能把完整参数写进调试文件。同用户进程和诊断工具可以读取这些参数,不能把这一实现描述为全进程树都不暴露 Key。原 gateway 路径也把带 Key 的自定义请求头放进程序化 settings,本次路由修复没有消除这项上游暴露。后续独立评估把 Key 仅放在 `options.env`、settings 仅保留非敏感路由;采用前必须验证用户和项目 settings 中旧 Key 的覆盖优先级,以及新建、历史恢复和切换服务商都使用所选凭据,不在当前修复中直接删去凭据覆盖层。 - **Codex 订阅是 Agent 绑定方式**:供应商管理增加“Codex 订阅”选项,但不往提交信息共用的 HTTP 服务商注册表写一个假供应商。已有配置缺少认证类型时仍解释为 API Key;订阅绑定不保存 provider ID,启动时不读取该服务商的 Key、URL 或默认模型。只在当前子进程中指定官方 `openai` 路由并清除继承的 API 覆盖,会话配置同时清空 `openai_base_url` 并指定官方 `chatgpt_base_url`,防止本机旧地址继续覆盖订阅路由;额度探测也使用同样的覆盖。保留用户的 `CODEX_HOME`、MCP 和 Skills。本机 Codex 拥有账号存储和刷新,Lithe 不读取或复制其订阅凭据。已有账号直接复用;未登录先提示,用户点击才通过 ACP `chat-gpt` 方法打开官方浏览器登录。等待有五分钟期限,取消停止连接并停留在可重试状态,避免 idle 空态自动重连;认证通知与请求响应先后不作为成功依据,必须等上游确认账号。账号退出或邮箱改变时断开旧连接,不静默改计费来源。订阅错误不附加未经脱敏的上游 stderr,因为 Lithe 并不知道其凭据值。 - **额度复用官方查询能力**:核对 [codex-acp v1.13.1](https://github.com/agentclientprotocol/codex-acp/blob/v1.13.1/src/CodexAcpServer.ts) 的扩展方法后,确认它提供连接级认证状态推送,但没有结构化额度接口;`/status` 只有显示文本,token 统计也不是订阅窗口。因此共享 host 短暂启动已检测到的本机 `codex app-server`,用官方 `account/rateLimits/read` 查询,禁止创建线程或发送 prompt。查询前后读取账号并与 ACP 提供的邮箱比较;没有邮箱时不能确认归属,只显示未知。上游 ACP 没有稳定账户 ID,同邮箱切换组织仍是当前身份校验的限制。每连接最多一分钟一次、不并发,查询二十秒超时;连接取消或查询结束清理整个进程树。将来 ACP 暴露结构化额度后用同一连接替换此探测,不长期维护第二套认证或私有 HTTP API。 @@ -114,9 +115,9 @@ Markdown 表格的表头和正文必须完整展示:长内容在单元格内 复制单个表格时提供 Markdown 和 HTML 两种剪贴板表示,去掉行内样式后必须转义字面 Markdown 语法、反斜杠与 HTML 实体起始符,单元格换行和边缘空白使用数值字符引用保留,避免拆成表外段落或被表格解析器裁掉;例如代码单元格中的 `**literal**` 再次粘贴渲染后仍保留星号,不能变成加粗文字。中文加粗补偿同样不能重新解释转义或实体生成的星号;链接标签保留 Foundation 的解析结果,不能跨两个独立链接配对字面 `**`,否则 `src/**/foo.swift` 一类路径会丢失星号。文件链接只解码一次:`file://` 已有解码后的 `URL.path`,不能再调用百分号解码,也不能从这个路径删除 `#L` 锚点,否则文件名中的 `%20` 或 `#` 会改变;锚点读取自 URL 的独立 fragment。`AgentMarkdownTests` 通过表格导出后重新解析、实体星号和特殊文件名链接验证这些行为,`AgentComposerPasteTests` 挂载真实输入框验证窗口与编辑器归属。上述处理只使用内存,不新增磁盘资源或 bundle 写入。 -转录行抽成按数据比较的 `AgentTranscriptRows`:外层视图会随额度、会话列表、统计等任意发布重新求值,行列表只在消息、已完成轮次、搜索词、推理展开状态变化时才重新分组和比对,滚动时不再被无关发布打断。工具调用参照 Claude 的展示:转录里是一行浅色摘要(按 ACP `kind` 统计“运行了 N 个命令,读取了 N 个文件”),展开为圆角列表,每行一个调用、点开看证据;长列表先显示 8 行。转录内的工具组不再嵌套纵向滚动视图(每组一个 NSScrollView 是滚动卡顿来源),只有活动弹窗保留四行高度的内部视口。订阅登录不经过本地代理,上下文明细和模型调用统计的说明在订阅模式下改为解释原因,而不是提示开启设置。 +转录行抽成按数据比较的 `AgentTranscriptRows`:外层视图会随额度、会话列表、统计等任意发布重新求值,行列表只在消息、已完成轮次、搜索词、推理展开状态变化时才重新分组和比对,滚动时不再被无关发布打断。工具调用在展开的整轮过程区内复用原展示:相邻工具是一行浅色摘要(按 ACP `kind` 统计“运行了 N 个命令,读取了 N 个文件”),展开为圆角列表,每行一个调用、点开看证据;长列表先显示 8 行。转录内的工具组不再嵌套纵向滚动视图(每组一个 NSScrollView 是滚动卡顿来源),只有活动弹窗保留四行高度的内部视口。订阅登录不经过本地代理,上下文明细和模型调用统计的说明在订阅模式下改为解释原因,而不是提示开启设置。 -编辑区内消息内容与输入区共用 80% 居中内容列,输入框的左右 8pt 内边距包含在列内。`AgentEditorColumn` 通过 SwiftUI 的局部布局协议直接从父级尺寸计算列宽和居中位置,不把连续缩放结果写入状态;标题栏和消息滚动容器保持面板整宽,原生滚动条仍在面板右缘。消息滚动区会扣除原生滚动条自身的占位,因此列宽以各自实际可用宽度为准;原生回归从 SwiftUI 内容文档测量列宽与居中位置,不能用可能带额外留白的裁剪视口宽度替代。未配置页也只约束正文与输入框,标题工具栏保留整宽,否则其最小宽度会把窄列撑偏。工具窗口仍使用自身可用宽度,并为右侧导航保留间距。固定 716pt 上限和固定左右 90pt 留白均被否决:用户要求列宽随窗口按比例变化,宽窗口不应留下过多空白。80% 来自用户明确的构图要求,不是 Community 公开 AI 插件常量;代价是超宽窗口的文字行会随面板增长。 +编辑区内消息内容与输入区共用 80% 居中内容列,输入框的左右 8pt 内边距包含在列内。`AgentEditorColumn` 通过 SwiftUI 的局部布局协议直接从父级尺寸计算列宽和居中位置,不把连续缩放结果写入状态;标题栏和消息滚动容器保持面板整宽,原生滚动条仍在面板右缘。消息滚动区会扣除原生滚动条自身的占位,因此列宽以各自实际可用宽度为准;原生回归从 SwiftUI 内容文档测量列宽与居中位置,不能用可能带额外留白的裁剪视口宽度替代。未配置页也只约束正文与输入框,标题工具栏保留整宽,否则其最小宽度会把窄列撑偏。工具窗口仍使用自身可用宽度,正文左侧只保留 12pt 基础边距,额外 28pt 只留在右侧供轮次导航使用;不要把右侧导航留白对称加到左侧,否则窄面板会白白损失正文宽度。固定 716pt 上限和固定左右 90pt 留白均被否决:用户要求列宽随窗口按比例变化,宽窗口不应留下过多空白。80% 来自用户明确的构图要求,不是 Community 公开 AI 插件常量;代价是超宽窗口的文字行会随面板增长。 切换图标使用 Community 915c3c26 的 expui/general/previewVertically.svg 及 dark 变体,16×16;按钮尺寸、选中/hover 与工具提示复用共享 owner。图标作为固定构建资源随包只读发布,不引入运行时下载或缓存。Community 提供通用标签/图标/工具窗口规则,AI 插件的聊天迁移页面未在公开仓库找到,不把当前页面组合与字号布局冒充它的私有源码常量。 @@ -173,9 +174,11 @@ macOS 的审批模式、模型和思考强度在等待、推理、输出、工 除自行附加的项目上下文在用户历史和自动标题中的呈现过滤外,共享 Host 把 ACP 会话更新(`SessionUpdate`)原样转发,以前 Swift 只消费消息、工具、标题、配置和用量,其余直接丢弃。参考 CC GUI 的可折叠思考块、计划面板与 `/` 命令补全后,macOS 现在消费下面四类标准更新;Host、C ABI 和 fixture 形状都不变,只在 fixture 中补样例并由 Rust 测试确认它们能被官方 SDK 原样往返。 -- **思考(`agent_thought_chunk`)**:存为独立的 `thought` 角色消息,不并进 Agent 回复。流式缓冲按角色记录,角色切换先刷新上一段,所以“思考 → 回复 → 工具 → 思考”保持原顺序。只有正在流式输出的最后一段思考默认展开,后续回复或工具到达后自动收起,用户手动展开的状态保留;搜索时临时展开匹配的思考,优先于已有的手动折叠选择;搜索期间不允许再次收起命中文字,清空搜索后恢复手动选择,不把搜索结果写回折叠偏好。手动偏好由 `AgentTranscriptView` 在视图局部内存中按会话 ID 与消息 ID 保存,再以绑定传给思考行;搜索先过滤消息,会移除并重建行,因此不能让行内状态成为偏好的所有者。切换会话保留各自选择,关闭会话或移除实际消息时释放对应偏好,销毁对话视图时一并释放;不写入功能模型、共享 Host、历史文件或安装目录,不影响代码签名和 Sparkle delta。`AgentThoughtTranscriptTests` 直接挂载实际对话容器,通过原生鼠标事件和完整渲染快照覆盖搜索移除/重建、命中时临时展开和会话切换。导出 Markdown 用引用块写出思考,便于和回复区分;历史消息数仍只统计用户与 Agent 回复。活跃思考块的标题显示“思考中…”,底部等待行改为“回复中…”并保留本轮计时,避免重复提示;取消时仍显示“正在停止…”。 +- **思考与执行过程**:`agent_thought_chunk` 继续存为独立的 `thought` 角色,不改写上游记录或导出内容。macOS 与 Windows 在呈现时,把同一轮的思考、工具调用和工具前后的过程说明合成一个“思考过程”入口,默认收起,流式时也不自动展开;收起后只保留这一行入口与后面的回复,不为内部消息预留行或高度。展开后按原顺序显示正文与相邻工具组,内部思考不再各带一层标题;过程区复用消息列表的滚动,不再放进固定高度的推理卡片。 + ACP 没有独立的最终回复与过程说明角色,因此按已观察到的位置分组:最后一条思考或工具以及它之前的模型文字属于过程,它之后的模型文字作为回复直接展示。用户消息与本地已记录的轮次结束各自切断分组,不能跨轮合并;没有思考和工具的直接回答不产生空入口,纯空白文字不产生行。例如“思考 → 先读 README → 读取文件 → 思考 → 最终回答”显示为一个过程入口加最终回答;不要为了合并而删除过程说明或把原始消息改成新的角色。流式模型文字先作为末尾回复呈现,若后续又收到工具或思考,才随新证据移入同一个过程;不猜测文字内容,也不声称上游提供了最终回复标记。 + 搜索命中时临时展开匹配过程与工具组,搜索期间不允许收起命中文字,清空后恢复手动偏好。macOS 偏好继续由 `AgentTranscriptView` 在视图局部内存中按会话与过程首条原始消息 ID 保存,Windows 复用会话对应的 surface store;不能让会被过滤销毁的行拥有该偏好。会话间互不干扰;macOS 在关闭会话或删除原始消息后释放,Windows 沿用工作区 surface store 重置时清理的生命周期。子代理内的独立思考卡片也默认收起,保留其有界内部视口。导出 Markdown 仍以引用块保留思考,历史消息数仍只统计用户与 Agent 回复;不新增持久化、下载或发行包写入,不影响签名和 Sparkle delta。 - **计划(`plan`)**:ACP 规定每次都发完整计划,所以整体替换,不做合并;空列表清除计划,格式错误的更新忽略,最多保留 100 条。计划条状态只认 `pending`、`in_progress`、`completed`,未知状态的条目丢弃而不是猜测。点击底部“任务”打开计划与进度;没有计划时显示“暂无任务”,不把工具调用充当计划。`session/load` 回放会自然恢复计划。 -- **斜杠命令(`available_commands_update`)**:按会话保存上游列表,最多读取前 200 条;同名命令只保留首条定义,保证命令行身份唯一。断连时清空每个会话的旧命令,重连加载后等待新进程重新上报,避免显示已经失效的能力。输入框以 `/` 开头且命令名中还没有空白时弹出过滤结果,前缀匹配排在包含匹配之前;选择后只把 `/name ` 填入草稿,命令文本仍作为普通 prompt 原样发送,由 Agent 自己解释。实测 codex-acp 1.13.1 把 Skill 以 `$name` 形式放进同一个命令列表,而它的命令解析会跳过 `/$name`,Skill 需要以 `$name` 提及。因此名字以 `$` 开头的命令补全为 `$name `;输入 `$` 只列 Skill,Agent 没有上报 Skill 时 `$` 仍是普通文字。Lithe 不扫描 SKILL.md 或命令目录,也不内置命令表,避免和 Agent 的真实能力不一致。方向键和 Tab 导航依赖 macOS 14 的 `onKeyPress`;macOS 13 保留鼠标选择和 Return 补全。Return、Esc、方向键与 Tab 共用视图局部的补全状态,Return 在命令名未完整时只补全、完整时发送;Esc 先关闭补全,再次 Esc 才停止回复,编辑草稿后重新显示匹配。补全高度由输入区内部的局部布局容器按剩余空间限制,先保留默认两行可编辑文字;最小输入区放不下一整行命令时,列表浮在输入框上方,通过锚点偏好在整个对话布局中绘制,让鼠标点击范围不被较小的输入面板限制;沿用工作台悬停提示的局部 overlay 模式,不新增全局位置状态或鼠标监听。输入区的最小高度包含上下栏、可编辑行和外边距,附带文件时再预留附件行;这个值只在附件有无改变时通过视图偏好更新,不跟随拖动逐帧发布。不要让固定高度的命令列表参与分隔条的最小尺寸计算,也不要把逐帧几何尺寸发布到会话模型。 +- **斜杠命令(`available_commands_update`)**:按会话保存上游列表,最多读取前 200 条;同名命令只保留首条定义,保证命令行身份唯一。断连时清空每个会话的旧命令,重连加载后等待新进程重新上报,避免显示已经失效的能力。输入框以 `/` 开头且命令名中还没有空白时弹出过滤结果,前缀匹配排在包含匹配之前;选择后只把 `/name ` 填入草稿,命令文本仍作为普通 prompt 原样发送,由 Agent 自己解释。实测 codex-acp 1.13.1 把 Skill 以 `$name` 形式放进同一个命令列表,而它的命令解析会跳过 `/$name`,Skill 需要以 `$name` 提及。因此名字以 `$` 开头的命令补全为 `$name `;输入 `$` 只列 Skill,Agent 没有上报 Skill 时 `$` 仍是普通文字。对话补全不扫描 SKILL.md 或命令目录,也不内置命令表,避免和 Agent 的真实能力不一致。独立技能设置页可以展示原生发现结果或明确标注的目录清单,但不得据此伪造会话补全和启用状态;管理边界见 [技能管理](../feature/2026-10-20-agent-skill-management.md)。方向键和 Tab 导航依赖 macOS 14 的 `onKeyPress`;macOS 13 保留鼠标选择和 Return 补全。Return、Esc、方向键与 Tab 共用视图局部的补全状态,Return 在命令名未完整时只补全、完整时发送;Esc 先关闭补全,再次 Esc 才停止回复,编辑草稿后重新显示匹配。补全高度由输入区内部的局部布局容器按剩余空间限制,先保留默认两行可编辑文字;最小输入区放不下一整行命令时,列表浮在输入框上方,通过锚点偏好在整个对话布局中绘制,让鼠标点击范围不被较小的输入面板限制;沿用工作台悬停提示的局部 overlay 模式,不新增全局位置状态或鼠标监听。输入区的最小高度包含上下栏、可编辑行和外边距,附带文件时再预留附件行;这个值只在附件有无改变时通过视图偏好更新,不跟随拖动逐帧发布。不要让固定高度的命令列表参与分隔条的最小尺寸计算,也不要把逐帧几何尺寸发布到会话模型。 - **模式通知(`current_mode_update`)**:Agent 自己切换模式(例如退出计划模式)时,只更新 `category == "mode"` 且包含该值的配置选项,不向上游回发设置请求;选项中不存在的模式 ID 忽略,不显示原始 ID。 - **即时通知(`notice`)**:共享 Host 启用固定 SDK 的 `unstable_session_notices` 能力,并声明 `clientCapabilities.session.notices: {}`,让适配器通过独立事件发送警告。macOS 每个会话只保存最新一条通知,用现有 `LitheBanner` 展示和关闭;通知不计入回复、历史导出或轮次统计,下一条 prompt 成功提交及断连时清除。上游通知字段限制为标题 256 字符、详情 4096 字符,未知严重程度和空标题忽略。 @@ -424,7 +427,7 @@ Windows 连接桥已接入共享 Host,面板 UI 也已落地(活动栏入口 - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentConversationPresentationTests|EditorTabOrderFeatureModelTests.*editorChatCenters'`:在生产输入组件中改变草稿,验证默认两行、四行自然增长、长草稿封顶且末尾可滚至、附件增高及删除/清空后收缩;同一原生窗口从 1100pt 缩至 320pt 后按新宽度换行,工具窗口和编辑页分别覆盖。布局等待只驱动原生事务并观察最终尺寸,每个阶段最多两秒,窗口和内容通过 `defer` 释放。列宽回归在 320/900/1400pt 下检查真实底栏位置,要求编辑页为 80% 居中列、工具窗口使用可用宽度。 -- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentTranscriptNavigationTests|AgentTranscriptScrollTests'`:真实原生会话视图在独立子进程中驱动四轮溢出历史、工具更新、编辑/执行权限、最终统计与窄宽窗口。宽编辑页另检查滚动容器到达面板右边缘,触发实际轮次导航按钮后首条消息进入视口,阅读列保持可用宽度的 80%;原生视口测试覆盖两轮同时可见、行卸载和已经排队的底部跟随取消。验证主动浏览不被拉走,权限卡片不遮住最新回复,回到底部后继续跟随;子进程有八秒期限,布局死循环也能由父进程结束并报告最后阶段。回放使用独立偏好域并隐藏详情栏,避免用户保存的布局改变消息面板边缘断言;辅助功能测试只启用当前进程的界面树并恢复原值。直接启动当前测试运行器并继承其环境,不能经 `/usr/bin/env` 中转,否则系统可能清除 XCTest 所需的动态库路径。最终窗口宽度变更后的底部检查最多等待两秒的原生布局事务,仍要求差距小于2pt,不强制滚动或放宽精度。完整工作区双窗口回放额外覆盖原卡点与连续面板宽度变化,不把合成事件回放等同于真实模型端到端通过。macOS 13 的源码兼容性与最低部署目标保留,旧系统运行仍须在对应系统复验。 +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentTranscriptNavigationTests|AgentTranscriptScrollTests'`:真实原生会话视图在独立子进程中驱动四轮溢出历史、工具更新、编辑/执行权限、最终统计与窄宽窗口。宽编辑页另检查滚动容器到达面板右边缘,触发实际轮次导航按钮后首条消息进入视口,阅读列保持可用宽度的 80%;原生视口测试覆盖两轮同时可见、行卸载和已经排队的底部跟随取消。工具窗口在深浅主题及 280/350/486/620pt 连续缩放中检查实际消息行边界,要求左侧 12pt、右侧 40pt,让正文用上左侧回收的空间并保留导航余量。验证主动浏览不被拉走,权限卡片不遮住最新回复,回到底部后继续跟随;子进程有八秒期限,布局死循环也能由父进程结束并报告最后阶段。回放使用独立偏好域并隐藏详情栏,避免用户保存的布局改变消息面板边缘断言;辅助功能测试只启用当前进程的界面树并恢复原值。直接启动当前测试运行器并继承其环境,不能经 `/usr/bin/env` 中转,否则系统可能清除 XCTest 所需的动态库路径。最终窗口宽度变更后的底部检查最多等待两秒的原生布局事务,仍要求差距小于2pt,不强制滚动或放宽精度。完整工作区双窗口回放额外覆盖原卡点与连续面板宽度变化,不把合成事件回放等同于真实模型端到端通过。macOS 13 的源码兼容性与最低部署目标保留,旧系统运行仍须在对应系统复验。 - `node --test scripts/test-codex-config-relay.mjs`:用受控协议流及进程替身验证创建/恢复/分叉配置、不受点号覆盖影响、其他供应商和工具消息照传、UTF-8 拆包、原生轮次与错误信息、格式损坏的有界停止以及 Windows npm cmd 的启动分支。同时验证恢复票据、真实原生快照、累计量不冒充占用、未知/失败保留原恢复路径、订阅重试不变。此测试进入两端 CI;模拟 Windows 分支不等于 Windows 原生运行验收。 @@ -443,7 +446,7 @@ Windows 连接桥已接入共享 Host,面板 UI 也已落地(活动栏入口 - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentActivityTests|AgentEditRestorerTests|AgentActivityPresentationTests'`:检查合并、上游统计缺失、部分更新、确认后的新基线、会话隔离、部分失败及取消;临时工作区验证逆序恢复、编码、唯一片段、歧义和重叠匹配、删除冲突及路径越界。原生鼠标验证三项入口、打开文件和保留回调;任务回归覆盖没有计划但已有工具调用、计划到达后显示 1/3、空计划及无计划恢复空态,普通工具不计为子代理,原生子代理展开实际输出且状态刷新不关闭面板,并保留深浅主题及中英文环境的渲染截图;从弹出面板打开真实差异 sheet 与回撤确认,确认后只提交捕获的文件选择;同步编辑器最后一次输入后拒绝脏缓冲区,测试窗口与临时目录均清理。生产包另执行 `./scripts/verify-runtime-bundle-immutability.sh`。 -- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter AgentThoughtTranscriptTests`:在生产对话容器中通过鼠标展开/收起思考,搜索隐藏行后进入空会话使列表实际销毁,再返回并清空搜索,确认手动选择保留;同时验证命中时保持文字可见、清空后恢复折叠、会话切换独立和关闭后重新加载的默认状态。渲染只等待可观察的原生画面,每次等待最多两秒;比较同一宿主的完整画面,不依赖系统字体快照基线。窗口和功能模型在成功、失败路径都清理。 +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentThoughtTranscriptTests|AgentConversationPresentationTests'`:验证整轮思考、过程说明和工具仅有一个默认折叠入口,最终回复保留在外,空白片段不占行;在生产对话容器中通过鼠标展开/收起思考,搜索隐藏行后进入空会话使列表实际销毁,再返回并清空搜索,确认手动选择保留;同时验证命中时保持文字可见、清空后恢复折叠、会话切换独立和关闭后重新加载的默认状态。渲染只等待可观察的原生画面,每次等待最多两秒;比较同一宿主的完整画面,不依赖系统字体快照基线。窗口和功能模型在成功、失败路径都清理。 - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter AgentConversationSelectionTests`:原生 SwiftUI 宿主不替换根视图,直接改变模块选择,验证 Agent、模型与连接立即同步;配置确认前保留旧值、确认后刷新,来回切换保持各自配置,删除当前 Agent 时正确回退,关闭全部测试窗口和连接。另对 Claude/Codex 显式驱动最后一个标签关闭→唯一新建请求→过期响应→新会话确认→发送,确认连接不变、准备期间不闪现模型、确认后底栏恢复且消息只交给新会话。显式控制切换→连接→ready→创建确认及历史加载的事件顺序,比较原生工具栏,验证准备期间改变本地默认模型不影响显示、过期响应不能解除等待、已确认连接复用、失败和无配置能力不会一直加载。2026-10-01 修复版真实窗口已验证 Codex 初始化加载提示到 Astra/审批控件、Astra→Luna→Astra 模型切换和重新展开的勾选、模型名称搜索与空结果、Claude/Codex 来回切换保留配置,Codex 最小消息收到预期回复。关闭测试项目后确认无 Agent 子进程,990 个 bundle 文件哈希不变;Windows 对话 UI 与运行验收仍待完成。 - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentConversationFeatureModelTests|AgentConversationPresentationTests'`:使用共享重试 fixture 验证无标题通知、静默等待不猜状态、输出恢复、工具/权限/取消优先级、后台会话隔离、迟到通知和失败/断连清理;注入可推进的单调时钟验证排队、后台会话、取消确认、失败和断连;共享 fixture 校验用量、零值与缺失值,统计行的分组边界与历史回放不伪造。真实 Agent 的 token 口径和连续计时仍需人工验收,不能以合成数据截图代替供应商运行验证。 @@ -451,7 +454,7 @@ Windows 连接桥已接入共享 Host,面板 UI 也已落地(活动栏入口 - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter AgentBrandIconResourceTests`:临时安装包布局覆盖 Codex/Claude 图标加载、资源包和图标缺失时安全回退、缓存隔离,比较读取前后的文件清单与内容,确认不改写发行资源;原生输入框 Menu 的像素验证同时覆盖 Claude/Codex 和深浅主题,确保橙色/灰色标志实际可见,染色不改变共用模板。 - 输入框的图标裁图与边距回归按辅助功能标识定位真实控件,不依赖按钮顺序或固定横向偏移;添加附件等操作后仍检查同一产品边界。原生辅助功能树只能在布局后读取,因此使用两秒单调时钟期限并在退出时恢复增强辅助功能设置,不用睡眠等待。macOS unit CI 启用已有截图开关,将受控夹具的输入区、Effort 和紧凑摘要深浅主题及窄宽布局随测试报告上传;截图属于当前 revision 的验证证据,不能复用为其他工作树的构建输入,也不能代替真实供应商工作流验收。 - 订阅新增测试覆盖旧配置兼容、显式登录、已有账号、认证通知顺序、登录取消和超时、账号变更、额度真实窗口/缺失值/多 bucket、仅查询不发送 prompt,以及临时失败保留旧值。Linux 已运行 Agent Host 的逐测试计时套件;macOS Swift 编译、真实账号登录及深浅主题/窄宽布局仍须在目标环境验证,不将代码存在等同于运行验证。 -- 真实账号验收:先用 API Key 对话,空闲切换 Codex 订阅,确认请求使用本机账号;未登录时确认打开面板不会启动浏览器,点击登录与取消正确;上下文右侧额度每分钟更新,断网后灰显,换账号后旧值清除,关闭项目不残留探测进程。Claude 不出现订阅入口。Windows 已有共享协议、连接桥与面板 UI,但订阅登录、供应商切换、额度展示、CLI 升级与配置导入的真机验收仍未执行,真实窗口里的系统与项目树拖入(含拖入高亮、文件夹引用与中英词条)及 `@` 选择器也只做过合成事件回归,连接桥还需要在 Windows 上做一次真实适配器的端到端验收。 +- 真实账号验收:先用 API Key 对话,空闲切换 Codex 订阅,确认请求使用本机账号;未登录时确认打开面板不会启动浏览器,点击登录与取消正确;上下文右侧额度每分钟更新,断网后灰显,换账号后旧值清除,关闭项目不残留探测进程。Claude 订阅:本机 Claude Code 已登录时直接对话,退出登录后面板提示用 /login 登录且不打开浏览器。Windows 已有共享协议、连接桥与面板 UI,但订阅登录、供应商切换、额度展示、CLI 升级与配置导入的真机验收仍未执行,真实窗口里的系统与项目树拖入(含拖入高亮、文件夹引用与中英词条)及 `@` 选择器也只做过合成事件回归,连接桥还需要在 Windows 上做一次真实适配器的端到端验收。 - `cargo test -p lithe-agent-host --manifest-path rust/Cargo.toml` - `cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent::`(Windows 连接桥:非法启动配置、空或重复连接 ID、启动失败上报、未知连接与非法命令、按窗口释放连接、跨窗口 send/close 被拒且连接仍在、关闭计数在移出连接时即生效、出口等待在有关闭时挂起并在最后一个关闭结束时返回、超时后放弃等待、退出 drain 后拒绝新连接;不需要真实 Agent 或网络。进程树回收用 `fake_acp_adapter` 假适配器覆盖两条路径——关闭连接、销毁窗口——断言外层进程和继承 stdout 的孙进程都已退出) diff --git a/.agents/notes/implemented/architecture/2026-10-12-tool-window-docking.md b/.agents/notes/implemented/architecture/2026-10-12-tool-window-docking.md index 121eab8561..1ce93c11a3 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-tool-window-docking.md +++ b/.agents/notes/implemented/architecture/2026-10-12-tool-window-docking.md @@ -73,7 +73,7 @@ macOS 的工具窗口位置由工作台维护,功能模块继续维护自己 ## 后果 收益是现有内容可以在不同区域显示,位置、顺序与尺寸可以重启恢复。 -代价是新增内置工具窗口必须提供稳定标识和默认布局;Windows 尚未同步此次移动能力,功能矩阵明确记录差距。 +代价是新增内置工具窗口必须提供稳定标识和默认布局;Windows 目前只支持终端底部与右侧互移,完整分组和排序的差距由功能矩阵记录。 移动可能重建原生呈现容器,但功能状态仍归原模块所有;终端与编辑器等原生资源必须继续遵守既有挂载生命周期。 ## 验证 @@ -94,6 +94,41 @@ macOS 的工具窗口位置由工作台维护,功能模块继续维护自己 - `macos/Sources/Lithe/Services/Workbench/WorkbenchLayoutStore.swift` - `macos/Sources/Lithe/Views/Workbench/` - `macos/Tests/LitheTests/ToolWindowDockingTests.swift` +- `windows/tauri/src/features/layout/components/terminal-dock-layout.tsx` +- `windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx` + +## Windows 终端入口拖动与左、右、底部停靠 + +Windows 可直接拖动活动栏的终端图标,停靠到左侧、右侧或底部;入口随位置移动到对应侧栏, +底部使用左侧入口。终端隐藏或其他工具活动时也能开始拖动,只有有效释放才打开终端, +普通点击或键盘激活仍切换显隐。两侧活动栏与内容布局处于同一拖动上下文,侧栏本身也是落点, +因此在右下角释放会选择右侧,而不会误判为底部。终端工具栏的移动按钮保留为补充入口。 +活动栏有效释放后先提交面板显隐和活动工具,React 完成 DOM 更新后才请求创建或聚焦终端; +不能在同一个 pointerup 处理器里对仍隐藏的 xterm 同步 focus。待激活请求绑定当前工作区, +提交期间切换项目或工具会丢弃请求,不使用固定延时,也不重新挂载终端。 +Esc、窗口失焦、窗口大小变化、无效区域释放、 +切换项目及卸载都会取消拖动,不保存位置。落点按拖动开始时的工作区和面板实际边界计算, +底部角落优先保留来源边,避免底部水平拖动突然跳到右侧。 + +布局使用 CSS Grid(网格布局)的区域名移动同一份 BottomPane,不能把它分别放到两个条件分支。 +例如正确做法是只把 panel 区域从底行改到左列或右列;不要销毁 TerminalContainer 再在侧边重建它, +否则会重复注册创建终端的监听器并丢失局部分屏状态。改变底部全宽/编辑区宽度、停靠、 +隐藏和再次展开都保留容器;PTY(终端进程连接)和 xterm 输出仍由原来的 TerminalHost 管理。 +原有终端标签页的拖动继续用于排序或送入编辑器,与整个面板的移动按钮分开。 + +位置、侧边宽度和底部高度使用现有 Windows 设置存储保存,作用于当前应用设置, +并非 macOS 的按工作区六组布局。旧设置默认底部、400px 宽、320px 高,未知位置安全回到底部; +尺寸在加载和使用时分别校验,并按剩余工作区限制。持续调整复用 document-resize-session, +每帧只改局部 DOM,结束才保存;切换位置/项目及卸载清理监听器。左侧分隔条放在面板右边缘, +向右拖动增加宽度;保持同一 DOM 子节点顺序,通过 CSS 排序移动分隔条,防止终端因换位而重建。 +其他底部工具仍使用原来的单活动工具语义,不支持和终端同时展开,也不随终端移到侧边。 +本次没有新增运行时资源目录或原生协议。 + +验证:Windows Frontend 计时 harness 运行 +`windows/tauri/src/features/layout/components/bottom-pane/bottom-pane-terminal-lifecycle.test.tsx`, +覆盖真实左右活动栏入口与 BottomPane 的三边停靠、预览先于保存、取消/迟到释放、宽度边界、 +全宽切换、隐藏再打开、输入 DOM 和请求监听器保留;设置归一化测试覆盖旧数据和损坏数据恢复。 +这些组件测试不等同于 Windows WebView2 实机验收,矩阵保持 pending。 拖动回归补充:`ToolWindowDockingTests.horizontalDragUsesTheBottomPanelHeightAndActualSplitBounds` 覆盖底部平移、编辑区排除、遮罩保持实际高度及不等宽双组; diff --git a/.agents/notes/implemented/architecture/2026-10-12-workbench-ui-contributions.md b/.agents/notes/implemented/architecture/2026-10-12-workbench-ui-contributions.md index 4254e6f9d7..0c627ad9ea 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-workbench-ui-contributions.md +++ b/.agents/notes/implemented/architecture/2026-10-12-workbench-ui-contributions.md @@ -28,7 +28,7 @@ Run、Debug 和 Stop 的主工具栏入口也使用宿主贡献 ID,与插件 多个窗口属于同一模块时,运行时合并在途构造。取消一个 UI 请求只撤回这个窗口的显示意图,另一个窗口仍可等待同一次模块激活。禁用和关闭先阻止新激活,再取消并等待已有激活清理,完成后才释放资源;晚到的激活不能重新发布能力和贡献。宿主发出 `module.activation-joined` 事件使这种等待可观察,正常取消不显示错误通知。这是宿主生命周期行为,不允许插件在清单里运行状态脚本。 -声明式 UI 目前只支持宿主渲染的静态文本窗口和打开本插件窗口的命令。窗口和命令可以声明宿主图标的稳定语义 ID,或包内 SVG/PNG 及可选亮暗变体。宿主统一图标槽位、着色和交互状态;命令面板选中行的模板图标使用宿主选中文字颜色,键盘移动选择时品牌图标仍保持对应亮暗资产的原色;品牌模式只保留资产原色,不改变外层按钮规范;宿主图标声明 original 模式时同样使用原生资源原色,缺少打包资源的调试运行仍使用宿主占位图标。工作台中的窗口与工具栏管理子菜单也复用已注册图标,通过共享下拉组件呈现,不单独定义菜单样式。内部旧 `icon` 字符串仍属于原生模块元数据,声明式字段通过有类型的 `uiIcon` 接入,不能把它解释成 SF Symbol 或任意资源路径。这个阶段范围保证第一条接入路径可以完整验证。API v2 的窗口必填文本、命令必关联窗口,以及无语言/无 UI 的拒绝规则继续保持兼容;这些规则不应复制到下一版公共接口。后续版本化原生组件、独立动作和领域 Provider 的设计见[公共扩展契约与原生 Provider 接入方向](../../proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md),当前尚未实现。 +API level 2 的声明式 UI 保持宿主渲染的静态文本窗口和打开本插件窗口的命令。ui 3 已新增树/列表、面板工具栏和右键菜单,范围与限制见[声明式原生插件树与动作](../feature/2026-10-21-declarative-native-plugin-ui.md)。窗口和命令可以声明宿主图标的稳定语义 ID,或包内 SVG/PNG 及可选亮暗变体。宿主统一图标槽位、着色和交互状态;命令面板选中行的模板图标使用宿主选中文字颜色,键盘移动选择时品牌图标仍保持对应亮暗资产的原色;品牌模式只保留资产原色,不改变外层按钮规范;宿主图标声明 original 模式时同样使用原生资源原色,缺少打包资源的调试运行仍使用宿主占位图标。工作台中的窗口与工具栏管理子菜单也复用已注册图标,通过共享下拉组件呈现,不单独定义菜单样式。内部旧 `icon` 字符串仍属于原生模块元数据,声明式字段通过有类型的 `uiIcon` 接入,不能把它解释成 SF Symbol 或任意资源路径。这个阶段范围保证第一条接入路径可以完整验证。API v2 的窗口必填文本、命令必关联窗口,以及无语言/无 UI 的拒绝规则继续保持兼容;这些规则不应复制到下一版公共接口。后续版本化原生组件、独立动作和领域 Provider 的设计见[公共扩展契约与原生 Provider 接入方向](../../proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md),当前尚未实现。 Agent 的配置入口由同一个目录补齐,移动到任何已有停靠区域后仍可找到;禁用 Agent 只停止模块资源,保留宿主配置说明。Services 的显示动作复用数据库按需激活入口,激活成功后只展开 Services,不额外打开数据库侧栏。动作可用性与内容可渲染性分别判断:未激活的 Services 仍能请求 Show,实际停靠内容必须同时满足 renderer 的可见/选中条件和动作可用性;旧布局中的 Maven 在非 Maven 项目不占据停靠区域。 diff --git a/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md b/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md index 99beece36e..657e05ee2b 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md +++ b/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md @@ -126,6 +126,15 @@ dev.lithe.plugin.java-support/ - 不要为了恢复某个语言的调试或测试,把能力目录的合并规则改成“插件没声明就保留宿主原有能力”,也不要在目录或注册表里按 `java` 放行。前者让 PHP、Go 的插件停用后宿主重新运行内置工具,后者是按名字的特判。 - 不要往插件包里写任何东西。可写状态用 `${stateDir}`、`${workspaceStateDir}`,或像 JDT 那样由集成放进宿主缓存。 +### Java 调用补全留空实参 + +构造器和方法补全由共享 JDT 适配器设置 `java.completion.guessMethodArguments: "off"`,Windows 和 macOS 一致。输入 `new No` 后接受 Node 候选得到 `Node()`,括号内保留光标,由用户填写实参;方法候选仍可展示签名,自动导入和其他语言的 snippet 不受影响。初始化、配置通知及按 section 拉取配置必须返回相同值,防止 JDT LS 后续恢复参数占位。 + +来源是当前固定的 [JDT LS v1.61.0](https://github.com/eclipse-jdtls/eclipse.jdt.ls/tree/v1.61.0):`Preferences.java` 默认选择 `INSERT_PARAMETER_NAMES`,`CompletionProposalReplacementProvider.appendGuessingCompletion` 在 `OFF` 时只插入光标。正确做法是使用该上游设置,同时保留插件声明的其他 completion 项;不要在 Monaco 中用正则删除参数或禁用通用 snippet。后两种方案会误伤方法体模板、其他语言和补全解析后的自动导入。代价是 Java 默认不再提供可逐个替换的参数名占位,JDT LS 也会将构造器重载候选合并为类型候选;这次不增加新的设置界面。 + +`java_completion_leaves_arguments_empty_across_configuration_channels` 和 `shared/fixtures/lsp/java-completion-v1.json` 验证三个配置入口及插件其他 completion 项的保留;实机验收仍需分别确认两端 Tab 接受候选后的位置和自动导入。 +运行会话保留初始化时插件声明的 completion 对象,配置通知和 `java`、`java.completion` 及其叶节点请求均从同一份对象应用 `guessMethodArguments=off`。例如插件声明的 `favoriteStaticMembers`、`filteredTypes` 不得在初始化后被通知或拉取结果清空;引擎测试同时检查真实会话接线,不能只用默认设置证明三个入口一致。 + ## 考虑过的备选方案 ### 只把文件搬进插件包,宿主继续扫描目录 diff --git a/.agents/notes/implemented/architecture/2026-10-17-build-system-plugin-contract.md b/.agents/notes/implemented/architecture/2026-10-17-build-system-plugin-contract.md index d46bac82bb..8262826890 100644 --- a/.agents/notes/implemented/architecture/2026-10-17-build-system-plugin-contract.md +++ b/.agents/notes/implemented/architecture/2026-10-17-build-system-plugin-contract.md @@ -35,7 +35,7 @@ Rust Core 负责校验声明、识别工程、展开每一条命令行,并把 没有声明的能力不报错,也不会出现在界面上。例如 Cargo fixture 没有声明依赖树,Build 工具窗口就不显示依赖区。 -字段全集只写在 `shared/contracts/plugins/v1/README.md` 的 “API level 3: build systems” 一节和 `plugin-manifest.schema.json` 里,本 Note 不重复。四个 fixture(`cargo-build`、`go-build`、`gradle-build`、`maven-build`)是完整示例,`gradle-provider`、`mix-provider` 分别示范 Provider 的两种写法。 +字段全集只写在 `shared/contracts/plugins/v1/README.md` 的 “API level 3: build systems” 一节和 `plugin-manifest.schema.json` 里,本 Note 不重复。四个 fixture 各示范一种模型来源,按能力命名而不是按真实插件命名:`build-static-file`(读项目文件,内容用 Cargo)、`build-command-stdout`(解码命令输出,内容用 Go)、`build-command-output-file`(解码命令写出的文件,内容用 Gradle)、`build-host-integration`(交给宿主集成,内容用 Maven)。`build-provider`、`build-provider-language-server` 分别示范 Provider 的两种写法。 ### 2. Core 与宿主的分工 @@ -333,11 +333,11 @@ macOS 接入规则(2026-10-18): ## 验证 - `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib build_systems`:四个 fixture 通过校验;`build-plans.json` 的全部标准答案;在临时工作区里验证 Cargo 运行入口与内置探测器逐字一致、嵌套、优先级与拒绝、Maven reactor 识别、Gradle 规范格式输出文件、JUnit 报告、同步指纹、Maven 任务参数与内置实现一致,以及互斥条件下允许重复引用选项。 -- `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib build_systems` 还覆盖 Provider:`build-plans.json` 里 `gradle-provider`、`mix-provider` 的请求计划和 `result` 解码;`provider_processes_are_install_review_programs`(自有进程和共享写法在安装审查中的条目);`launch_plans_name_exactly_one_process`;`provider_models_are_not_read_during_detection`。 +- `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib build_systems` 还覆盖 Provider:`build-plans.json` 里 `build-provider`、`build-provider-language-server` 的请求计划和 `result` 解码;`provider_processes_are_install_review_programs`(自有进程和共享写法在安装审查中的条目);`launch_plans_name_exactly_one_process`;`provider_models_are_not_read_during_detection`。 - `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib declarative_plugins`:`invalid-manifests.json` 里的非法清单,包括社区插件声明 `hostIntegration`、未声明权限、路径越界、未保护的模板变量和选项重复放置,以及 Provider 的十种错误写法(没有来源使用、混写、缺少标志、缺少权限、入口越界、不支持的运行时、未声明的设置等),和裁剪后不再接受的九种写法(静态 `yaml`、命令 `json`、任务规则、顶层目标、`parent`、映射标签、`parentDirectory`、依赖 `children`、`layout.generated`);`command-plans.json` 里按 `buildSystemID` 选中的启动计划。 - `declarative_plugins::build_providers_require_their_own_host_capability` 与 `marketplace_plugins::build_provider_versions_require_host_support_before_download` 验证 Provider 能力与声明式构建分开,安装审查的共享 fixture 同时覆盖自有进程和共享写法。 - `cargo test -p lithe-core --lib lsp`:`lsp/interface/build_provider.rs` 验证只有宿主提供过的版本算协商成功、版本 1 只接受两个方法;`engine.rs` 的测试验证协商后的会话写出 `lithe/build/*` 并返回结果,未协商或方法不在版本里时直接失败、不写入进程。 -- `./scripts/test-macos.sh --filter 'BuildSystemServiceTests'` 的 Provider 用例:`providerModelSendsThePlannedRequestAndDecodesItsReply`、`providerDependenciesDecodeTheProvidersReply` 验证请求原样发给按插件、构建系统和工作区确定的目标,回复作为 `providerResult` 交给 Core 解码且不启动命令;`workspaceSwitchAndDisabledPluginStopTheirProviders` 验证停用插件和切换工作区停止进程、插件列表不变时保留;`providerModelFailsWithoutAProviderHost`、`failedProviderRequestReportsItsMessage` 验证失败路径。`providerModelLoadCarriesTheResolvedToolWithoutRefresh`、`providerReloadsAskForARefresh`、`providerLoadsWithoutTheToolWhenResolutionFails`、`commandModelLoadsDoNotResolveAProviderTool` 验证加载原因和工具路径:首次加载不带 `refresh`、重新加载的模型和依赖都带,工具只在 Provider 请求时解析,解析失败时请求照常发出且 Build Output 写明原因;`MacBuildProviderSessionsTests.providerHintsReachTheProcessUnchanged` 验证这两个字段原样到达进程,`PluginWebBridgeTests.pageRefreshReloadsTheModel` 验证网页的 `build.refresh` 算重新加载。`build-plans.json` 的 `gradle provider … refresh …`、`… tool must be an absolute path`、`gradle model command ignores …` 用例覆盖 Core 的写入、校验和命令来源忽略。`PluginMarketplaceServiceTests.buildProvidersAreAcceptedByTheMacOSHost` 验证 `gradle-provider`、`mix-provider` 能通过 macOS 的安装审查。`MacBuildProviderSessionsTests` 用真实的 `MacBuildProviderSessions` 和脚本化 Core 验证:重复请求复用进程,按插件停止后下一次请求重新启动;设置变化后按新参数重启;包内入口不可执行时不启动进程。`PluginPackageStoreTests.unusablePackagedBuildProviderIsRefused`、`buildProviderRuntimeEntryMustBeInThePackage` 验证安装时校验 Provider 的包内入口:缺失、不可执行、经过符号链接都被拒绝,运行时入口缺失也被拒绝。 +- `./scripts/test-macos.sh --filter 'BuildSystemServiceTests'` 的 Provider 用例:`providerModelSendsThePlannedRequestAndDecodesItsReply`、`providerDependenciesDecodeTheProvidersReply` 验证请求原样发给按插件、构建系统和工作区确定的目标,回复作为 `providerResult` 交给 Core 解码且不启动命令;`workspaceSwitchAndDisabledPluginStopTheirProviders` 验证停用插件和切换工作区停止进程、插件列表不变时保留;`providerModelFailsWithoutAProviderHost`、`failedProviderRequestReportsItsMessage` 验证失败路径。`providerModelLoadCarriesTheResolvedToolWithoutRefresh`、`providerReloadsAskForARefresh`、`providerLoadsWithoutTheToolWhenResolutionFails`、`commandModelLoadsDoNotResolveAProviderTool` 验证加载原因和工具路径:首次加载不带 `refresh`、重新加载的模型和依赖都带,工具只在 Provider 请求时解析,解析失败时请求照常发出且 Build Output 写明原因;`MacBuildProviderSessionsTests.providerHintsReachTheProcessUnchanged` 验证这两个字段原样到达进程,`PluginWebBridgeTests.pageRefreshReloadsTheModel` 验证网页的 `build.refresh` 算重新加载。`build-plans.json` 的 `gradle provider … refresh …`、`… tool must be an absolute path`、`gradle model command ignores …` 用例覆盖 Core 的写入、校验和命令来源忽略。`PluginMarketplaceServiceTests.buildProvidersAreAcceptedByTheMacOSHost` 验证 `build-provider`、`build-provider-language-server` 能通过 macOS 的安装审查。`MacBuildProviderSessionsTests` 用真实的 `MacBuildProviderSessions` 和脚本化 Core 验证:重复请求复用进程,按插件停止后下一次请求重新启动;设置变化后按新参数重启;包内入口不可执行时不启动进程。`PluginPackageStoreTests.unusablePackagedBuildProviderIsRefused`、`buildProviderRuntimeEntryMustBeInThePackage` 验证安装时校验 Provider 的包内入口:缺失、不可执行、经过符号链接都被拒绝,运行时入口缺失也被拒绝。 - `./scripts/verify-shared-contracts.sh`:schema 与 fixture 一致;`build-plans.json` 的用例名唯一、操作合法、拒绝码都有文档、不含机器路径;`command-plans.json` 的每个用例只给出 `languageID`、`buildSystemID` 之一。 - `./scripts/verify-rust-core-comments.sh`:`build/` 模块的注释规范。 - `./scripts/test-macos.sh --filter 'BuildSystemToolWindowTests'`:选项根选择保留各实例的候选和生效值,原生菜单动作覆盖自定义单选替换、多选追加及取消已选自定义值;原生 `NSTextField` 提交验证 text/file/directory 清空后提交 `""` 而非重置。设置 `LITHE_BUILD_CAPTURE_DIR` 后可导出选项行的明暗截图。`BuildSystemServiceTests.instanceOptionEditsPreserveSiblingOnlySelections` 验证编辑一个实例不会丢掉另一个实例独有的选择;`languageImportPublishesTheSameEffectiveOptionsAsBuildPlansAndClearsDisabledPlugins` 验证插件配置与构建计划使用同一份生效值、修改后重新发布、停用及重置清理;`RustBuildSystemOperationsTests.languageImportMapsPluginOptionIDsWithoutBuildSystemNameAssumptions` 验证真实 Core 的导入映射和 Maven 可选执行字段解码。本地 Maven、Gradle fixture 副本经真实 Core 校验后读出 `icon`、`presentation`、选项图标和 `category`;模型任务自带的 `category`;每个构建系统推导出的窗口和命令(ID、标题、停靠、命令与窗口分开的动作),以及注册表校验通过。补充回归覆盖窗口所属模块激活、命令 pending 状态、独立窗口停靠与显隐恢复、旧 Maven 贡献及命令移除、参数任务根工程与目标作用范围。临时插件包中的窗口与选项明暗 SVG 加载后,目录清单和每个文件的字节必须完全不变。 diff --git a/.agents/notes/implemented/architecture/2026-10-17-plugin-capability-versions.md b/.agents/notes/implemented/architecture/2026-10-17-plugin-capability-versions.md index 874a938e90..06dc242628 100644 --- a/.agents/notes/implemented/architecture/2026-10-17-plugin-capability-versions.md +++ b/.agents/notes/implemented/architecture/2026-10-17-plugin-capability-versions.md @@ -32,6 +32,7 @@ | `ui` | `ui`;版本 `2` 增加工具窗口里的网页内容 | | `buildSystems` | 声明式构建识别、模型和命令计划 | | `buildProviders` | 任一 `buildSystems[].provider`,包括自有进程和共享语言服务器两种写法 | +| `vcsProviders` | `vcsProviders[]`:插件进程实现的版本控制系统(工作副本标记、元数据路径、命令行工具) | 规则: @@ -46,6 +47,8 @@ 为什么 `serverProcess` 单独作为一个能力:启动字段由语言服务器和构建系统的自有 Provider 共用。以后新增一种启动方式时,只升级 `serverProcess`,`languageServers` 和 `buildSystems` 的其它字段不受牵连。共享写法的 Provider 复用插件里的语言服务器,所以不单独计入 `serverProcess`。两种 Provider 都必须声明 `buildProviders: 1`:能启动语言服务器或执行构建命令,并不代表能完成构建协议协商、请求和生命周期管理。纯模板构建插件不声明 `buildProviders`,否则会因多列能力被拒绝。 +`vcsProviders` 同样依赖 `serverProcess`:每个 VCS Provider 都是插件自己的进程,启动字段按 `serverProcess` 校验,所以清单要同时写 `vcsProviders: 1` 和 `serverProcess: 1`。只写前者,Core 会报少列了 `serverProcess`。工作副本标记和元数据路径留在 `vcsProviders` 里,因为宿主在进程启动前就要靠它们找到工作副本根,见[统一 VCS 模型提案](../../proposed/architecture/2026-10-22-unified-vcs-provider-model.md)。 + ### 2. 宿主自己声明能运行什么 Core 能校验一个能力,不代表宿主能运行它。宿主在 `plugins.declarative.validate`、`plugins.marketplace.validateIndex` 和 `plugins.marketplace.installReview` 里发送 `hostCapabilities: {"<能力>": [版本…]}`,由 Core 判断: @@ -56,7 +59,7 @@ Core 能校验一个能力,不代表宿主能运行它。宿主在 `plugins.de 能力表放在宿主代码里,写在能力实现的旁边,Core 不再按平台去猜。原来按 `darwin-` 前缀和级别写死的判断,就是 Core 替宿主猜、又容易和宿主实现漂移的例子。 -macOS 宿主接受级别 1 到 3(`supportedDeclarativeAPIVersions = [1, 2, 3]`),能力表是 `PluginManifest.hostCapabilities`,在校验、索引和安装审查时都发给 Core;其中的 `buildSystems: [1]` 随 macOS 的 Build 工具窗口一起加入,但 macOS 尚未实现 Provider,所以不声明 `buildProviders`;市场过滤、清单校验和安装审查都会拒绝需要它的插件,纯模板构建插件仍可用。Core 自己支持 `buildProviders: 1`,不传宿主表时仍可校验并生成 Provider 计划。Windows 还没有声明式插件宿主,不发送能力表,所以 Windows 不列出级别 3 的插件。宿主接入某个能力时,要做两件事:让宿主接受级别 3,并在宿主自己的能力表里加上这个能力的版本,而且只在宿主真正能运行它时才加。 +macOS 宿主接受级别 1 到 3(`supportedDeclarativeAPIVersions = [1, 2, 3]`),能力表是 `PluginManifest.hostCapabilities`,在校验、索引和安装审查时都发给 Core;其中的 `buildSystems: [1]` 随 macOS 的 Build 工具窗口一起加入,`buildProviders: [1]` 在 macOS 接入构建 Provider 会话后才加入。`vcsProviders: [1]` 同样等 macOS 接入 VCS Provider 会话(版本控制工具窗口、项目树状态和文件移动)后才加入,见[统一 VCS 模型提案](../../proposed/architecture/2026-10-22-unified-vcs-provider-model.md)的 P3。Core 自己支持 `buildProviders: 1` 和 `vcsProviders: 1`,不传宿主表时仍可校验清单、生成 Provider 计划、识别工作副本根并转发请求。Windows 还没有声明式插件宿主,不发送能力表,所以 Windows 不列出级别 3 的插件。宿主接入某个能力时,要做两件事:让宿主接受级别 3,并在宿主自己的能力表里加上这个能力的版本,而且只在宿主真正能运行它时才加。 ### 3. 市场索引带上能力版本 @@ -69,7 +72,7 @@ macOS 宿主接受级别 1 到 3(`supportedDeclarativeAPIVersions = [1, 2, 3]` 清单回答的问题只有三个:启动什么进程、需要哪些权限、对哪些文件和工程生效。进程启动以后能回答哪些请求、支持哪些可选数据,在 LSP `initialize` 里协商: -- 宿主发送 `capabilities.experimental.litheBuild: {"versions": [1]}`。 +- 宿主发送 `capabilities.experimental.litheBuild: {"versions": [1]}`。VCS Provider 用同样的方式协商 `litheVcs`,回复里还带上它实现了哪些 VCS 能力(例如 `history`、`annotate`),宿主据此决定显示哪些操作,见 `shared/contracts/vcs-provider-v1.md`。 - Provider 从中选一个版本,回复 `{"version": 1}`。没有回复版本、或回复了宿主没提供的版本时,宿主不把它当作 Provider 使用。 - 双方都忽略协商对象、请求参数和结果里自己不认识的字段,所以同一个版本内可以继续增加可选数据。另一方不能忽略的改动,才需要新的协议版本。 diff --git a/.agents/notes/implemented/architecture/2026-10-18-plugin-web-content-in-host-windows.md b/.agents/notes/implemented/architecture/2026-10-18-plugin-web-content-in-host-windows.md index 00cb60e35f..d31ed74005 100644 --- a/.agents/notes/implemented/architecture/2026-10-18-plugin-web-content-in-host-windows.md +++ b/.agents/notes/implemented/architecture/2026-10-18-plugin-web-content-in-host-windows.md @@ -33,11 +33,12 @@ ### 桥接对象 -宿主在页面脚本执行前注入 `window.lithe`(第 1 版):`context`、`request(method, params)` 和 `on(event, listener)`。方法和事件的全集写在 `shared/contracts/plugins/v1/README.md` 的 “Web content (`ui` 2)” 一节,这里只记录边界: +宿主在页面脚本执行前注入 `window.lithe`(当前第 2 版,`lithe.version` 给出版本号):`context`、`request(method, params)` 和 `on(event, listener)`。方法和事件的全集写在 `shared/contracts/plugins/v1/README.md` 的 “Web content (`ui` 2)” 一节,这里只记录边界: - 页面只能操作本插件声明的构建系统和当前工作区,越界返回 `plugin.web.forbidden`。 - 凡是会启动进程的请求(刷新模型、跑任务、跑测试),都要经过与宿主自身按钮相同的信任和同意流程,输出进入宿主的 Build Output。页面不能提交或修改进程 argv,也不能自己拼命令;模型保留 Core 返回的声明运行项 `run` 和任务 `category` 供展示,执行入口仍只有宿主校验过的任务与测试。 - 选项写入沿用 `build.resolveOptions` 和宿主的选项存储,因此构建选项转交给 JDT 的方式与宿主自己的选项列表一致。 +- 第 2 版只多了文件对话框 `dialog.openFile` / `dialog.saveFile`,供数据库这类带后端的插件导入导出文件。对话框由宿主弹出,说明里写明是哪个插件在请求;同一页面同时只能开一个;页面只拿到用户选中的绝对路径,真正的读写交给插件后端,页面本身仍然碰不到文件。页面在对话框打开期间被关闭,就拿不到选择结果。旧宿主没有这两个方法(返回 `plugin.web.unknownMethod`),所以页面先看 `lithe.version` 再决定是否提供导入导出。之所以加桥接方法而不是让后端自己弹窗,是因为后端是无界面的子进程,弹窗必须由宿主窗口负责,也只有宿主能把插件身份写进对话框,防止页面冒充 Lithe。 ### 正确做法 diff --git a/.agents/notes/implemented/architecture/2026-10-20-host-plugin-function-invocation.md b/.agents/notes/implemented/architecture/2026-10-20-host-plugin-function-invocation.md index 8f7987e6c1..0d0e0ba8d2 100644 --- a/.agents/notes/implemented/architecture/2026-10-20-host-plugin-function-invocation.md +++ b/.agents/notes/implemented/architecture/2026-10-20-host-plugin-function-invocation.md @@ -148,13 +148,13 @@ if pluginID == "dev.lithe.maven" { 启动带函数的会话 } // 宿主按插 - 每个 Provider 带独有的 `LITHE_PLUGIN_BACKEND_SCOPE`,会话结束后宿主按该标记优雅结束遗留进程。 - `./scripts/verify-runtime-bundle-immutability.sh`:确认没有新增写入 bundle 的路径。 - macOS 原生验收(2026-10-20,PR #1366 合并 PR #1365 后的 debug 应用): - - 环境:`sdk/node/examples/sample-build` 加一个只用于验收的网页窗口,作为开发包从磁盘安装;另装一个带自己构建系统和网页窗口的验收插件;示例工作区含成功、失败(退出码 3)和长时间运行三个项目。页面把每一步结果写进 `ui.setState`,验收读取 Application Support 中的状态文件核对。 + - 环境:Node SDK 的示例插件 `sdk/node/examples/sample-build`(当时在主仓库,现位于 lithe-plugin-docs)加一个只用于验收的网页窗口,作为开发包从磁盘安装;另装一个带自己构建系统和网页窗口的验收插件;示例工作区含成功、失败(退出码 3)和长时间运行三个项目。页面把每一步结果写进 `ui.setState`,验收读取 Application Support 中的状态文件核对。 - `functions.list` 返回 `snapshot.read`、`workspace.build`;一次成功构建后再列出,多了运行时注册的 `snapshot.clear`。 - `functions.invoke` 带 `call` 时,页面依次收到 `Preparing`、`Building app`、`Done` 三个 `functions.progress`,结果作为 `{ value }` 到达;失败项目返回 `plugin.function.failed`,并带上插件自己的 `pluginErrorCode: sample.buildFailed`。 - `functions.cancel` 后调用以 `requestCancelled` 结束;构建子进程在取消后立刻退出(采样中最后一次出现早于取消约 0.1 秒)。对已结束的 `call` 再取消返回 `{}`。未注册的函数返回 `plugin.function.unknown`,`params` 不是对象返回 `plugin.web.invalidRequest`。 - 另一个插件的页面用 `sample:.` 调用 `functions.list` / `functions.invoke` 都得到 `plugin.web.forbidden`,并且在弹同意框之前就被拒绝;它调用自己实例上的 `snapshot.read` 得到 `plugin.function.unknown`,说明函数按进程隔离。 - 进程清理:调用进行中关闭项目、或在设置里停用插件,Provider 及其构建子进程都会退出(`ps`/`pgrep` 确认);从磁盘替换插件包时旧 Provider 也会停止;退出应用后没有任何 Lithe 或插件进程残留。重启应用后 `snapshot.read` 返回上次会话的快照。 - - 验收发现示例的路径检查在宿主 `rootUri` 带尾部斜杠时拒绝所有路径(macOS 传的是目录 URL 的 `absoluteString`),所有构建都报 `sample.config`。已在示例里用 `path.resolve` 规范化根目录,并让 `rust/lithe-core/tests/plugin_sdk_node.rs` 用带尾部斜杠的 `rootUri`;未修复时该测试失败,修复后通过。 + - 验收发现示例的路径检查在宿主 `rootUri` 带尾部斜杠时拒绝所有路径(macOS 传的是目录 URL 的 `absoluteString`),所有构建都报 `sample.config`。已在示例里用 `path.resolve` 规范化根目录,并让当时主仓库的 `rust/lithe-core/tests/plugin_sdk_node.rs`(现为 lithe-plugin-docs 的 `sdk/e2e/plugin_sdk_node.rs`)用带尾部斜杠的 `rootUri`;未修复时该测试失败,修复后通过。 - `./scripts/verify-platform-feature-matrix.sh`:`plugin-functions` 的 macOS 证据已更新。 ## 适用范围 diff --git a/.agents/notes/implemented/architecture/2026-10-20-plugin-backend-sdk.md b/.agents/notes/implemented/architecture/2026-10-20-plugin-backend-sdk.md index fd18a15874..93ad659a8c 100644 --- a/.agents/notes/implemented/architecture/2026-10-20-plugin-backend-sdk.md +++ b/.agents/notes/implemented/architecture/2026-10-20-plugin-backend-sdk.md @@ -8,7 +8,9 @@ Lithe 有两个官方插件后端 SDK:`sdk/node`(包名 `@lithe/plugin-sdk` SDK 只**实现**契约,不**定义**契约。协议仍以 `shared/contracts/plugins/v1/README.md` 为准,SDK 不新增任何线上字段;不用 SDK 手写的后端同样合法。SDK 的价值是把“不能破坏 Lithe 统一管理”的规则变成默认行为:不发 `lithe/*` 回调、stdout 只走协议、状态只写宿主给的目录、调用结束时进程树一起结束。 -两个 SDK 各有一个示例插件(`sdk/*/examples/sample-build`),在 Rust Core CI lane 里经真实 Core 会话跑**同一套**端到端场景(`rust/lithe-core/tests/plugin_sdk/mod.rs`)。所以任一 SDK 与契约或另一个 SDK 的行为出现分叉时,测试都会失败。 +两个 SDK 各有一个示例插件(`sdk/*/examples/sample-build`),经真实 Core 会话跑**同一套**端到端场景(`sdk/e2e/plugin_sdk/mod.rs`)。所以任一 SDK 与契约或另一个 SDK 的行为出现分叉时,测试都会失败。 + +2026-10-23 起,两个 SDK 的源码、发版和端到端测试都在 [lithe-plugin-docs](https://github.com/Lithe-IDEA/lithe-plugin-docs) 的 `sdk/` 下,主仓库不再保存 SDK。插件开发者只看那个仓库就能拿到契约副本、SDK 和教程。迁移原因见 [插件契约、SDK 与开发者资料的发布](2026-10-23-plugin-contract-publishing.md);下文出现的 `sdk/...` 路径都指 lithe-plugin-docs。 ## 问题 @@ -23,13 +25,15 @@ Issue #1351 的目标是给插件开发者开放官方 SDK,同时“保留创 ## 决策 -### 1. 先做 Node,放在仓库根目录 `sdk/node` +### 1. 先做 Node,SDK 单独成一个目录 -Lithe 的 `runtime` 启动方式已支持 `node` 和 `java`。Node 无需编译、单文件即可分发、启动快,最适合作为第一个 SDK。目录放在根目录 `sdk/`,与 `shared/`(契约)、`Plugins/`(具体插件包)分开:`sdk/` 是可被插件复制使用的库代码,不属于任何一个平台,也不是契约本身。 +Lithe 的 `runtime` 启动方式已支持 `node` 和 `java`。Node 无需编译、单文件即可分发、启动快,最适合作为第一个 SDK。SDK 与 `shared/`(契约)、`Plugins/`(具体插件包)分开:它是可被插件复制使用的库代码,不属于任何一个平台,也不是契约本身。最初放在主仓库根目录 `sdk/`,后来整体移到 lithe-plugin-docs 的 `sdk/`。 ### 2. 零依赖、复制进插件包使用 -插件包安装后只读、没有 `npm install` 步骤、必须能离线运行。所以 SDK 不依赖任何 npm 包,插件作者把 `src/` 复制到包内用相对路径导入。`package.json` 标 `private: true`,暂不发布到 npm;等 API 稳定后再决定发布方式。 +插件包安装后只读、没有 `npm install` 步骤、必须能离线运行。所以 SDK 不依赖任何 npm 包,插件作者把 SDK 复制到包内用相对路径导入。 + +发布方式和 Java SDK 相同:lithe-plugin-docs 打 `sdk-node-v<版本>` 标签,它的 `.github/workflows/release-node-sdk.yml` 先在 Node 20 上跑 SDK 测试,再用 `npm pack` 生成 `lithe-plugin-sdk-node-<版本>.tgz` 和 `.sha256`,附在 GitHub Release 上。插件仓库在构建时按固定版本和 SHA-256 下载、校验并解开,把 `package/` 目录放进插件包;运行时不下载任何东西。这样外部开发者不用克隆主仓库,也不用给主仓库提 PR。`package.json` 仍标 `private: true`,`npm pack` 不受影响,但 SDK 不会被误发到 npm;等 API 稳定后再决定是否发布到 npm。 ### 3. 规则变成默认行为,而不是文档里的建议 @@ -57,7 +61,7 @@ Maven、Gradle 这类插件需要在后端里调用 JVM 生态的工具,并长 - **最低 Java 8。** 很多企业项目仍在 Java 8 上构建,后端最好能直接用项目那个 JDK 启动。代价是 SDK 只能用 Java 8 的 API。 - **零运行时依赖,自带最小 JSON 读写(`Json`)。** 插件要把 SDK shade(打进同一个 JAR)到自己的后端里。如果 SDK 依赖 Gson 或 lsp4j,就会和插件自己的同名依赖冲突,许可证审查范围也会变大。 -- **发布方式:主仓库打 `sdk-java-v<版本>` 标签,Release 附带 JAR 和 `.sha256`。** 工作流是 `.github/workflows/release-java-plugin-sdk.yml`,暂不发到 Maven 仓库。插件仓库在构建时按固定版本和 SHA-256 下载并校验,再 shade 进后端 JAR,运行时不下载任何东西。 +- **发布方式:打 `sdk-java-v<版本>` 标签,Release 附带 JAR 和 `.sha256`。** 0.1.0 是从主仓库发布的,已锁定它的插件不用改;之后的版本由 lithe-plugin-docs 的 `.github/workflows/release-java-sdk.yml` 发布。暂不发到 Maven 仓库。插件仓库在构建时按固定版本和 SHA-256 下载并校验,再 shade 进后端 JAR,运行时不下载任何东西。 - **处理器各自跑在守护线程池上,读循环只负责分帧和派发。** 取消不打断(interrupt)线程,只置位取消令牌,并结束该调用启动的进程。这是因为中断会让处理器里正在进行的文件写入半途失败。 - **进程按调用清理,用进程组,与 Node SDK 一致。** 常驻后端不会因为一次调用结束而退出,宿主按 `LITHE_PLUGIN_BACKEND_SCOPE` 的清理要等会话结束才发生,所以 SDK 必须在调用结束或取消时自己结束工具留下的进程。Java 没有进程组 API,macOS 和 Linux 上 SDK 通过系统 `perl`(`setpgrp(0, 0)` 后 `exec` 成工具,PID 不变)让每个工具成为自己进程组的组长:取消时先给整组发 SIGTERM,宽限期后发 SIGKILL;工具正常退出后立即对整组发 SIGKILL,结束它留下的后台进程。信号经 `/bin/kill -- -` 发出,因此 Java 8 和 Java 9+ 行为相同;只要组里还有成员,组 ID 就不会被复用,所以不会误伤其他并发调用。`PERL5OPT` 会让 perl 启动时加载模块,SDK 先从 perl 的环境里拿掉,再在 `exec` 前还给工具。启动失败时 launcher 以 127 退出并带固定前缀,SDK 仍抛 `IOException`,与 `ProcessBuilder` 一致。Windows 或没有 `perl` 时退回 `ProcessHandle.descendants()`(Java 9+,Windows 不会重挂父进程,工具退出后仍能找到子孙);Java 8 只能结束直接子进程。用 `setsid` 脱离进程组的进程,以及退回路径够不到的进程,仍由宿主按后端标记在会话结束时兜底。 - **取消监听器互相隔离。** 一个 `onCancel` 回调抛异常只记录到 stderr,不影响其余回调;调用收尾放在 `finally` 里,保证请求一定回复,宿主不会只等到超时。 @@ -106,28 +110,23 @@ Maven、Gradle 这类插件需要在后端里调用 JVM 生态的工具,并长 ## 后果 - 收益:插件作者只写处理器;取消、清理、错误映射和状态存储默认正确,Lithe 的统一管理规则不依赖作者自觉。 -- 收益:示例插件经过真实 Core 会话验证,契约改动若破坏 SDK,Rust Core lane 会直接失败。 +- 收益:示例插件经过真实 Core 会话验证。lithe-plugin-docs 每次提交都用契约副本锁定的那个主仓库 commit 跑端到端测试,每周再用主仓库 `preview` 分支的最新 Core 跑一次,Core 改动破坏 SDK 时会在这里失败。 - 代价:Node SDK 以复制源码方式分发,Java SDK 被 shade 进插件 JAR,插件里的副本都不会自动升级;修复安全问题时需要插件重新发布。 -- 代价:Rust Core lane 需要 Node、JDK 和 Maven;CI 用 Java 8 运行时跑 Java SDK 测试和示例后端,用 JDK 21 编译。 -- 代价:仓库新增一个顶层目录和一条 CI 测试,`sdk/*` 改动会触发 Rust Core lane。 +- 代价:SDK 移出主仓库后,主仓库的 PR 不再当场发现“Core 改动破坏 SDK”,最晚由 lithe-plugin-docs 的每周检查发现。改 Core 的插件后端协议时,应在合并前用 lithe-plugin-docs 的 `node scripts/sdk-e2e.mjs --source <本地主仓库> --ref <分支>` 手动跑一次。 +- 代价:lithe-plugin-docs 的端到端测试需要 Rust、Node、JDK 和 Maven,每次都要编译一遍 Core;CI 用 Java 8 运行时跑 Java SDK 测试和示例后端,用 JDK 21 编译。 - 代价:宿主还没有调用函数的界面入口,也没有宿主能力(`lithe/host/*`);Windows 没有插件后端宿主。SDK 目前对用户不可见,只给开发者和后续 PR 使用。 ## 验证 -- `node --test sdk/node/test/*.test.mjs`:协商、ID 规则(对照 `function-protocol.json`)、保留方法、调用/进度/错误/取消、运行时注册、构建结果、生命周期和退出码、分帧、进程树取消、状态存储与只读包拒绝、类型声明与运行时导出一致。 -- `LITHE_PLUGIN_SDK_NODE="$(command -v node)" cargo test --manifest-path rust/Cargo.toml -p lithe-core --test plugin_sdk_node`:示例插件经 Core 校验、启动计划、版本检查、构建模型/依赖、函数结果/进度/失败/取消、重启后状态恢复,最后确认插件包文件清单不变。 +以下命令都在 lithe-plugin-docs 中运行: + +- `node --test sdk/node/test/*.test.mjs`:协商、ID 规则(对照 `contract/v1/fixtures/function-protocol.json`)、保留方法、调用/进度/错误/取消、运行时注册、构建结果、生命周期和退出码、分帧、进程树取消、状态存储与只读包拒绝、类型声明与运行时导出一致。 - `mvn -B -f sdk/java/pom.xml verify [-Djvm=]`:Java SDK 的 JUnit 测试,覆盖与 Node 测试相同的协议场景,另含 JSON、分帧、进程树取消和状态存储。 -- `cargo test … --test plugin_sdk_java`(需要 `LITHE_PLUGIN_SDK_JAVA`、`LITHE_PLUGIN_SDK_JAVA_SAMPLE`、`LITHE_PLUGIN_SDK_NODE`):Java 示例按插件实际打包方式(`plugin.json` + shade 后的 JAR)装进临时包目录,跑同一套 Core 场景。 -- `./scripts/verify-rust-core-crates.sh`:CI 中运行以上全部;设置 `LITHE_PLUGIN_SDK_JAVA_RUNTIME` 时,Java 测试和示例后端在该运行时(CI 为 Java 8)上运行。 -- `bash scripts/test-classify-ci-changes.sh`:`sdk/*` 改动被分到 Rust Core lane。 +- `node scripts/sdk-e2e.mjs`:把 `sdk/e2e` 复制进锁定版本主仓库的 `rust/lithe-core/tests/`,运行 `plugin_sdk_node` 和 `plugin_sdk_java`。Node 示例经 Core 校验、启动计划、版本检查、构建模型/依赖、函数结果/进度/失败/取消、重启后状态恢复,最后确认插件包文件清单不变;Java 示例按插件实际打包方式(`plugin.json` + shade 后的 JAR)装进临时包目录,跑同一套场景。设置 `LITHE_PLUGIN_SDK_JAVA_RUNTIME` 时,Java 示例在该运行时(CI 为 Java 8)上运行。 +- `node scripts/sdk-e2e.mjs --ref preview`:同上,换成主仓库 `preview` 分支的最新 Core;CI 每周运行。 ## 适用范围 -- `sdk/node/` -- `sdk/java/` -- `rust/lithe-core/tests/plugin_sdk/`、`plugin_sdk_node.rs`、`plugin_sdk_java.rs` +- `https://github.com/Lithe-IDEA/lithe-plugin-docs` - `rust/lithe-core/src/plugins/declarative.rs`(运行时启动计划的版本探测参数) -- `.github/workflows/release-java-plugin-sdk.yml` -- `scripts/verify-rust-core-crates.sh` -- `scripts/classify-ci-changes.sh` - `shared/contracts/plugins/v1/README.md` diff --git a/.agents/notes/implemented/architecture/2026-10-22-windows-declarative-plugin-host.md b/.agents/notes/implemented/architecture/2026-10-22-windows-declarative-plugin-host.md new file mode 100644 index 0000000000..14cca53000 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-windows-declarative-plugin-host.md @@ -0,0 +1,149 @@ +# Agent 笔记:Windows 声明式插件宿主 + +状态:已实现 + +## 先说结论 + +Windows 新增一套契约 v1 声明式插件宿主,与现有 Worker 扩展模型长期并行:需要宿主代用户拉起进程的语言与构建插件走契约宿主,宿主内运行 UI 与服务行为的扩展(主题、AI provider 等)留在 Worker 模型。契约引擎不需要新写——`lithe-core` 已经编译进 Windows 宿主,经 `core_execute` 就能调用 `plugins.declarative.*` 与 `plugins.marketplace.*`。M1 已落地:包存储、启动扫描与隔离、语言服务器启动计划解析,以及共享 fixtures 的全量验收;M2 已补磁盘导入安装链、设置内独立管理页和 runtime 启动的 `minimumVersion` 探测;M3 已补声明式设置、运行/测试计划命令与契约版 PHP bundled 包;M4 已补 Web 工具窗口(`lithe-plugin://` scheme、CSP、`window.lithe` 桥 v0)。能力声明开放 `languages:1`、`languageServers:[1,2]`、`serverProcess:1`、`settings:[1,2]`、`ui:[1,2]`,其余能力(`executionProviders`、`buildProviders`、`buildSystems`)仍由 Core 自动拒绝。 + +## 问题 + +- 功能矩阵 `declarative-plugin-contract`、`plugin-functions` 对 Windows 记为 missing(2026-10-22 矩阵对齐时确认);[声明式语言插件](2026-10-12-declarative-language-plugins.md) 也写明"Windows 现有扩展清单暂未改造,接入时必须通过同一组 fixture"。 +- Windows 当前插件生态是渲染进程内的 Worker 扩展:`Plugins/win/Official/PhpSupport` 是 Worker 包格式(`main: "plugin.js"`),与契约包不同,无法复用 Lithe Store 面向 macOS/契约发布的语言插件包。 +- Windows 没有插件信任链与仓库源管理;`install_extension_from_url` 前端已调用但 Rust 宿主从未注册,非语言扩展的落盘安装链路实际不可用。 +- 契约的确定性逻辑(清单校验、启动计划展开、安装审查)按仓库规则只在 `rust/lithe-core/src/plugins/` 实现一次,而 `windows/tauri/src-tauri` 已依赖 `lithe-core`——差的只是宿主侧接线,不是契约本身。 + +## 决策 + +### 双模型边界 + +判定句:扩展需要"宿主代表用户拉起进程"(语言服务器、构建系统、运行/测试提供方)就走契约宿主;扩展需要"在宿主内运行自己的代码来表达 UI 或服务行为"(颜色主题、图标主题、AI provider、数据库 sidecar、ACP agent)就留在 Worker 模型。 + +- 正确做法:新增 Go 语言支持时写契约 `plugin.json` 声明 `languageServers` 和 `executionProviders`,由 Core 展开启动与运行计划。 +- 不要这样做:把主题扩展改写成契约包;为 Worker 扩展桥接一套函数通道冒充 litheFunctions;在宿主里按插件 ID 写 `if` 分支。 + +### 能力声明渐进开放 + +宿主在 `plugins.declarative.validate` 的 payload 里声明 `hostCapabilities = { languages:[1], languageServers:[1,2], serverProcess:[1], settings:[1,2], ui:[1,2] }`。Core 的 `capabilities.rs` 会拒绝声明了宿主未开放能力的插件,宿主不需要自己写拒绝逻辑。M3 已把 `languageServers` 与 `settings` 提到 2,注册设置页并下推 `workspaceConfiguration`;M4 已增 `ui:[1,2]`。`executionProviders`、`buildProviders` 与 `buildSystems` 仍未开放,运行/测试计划命令的存在不代表这些插件能力可安装。宿主不声明的能力,市场侧按不支持展示。 + +### 存储、信任与安装(v0) + +- 安装根为 `app_local_data_dir()/plugins///`,`installation.json` 记录版本与启用状态;记录缺失或损坏的包隔离展示,绝不静默加载。 +- bundled 包从 `resource_dir()/OfficialPlugins` 只读加载,多版本目录取最高版本;运行时不写安装目录与资源目录,遵守发行基线只读边界。 +- 每个插件的语言服务器状态目录在 `app_local_data_dir()/plugin-state//`,由宿主注入启动计划请求。 +- 信任模型 v0:官方 ID 防冒用由 Core 校验兜底;bundled 包随安装器分发;磁盘导入(已落地)与市场下载(M3+)一律走 `plugins.marketplace.installReview` 全量审批(权限 + 完整程序清单),审批界面明示"未认证发布者"。publisher Ed25519 验签明确延后,这是 v0 的已知缺口,Windows 验签方案(Authenticode 还是包内 Ed25519)单独立项评估。 +- 磁盘导入的私有快照、staging 重验、内容摘要复查与回滚语义照抄 macOS 侧规则(见声明式语言插件 Note 的磁盘安装节),Windows 不另造审查语义。 + +### 启动接线(M1 落地形状) + +- `plugin_host` 模块(`store.rs` 存储布局、`scan.rs` 扫描与校验、`launch.rs` 启动计划解析)是宿主侧唯一入口;校验与计划展开全部经 `lithe_core::execute_json` 调 Core 命令,宿主不复制任何清单语义。 +- `packaged` 启动入口解析后必须是包内普通文件:拒绝 reparse point(符号链接与 junction)与规范化后的路径逃逸,这是宿主执行每个启动前的统一闸门。 +- `runtime` 与 `systemCommand` 启动经 PATH 探测(复用 `language_tools` 的系统工具查找),并按 macOS 同一约定导出 `LITHE__PATH`、把 packaged runtime 目录前置到 `PATH`;`minimumVersion` 探测随 M2 安装链一起接入。 +- 语言服务器进程仍由共享 Core 的 `lsp.startServer` 拉起:前端 `resolve-editor-lsp-launch.ts` 在 Worker 扩展清单没有对应服务器时,回落到 `declarative_plugins_language_launch`(契约插件排在 Worker 之后,已由 Worker 声明的语言保持现状,直到 M3 包迁移)。 +- 启动扫描在应用 bootstrap 的 "declarative plugins" 步骤执行一次;扫描结果缓存在 Tauri state,前端经 `declarative_plugins_state` 只读消费。 + +### 网页与版本探测的运行边界 + +Tauri 把插件自定义协议页面视为远端来源,空 capability 会在进入 Rust handler 前拒绝所有 IPC。宿主采用 Tauri 官方 inlined plugin(应用内命令插件)扩展点:`declarative-web` 命名空间只声明一个 `request` 权限,仅匹配 `plugin-web-*` 窗口和 `http://lithe-plugin.*/*` 来源;不授予 fs/http/event 等命令,也不启用会改变全部主窗口命令的 app ACL。编译后的 Tauri authority 回归验证这条边界,不能用只检查 handler 的测试代替。 + +插件网页的窗口标签只用于定位所属包,不能单独证明当前文档可信。`web.rs` 使用 Tauri 的导航、新窗口和下载拦截器,只允许本包来源;每次桥接请求还读取 WebView 当前 URL,再核对插件来源、启用状态和窗口声明。宿主脚本也只在本包主框架定义桥,窗口使用非持久浏览会话,避免外站文档继承桥或页面存储。例如本包的 `ui/next.html` 可以导航,`https://example.com`、另一个插件的来源、带用户信息或端口的地址会被拒绝;不要用 CSP 或窗口标签代替导航与请求来源检查。 + +WebView2 的 NavigationStarting 可以阻止外站文档提交,但实测外站服务器仍可能先收到 GET。因此 `web_network.rs` 使用微软的 WebResourceRequested 接口,在请求层把非本包、非 Tauri IPC 的网络请求完成为本地 403。窗口先创建为惰性空白页,五秒期限内安装过滤器成功后才导航到包页面;失败时销毁空白窗口,不能留下可运行但未受保护的页面。Wry 只在初次创建时转换自定义协议,延迟 navigate 必须显式使用同一 http://lithe-plugin.<插件 ID>/ 映射地址,否则页面会停在空白文档;这个地址构造和来源检查共用同一回归。注册跟随 WebView 的生命周期,不在页面每次加载时叠加。复用 Wry 已依赖的 webview2-com 0.38.2 API,独立声明同一固定版本,没有新增另一套浏览器或网络后端。 + +`workspace.openFile` 必须先经原生文件系统解析真实目标,再检查它仍在同样经过解析的工作区根内。前端把这个目标交给既有文件打开入口,保留行列位置,并在等待解析后复查工作区有没有切换。工作区内指向内部文件的链接可以打开,指向外部目录的 junction(Windows 的目录联接)不能借路径文本绕过检查。不能仅比较 `C:/work/link/file.txt` 的文本前缀。 + +网页的语言、浅色或深色状态、颜色和字体变量从已解析的 Windows 工作台主题取得。主窗口创建页面时传入初值,主题注册表与设置的订阅把后续变化送到原生页面;根元素的 `data-theme`、`--lithe-*` 变量和只读 `lithe.context` 随之更新,再发 `ui.themeChanged`。每个 HTML 响应会在插件脚本前加载一个保留路径下的宿主同源外部脚本,由原生协议 handler 按可信窗口标签读取最新快照;因此页面重载、同包导航和带 UTF-8 BOM 的页面在运行插件代码前也能取得当前主题,HTML parser 已创建根元素。宿主脚本插在真实 head 起始标签后;省略 head 时退到 html 后,两者都省略时放在文档声明之后。扫描仅处理 HTML 前导区,跳过注释并识别带引号的属性,保留原有根样式、head 属性、BOM 和文档模式。这种响应字节插入不改包文件,也不给 CSP 增加内联脚本例外。订阅随 bootstrap 清理,不在插件内复制另一套主题值。 + +版本探测复用 `lithe_git_host::run` 的原生进程执行器。虽然它位于 Git adapter crate,其进程树、非阻塞管道与清理能力不解释 Git 参数;探测只设置五秒取消期限和每条输出流 64 KiB 上限,解析仍交 Core。Windows 在进程运行第一条指令之前把它放入 Job Object(内核管理的进程组),因此父进程退出后仍占用 stdout 的后代也会被清理。不要只给 `try_wait` 加超时后无期限 `join` 读取线程,也不要靠退出后遍历父 PID 找后代。代价是异常进程退出后最多还需执行器自身的有界清理时间。 + +### 禁用、替换和卸载必须先收回使用权 + +插件包正在执行时直接删目录,会在 Windows 上遇到可执行文件被占用,并可能只删掉一部分文件。因此所有启停、安装替换、回滚和卸载先经过 `lifecycle.rs`:禁止新的原生启动计划和网页读取,等待已经开始的版本探测、校验进程、网页创建和资源读取退出,再关闭所有所属页面,并要求每个产品窗口确认其运行与 Core LSP 会话都已停止。原生使用计数和禁止新请求共用锁,避免刚检查完就漏进新的操作;等待原生使用者和窗口确认各有 45 秒上限。失败时保留包文件并继续禁止使用,用户重试管理操作后才能恢复。 + +前端在调用第一个异步操作前登记启动归属。停止操作必须覆盖已启动和正在等待 Core 响应的会话;晚到的会话先停止、再拒绝返回。停止失败的会话仍留在登记表中,后续管理操作必须继续等它,不能误报没有进程。包目录与扫描目录只有在变更和重新扫描都成功后才恢复使用。完整扫描从发现到提交缓存共用一把锁,避免不同插件并发管理时,旧扫描覆盖新目录。 + +正确做法:先收回新请求、等待所有使用者退出,再修改包;不要发一个不等待结果的停止事件就删除文件。每个窗口只能确认自己的操作,窗口消失算失败,不能假定它的 Core 子进程已经退出。探测与读取沿用既有 `plugin-state` 和包读取路径,没有新增可跨工作树复用的资源;校验命令在平台可写的插件状态目录执行,发行资源保持只读。 + +### 插件身份、工作区和声明式界面 + +`pluginId` 是 Core 校验配置更新的插件身份,`providerId` 是语言服务实例的来源,两者必须分别传递和保存。设置的即时生效请求必须带插件 ID,并只发送到仍有效且属于该插件的会话;Java 和 Worker 启动继续使用自己的来源。 + +每个网页由创建它的产品窗口、工作区和插件共同持有,窗口标签使用无碰撞的编码。状态按这组归属隔离,关闭工作区或禁用插件时清理;工作区关闭会提升该工作区页面的版本,关闭前已发起但尚未注册的页面也不能复活。状态写入与文件打开的登记和撤销共用所有者锁,旧页面实例不能在重新打开后继续写状态;网页不能通过参数自行指定所属工作区。`workspace.openFile` 只发给所属窗口,并等待带操作 ID 的确认。目标窗口在真实路径解析前后都复核当前工作区,复核目标缓冲区确实存在且处于活动状态后才确认成功;共享文件打开函数在读取失败或请求过期时也可能正常返回,不能仅凭其 Promise 完成判断成功;工作区切换、窗口关闭和十秒确认超时都返回失败。待确认请求最多 128 个,避免插件无限积累请求。 + +启动前检查 Core 的 `requiredPackagePaths`、`runtimeRequirements` 和 `validationArguments`。包资源允许普通目录,但实际可执行入口必须是普通文件;路径逃逸和 reparse point 都拒绝。依赖与发现型启动运行时优先使用用户显式导出,再查系统 PATH;包内 `packagedRuntime` 则严格执行已审批的那个文件,不接受用户覆盖,满足最低版本才继续;校验命令即使输出文本,只要退出码非零仍失败。探测复用现有五秒、每条输出流 64 KiB 的原生执行器和进程树清理能力。 + +`ui:1` 的文本工具窗口由宿主生成转义后的 HTML,`ui.commands` 接入已有的命令、菜单和工具栏展示注册表,工具窗口也有活动栏入口。复用展示注册表不等于进入 Worker 执行模型。禁用时同步撤销入口,异步刷新按插件检查版本,避免撤销 B 时取消 A 的刷新,防止旧结果恢复入口。目前页面仍是独立 WebView 窗口;声明的停靠位置、图标、动作可用/勾选状态和布局持久化尚未与 macOS 对齐,因此矩阵继续记为 partial,不能称为完整的 ui 1/2 产品形态。 + +### 与 Worker 扩展模型共存(产品分工) + +- 两套模型互不桥接:契约插件不进 Worker 宿主,Worker 扩展不冒充契约能力。 +- PhpSupport 迁移(M3 调整):契约版 PHP 包(`Plugins/win/Official/PhpSupportContract`,`sameTeamAsHost` bundled)随安装器分发并完成 validate/launch/runPlan 端到端测试,但 Worker 版**保留不移除**。原因:Worker 版提供 composer scripts 的动态运行动作发现(读取工作区 composer.json 生成动作列表),契约 v1 的 `executionProviders` 是静态声明,没有等价能力;删除 Worker 版会丢失用户可见功能。完整迁移等契约补齐动态运行动作后再做。两包并存时 Worker 版优先(Worker 扩展清单先解析),契约包不重复注册语言。 +- `install_extension_from_url` 等 Worker 非语言市场命令(M2 已处置):在 Rust 宿主注册为显式占位错误(`extensionStore.notAvailable`),调用方得到可诊断的结构化失败而不是"命令不存在";完整落盘后端是独立工作项,不与契约宿主混淆。 +- 管理界面按模型分组展示两类扩展并使用不同文案,避免用户把 Worker 扩展当成契约插件。 + +### 实施切分 + +M1 包存储 + 启动校验 + 语言注册 + LSP 启动(已落地);M2 磁盘安装链路 + 管理界面 v1 + 死链处置(已落地);M3 声明式设置 + 运行/测试计划 + PHP 契约包(已落地,Worker 版保留见上文);M4 Web 工具窗口(已落地,桥 v0 只含 state/openFile/主题,build.* 与 functions.* 等 Windows 构建系统能力接入后开放)。每个里程碑独立 PR,矩阵条目随代码同 PR 更新。总体方向遵循[公共扩展契约与 Provider 接入方向](../../proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md),本 Note 只落地其中 Windows 契约宿主的第一阶段。 + +## 考虑过的备选方案 + +- 把 Worker 扩展模型整体改造成契约宿主:契约没有主题、AI provider 这些概念,改造成本高且无收益,不采用。 +- 在 Tauri 侧另写一套清单校验:违反"校验只在 Core 实现一次",共享 fixtures 无法复用,双端语义必然漂移,不采用。 +- 把 Worker RPC 桥接成 litheFunctions:两者协议语义不同,会同时存在两套函数协议,等有真实插件需求时再按[插件函数契约](2026-10-20-plugin-function-contract.md)接入。 +- 信任模型一步到位 publisher 签名:Windows 验签载体需要单独评估,v0 以 bundled 加全量审批兜底,签名延后是显式记录的缺口而不是遗漏。 +- 在 `lsp_start` 一类原生命令里拉起插件服务器:现有 LSP 会话全部由 Core 统一管理(会话、取消、事件泵都在 Core),前端只解析可执行路径与参数;另建原生拉起路径会分裂会话所有权,不采用。 + +## 后果 + +- 收益:Windows 用户开始能安装与运行契约 v1 语言插件,语言插件包与 macOS/Lithe Store 同格式;契约验收(同组 fixtures)成为 Windows 持续回归的一部分;后续里程碑只增不改这条链路。 +- 代价:双模型并存是用户可见的复杂度,管理界面必须分组。`minimumVersion`、内容摘要复查与启动前校验已接入;发行者签名仍延后,不能把完整性检查当作发布者身份验证。 +- 已知风险:签名延后意味着 v0 社区包信任完全依赖审批界面(M2),文案必须明示未认证状态;Windows 的 reparse point/junction 语义与 macOS symlink 不完全相同,包内路径逃逸判断已按 Windows 语义单独测试;WebView2 对自定义 scheme 与 CSP 的实现细节与 WKWebView 有差异,`ui:2` 网页链路已用 Windows Release/WebView2 实机验证,停靠、图标、动作状态及布局仍为 partial。 +- M2–M4 里程碑:磁盘/市场安装链路 + 管理界面;声明式设置 + 运行/测试计划 + PHP 契约包替换;Web 工具窗口。每个里程碑独立 PR,能力声明与矩阵条目随代码同 PR 更新。 + +## 验证 + +- 多窗口事件投递必须显式指定当前产品窗口的 `Window` target。Tauri 默认 `Any` 监听也接收发给其他 label 的事件,原生 `emit_to(ownerLabel)` 本身不足以隔离接收者;文件请求与生命周期确认均绑定当前窗口,防止其他工作区误处理请求或重复确认。`plugin-lifecycle-listener.test.ts` 检查两类监听的目标。 +- 2026-10-24 Windows Release 实机验收:Playwright/CDP 连接真实 WebView2,测试包安装、ui 1 文本渲染、工作区关闭重开、状态隔离、两个产品窗口归属、主题/语言与重载、过期工作区 openFile 拒绝、双窗口卸载关闭全部插件页均通过;无重复确认异常,外站 HTTP GET 为零,安装包及发行资源哈希不变,应用正常退出。该测试包不替代真实语言服务器的完整安装→启动→即时设置验收,也不覆盖停靠布局。 +- 管理并发回归:`lifecycle.rs` 覆盖原生使用计数排空、禁止晚到获取、逐窗口确认、停止失败、窗口断开和有界超时;`lsp-core-adapter.test.ts` 用可控 Core 响应覆盖停止期间晚到的服务器,断言先停止再拒绝,且 `pluginId` 没有丢失。`plugin-lifecycle-listener.test.ts` 验证收到停用事件立即撤销入口、等待运行和 LSP 两方后才确认。 +- 工作区与界面回归:`web.rs` 覆盖双工作区窗口/状态键隔离和文本转义;`plugin-lifecycle-listener.test.ts` 验证其他工作区请求、路径解析中切换工作区以及成功确认;`workspace-lifecycle.test.ts` 验证关闭工作区时清理网页。`declarative-plugin-ui.test.ts` 用 Hello Lithe fixture 验证文本窗口与命令面板、菜单、工具栏三处贡献及撤销,受控刷新同时覆盖相关与无关插件的撤销。CI 按文件分进程执行插件套件,避免原生命令和事件 mock 跨套件泄漏。 +- 启动计划回归:`launch.rs` 验证必需资源缺失、运行时依赖缺失、目录资源与普通文件入口的区别,以及有输出但非零退出的校验命令。`tests.rs` 还覆盖完整扫描的有序发布,`web.rs` 用有界通道覆盖写入与撤销交错、旧页面实例和旧工作区版本拒绝。Windows Rust 最终计时运行记录 357 项,其中 354 通过、3 项夹具跳过;前端受影响测试分别在隔离进程中运行,报告保存在 `.artifacts/test-stability/`。这些测试不替代双工作区真实 WebView2、真实语言服务器与停靠界面的产品验收。 + +- 网页隔离与上下文回归:`web.rs` 覆盖同包和外站 URL 的导航/桥接共用检查、真实路径与内部/外部目录链接;Windows 用无需提权的 junction,Unix 用目录符号链接。`plugin-web-appearance.test.ts` 执行原生宿主桥脚本,验证语言、主题、根变量、主题事件与外站不暴露桥;前端文件测试验证原生真实目标传递与拒绝传播。 +- 版本探测回归:`runtime_probe_collects_output_and_rejects_overflow`、`runtime_probe_reclaims_inherited_pipes_after_parent_exit` 使用当前测试二进制作为固定夹具;后者让父进程退出、后代保留管道,断言调用有界结束且后代已退出。测试环境变量仅附加到子进程,不改测试运行器的全局环境。运行 WindowsRust 与 Frontend 计时 harness,检查 `.artifacts/test-stability/index.html`;真实 WebView2 验证还要由本机 HTTP 计数器确认外站 GET 为零,同时检查导航、弹窗/下载、动态主题、包文件和发行目录哈希不变;单凭页面 URL 没变不能证明请求未外发。 + +- M4 Web 工具窗口:`plugin_host/web.rs` 测试覆盖包路径规范化(`..`、编码分隔符、空段拒绝)、CSP 内容断言(default-src none、无 wasm-unsafe-eval、frame-ancestors none)、桥脚本冻结与作用域、状态键隔离;web-panel fixture 随 `ui:[1,2]` 声明提升转为被宿主接受(能力门控测试同步)。桥 v0 的 build.*/functions.* 返回 `plugin.web.unknownMethod`,等构建系统能力接入。 + + +- M3 设置链路:`settings_tests`(`plugin_host/tests.rs`)覆盖项目覆盖往返、其他插件条目保留、损坏 store 报错不重置;能力门控测试随声明提升更新(settings-v2 fixture 被接受)。设置编辑 UI(`declarative-plugin-settings-editor.tsx`)按声明的 scope/effect 渲染并显示值来源徽标。 +- M3 运行计划:契约 PHP 包端到端测试(`bundled_php_contract_package_expands_plans`)走真实 Core 命令验证 validate、runPlan(`${file}`/`${userArguments}` 展开)、testPlan(workspace scope + 项目标记)、resolveSettings。 + + +- M2 磁盘导入链测试(`windows/tauri/src-tauri/src/plugin_host/install_tests.rs`):目录快照隔离(symlink 拒绝)、ZIP 解包与恶意路径(`../` 逃逸条目)拒绝、staging 安装(坏包失败保留现行版本且清空 staging)、回滚版本选择;runtime 启动的版本探测输出解析(node/java/go 真实输出格式)在 `launch.rs` 测试覆盖。 +- 管理界面(`declarative-plugins-section.tsx`)挂在设置的独立一级页,`settings-hierarchy.test.tsx` 的布局断言随导航更新;i18n 提供 en-US 与 zh-CN 全套文案。 + + +- 共享 fixtures 全量验收在 Windows 二进制内完成:`invalid-manifests.json` 全部 154 个用例经宿主校验路径断言同一稳定错误码(不带 hostCapabilities 的契约等价模式),合法样例在不带与带 v0 能力声明两种模式下分别断言通过与 `plugin.api.unsupported` 门控。测试入口:`windows/tauri/src-tauri/src/plugin_host/tests.rs`,运行 `cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml plugin_host`。 +- 存储与启动的单测覆盖安装记录缺失/损坏隔离、bundled 最高版本选择、版本比较(含 prerelease)、扩展与文件名匹配(大小写与前导点)、packaged 路径逃逸拒绝、runtime 的 `LITHE__PATH` 导出与 `PATH` 前置。 +- 前端回落链路测试:`windows/tauri/src/features/editor/lsp/resolve-editor-lsp-launch.test.ts` 断言契约插件启动映射与无归属文件返回 null;启动扫描步骤在 bootstrap 中执行。 +- M1 尚属静态与进程内验证:真实插件包安装→启用→语言服务器拉起的全链路实机验收随 M2/M3 完成,期间矩阵 `verificationStatus` 保持 pending。`./scripts/verify-windows-boundaries.sh`、`./scripts/verify-platform-feature-matrix-change.sh HEAD` 随 PR 通过。 + +## 适用范围 + +- `windows/tauri/src-tauri/build.rs`、`capabilities/plugin-web.json` 与 `permissions/declarative-web/bridge.toml`(应用内桥插件及最小来源权限) + +- `windows/tauri/src-tauri/src/plugin_host/`(存储、扫描、启动接线与 fixtures 验收测试) +- `windows/tauri/src-tauri/src/main.rs`(模块注册与命令注册) +- `windows/tauri/src-tauri/src/language_tools.rs`(复用的系统工具查找) +- `windows/tauri/src-tauri/src/plugin_host/install.rs`(磁盘导入、卸载、回滚、启停) +- `windows/tauri/src-tauri/src/extension_store_placeholder.rs`(Worker 扩展落盘命令的显式占位) +- `windows/tauri/src/features/plugins/declarative-plugin-launch.ts`(前端宿主 API) +- `windows/tauri/src-tauri/src/plugin_host/settings.rs`(设置双层存储与命令) +- `windows/tauri/src-tauri/src/plugin_host/run_plans.rs`(运行/测试计划命令) +- `windows/tauri/src/features/plugins/declarative-plugin-settings-editor.tsx`(设置编辑) +- `windows/tauri/src/features/plugins/declarative-plugin-run.ts`(契约插件运行入口) +- `windows/tauri/src-tauri/src/plugin_host/web.rs`(Web 工具窗口 scheme、桥与状态) +- `Plugins/win/Official/PhpSupportContract/`(契约版 PHP bundled 包) +- `windows/tauri/src/features/plugins/declarative-plugins-section.tsx`(设置内管理页) +- `windows/tauri/src/features/editor/lsp/resolve-editor-lsp-launch.ts`(契约插件回落来源) +- `windows/tauri/src/features/bootstrap/bootstrap-async.ts`(启动扫描步骤) +- `shared/platform-feature-matrix/features/declarative-plugin-contract.json`、`plugin-marketplace.json`、`plugin-extension-management.json`、`declarative-plugin-settings.json`、`php-optional-plugin.json`、`plugin-web-tool-windows.json` diff --git a/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md b/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md new file mode 100644 index 0000000000..37783dfe71 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md @@ -0,0 +1,203 @@ +# Agent 笔记:插件契约在主仓库定义,完整副本、SDK 和开发者资料发布到 lithe-plugin-docs + +状态:已实现 + +## 先说结论 + +插件契约(`shared/contracts/plugins/v1/`,规定 `plugin.json` 每个字段的含义、错误码和标准答案用例)仍然在主仓库定义,由 Rust Core 执行。公开仓库 `Lithe-IDEA/lithe-plugin-docs` 的 `contract/v1/` 保存这个目录的**完整逐字节副本**,另外放开发指南、从 Lithe Store 搬过来的两篇教程、两个官方 SDK(源码、发版和端到端测试)和示例插件列表。插件开发者只看 lithe-plugin-docs 就能开发,写插件、发布插件都不需要克隆主仓库,也不需要给主仓库提 PR。 + +日常开发要记住四件事: + +1. 契约目录下任何文件合并后,都要到 lithe-plugin-docs 运行同步脚本。这条规则也写在根目录 `AGENTS.md`。 +2. 契约 README 里指向目录外的链接必须用绝对 URL,因为副本只包含这个目录。 +3. 契约 fixture(测试用的示例清单)按“演示什么能力”命名,不按真实插件命名。 +4. 主仓库不再保存 SDK。SDK 的修改和发版都在 lithe-plugin-docs;改 Core 的插件后端协议时,合并前要用它的 `scripts/sdk-e2e.mjs` 跑一次 SDK 端到端测试。 + +## 问题 + +主仓库里有很多插件 JSON,看起来像是各个插件仓库的配置副本。逐个盘点后,它们分成三类: + +1. **契约本身**:schema、README 和错误码。Rust Core 的测试用 `include_str!` 直接读取它们,Swift 测试也读取其中的 fixture,所以它们必须和执行它们的代码在同一个提交里修改。 +2. **契约 fixture**:文件名是 `maven-build`、`php-support`、`cargo-build` 这类真实插件名,ID 也是 `dev.lithe.fixture.maven-build`。实际上它们只是借 Cargo、Go、Gradle、Maven 当真实输入,用来演示某种能力,例如“从命令输出解码项目模型”。名字让人误以为主仓库在维护这些插件的配置。 +3. **确实属于插件仓库的副本**:例如 `rust/lithe-core/src/tests/fixtures/community_build_plugins/` 中 Maven、Gradle 插件的快照。这一类本次不处理,留作后续迁移。 + +除此之外,外部开发者需要的东西分散在主仓库和 Lithe Store: + +- 字段语义、错误码和标准用例只在主仓库。 +- schema 的 `$id` 是 `https://lithe.dev/...`,但这个域名不归我们所有,地址解析不到,编辑器没法用它。 +- 两个 SDK 的源码在主仓库,Node SDK 也没有正式发布,只能从主仓库复制 `src/`。 +- 教程在 Lithe Store,Store 还有一份已经过期的 schema 副本。没有一个“我要写插件,从哪里开始”的入口。 + +## 决策 + +### 契约在主仓库定义 + +契约和执行它的代码(Rust Core 的校验和标准答案测试)必须在同一个 PR 里修改。如果把契约的源头移到别的仓库,改一个字段就要两个 PR,中间还会出现“契约写的是 A、Core 做的是 B”的时间窗口。外部开发者只需要**读**契约、不需要**改**契约,读的需求由完整副本满足。 + +### lithe-plugin-docs 是开发者的唯一入口 + +- `README.md`:中文开发指南,讲插件结构、API 等级怎么选、编辑器校验、后端 SDK 和示例插件。 +- `guides/`:从 Lithe Store 搬来的《LSP 插件完整生命周期》和《构建、运行与测试接入》。契约链接改为本仓库的 `contract/v1/`,发布相关链接指向 Store。 +- `contract/v1/`:主仓库 `shared/contracts/plugins/v1/` 整个目录的逐字节副本,包括 README、两份 schema 和全部 fixture。 +- `contract.lock.json`:记录来源仓库、来源目录、commit 和每个文件的 SHA-256,格式与 Cargo 插件的 `sdk.lock.json` 相同。 +- `sdk/node`、`sdk/java`:两个官方 SDK 和各自的示例插件;`sdk/e2e`:两个示例共用的端到端场景(Rust 测试文件)。 +- `scripts/contract.mjs` 提供四个命令: + - `sync --revision `:按来源目录在该 commit 的 git tree 整体替换副本,新增、删除、修改的文件都跟上,然后重写锁文件。 + - `verify`:离线检查锁文件、哈希、schema `$id`、Markdown 相对链接和未锁定文件。 + - `verify --upstream`:用 GitHub API 取锁定 commit 的 tree,按 git blob ID 比较整个目录。 + - `drift`:检查主仓库 `preview` 分支上的契约是否已经变化。不用默认分支:主仓库的默认分支是稳定版 `main`,只在发版时追上 `preview`,拿它比较会让刚同步过的副本每周都报差异。 + + docs 仓库的 CI 每次提交运行前两项检查和脚本测试,每周运行一次 `drift`。 +- `scripts/sdk-e2e.mjs`:SDK 端到端测试,见下文“SDK 整体移到 lithe-plugin-docs”。 + +只复制 schema 不够:字段之间的约束、错误码和预期结果都写在契约 README 和 fixture 里,只有 schema 的话,开发者仍然要回主仓库查。 + +### Lithe Store 只管收录和上架 + +Store 保留 `publishing.md`、`versioning-and-updates.md`、`maintainers.md` 和 `CONTRIBUTING.md`,讲怎么提交收录、怎么追踪版本。两篇教程搬到 lithe-plugin-docs,Store 的 `schema/plugin-index.schema.json` 副本删除。 + +Store 的 `validate-index.mjs` 仍然实现索引规则。它的 CI 原来比较本地 schema 副本和主仓库 preview,现在改为检查 lithe-plugin-docs 上 `plugin-index.schema.json` 的 SHA-256 是否等于工作流里记录的值。不相等说明索引契约变了,Store 维护者更新校验脚本后再更新记录的哈希。 + +### Schema 地址固定为 docs 仓库的 raw 地址 + +两份 schema 的 `$id` 是它在 docs 仓库里的地址: +`https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/<文件名>`。 + +两边都对这个地址做严格检查: + +- 主仓库的 `scripts/verify-shared-contracts.sh` 用常量 `published_plugin_schema_base` 要求 `$id` 完全相等。 +- docs 仓库的 `contract.mjs` 要求每份 schema 的 `$id` 等于它自己的发布地址。 + +因此,如果主仓库改了地址而 docs 仓库没跟上,或者 docs 仓库里的文件不是从主仓库同步来的,CI 都会失败。 + +### 契约 README 的外链写绝对地址 + +副本只包含契约目录,`../` 开头的相对链接在 docs 仓库里会失效。所以契约 README 指向 `rust-core-api.md`、Agent Notes 的链接都写成 `https://github.com/1lck/Lithe-IDEA/blob/preview/...`,指向 SDK 的链接写成 lithe-plugin-docs 的地址。 + +两边都检查这条规则: + +- 主仓库的 `verify-shared-contracts.sh` 拒绝离开目录或指向不存在文件的相对链接。 +- docs 仓库的 `contract.mjs` 在同步和校验时也拒绝这类链接。 + +### `plugin.json` 里不能写 `$schema` + +Core 解析清单时拒绝未知字段(serde `deny_unknown_fields`),schema 本身也是 `additionalProperties: false`。如果开发者按习惯加一行 `"$schema"`,插件会因为 `plugin.manifest.invalid` 装不上。所以指南要求在编辑器里按文件名关联 schema,例如 VS Code 的 `json.schemas`。是否在以后的契约版本里允许 `$schema` 另行决定:那需要提高能力版本,老版本 Lithe 会拒绝新格式。 + +### fixture 按能力命名 + +`shared/contracts/plugins/v1/fixtures/` 中的 fixture 改为按演示的能力命名,ID 同步改成 `dev.lithe.fixture.<新名字>`: + +| 原名 | 新名 | 演示什么 | +| --- | --- | --- | +| `php-support` | `language-packaged-server` | 随包附带语言服务器 | +| `go-support` | `language-system-command` | 使用用户安装的命令作为语言服务器 | +| `java-support` | `language-host-integration` | 交给宿主集成的语言服务器 | +| `cargo-build` | `build-static-file` | 从项目文件读取构建模型 | +| `go-build` | `build-command-stdout` | 解码命令的标准输出 | +| `gradle-build` | `build-command-output-file` | 解码命令写出的文件 | +| `maven-build` | `build-host-integration` | 把构建模型交给宿主集成 | +| `gradle-provider` | `build-provider` | 构建后端 | +| `mix-provider` | `build-provider-language-server` | 与语言服务器共用进程的构建后端 | + +fixture 的内容仍然用 Cargo、Go、Gradle、Maven 作为真实输入,契约 README 写明“它们不是这些插件”。macOS 测试中同 ID 的展示用副本(`macos/Tests/LitheTests/Fixtures/BuildSystems/`)一起改名。 + +`shared/fixtures/plugins/language-support-v1.json` 保留旧 ID `dev.lithe.fixture.go-support`。它是已退役的 schema 1 格式,只被 `rust/lithe-core/src/tests/plugins.rs` 的旧格式测试读取,与声明式契约无关。 + +### SDK 整体移到 lithe-plugin-docs + +SDK 是给插件开发者用的库,不是契约。开发者要读它的 README、看示例、提问题、下载发布包,这些都应该在 lithe-plugin-docs 完成,所以 `sdk/`、SDK 端到端测试和两个发版工作流都从主仓库移走,主仓库只保留契约。 + +- **发版**:lithe-plugin-docs 打 `sdk-node-v<版本>` 或 `sdk-java-v<版本>` 标签,`release-node-sdk.yml`、`release-java-sdk.yml` 发布包和 `.sha256`。具体规则见 [官方插件后端 SDK](2026-10-20-plugin-backend-sdk.md)。Java SDK 0.1.0 是迁移前从主仓库发布的,那个 Release 保留,Maven 插件锁定的下载地址不变。 +- **单元测试**:SDK 自己的测试读 `contract/v1/fixtures/function-protocol.json`,也就是契约副本,不再读主仓库。 +- **端到端测试**:场景仍是 Rust 测试,要链接 Rust Core。`sdk-e2e.mjs` 把锁文件记录的主仓库 commit 稀疏检出到 `.artifacts/lithe`(只取 `rust/` 和 `shared/`),把 `sdk/e2e` 复制进它的 `rust/lithe-core/tests/`,再用 `cargo test --locked` 运行。`LITHE_PLUGIN_SDK_ROOT` 告诉测试示例插件在哪里。这和 Cargo 插件测 Core 的方式相同。 +- **两个版本的 Core**:每次提交测契约副本锁定的那个 commit,保证 SDK、契约副本和 Core 是同一个版本;每周再用 `--ref preview` 测主仓库 `preview` 分支的最新 Core,提前发现 Core 的改动破坏了 SDK。 + +代价是主仓库的 PR 不再当场发现“Core 改动破坏 SDK”。改插件后端协议(`rust/lithe-core/src/lsp/interface/plugin_function.rs`、`litheBuild` 相关代码)时,合并前要在 lithe-plugin-docs 用 `--source <本地主仓库> --ref <分支>` 手动跑一次。 + +### 正确做法 + +- 修改契约:在同一个 PR 里改 `shared/contracts/plugins/v1/`、Core 和测试。合并后,在 lithe-plugin-docs 运行 `node scripts/contract.mjs sync --revision <合并后的 preview commit>`,然后提交。 +- 改了 `plugin-index.schema.json`:同步 docs 仓库之后,Lithe Store 的 CI 会失败,按 Store 的 `docs/maintainers.md` 更新索引校验和记录的哈希。 +- 在契约 README 里引用目录外的文件:写 `https://github.com/1lck/Lithe-IDEA/blob/preview/<路径>`。 +- 新增 fixture:按演示的能力命名,例如 `build-command-stdout.plugin.json`,ID 使用 `dev.lithe.fixture.` 前缀。 +- 新增面向插件开发者的说明:写进 lithe-plugin-docs 的 README 或 `guides/`。 +- 修改或发布 SDK:在 lithe-plugin-docs 提 PR、打标签。 +- 修改插件后端协议:合并前在 lithe-plugin-docs 运行 `node scripts/sdk-e2e.mjs --source <本地主仓库> --ref <分支>`。 + +### 不要这样做 + +- 在 lithe-plugin-docs 里直接编辑 `contract/` 下的文件,或者让主仓库 schema 的 `$id` 指向别的地址。两边的 CI 都会失败。 +- 在契约 README 里写 `../` 开头的相对链接。 +- 用真实插件名命名 fixture,例如 `maven-build.plugin.json`。读者会以为主仓库在维护那个插件的配置。 +- 在指南或示例里让开发者往 `plugin.json` 写 `$schema`。 +- 把插件开发教程写回 Lithe Store 或主仓库的 `Plugins/`。 +- 在主仓库重新加 `sdk/` 目录,或者让主仓库 CI 检出 lithe-plugin-docs 跑 SDK 测试。依赖方向是 docs 仓库依赖主仓库,反过来会让主仓库的结果取决于另一个仓库的 `main`。 + +## 考虑过的备选方案 + +### 把契约的源头移出主仓库 + +开发者只看一个仓库,这个目标现在由完整副本实现。真正把源头移走有两种做法,都没有采用: + +- **git submodule(子模块)**:主仓库测试读取子模块里的文件。每次改契约都要先在契约仓库合并,再在主仓库更新子模块指针;所有克隆和 CI 都要多一步初始化子模块。 +- **测试时按锁文件下载契约**:主仓库只保存锁文件,构建前下载。Rust 测试用 `include_str!` 在编译期读取 fixture,离线构建和每个 worktree 都要先准备下载物,还要纳入 worktree 资源复用清单。 + +两种做法都让“改契约、改 Core、改测试”变成跨仓库的多步操作,复杂度高于每次合并后同步一次副本。 + +### 只发布两份 schema 副本 + +这是最初的方案,docs 仓库只放 schema。没有采用,因为字段语义、错误码和标准用例都在契约 README 和 fixture 里,开发者遇到 schema 表达不了的规则时仍然要回主仓库。 + +### 把 schema 放到主仓库的 GitHub Pages 或主仓库 raw 地址 + +这样不用多建一个仓库。但 Pages 目前用来发布 Agent Notes 看板,自定义域名 `notes.lithe.top` 还没有 HTTPS 证书。如果用主仓库 raw 地址,开发者的入口仍然是主仓库,而且主仓库调整目录结构就会改变公开地址。 + +### 教程和契约副本都放在 Lithe Store + +Store 原来已经有教程和一份 `schema/plugin-index.schema.json`。没有采用,是因为 Store 负责收录和上架,它的 schema 副本已经和主仓库不一致(`$id` 还是 `lithe.dev`)。把“怎么写插件”和“怎么上架”分开,Store 的收录规则可以保持单一职责。 + +### SDK 留在主仓库,只单独发版 + +这是本次最初的做法:端到端测试留在 Rust Core lane,改 Core 时当场发现 SDK 被破坏。没有采用,因为开发者仍然要到主仓库读 SDK 的 README、看示例、提 issue,改 SDK 也要给主仓库提 PR,和“开发者只看 lithe-plugin-docs”的目标冲突。端到端测试改成按锁文件检出 Core 以后,跨仓库固定 Core 版本的成本可以接受。 + +### 主仓库 CI 检出 lithe-plugin-docs 跑 SDK 端到端测试 + +这样改 Core 时仍能当场发现问题。没有采用:主仓库的测试结果会取决于另一个仓库 `main` 分支的状态,docs 仓库合并一个坏提交就会让主仓库所有 PR 失败;同时形成两个仓库互相依赖。 + +### 把 Node SDK 发布到 npm + +npm 是 Node 开发者最熟悉的方式。但插件包安装后没有 `npm install` 步骤,开发者最终还是要把 SDK 文件复制进插件包。发布到 npm 还要管理组织和令牌,目前没有必要。 + +## 后果 + +- 收益:外部开发者只需要 lithe-plugin-docs;schema 地址可以真正解析,编辑器能直接提示;fixture 名字不再误导;两个 SDK 的源码、发版和问题都在同一个仓库,Node SDK 也有带哈希的正式下载。 +- 代价:契约变更后要多做一次同步。同步之前,docs 仓库的副本会落后于主仓库,每周的 `drift` 检查会失败提醒。 +- 代价:Core 改动破坏 SDK 时,主仓库 PR 不会失败,最晚由 docs 仓库的每周检查发现;docs 仓库的端到端测试每次都要编译一遍 Core。 +- 代价:契约 README 不能用相对链接引用主仓库的其他文件,这些绝对链接指向 `preview`,会随主仓库调整目录而失效。 +- 代价:schema `$id` 绑定了 docs 仓库 `main` 分支的 raw 地址。仓库改名或换默认分支都会改变公开地址,必须同时改两边的检查。 +- 后续工作: + - `community_build_plugins` 中 Maven、Gradle 快照迁回插件仓库。 + - Lua、Gradle、Maven 插件 README 里指向 Store 教程和主仓库契约的链接,改为指向 lithe-plugin-docs。 + - Windows PhpSupport 插件迁移。 + - 其他共享 schema 仍然使用 `lithe.dev` 作为 `$id`。 +- 重新评估的条件:如果契约开始需要外部贡献者直接修改,或者 Rust Core 拆成独立仓库,就重新考虑契约放在哪里。 + +## 验证 + +- `./scripts/verify-shared-contracts.sh`:两份 schema 的 `$id` 必须等于 docs 仓库的发布地址;契约 README 的相对链接不能离开目录;fixture ID 必须以 `dev.lithe.fixture.` 开头。 +- `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib -- declarative_plugins build_systems marketplace_plugins build_runtime_versions`:改名后的 fixture 仍然得到相同的标准答案。 +- `bash scripts/test-classify-ci-changes.sh`:主仓库的 CI 分类不再有 `sdk/` 路由。 +- 在 lithe-plugin-docs 运行 `node --test scripts/*.test.mjs` 和 `node scripts/contract.mjs verify --upstream`:同步脚本的测试通过,副本与锁定 commit 的契约目录完全一致。 +- 在 lithe-plugin-docs 运行 `node --test sdk/node/test/*.test.mjs`、`mvn -B -f sdk/java/pom.xml verify` 和 `node scripts/sdk-e2e.mjs`:两个 SDK 的单元测试读契约副本通过,两个示例后端在锁定版本的 Core 上跑完端到端场景。 + +## 适用范围 + +- `AGENTS.md` +- `shared/contracts/plugins/v1/` +- `scripts/verify-shared-contracts.sh` +- `scripts/verify-rust-core-crates.sh` +- `scripts/classify-ci-changes.sh` +- `.github/workflows/ci-macos.yml` +- `Plugins/README.md` +- `https://github.com/Lithe-IDEA/lithe-plugin-docs` +- `macos/Tests/LitheTests/Fixtures/BuildSystems/` diff --git a/.agents/notes/implemented/architecture/2026-10-23-shared-i18n-generation.md b/.agents/notes/implemented/architecture/2026-10-23-shared-i18n-generation.md new file mode 100644 index 0000000000..a70fb27a65 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-23-shared-i18n-generation.md @@ -0,0 +1,108 @@ +# Agent 笔记:共享翻译来源与双端构建时生成 + +状态:已实现 + +## 先说结论 + +macOS 和 Windows 的全部自有翻译资源改为在 `shared/i18n/` 维护,再在构建前生成各自的 +资源文件。开发者修改一次翻译即可更新两端,现有页面仍使用各自的翻译接口。 +原生字符串表、Windows 原字典及页面内的中英文文案已完整迁入;上游消息、用户数据和搜索同义词仍由各自所有者维护,不增加在线语言包。 + +## 问题 + +两端原来各自维护中英文,macOS 多用英文原文作 key,Windows 使用语义标识;参数还 +分别使用 `%@` / `%lld` 和 `{name}`。直接合并会丢失语境与参数类型,也会把 Finder +和 File Explorer 等合理差异抹掉。语言选项、Swift 枚举和原生打包列表原本也固定为两项。 + +## 决策 + +共享语言清单声明语言标识、自称名称、回退语言和各平台默认值。生成器输出语言选项、 +Swift 枚举、Windows 类型及字典;macOS 打包从同一清单读取列表。保留用户已经保存的 +`en` / `zh-Hans` 与 `en-US` / `zh-CN`,也保留 macOS 默认英文、Windows 默认中文。 +增加内置语言只需扩充清单和语言文件,再生成资源,不用修改语言选择控件。 + +词条使用描述用途的语义 key。相同英文并不自动表示相同语义,平台适用范围也不代表 +功能已经实现;能力对齐继续由功能矩阵记录。平台覆盖(override,即单个平台需要 +不同措辞)必须说明原因,第一批覆盖保留现有文案,包括文件管理器名称。 + +macOS 继续复用 Foundation 的原生字符串表与格式化能力,Windows 保留现有翻译接口。 +生成器不实现运行时语言引擎:它把共享词条放进既有 `Localizable.strings` 的生成区域, +同时输出语义 key 和旧英文 key 的兼容别名。新旧页面因此读取同一张表,不用一次替换 +全部 SwiftUI 隐式本地化调用。Windows 仅使用生成字典,原独立字典已删除;所有词条在生成时展开显式回退链。页面不再用中英文分支保存翻译,明确语言的呈现经由 AppLanguage 原生资源接口。 + +正确做法:修改 `locales/zh-CN.json` 的 `ui.save`,重新生成,提交源文件和生成结果。 +不要在 macOS 生成区或 Windows 旧字典里再写一个 Save;校验会拒绝双重维护。 +当两个用途需要不同翻译时建立两个语义 key,不能让同一旧别名归属于两个 key。 +同步其他功能分支时,先把新加入的原生旧 key 和 Windows 字典词条迁入共享源, +保留已有译文、参数宽度和位置格式,再重新生成。不要把两套字符串表拼接到生成区域; +以同步分支的实际资源逐 key 比对生成结果,确认没有丢词或改写平台措辞。 + +参数必须声明为字符串或整数。macOS 别名还声明旧调用方传参顺序,生成器转换成 +对应的原生格式;历史 `%d` / `%llu` 的 ABI 类型由显式 formats 保留,不能改变传参宽度。翻译调整顺序或重复参数时使用带位置的格式,并转义普通百分号。 +Windows 只替换一轮占位符,避免把用户传入的 `{name}` 当成另一个参数展开。 +当前不支持复杂复数、任意 printf 指令或 ICU 模板;不在缺少实际需求时自建这些能力。 + +语言文件的覆盖率统计全部共享词条。完整语言缺词会失败;明确允许回退的语言可缺词, +但回退链必须到达完整默认语言。生成器还拒绝重复 key、未知字段、非法平台、越界覆盖、 +参数不匹配、原生位置格式越界或混用、Swift 关键字及生成成员重名、双重定义和过期产物。 +检查模式只读;所有输入及生成计划验证完成后才写文件。 +生成文件通过 `.gitattributes` 固定为 LF 换行,避免 Windows Git 自动转换换行后误报 +产物过期;集成测试在临时仓库中开启 `core.autocrlf`,实际检出后比较生成文件字节。 + +资源生命周期由 Git 和本次构建管理,不创建下载、运行时缓存或可跨工作树复用的生成物。 +原生打包只在目标副本转换二进制 plist,随后签名;运行时不写 bundle 或 Windows 安装 +资源,所以不改变代码签名及 Sparkle 增量更新基线。资源复用清单明确拒绝复制这类文件, +每个工作树根据自己的源版本重新生成。 + +## 考虑过的备选方案 + +- 两端运行时读取共享 JSON:需要新增原生加载与回退路径,破坏现有 SwiftUI 字符串表 + 查找方式,增加启动和语言切换状态,当前没有收益足以承担这个代价。 +- 一次替换全部词条:会混淆同名不同义文案,改动面大,回归难定位。先迁移通用操作和 + 少量设置标签验证架构,再完整迁入原有字典并比对每个原 key 的值,保证迁移不改变现有文案。 +- 为每个平台保留独立共享表:容易使 SwiftUI 的默认表和显式查找得到不同结果,因此 + 在同一个默认表中划出可检查的生成区域。 +- 使用新的多语言运行时库:当前已有原生表和简单命名插值,复用其完整调用边界即可。 + 复杂复数或第三方包出现实际需求后再评估成熟工具,不提前设计下载与热加载协议。 + +## 后果 + +已迁移词条有一个编辑来源,新增语言能贯通选择、生成和打包;平台差异和旧设置值保留。 +代价是提交中包含可复验的生成产物,并需要同时提交共享源与生成结果。macOS 旧英文 key 的语义 +歧义仍存在,新用途应使用独立语义 key;原资源缺少的译文以英文回退,不声称这些句子都已人工翻译。 +现有词条生成检查通过不等于两端整机语言切换验收完成。 + +## 验证 + +- `node scripts/generate-i18n.mjs --check`:清单、参数、覆盖和产物一致性;只读。 +- `node --test --test-timeout=60000 scripts/test-i18n.mjs`:非法输入、生成物所有权、新增语言、 + 平台覆盖、回退链和 macOS 二进制打包;比较打包前后源文件字节。 +- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh`:稳定性静态规则。 +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'SharedI18nTests|AppLocalizationTests|LocalizationPackagingTests|GitLocalizationTests'`:原生资源查找、兼容 key、实际参数格式化、切换及原有行为。 +- `node .agents/skills/write-stable-tests/scripts/run-bun-tests-with-timing.mjs -- src/i18n/locale.test.ts src/i18n/shared-i18n.test.ts src/features/settings/lib/settings-normalization.test.ts`:Windows 前端资源与设置;宿主平台必须在报告中注明。 +- `./scripts/verify-runtime-bundle-immutability.sh`:同时检查 macOS 与 Windows 运行时资源路径。 +- `node --test --test-timeout=15000 scripts/test-reuse-worktree-resources.mjs`:拒绝跨工作树资源复制。 +- `./scripts/verify-shared-contracts.sh`、`./scripts/verify-windows-boundaries.sh` 和 + `./scripts/verify-platform-feature-matrix.sh`:共享边界与能力记录。 + +生成器测试与原生测试共同消费 `shared/fixtures/i18n/format-v1.json` 和 `native-abi-v1.json`,覆盖32位有符号、64位无符号参数及参数重排、重复、 +大整数、零、Unicode 和用户值中的格式符。独立 CI 在 Linux、macOS、Windows 执行生成器 +测试并输出单项计时;Draft PR 只运行 Linux,转为 ready 后再触发三系统校验, +同一 PR 的旧 i18n 校验会取消。macOS 才运行原生打包用例。完整产品在中英文设置中切换、关闭重开并 +检查保存值和其他模块页面;Windows WebView2 整机验收仍须在 Windows 上执行。 + +Node 22 会把命令行超时同时应用到整份文件。词条增加后,macOS CI 在第 32 项之后 +触及原 15 秒文件预算,单项没有断言失败。文件预算因此为 60 秒;源码中单项最长 +15 秒、打包子进程 5 秒和 CI 步骤两分钟的边界都保留,不能通过扩大单项预算处理套件成本。 + +## 适用范围 + +- `shared/i18n/` +- `shared/fixtures/i18n/format-v1.json` +- `scripts/generate-i18n.mjs` +- `scripts/package-macos-localizations.sh` +- `windows/tauri/src/i18n/` +- `macos/Resources/en.lproj/Localizable.strings` +- `macos/Resources/zh-Hans.lproj/Localizable.strings` +- `macos/Sources/Lithe/Models/Settings/AppLanguage.generated.swift` +- `.github/workflows/verify-i18n.yml` diff --git a/.agents/notes/implemented/architecture/2026-10-24-database-provider-plugin.md b/.agents/notes/implemented/architecture/2026-10-24-database-provider-plugin.md new file mode 100644 index 0000000000..748236ce0c --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-24-database-provider-plugin.md @@ -0,0 +1,233 @@ +# Agent 笔记:数据库拆成双端共用的独立插件 + +状态:已实现 + +## 先说结论 + +数据库连接功能已经从 Lithe 主仓库和主安装包中移出,成为组织仓库 `Lithe-IDEA/lithe-plugin-database` 中的社区插件 `io.github.lithe-idea.database`,在 Lithe Store 上架。只装 IDE 时没有任何数据库入口;从插件市场安装后才能使用。插件的界面是包内网页,后端用 Rust 编写,macOS 和 Windows 将共用同一份代码。 + +主仓库只保留通用的插件能力:不依附构建系统的插件后端、宿主凭据存储,以及一个过渡垫片——把旧版内置模块保存的连接、历史和密码迁入插件,并提示老用户安装。macOS 先上线;Windows 等插件宿主和插件市场就绪后,补一轮编译、适配和测试即可使用,插件本体不重写。 + +开发时记住一条:主仓库里不再为“数据库”写任何专属代码。SQL 判定、界面、驱动都只改插件仓库;主仓库只改通用的插件能力。 + +## 问题 + +### 体积:数据库在主包里占多少 + +2026-10-24 在本机 Lithe 0.6.0(arm64)上测量: + +| 部分 | 大小 | +| --- | --- | +| 整个 `Lithe.app` | 557 MB | +| `Contents/Resources/LanguageServers` | 397 MB | +| `Contents/MacOS/Lithe`(主程序,含数据库界面代码) | 105 MB | +| `Contents/Helpers/lithe-db-sidecar` + `lithe-db-mcp` | 约 9.4 MB | + +数据库的体积主要来自两个后端程序。不过,这次拆分的目的不只是体积:数据库还应该能单独安装、更新和卸载,并且两端共用一份实现。 + +### 现状:逻辑分布在哪里 + +| 层 | macOS | Windows | 是否共享 | +| --- | --- | --- | --- | +| 驱动与执行 | `rust/lithe-db-sidecar`,约 5400 行,支持 mysql、mariadb、postgresql、sqlite、sqlserver、mongodb、redis、nacos | `package-database-sidecars.ts` 要构建 `lithe-database` crate,仓库中没有这个 crate | 否 | +| 协议 | Swift 结构体(`DatabaseSidecarService.swift`)与 Rust serde 各自定义 | `features/database/types/` 里另一套 TS 类型 | 否,没有共享契约和测试样例(fixtures) | +| 领域规则 | `LitheDatabaseModule` 的 Models、Services,以及 `DatabaseFeatureModel` | TS store | 否 | +| 界面 | `Views/Database/`,约 6700 行 SwiftUI | `features/database/`,约 9400 行 React | 各写一套 | + +### 已经出现的偏差 + +1. **Windows 从来没有能用的数据库功能。** + - 前端调用 `connect_database`、`run_database_provider_command` 等 9 个 Tauri 命令,`windows/tauri/src-tauri/src/main.rs` 的命令注册表里一个都没有。 + - 这批前端代码在 2026-08-14 “React Tauri 替换 Qt 前端”时一次导入。仓库任何分支的历史里都没有对应后端,Qt 时期也没有数据库功能。 + - 用户能碰到这条坏路径:`file-utils.ts` 把 `.db`、`.sqlite`、`.duckdb` 文件交给数据库查看器,随后调用失败。 + - 功能矩阵把 Windows 的四项数据库能力标为 `implemented`;README 把数据库工作台列为核心功能,没有注明只支持 macOS。 +2. **macOS 把领域规则写在了壳里。** + - SQL 分析、语句拆分和格式化(`DatabaseSQLSupport.swift`); + - 结构对比、按方言生成 DDL(`DatabaseSchemaDiff.swift`); + - DBX 导入解密(`DatabaseDBXImportService.swift`); + - 字段遮罩、查询历史和定时备份。 + + 这些都写在 Swift 里,`DatabaseFeatureModel.swift` 有 2700 多行。 +3. **同一条规则写了三份,并且已经不一致。** + - “SQL 是什么类型、是否危险”分别由 Swift 的 `DatabaseSQLAnalyzer`(先去注释再按词判断)、sidecar 的 `is_dangerous_sql`(字符串匹配)和 mcp 的 `classify_sql`(另一套前缀匹配)判断。 + - 一个例子(由代码推导,未实机复现):连接开启生产保护后,执行 `DELETE FROM users`,换行后再写 `WHERE id = 1`。Swift 判断不需要确认,sidecar 却因为找不到 `" WHERE "` 而拒绝执行。macOS 不处理它返回的 `confirmation_required`,用户只看到报错,没有确认入口。 + - 反过来,开头带一行注释或使用 `WITH … DELETE` 的语句,能绕过 sidecar 的判断。 + +### 现有插件契约为什么装不下 + +- **网页界面只能操作构建系统。** 网页桥接对象(`window.lithe`)只能操作本插件声明的构建系统;插件函数在 macOS 上也只由构建 provider 的会话提供。数据库插件只能伪装成构建系统,违反[公共扩展契约](../../proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md)的“不要伪造声明”。 +- **插件拿不到宿主的凭据存储。** 插件调用宿主服务(`lithe/host/*`)在协议 v1 中全部被拒绝,没有地方安全地保存密码。 +- **Agent 用不上数据库能力。** `lithe-db-mcp` 只被打包进 `Contents/Helpers`,没有任何代码启动或登记它,Agent 实际调用不到。Lithe 已有自己的 `lithe` MCP(`lithe-mcp` 加 IDE 宿主的能力目录和项目授权),插件能力应当从这里进入,而不是再起一个独立 MCP 进程。 +- **Windows 还没有声明式插件宿主和插件市场。** 声明式插件宿主正在 #1424 中开发;插件市场目前只有 macOS 版。 + +## 决策 + +### 一、最终用户看到什么 + +- 只装 Lithe 时,没有数据库工具窗口、菜单或 Services 条目。 +- 在插件市场安装“数据库”插件后,数据库工具窗口出现:连接树、SQL 控制台、数据表格、结构对比,以及 Redis 和 Nacos 视图,都在宿主管理的窗口里,以插件网页显示。 +- 停用或卸载后,入口消失,插件拥有的进程全部结束。 +- macOS 先提供;Windows 在插件宿主和插件市场就绪后提供,使用同一个插件版本。 + +### 二、代码归属 + +| 位置 | 内容 | +| --- | --- | +| 组织仓库 `Lithe-IDEA/lithe-plugin-database`(新建) | Rust 后端(从 `lithe-db-sidecar` 演进,吸收 `lithe-db-mcp` 的查询能力)、网页界面、`plugin.json`、数据库专属的测试样例和兼容性测试 | +| 主仓库 | 只保留通用能力:契约、Rust Core 校验、两端宿主实现、Rust 后端 SDK 的契约测试 | +| 主仓库最终删除 | `macos/Sources/LitheDatabaseModule/`、`macos/Sources/Lithe/Views/Database/`、`rust/lithe-db-sidecar/`、`rust/lithe-db-mcp/`、`windows/tauri/src/features/database/` 及打包脚本中的数据库部分 | + +插件仓库只依赖公开契约:`lithe-plugin-docs` 中锁定的契约副本加官方 SDK,和 Cargo、SVN 插件的做法相同。主仓库不再按“数据库”写任何分支。 + +### 三、插件身份与分发 + +- 先作为社区插件发布:ID 为 `io.github.lithe-idea.database`,`vendor.signatureRequirement` 为 `"none"`,和 Cargo、SVN 等 7 个插件一样在 Lithe Store 上架。宿主按索引固定的 SHA-256 校验下载包,安装确认列出权限和后端程序,加载时重新核对安装文件的内容摘要。 +- 为什么暂不做官方签名:官方签名的完整路径(`lithe-official-plugins-v2` 公钥、`release-official-plugins.yml`、官方索引 `Plugins/marketplace/index.json`)从未发布过任何插件。v2 私钥在原组织主仓库的维护者手里,该仓库已改名归档,官方索引地址也还指向它。另配一把密钥是信任根变更,需要单独决定,所以先不走这条路。 +- 插件仓库的发布工作流在 `v*` 标签上测试、打包 universal 后端、跑端到端检查,然后发布 GitHub Release(zip 与 SHA-256);上架和升级是向 Lithe Store 提 PR 登记下载地址和 SHA-256。 +- 网页界面和清单在各平台通用,只有 Rust 可执行文件按平台编译;macOS 发一个 universal 包,同时登记 `darwin-arm64` 和 `darwin-x64`,Windows 就绪后再加 Windows 的包。 +- 构建阶段固定依赖,规则沿用[LSP 插件构建与资源归属](2026-09-30-lsp-plugin-build-and-distribution.md)。安装目录只读,运行状态写进平台存储。 + +### 四、主仓库要新增的通用能力 + +每一项都先写进契约、Rust Core 和测试样例,再由 macOS 宿主实现;Windows 宿主(#1424)按同一份契约实现。 + +1. **插件后端(`backend` 1,已在契约中实现)。** 清单的 `backend` 字段只含 `serverProcess` 的启动字段,需要 `backend.launch` 权限和一个网页工具窗。设计取舍: + - **每个插件一个进程,宿主全局,不按工作区划分。** 数据库连接属于用户而不是项目,按工作区起多份进程只会重复连接池;因此后端不能使用 `${workspaceStateDir}`,状态写在 `${stateDir}`,宿主以 `${stateDir}` 作为根目录和工作目录启动它。 + - **按需启动、随插件结束。** 本插件网页第一次调用 `functions.*` 时启动;插件停用、更新、卸载或宿主退出时结束。 + - **授权就是安装审查。** 后端在安装审查中以 `packagedBackend` 等条目列出,调用时不再按工作区弹同意框;带 `instance` 的构建 provider 调用仍沿用原来的同意规则。 + - **不承担领域契约。** 后端只回答插件函数,不回答 `lithe/build/*` 或 VCS 请求,避免再次“伪装成构建系统”。 + - 网页桥接 `functions.list` / `functions.invoke` 省略 `instance` 即调用本插件后端,复用现有[插件函数协议](2026-10-20-plugin-function-contract.md)的注册、调用、进度、取消和超时规则。 +2. **宿主凭据存储(插件函数协议 v2,已在契约中实现)。** v2 是第一个提供宿主服务的协议版本:`lithe/host/credentials/{get,set,delete}`,需要 `credentials.store` 权限(没有后端时声明即无效)。Core 在宿主看到请求前校验协议版本、授权和参数(key 只允许 `[A-Za-z0-9._:-]` 且不超过 128 字节,secret 不超过 16 KiB,拒绝未知字段以便后续版本扩展),通过 `hostRequest` 事件交给宿主,宿主用 `lsp.hostResponse` 应答;每个会话最多 16 个未应答请求。macOS 使用 Keychain,按插件 ID 划分命名空间;插件读不到别的插件或宿主自身的凭据。生命周期:停用和更新时保留,卸载时与 `${stateDir}` 一起删除。 +3. **Agent 工具经由 `lithe` MCP(后续)。** 不新增“插件声明 MCP 服务”的契约,也不保留独立的 `lithe-db-mcp` 进程。之后由插件以插件函数的形式提供 Agent 工具,IDE 宿主把它们登记进现有 `lithe` MCP 的能力目录,沿用其项目授权;插件停用时工具随之消失。这一项在数据库插件上线后单独设计,不阻塞第 1 到第 5 期。 +4. **按平台的可执行文件:不改契约。** 每个平台单独出下载包,可执行文件放在包内固定路径(例如 `bin/backend`),macOS 包可以是 universal 二进制。`${arch}` 本来就不能用于 `packaged` 路径,这样也不需要。Windows 的 `.exe` 等 Windows 宿主就绪时再定。 +5. **Rust 后端 SDK。** 在 `lithe-plugin-docs` 中新增 `sdk/rust`,实现插件函数协议和宿主能力,并补端到端样例,和 Node、Java SDK 对齐。 + +### 五、规则只保留一份 + +- SQL 分析、语句拆分、标识符引用、DDL 生成和危险语句判定,全部只在插件的 Rust 后端里实现。界面要不要弹出确认框、执行时要不要拦截,都使用同一个分析结果。 +- 插件仓库维护数据库专属的黄金样例。必须覆盖:换行或制表符分隔的 `WHERE`、开头的行注释和块注释、`WITH … DELETE`、多语句、无法识别的语句。 +- 主仓库只维护通用能力的样例(`fixtures/backend.plugin.json`、`function-protocol.json` 的 `hostRequests`),不再维护数据库样例。 + +### 六、老用户迁移 + +- **只迁给 Lithe Store 上的那份插件。** 社区包不带 Lithe 签名,插件 ID 谁都能用,所以垫片不认 ID,只认来源:安装记录必须是从 Lithe Store(`PluginMarketplaceSource.communityID`)安装的社区包,且索引条目的仓库地址正是 `https://github.com/Lithe-IDEA/lithe-plugin-database`(`DatabasePluginTransition.repository`)。从磁盘安装、来自其他市场来源或仓库地址不同的同 ID 包得不到任何旧数据和密码,也不写迁移标记,之后改从 Lithe Store 安装仍会迁移。安装后的文件每次加载都按内容摘要复核,安装后被改动的包不会加载。 +- 迁移由宿主的过渡垫片 `MacDatabasePluginMigration` 完成。它挂在插件后端的启动路径上(`MacPluginBackendSessions` 的 `prepareState`),在数据库插件后端第一次启动、读取状态之前运行,所以插件永远不会先看到空状态。 +- 迁移内容:偏好里的连接(`database.profiles.v1`)、文件夹(`database.connection-folders.v1`)和 SQL 历史(`database.sql-history.v1`)写进插件的 `${stateDir}`(`connections.json`、`sql-history.json`);“永久”密码从旧 Keychain 服务移到插件凭据(key 为 `connection::password`)。旧的 `group` 字段变成顶层文件夹;历史只保留属于现存连接的最近 100 条,时间从 2001 年起算的秒换成 1970 年起算的毫秒。“直到重启”的密码本来就不落盘,不迁移。 +- 顺序有讲究:先迁密码,再写历史,最后写 `connections.json`,然后才写迁移标记。这样任何一步失败,下次启动都会重试;`connections.json` 的存在表示插件已有自己的状态,之后永远不会被覆盖。迁不动的密码只记日志(不含密码本身),对应连接改为重新询问。 +- 旧数据原样保留,旧版 Lithe 仍能读到。备份计划和恢复点没有迁移,因为旧模块里没有任何界面能列出或使用它们。 +- 升级后,保存过连接、又没装插件的用户会看到一次通知“数据库已改为插件……”,带“打开插件市场”按钮(`DatabasePluginTransition`、`AppModel+DatabasePlugin`)。不自动在后台下载。通知按钮是通用能力:带动作的工作台通知会一直保留到用户处理。 +- 垫片同时知道旧模块和插件的存储格式,这是有意为之的过渡代码;等所有受支持的 Lithe 版本都不再内置数据库模块后删除。 + +### 七、Windows + +- 功能矩阵中 Windows 的数据库能力为 `missing`;`.db`、`.sqlite`、`.duckdb` 文件按普通文件打开。 +- **暂不适配:** 这次改造不为 Windows 做任何插件侧适配,也不要求 #1424 现在对齐。等 Windows 插件宿主和插件市场改造完成后,再按当时的宿主能力小幅调整插件逻辑,补 Windows 构建、WebView2 回归测试和发布。 +- 原有约 9400 行 Windows 数据库前端没有迁移,已随主仓库的数据库代码一起删除。暂不支持 DuckDB。 + +### 八、分期 + +| 期 | 内容 | 依赖 | +| --- | --- | --- | +| 1 | Windows 矩阵、`.db` 拦截、README 修正;修复 `WHERE` 误判,让 sidecar 与界面使用一致的词法判断,并让 macOS 处理 `confirmation_required` | 无 | +| 2 | 契约:插件后端、函数协议 v2 凭据宿主服务;Rust Core 校验与样例 | 无 | +| 3 | macOS 宿主实现第 2 期能力;`lithe-plugin-docs` 新增 Rust SDK | 第 2 期 | +| 4 | 新建 `lithe-plugin-database`:Rust 后端、网页界面、样例;先在 macOS 上以本地开发包验证 | 第 3 期 | +| 5 | macOS 发布插件,加入迁移提示,从主仓库删除数据库代码 | 第 4 期 | +| 6 | Windows 插件宿主和插件市场就绪后,按当时的宿主能力小幅调整插件,补 Windows 构建后发布 | Windows 插件市场 | + +第 1 到第 5 期不需要等待 Windows,已在同一个 PR(#1470)中分步提交。第 6 期只做小幅调整和 Windows 构建,插件本体不重写。 + +### 九、远程开发 + +第一版数据库后端只在本机运行,连接从本机发起;需要访问远端网络中的数据库时,使用连接自带的 SSH 隧道。是否让后端随远程开发会话运行在远端,等远程开发的插件运行模型确定后再评估。 + +### 正确做法 + +- 新增“执行前检查是否含 DROP”:在插件后端的 SQL 分析中加规则和黄金样例,网页界面根据返回结果显示提示。两端自动一致。 +- 新增一种数据库:只改插件后端和样例。主仓库不改一行代码。 + +### 不要这样做 + +- 在主仓库的 Swift 或 TS 中,为数据库再写判断逻辑或专属界面。 +- 为了让数据库插件通过校验,把它声明成构建系统,或者造一个空语言。 +- 让插件把密码写进自己的状态文件,或者写进插件安装目录。 +- Windows 宿主为数据库插件单独做一套桥接方法。 + +## 考虑过的备选方案 + +### 界面留在宿主,两端各画一套原生界面,驱动做成插件 + +它最吸引人的地方是 macOS 保持 SwiftUI 原生体验,宿主侧改动小。不采用,因为主仓库仍要保留数据库专属界面,两端还是各写一套;“代码移出主仓库、双端一份”的目标达不到。 + +### 宿主提供通用的表格、SQL 编辑器组件,插件只提供数据 + +它最吸引人的地方是两端都能保持原生控件,组件也能给其他插件复用。暂不采用,因为两端都要实现带编辑的数据表格和 SQL 编辑器这类重组件,契约面很大,会拖慢数据库上线。以后可以先从数据库树这类简单结构开始,逐步改成宿主原生渲染。 + +### 带 Lithe 发布者签名的官方插件 + +它最吸引人的地方是信任最强:只有带 Lithe 签名的包才能用这个 ID,迁移密码时不必另外核对来源。最初就是这样设计的。暂不采用,因为官方签名路线从未真正发布过插件,v2 私钥不在当前维护者手中,官方索引也指向已归档的仓库;要用就得另配一把密钥并改信任表,这是信任根变更。改为社区插件后,冒充风险由“迁移只认 Lithe Store 来源和仓库地址”来堵。以后官方签名路线恢复时,可以再把插件迁成官方身份(参照 Go、PHP 的 `migratedToCommunity` 反向处理)。 + +### 只把两个程序改成按需下载 + +它最吸引人的地方是最快就能减小体积。不采用,因为它不解决规则多份和双端不一致的问题,Windows 也用不上,迟早要返工。 + +## 后果 + +- `Lithe.app` 不再包含 `lithe-db-sidecar`、`lithe-db-mcp`、数据库图标和数据库界面代码;主仓库删除了 `LitheDatabaseModule`、`Views/Database/`、两个 Rust crate、Windows 的 `features/database/`、数据库 CI 工作流与分流、打包脚本中的数据库部分和对应测试。 +- **发布顺序是硬约束:** 主程序删除数据库后,老用户只能从插件找回功能。所以插件的第一个版本必须先在插件仓库发布,并在 Lithe Store 的 `lithe-plugin-index.json` 登记,之后才能发布不含数据库的 Lithe。 +- **信任边界:** 不带签名,信任来自 Lithe Store 的收录审查和索引固定的 SHA-256。如果用户移除了 Lithe Store 来源,或只从磁盘安装,旧连接和密码就不会迁入(旧数据仍保留)。Lithe Store 收录这个插件的更新时,维护者要像审查代码一样审查,因为它会拿到迁移过来的密码。 +- **网页界面体验:** macOS 上不再是原生 SwiftUI。插件使用宿主注入的主题变量和语言上下文贴近宿主风格;数据表格和 SQL 编辑器由插件自带,包体积和性能需要随版本实测。 +- **Windows 暂时没有数据库功能:** 第 6 期依赖 Windows 插件宿主和插件市场;在此之前 Windows 用户看不到数据库入口(以前的入口本来就不能用)。 +- Agent 用的数据库工具暂时没有。以后由插件以插件函数提供,登记进现有 `lithe` MCP,不再单独起 MCP 进程。 + +## 验证 + +- `./scripts/test-macos.sh --filter "MacDatabasePluginMigrationTests|MacPluginBackendSessionsTests|WorkbenchNotificationTests"`:迁移的内容、格式、只执行一次、不覆盖插件自有状态、密码迁移失败的处理;磁盘安装、其他来源和其他仓库地址的同 ID 包得不到旧数据,之后从 Lithe Store 安装仍会迁移;迁移先于后端启动并拿到安装记录;带动作的通知和一次性安装提示。 +- `./scripts/verify-module-boundaries.sh` 与 `./scripts/verify-shared-contracts.sh`:内置模块列表和共享模块样例中不再有数据库模块。 +- `bash scripts/test-classify-ci-changes.sh` 与 `node --test scripts/ci-workflow-gates.test.mjs`:CI 分流不再有数据库通道。 +- `./scripts/verify-runtime-bundle-immutability.sh`:运行时不向发行目录写入。 +- 2026-10-24 手工验收(macOS 开发版身份 `app.lithe.desktop.dev`,与正式版偏好和 Keychain 隔离):写入旧模块的连接、文件夹、历史和密码后启动,出现一次性通知,经“打开插件市场”从 Lithe Store 安装 Database 0.1.0,重启后打开工具窗口,连接全部可见;插件状态与凭据按“六、老用户迁移”的规则落地。验收数据随后全部清除。 +- 插件仓库的 `node scripts/e2e.mjs`:用 Lithe 的 Rust Core 启动打包好的插件后端,回答凭据请求,走完 SQLite 连接的全部页面函数,并确认安装包未被改写、状态目录只有 `connections.json` 和 `sql-history.json`。 + +## 已定决策 + +2026-10-24 与用户确认: + +- 界面使用插件网页,不保留 SwiftUI 原生数据库界面; +- 插件放在组织仓库,主仓库不保留数据库专属代码; +- ~~使用带 Lithe 签名的官方身份~~(同日改为先作为社区插件发布,见下); +- 后端为长期运行的 Rust 插件后端; +- macOS 先上线,Windows 等宿主和插件市场就绪后跟上; +- 老用户提示一键安装,不自动下载; +- 现有 Windows 数据库前端不迁移; +- 暂不支持 DuckDB; +- ~~签名在组织仓库 CI 中完成,私钥是插件仓库的受保护 secret~~(随社区插件决定取消); +- 远程开发第一版只在本机运行数据库后端,远端数据库通过 SSH 隧道访问; +- Windows 这次不做插件侧适配,等 Windows 插件市场就绪后再小幅调整; +- 第 1 到第 5 期在同一个 PR 中分步提交,不拆成多个小 PR; +- 官方签名路线从未发布过插件、v2 私钥拿不到,因此数据库插件先当作社区插件,在 Lithe Store 上架,不作为官方插件。 + +按用户授权由实现方确定(2026-10-24): + +- 后端每个插件一个进程、宿主全局,不按工作区划分;授权由安装审查覆盖; +- 凭据停用和更新时保留,卸载时删除; +- 按平台可执行文件不改契约,各平台下载包使用固定路径; +- Agent 工具之后经由现有 `lithe` MCP 暴露,不单独起 MCP 进程; +- 网页界面全新编写(TypeScript,符合 CSP 的打包产物),参照 macOS 现有功能; +- 老用户迁移由宿主侧的过渡垫片完成,挂在插件后端启动路径上,只迁移一次; +- 社区插件下,迁移只认 Lithe Store 来源和插件仓库地址,不认插件 ID。 + +## 适用范围 + +- `macos/Sources/Lithe/Platform/MacOS/Plugins/MacDatabasePluginMigration.swift` +- `macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginBackendSessions.swift` +- `macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginCredentialStore.swift` +- `macos/Sources/Lithe/Application/Features/DatabasePluginTransition.swift` +- `macos/Sources/Lithe/Application/Features/PluginHostServices.swift` +- `macos/Sources/Lithe/Models/AppModel/AppModel+DatabasePlugin.swift` +- `shared/contracts/plugins/v1/` +- `rust/lithe-core/src/plugins/backend.rs` +- `rust/lithe-core/src/lsp/interface/plugin_function.rs` +- `shared/platform-feature-matrix/features/` + +相关 Note:[统一 VCS 模型与 Provider 接入](../../proposed/architecture/2026-10-22-unified-vcs-provider-model.md)(同类 provider 做法)、[模块运行时抽取路线](../../proposed/architecture/2026-09-13-module-runtime-extraction-roadmap.md)(数据库被列为适合插件化)、[插件自带网页界面](2026-10-18-plugin-web-content-in-host-windows.md)、[插件市场](../feature/2026-10-12-plugin-marketplace.md)。 diff --git a/.agents/notes/implemented/architecture/2026-10-24-remote-workspace-routing.md b/.agents/notes/implemented/architecture/2026-10-24-remote-workspace-routing.md new file mode 100644 index 0000000000..1f8dd8d73e --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-24-remote-workspace-routing.md @@ -0,0 +1,58 @@ +# Agent 笔记:远程工作区按根目录路由 + +状态:已实现 + +## 先说结论 + +远程文件夹以“本地占位根目录”的身份打开成工作区:根目录是 `Application Support/Lithe/RemoteWorkspaces/` 下的一个空目录,工作台照常拿到一个文件 URL;这个根目录下面的所有文件都在远端。文件树和编辑器经过的两个端口 `WorkspaceOperations` 和 `WorkspaceFileOperations` 外面各包了一层路由:URL 落在远程根目录下就交给远端,其余原样交给本地实现。第一版只让文件树、打开、编辑和保存走远端;Git、搜索、构建、语言服务和终端在远程工作区里看到的仍是本地空目录。 + +## 问题 + +工作台的所有功能都以“工作区根目录是一个本地文件 URL”为前提:窗口、会话恢复、项目设置、文件树、编辑器都按 URL 识别。如果为远程项目换一种 URL(例如自定义 scheme),几乎每个功能都要改;而一次性把所有功能都接到远端,改动面太大,也无法分步验证。 + +## 决策 + +- **占位根目录**:`MacRemoteWorkspaceRoots` 在 `Application Support/Lithe/RemoteWorkspaces/<连接 id>/<远程路径哈希>/<文件夹名>` 创建一个空目录,用它作为工作区根。目录名是远程文件夹名,所以项目名显示正确。它不在 app bundle 里,不影响签名和 Sparkle 增量包;里面没有用户数据,跨启动保留,让按根目录保存的项目状态得以延续。 +- **注册表**:`RemoteWorkspaceRegistry` 记录每个占位根对应的连接、远程路径和 Core 会话 id。打开远程项目时登记,同一个根再次打开时,旧会话会被替换并断开。 +- **路由**:组合根(`MacServiceContainer`,负责组装依赖的地方)把本地实现包进 `RemoteRoutingWorkspaceOperations` 和 `RemoteRoutingFileOperations`。路由层对端口里的**每个**方法都显式转发:如果只覆盖一部分,没覆盖的方法会悄悄走协议扩展里的默认实现,本地文件的行为就变了。 +- **文档版本**:和本地一样,文档标识是读到的原始字节的 SHA-256。远端用大小和修改时间做冲突检测,所以路由层为每个读到的标识记住远端版本(最多 512 个),保存时把它作为 `expected` 传给 `remote.writeFile`;不认识的标识会先读当前内容比对哈希。文件变了就返回冲突,不覆盖。 +- **最近项目**:占位根目录不进“最近项目”,因为它离开连接就只是个空目录;重新打开要走“远程开发”窗口。 + +### 正确做法 + +新增一种需要读写工作区文件的能力时,通过这两个端口访问文件,远程工作区就会自动可用。 + +### 不要这样做 + +- 不要在功能代码里用 `FileManager` 直接读工作区下的文件:在远程工作区里它只会看到空目录。 +- 不要从占位根目录推导“这是远程项目”再写特殊分支,应该询问 `RemoteWorkspaceRegistry`。 + +## 考虑过的备选方案 + +### 为远程文件使用自定义 URL scheme + +语义最清楚,但工作台里几乎所有地方都假定是文件 URL,第一步就要改全部功能。 + +### 把远程目录同步到本地再打开 + +现有功能全都能直接用,但大项目同步慢、两边会不一致,构建和运行也会发生在本地而不是项目所在的机器上,和整体远程开发设计(构建、LSP 都在远端运行)相冲突。 + +## 后果 + +- 远程工作区第一版:文件树按需列目录、打开和保存 UTF-8 文件(含 BOM)、保存时检测冲突。 +- 暂不支持:新建、删除、重命名、复制(报“不支持”);外部修改监听;Git、搜索、构建、语言服务和远端终端。这些要逐个接到 `lithe-remote-client` 已有的远端能力上。 +- 连接在工作区关闭后仍保留,直到 App 退出。 + +## 验证 + +```bash +./scripts/test-macos.sh --filter RemoteConnections +./scripts/verify-service-boundaries.sh +``` + +## 适用范围 + +- `macos/Sources/Lithe/Services/Remote/RemoteWorkspaceRouting.swift` +- `macos/Sources/Lithe/Platform/MacOS/Remote/MacRemoteWorkspaceRoots.swift` +- `macos/Sources/Lithe/Platform/MacOS/MacServiceContainer.swift` +- `macos/Sources/Lithe/Models/AppModel/AppModel+RemoteConnections.swift` diff --git a/.agents/notes/implemented/bug-fix/2026-09-27-windows-git-path-identity.md b/.agents/notes/implemented/bug-fix/2026-09-27-windows-git-path-identity.md index d35c1f5c38..83eb3692ce 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-27-windows-git-path-identity.md +++ b/.agents/notes/implemented/bug-fix/2026-09-27-windows-git-path-identity.md @@ -33,6 +33,7 @@ Issue #627 中,前端先判断绝对路径再替换反斜杠,把原生 UNC - `shared/fixtures/git/windows-paths.json` 由前端与 Rust 测试共同读取。 - `git_path_roundtrip` 原生集成测试使用真实 Git 仓库,把发现结果交给实际前端 TypeScript 规范化函数后回传 Core,核对提交、分支、stash 和链接工作树的监听目录。它需要 Git 和 Node.js 22.6+,子进程通过现有 Git Host 设置本地期限并清理进程树。 + - Windows CI 39933419913 的超长路径用例在 Node 子进程十秒截止时返回 `Cancelled`,没有产生路径结果不匹配。测试仅将 Windows 的真实 Node/TypeScript 加载期限改为二十秒,Git 夹具命令仍为十秒、Core 请求仍为五秒;SharedRust 与真实 JDT 两个入口为 `git_paths_roundtrip_` 设置单例六十秒上限,容纳多次进程交换,不调整其他测试或总套件期限。超时仍走原生进程树清理,并在失败诊断保留进程状态与 stderr;不能通过重试断言、复制前端逻辑或跳过长路径用例来绕开验证。 - 长路径用例先在短路径建立真实仓库,再移入超长目录。Windows CI 已观察到文件系统可访问但创建 Git 进程的工作目录被 `ERROR_DIRECTORY` 拒绝;用例仅接受成功读到原提交,或带原生错误 267 的 `process_start_failed`,不能返回空仓库或访问其他目录。 - Windows 专属测试真实创建末尾点/空格目录及普通同名目录,确认请求明确拒绝,不误用普通目录。 - 原生 watcher 测试用有界事件接收验证外部提交/切换所写的共享引用与独立 HEAD 元数据会通知对应工作树;没有用 sleep 等待防抖。 diff --git a/.agents/notes/implemented/bug-fix/2026-10-05-project-tree-selection-and-moves.md b/.agents/notes/implemented/bug-fix/2026-10-05-project-tree-selection-and-moves.md index e4d49db3ff..8a90feb70e 100644 --- a/.agents/notes/implemented/bug-fix/2026-10-05-project-tree-selection-and-moves.md +++ b/.agents/notes/implemented/bug-fix/2026-10-05-project-tree-selection-and-moves.md @@ -18,6 +18,17 @@ macOS 使用系统多文件拖动,工作区模块继续负责实际文件操 ## 决策 +- Windows 的“打开所选文件”命令为 `file.openSelected`,默认 F4,沿用现有快捷键设置的 + 覆盖与禁用规则。查询输入框复用统一的两段式快捷键状态机;完成、未匹配按键和超时均重置, + 监听器因首段输入重新注册时保留超时,组件卸载时清理计时器。 + 当前获得 DOM 焦点(页面里真正接收键盘输入的元素)的区域提供打开动作, + 项目树仍持有焦点路径,搜索仍持有匹配行列,不把这些状态再复制到快捷键 store。 + 项目树打开有效的可见文件并固定标签页;目录、失效路径、重命名及树搜索输入框不执行。 + 内容搜索的查询框打开当前键盘选中的匹配,结果文件行与片段打开自身文件及一基行列位置; + 搜索等待期间、空结果、替换及过滤输入框、编辑器和终端不借用其他区域的选择。 + 编辑器开启 Vim 时,这些文件区域仍能使用 F4。区域卸载即注销动作,弹窗通过真实 DOM + 焦点隔离。例如搜索选中了 A,但焦点在 B 的文件行时,应打开 B;不要读取侧栏旧路径而打开 A。 + - 文件、目录、项目根和多选右键菜单复用已有项目范围查找与替换入口,沿用整个项目范围、 选中文字预填和既有替换预览/确认流程;树视图只提供入口,实际查找和替换由已有功能处理。 @@ -77,6 +88,8 @@ macOS 使用系统多文件拖动,工作区模块继续负责实际文件操 ## 考虑过的备选方案 +- 在项目树和搜索框分别硬编码 F4:改绑和禁用后仍可能触发,也会与全局捕获监听冲突, + 因此只在集中命令目录注册一次,复用各区域已有的文件打开动作。 - 监听侧栏活动路径来跟随编辑器:该路径同时接受目录点击,无法区分树选择与编辑器切换, 会把刚选中的目录改回编辑文件;改为复用同步 Hook 已有的编辑器目标,不另建缓冲区选择器。 - 在树菜单单独实现搜索或直接替换文件:会绕过已有搜索状态和替换预览/确认流程,不采用。 @@ -89,6 +102,10 @@ macOS 使用系统多文件拖动,工作区模块继续负责实际文件操 ## 后果 +F4 不新增文件解析、导航历史或原生调用;代价是文件区域需要注册和清理自己的焦点动作。 +搜索片段内的代码行按钮以自己的映射行作为 F4 目标,匹配行保留匹配列,上下文行从第一列打开;片段标题或容器仍使用该片段的已选匹配,避免同一片段里 Tab 聚焦到另一行后跳回旧位置。 +本次只补齐 Windows,macOS 的 F4 行为仍需独立实现与验收。 + 独立选择减少误解,目录移动不需要枚举磁盘后代,懒加载避免为屏幕外每行启动图标任务。 代价是维护一个原生输入适配器;真实拖动、上下滚动及大目录性能仍需 macOS 实机验收。 Windows 现已加入 Ctrl/Shift 可见范围多选、批量复制/剪切/删除及已选集合拖动,目录单击选择、双击/箭头展开。拖动预检重名和父子重复,底层用不覆盖发布防止同名文件丢失;逐项操作并在工作区切换后停止后续项。Windows 原生 WebView2 的拖动、焦点与大树验收仍 pending,不能用选择工具函数测试代替。 @@ -96,6 +113,11 @@ Windows 现已加入 Ctrl/Shift 可见范围多选、批量复制/剪切/删除 ## 验证 +- Windows F4 回归使用 `test-stability-windows.ps1 -Scope Frontend` 运行 + `file-explorer-selection.test.tsx`、`f4-file-navigation.integration.test.tsx`、 + `use-keymaps.integration.test.tsx` 与 `tree-dom-event.test.tsx`。 + 验证方向键后的目标、搜索一基行列、焦点文件与旧选择不一致、改绑与禁用、Vim 设置、 + 输入框和无效目标隔离;真实 WebView2 焦点及四项修复的完整 Windows 构建另行验收。 - `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh` - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'ProjectTree|batchMove'` - `./scripts/verify-service-boundaries.sh` @@ -129,6 +151,11 @@ Windows 现已加入 Ctrl/Shift 可见范围多选、批量复制/剪切/删除 ## 适用范围 +- `windows/tauri/src/features/keymaps/hooks/use-file-open-target.ts` +- `windows/tauri/src/features/global-search/components/global-search-buffer.tsx` +- `windows/tauri/src/features/global-search/components/global-search-results.tsx` +- `windows/tauri/src/features/global-search/components/search-excerpt-results.tsx` +- `windows/tauri/src/features/global-search/components/f4-file-navigation.integration.test.tsx` - `windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx` - `windows/tauri/src/features/file-explorer/hooks/use-file-explorer-sync.ts` - `windows/tauri/src/features/file-explorer/components/file-explorer-reveal.test.tsx` diff --git a/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md b/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md index ca2fcc3054..2a9e29bb71 100644 --- a/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md +++ b/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md @@ -100,6 +100,7 @@ Windows 原来有五个漏洞,叠在一起导致“关闭 Lithe 后服务还 - `./scripts/test-macos.sh --filter '(MacProcessRunnerTests|ExecutionModule)'`。 - `rust/lithe-git-host/tests/process.rs`:`process_tree_terminate_ends_the_root_and_its_descendants` 启动一个会再启动后代的进程,确认 `terminate` 返回时两者都已结束;Windows 上 `dropping_a_process_tree_ends_it_like_a_crashed_host` 只丢弃句柄,确认内核结束整组。 - `windows/tauri/src-tauri/src/run.rs` 测试(仅 Windows):关闭一个窗口只结束它自己的进程树、丢弃会话等同 Lithe 崩溃、原生列表按启动顺序包含全部会话。 + - Windows CI 39919376382 在关闭窗口和丢弃会话两例中,根进程已退出,但立即读取的 `sysinfo` 快照仍包含后代 PID。测试改为停止前打开后代的只等待句柄,停止后分别在两秒内等待根进程和后代的退出信号;另一窗口的后代用零等待确认仍在运行。依据 [Windows 进程终止语义](https://learn.microsoft.com/en-us/windows/win32/procthread/terminating-a-process),退出进程的对象可以因尚未关闭的句柄而继续存在,进程退出信号才是结束依据。保留原来的窗口隔离、重复关闭和崩溃清理断言,不改产品停止流程。夹具的首字节就绪信号有两秒期限,失败路径和断言展开时也按所属会话清理进程树、等待退出并释放管道和句柄;不依赖任意睡眠或 PID 重查。 - `rust/lithe-core/src/lsp/interface/process.rs` 测试(仅 Windows):结束批处理启动的语言服务器时,脚本启动的子进程一起结束。 - `shared/fixtures/process-lifecycle/exit-plan-v1.json`、`settle-v1.json`:运行服务在退出、关闭项目、直接停止设置和崩溃恢复下的动作;macOS `ProcessLifecycleFixtureTests` 用 Swift 类型解码同一批 fixture,并覆盖提问顺序、取消和规则失败时仍询问。 - `windows/tauri/src/features/window/services/run-services-exit.test.ts`:询问、取消、停止失败后重试、直接停止、后台任务不询问、Core 不可用时询问。 diff --git a/.agents/notes/implemented/feature/2026-09-27-workspace-git-commit-plans.md b/.agents/notes/implemented/feature/2026-09-27-workspace-git-commit-plans.md index 69bded6517..a46549b4ed 100644 --- a/.agents/notes/implemented/feature/2026-09-27-workspace-git-commit-plans.md +++ b/.agents/notes/implemented/feature/2026-09-27-workspace-git-commit-plans.md @@ -59,6 +59,7 @@ Git 的多个仓库没有跨仓库原子事务,已成功的提交不会因后 - `rust/lithe-core/src/git/workspace_commit/tests.rs` 覆盖干净祖先联动、关闭联动、独立嵌套仓库、过期计划、失败依赖阻塞、仅重推和外部推进后再次推送。 - `rust/lithe-core/src/tests/git_workspace_commit.rs` 使用真实 Git 验证过期索引拒绝、子模块状态、只更新引用且不带入父文件、首次提交前取消暂存保留后续编辑,暂存后在工作树删除的文件仍可见、子仓库元数据被删除后拒绝回退父仓库,以及共享夹具序列化。真实 Git 还验证共享计划执行、推送失败后的重试、过期步骤重放拒绝、取消与成功提交返回竞争。 - 使用 `write-stable-tests` 的 Rust 逐例计时入口生成 HTML/JUnit;macOS 使用该 Skill 的 macOS 计时入口。 +- Windows CI 39914734587 中,逐仓库推送确认用例在未出现断言失败时达到 15.233 秒进程截止;该用例创建两份分叉仓库及远端,执行三轮准备校验和独立提交/推送。SharedRust 与真实 JDT 两个入口只给这一用例 30 秒预算,默认用例和整套截止不变,真实 Git 进程仍按原有界规则清理,不跳过确认或推送断言。 - 运行共享契约、服务边界、功能矩阵、发行包只读边界和 Agent Notes 检查。 - 当前实现环境是 Linux;本地执行共享 Core 和 Windows 前端测试,原生构建及测试交给对应 CI,macOS/Windows 界面实测仍需平台验收,不将逻辑测试通过记作界面已验证。 diff --git a/.agents/notes/implemented/feature/2026-10-08-windows-settings-hierarchy.md b/.agents/notes/implemented/feature/2026-10-08-windows-settings-hierarchy.md index 7b749c3d87..bd1e6d9d19 100644 --- a/.agents/notes/implemented/feature/2026-10-08-windows-settings-hierarchy.md +++ b/.agents/notes/implemented/feature/2026-10-08-windows-settings-hierarchy.md @@ -79,7 +79,7 @@ Windows 设置组件使用 `settings-panels.tsx`、`SettingsPanel` 和 `Settings - `windows/tauri/src/features/settings/components/settings-section.tsx` - `windows/tauri/src/features/settings/components/settings-panels.tsx` - `windows/tauri/src/i18n/locale.ts` -- `windows/tauri/src/i18n/ai-commit.ts` +- `shared/i18n/messages.json` - `windows/tauri/src/features/settings/lib/settings-effects.ts` - `windows/tauri/src/features/settings/lib/theme-resolution.ts` - `windows/tauri/src/features/settings/lib/settings-theme-sync.test.ts` diff --git a/.agents/notes/implemented/feature/2026-10-12-plugin-marketplace.md b/.agents/notes/implemented/feature/2026-10-12-plugin-marketplace.md index d90ec7910e..c9df1e6d1b 100644 --- a/.agents/notes/implemented/feature/2026-10-12-plugin-marketplace.md +++ b/.agents/notes/implemented/feature/2026-10-12-plugin-marketplace.md @@ -59,6 +59,17 @@ Lithe 的插件管理改成和 IntelliJ IDEA 一样的“插件市场 / 已安 **展示的版本。** Core 会跳过当前平台或插件 API 不满足的新版本,选择能安装的旧版本。详情里的版本、发布日期和支持平台都描述“点安装会装的那个版本”;最新版本不同时,单独一行注明它在当前 Lithe 或这台 Mac 上无法使用。 +**市场详情的分栏边界。** 市场和“已安装”使用不同的详情视图,必须分别验证。 +市场信息行和来源选择器有最小宽度;直接把它们放在纵向 ScrollView(滚动视口)里, +会让 SwiftUI 按内容宽度重新居中,拖窄后文字和图标就跨到分隔线左侧。 +市场视口现在先接受分栏给出的矩形,再以该宽度布局详情。正文最小宽度由现有 +标签列、间距、来源选择器和提示卡的尺寸推导;较窄时可横向滚动查看完整内容, +较宽时描述仍按可见宽度换行。短详情在高度变化后也保持顶部对齐。 +这里只测量视口的宽高,内容在测量闭包外构建;不在拖动中重读市场、重建模型或写偏好。 +正确做法是约束视口并保留内容的滚动可达性;不要只裁掉越界正文、把详情最小宽度 +传回外层撑宽设置窗口,或因为“已安装”测试通过就认为市场也通过。 +这是原生视图布局修改,没有新增运行时文件、缓存或安装资源写入。 + 正确做法:新增官方插件时登记到 `official-plugins.json`,改插件内容时同时升 `plugin.json` 的版本。 不要这样做:手工改 `index.json` 里的下载地址或 SHA-256,或者把插件包重新挂到 Lithe 的版本 Release 上。 @@ -93,6 +104,7 @@ Lithe 的插件管理改成和 IntelliJ IDEA 一样的“插件市场 / 已安 - `cargo test --manifest-path rust/lithe-core/Cargo.toml --lib marketplace_plugins`:索引校验、平台与 API 判断、命名空间、源地址解析、安装审查的标准答案。 - `./scripts/verify-shared-contracts.sh`:市场 fixture 与错误码登记。 +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'PluginMarketplaceLayoutTests|PluginDetailLayoutTests|PluginSettingsLayoutTests'`:市场用内存索引驱动真实页面,覆盖中英文描述、深浅主题、窗口宽高变化、原生分隔条拖动、释放后刷新不移位,以及窄视口横向滚动到正文末端;单独保留“已安装”详情和路径控件回归。可用 `LITHE_MARKETPLACE_LAYOUT_CAPTURES` 导出市场原生视图截图;测试拥有并清理自己的偏好域和窗口。 - `node --test scripts/official-plugin-releases.test.mjs`:只为版本变化的插件生成标签,内容变化未升版本时失败;已发布未登记的版本会被重新登记。 - `node scripts/test-reuse-worktree-resources.mjs`:磁盘快照、安装 staging、版本和审批记录不出现在可复用列表,显式选择时在访问工作树或复制前拒绝。 - `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'PluginMarketplace|PluginDiskInstallation|MacPluginDiskPackageImporter|MacPluginPackageDownloader|PluginManagementPresentation|PluginManager|PluginPackageStore'`:校验和先于解压、下载和展开上限、磁盘快照隔离、源与签名要求、完整程序审查、安装 staging/扫描/回滚摘要复查、来源保留及同 ID 不自动接管。过滤器只是运行入口,必须核对报告确实包含 `MacPluginDiskPackageImporterTests`、`PluginDiskInstallationTests` 和磁盘 FeatureModel 场景,不能以零匹配当作通过。 diff --git a/.agents/notes/implemented/feature/2026-10-15-macos-settings-hierarchy.md b/.agents/notes/implemented/feature/2026-10-15-macos-settings-hierarchy.md index 8821f32d6b..bec522bcc8 100644 --- a/.agents/notes/implemented/feature/2026-10-15-macos-settings-hierarchy.md +++ b/.agents/notes/implemented/feature/2026-10-15-macos-settings-hierarchy.md @@ -22,10 +22,12 @@ macOS 设置侧栏现在只有七个一级入口:外观与行为、插件、 | 构建、执行、部署 | 构建工具、项目 JDK、运行配置、服务运行,以及框架和构建工具注入的配置 | | 语言与框架 | 语言服务(LSP)的开关,以及 Spring Boot 等框架的配置 | | 工具 | 终端、日志与诊断(查看、目录、清理和诊断包导出)等通用工具;插件带来的专用工具(例如 `.http` 请求)可以放进对应工具页 | -| AI | 凡是和 AI 有关的都放这里:MCP、Skill、AI 提交信息、Agents、AI 服务商 | +| AI | 凡是和 AI 有关的都放这里:MCP、Skill、全局指令、AI 提交信息、Agents、AI 服务商 | 插件如果带来了新的设置,默认放进“插件”页;只有它新增的是上表中某个分组本来就该有的能力(例如新的版本控制系统)时,才放进那个分组。 +AI 分组内按“AI 服务商 → Agent 管理 → 指令 → 技能 → Hooks → MCP 配置 → AI 与提交”排列:先放基础配置,再放行为规则和扩展能力,最后放具体应用功能。顺序仍只在 `SettingsGroup.categories` 中声明,搜索过滤后的侧栏也沿用这一顺序。 + ### 插件注册的页面 插件可以在清单里把设置和按钮放到“语言与框架 › <语言>”或“构建、执行、部署 › <构建系统>”。这些页面不是 `SettingsCategory` 的 case,而是由 `PluginSettingsRegistry` 从已启用插件的声明推导,跟在对应分组的静态页面后面;面包屑和搜索同样从注册结果推导。宿主不为任何具体语言或构建工具写专用页面,规则见 [插件设置的通用注册面与生效方式](../architecture/2026-10-21-plugin-settings-surfaces.md)。 @@ -59,7 +61,7 @@ Issue 希望“构建工具 → Maven”“服务运行”“框架支持”也 ## 验证 -- `SettingsSidebarLayoutTests` 保证每个页面恰好属于一个分组或是一级页面,并固定本表的分组内容。 +- `SettingsSidebarLayoutTests` 保证每个页面恰好属于一个分组或是一级页面,并固定本表的分组内容与 AI 页面顺序。 - `SettingsSearchVocabularyTests` 保证每个页面仍能被检索到。 - `./scripts/test-macos.sh --filter SettingsSidebarLayoutTests`。 - 实机查看设置窗口的侧栏、面包屑、搜索展开和从其他入口跳转到某一页时展开所属分组。 diff --git a/.agents/notes/implemented/feature/2026-10-17-mcp-server-library.md b/.agents/notes/implemented/feature/2026-10-17-mcp-server-library.md index 562d0a2c6e..ae441ed0d5 100644 --- a/.agents/notes/implemented/feature/2026-10-17-mcp-server-library.md +++ b/.agents/notes/implemented/feature/2026-10-17-mcp-server-library.md @@ -59,7 +59,110 @@ - env 和 header 值在编辑器中用 `SecureField` 遮蔽,但配置文件中仍是明文;页面明确写出这一点,不制造“已经加密”的错觉。列表摘要及其默认辅助功能文本不显示 stdio 参数,仅显示经过安全字符校验的可执行文件名;HTTP/SSE URL 会移除 userinfo、查询参数和 fragment,无效 URL 仅显示传输方式。例如 `--token=secret` 不能出现在摘要中,`https://user:secret@example.test/mcp?token=secret` 只能显示 `https://example.test/mcp`。 - 错误消息、日志和 Core 返回值都不回显配置内容。解析失败只报告文件路径和错误类型。 -- 首版不自动启动 MCP 服务器做健康检查。启动服务器会执行用户配置的命令,资源库只负责配置,不在用户没有要求时执行项目代码。 +- 打开页面或展开工具区只读;只有点击“检测工具”才连接,stdio 还需确认执行通知。 + 检测会运行配置的命令,可能下载包,因此不能在导入、保存或展开时自动触发。 + +### 紧凑编辑器与工具检测 + +配置弹窗参考 Magpie `ccbc7278180e513bf95461c62eeb15729fc7cae7` 的 +`internal/gui/assets/plugins/library/client.js`;工具排除机制核对其 +`internal/library/mcp_tools_off.go`,并核对 Codex 原生 `disabled_tools` 与 Claude +官方 permissions 文档。独立实现原生页面,不复制 Magpie 的网关和多 Agent 框架。 +命令与参数投影成一行,Rust Core 的 shell-words 1.1.1 只解析/引用 argv,保留空参数、 +内嵌换行和路径空格,不执行 Shell。Agent 用现有品牌图标的选择按钮,帮助仍放在 ⓘ。 + +检测复用仓库已有的官方 rmcp 3.5.0 SDK,初始化、会话、SSE 响应和分页相关语义留给 +SDK;Agent Host 管理执行、取消和进程清理。只增加 client/streamable HTTP 能力, +沿用 reqwest 0.12 的 TLS/网络栈;自定义字节 adapter 在 JSON、错误和 SSE 解码前 +限制 2 MiB,避免 SDK 默认 HTTP JSON 读取无上限。工具最多 1000 项、20 页,重复 +游标拒绝;请求期限 30 秒并有有界清理,stdio 子进程/进程组在结束、失败、取消后回收。 +不发送 tools/call,不处理 OAuth 登录、不读取其他 Agent 的登录令牌;不支持的旧式 +SSE 检测明确报错,仍允许用户编辑原有 SSE 配置。成功只代表这次检测,不能当成现有 +Agent 会话的连接保证。 + +工具区按已注册的 Agent 独立切换。Codex 修改自身条目的 disabled_tools;Claude 修改 +用户 settings.json 的 permissions.deny,只写精确 mcp__规则。外部精确/通配 deny、 +Codex enabled_tools 以及名称规范化冲突保持不可放宽,页面显示只读限制;只移除本库 +登记过的精确禁用项,文件过期时保留外部修改。规则与全局原生文件由现有写入 adapter +备份、条件替换,偏好只登记工具名所有权,不存令牌。Codex 更改规则会改变完整来源 +哈希,页面只推进此前匹配的服务器所有权,不能把一次工具操作当成接管外部服务器。 + +工具规则按原生路径/服务器名保留,即使暂时关闭服务器也保留限制。Codex 删除条目前, +Core 提取原生允许/禁用名单,整批条件写入成功后才把这些工具名保存在原服务器账本的 +`toolFilters` 中;保留记录没有 `sourceHash`,不能授权修改外部重新创建的条目。重新添加 +缺失条目时恢复名单,并根据恢复后的完整内容登记新所有权。现存条目和重命名沿用原生 +规则,不能被旧保留值覆盖;空允许名单仍表示不允许任何工具,格式错误则拒绝删除。 +这份状态随用户资料保留,不保存命令、env、header 或令牌;新页面和应用重启都能恢复。 +Codex 重命名还必须移动相同原生文件路径下、由 Lithe 登记的精确工具名所有权;只搬原生表会让原来可编辑的禁用项被误判为外部限制。迁移在整批文件条件写入成功后执行,失败或恢复不发布新名称所有权;清除旧名称记录,也不合并新名称处可能过期的记录。外部 deny 和 allow-list 不因此取得所有权,其他配置路径和 Agent 保持独立。 +Claude 的规则引用 +服务器名称,原生用户配置存在 MCP 限制时拒绝在本页重命名,避免旧限制未迁移而让 +工具重新可用;用户需先在原生规则中处理。项目/受管规则仍由上游 Agent 决定,页面 +不会显示为已解除全局之外的限制。生命周期退出会取消探测、丢弃迟到结果并释放内存 +快照;这些状态登记在 mcp-tool-probe-state,没有工作树复用阶段或发行目录运行时写入。 + +同一连接新增或移除 Agent 时,服务器行的名称和定义哈希不变,不能只按行 ID 保留旧策略。工具模型记录每行最新来源,来源改变时释放旧策略快照、按现有工具清单重读本地规则;探测尚未返回时也以最新来源读取策略。此刷新不重新连接第三方服务器,保留已检测工具;单个 Agent 的读取失败明确显示错误,其他 Agent 的可用策略保留。 + +检测的 macOS 适配器使用 8 MiB 栈的独立原生线程执行有界 C ABI 调用,避免阻塞 Swift 并发执行器,以及调试版 SDK/TLS 调用在小栈线程上溢出。取消与指令保存共用 `RustCoreCancellation`:Core 登记操作后、执行命令前同步回调,补传注册前到达的取消,不需要重试计时器;页面状态仍拒绝迟到结果。显式设置 `LITHE_MCP_PROBE_INTEGRATION=1` 可运行 Context7 的真实 HTTP 工具列表验证,普通测试不依赖公共网络。 + +### 发现市场与弹窗编辑 + +资源库和“发现 MCP 服务器”在同一页纵向排列,上方管理已添加服务器,下方搜索官方目录;项目级 Lithe 授权在页末。 +添加或编辑使用平铺表单的原生弹窗,只有字段区域滚动,取消和保存固定在底部;资源库超过五项时提供名称与安全摘要搜索。 +发现区标题右侧以灰色普通文字显示“精选,以及官方 MCP Registry”,作为目录来源标记,不提供 Registry 首页直达链接。 +市场卡片直接显示发布者、版本和两行简介,这些是用于选择服务器的目录内容;设置行为和授权解释仍放在 ⓘ。卡片按窗口宽度自动排列为一列或多列。 +标准 `mcpServers` JSON 经既有 Core 解析器生成预览,用户逐个选择条目检查后再保存。 +导入只带入现有编辑器支持的字段,认证与客户端专有字段需另行配置;界面的 ⓘ 明确 +说明这个边界。导入不读环境变量、不写文件,也不接管已有条目。 + +交互参考 Magpie 的 `86254d4049cb6be4a2ccdab02edf0bd59860c603`,目录行为另核对 +`ec7e50b8287462efcb6153fde67308e66b5aaf51` 的 `internal/library/market.go`,尤其 +`internal/gui/assets/plugins/library/client.js` 的发现卡片与编辑流程,以及 +`internal/gui/library.go` 对目录失败的独立提示。只参考公开交互与 API 使用方式, +继续复用 Lithe 的原生控件、配置端口、格式保留与所有权模型,不搬入其 Web UI 或网关。 + +打开页面先展示随版本提供的精选目录,不发网络请求。Magpie 的 `MarketServers` 同样 +先筛选内置 `featured`,只有搜索词非空时才请求 Registry,部分失败仍返回精选结果; +因此“打开时有卡片”不代表该机器已经连通官方目录。Lithe 的精选 JSON 由 Git 管理并 +编译进 Core,`mcp.featuredRegistry` 复用公开目录的校验逻辑返回卡片。包版本从官方 npm +元数据核对后固定,远程地址按发布者文档核对;中文介绍随应用本地化。维护目录时 +必须重新核对包版本、远程地址和文档,不复制 Magpie Image 等产品专属能力。 + +精选包含 Context7、Playwright、Chrome DevTools、Microsoft Learn、GitHub、DeepWiki、 +Exa、Filesystem、Fetch 和 Memory。2026-10-22 核对发布者文档与 npm/PyPI 元数据, +Filesystem/Memory 固定为 2.0.0(需要 Node.js 24+),Fetch 固定为 2026.10.19 +(需要 uv 和 Python 3.10+);GitHub、DeepWiki、Exa 使用发布者的 HTTPS 入口。 +GitHub 的令牌和 Filesystem 的允许目录不能猜测,因此只有版本内的精选可声明 +`setup`,让编辑器显示说明并阻止缺少必填内容的保存。GitHub 预置空 Authorization +请求头,用户填写 Bearer 令牌;Filesystem 必须在包参数后逐行填写至少一个绝对目录, +路径中的空格保留。公开 Registry 不能声明这一可信模板类型,原有复杂输入限制不变。 + +卡片图标使用发布者的品牌标记,不再让所有远程服务都显示地球、所有命令都显示终端。 +图标源文件与来源、审核日期、SHA-256 清单位于 `Resources/McpIcons`,Playwright 的 +Apache-2.0 许可一同保留。它们是 Git 跟踪、SwiftPM 打包的固定输入;运行时只从资源 +bundle 读取并在内存缓存,不访问图片服务器、不下载头像、不写 bundle,因而不改变 +代码签名或 Sparkle delta。三个参考服务器共用 MCP 标记,未认识的条目保留传输方式 +图标。只按可信精选的完整 ID 选择有限文件名,公开条目即便伪装成同名也不使用品牌图标。 +深色页面的品牌标记放在白色小底板上,保留原始颜色和几何;不是工具栏动作图标。 + +搜索时使用官方 `registry.modelcontextprotocol.io/v0.1/servers`,按名称搜索、只查最新 +版本,并使用返回的 opaque cursor(必须原样传回的分页标记)。这是一份目录,不是 +Lithe 的托管服务器或插件市场,也不把收录标成安全审核。Core 的 `mcp.parseRegistry` +把公开元数据转换为可显示的卡片;静态远程地址和没有额外输入的固定版本 npm/PyPI +入口才能生成配置草稿;自定义包源、非默认 runner、npm 可变标签和主/次版本范围都只保留文档。需要密钥、动态地址或复杂参数的记录保留文档入口,不猜命令。 +草稿不默认选择 Agent,用户确认后走原来的保存流程;不会下载、安装或运行服务器。 + +macOS adapter 使用无持久缓存、无 Cookie 和无凭据存储的 URLSession,限制响应为 +1 MiB,请求/总资源期限分别为 15/20 秒。搜索任务归 SwiftUI 页面拥有,离开页面取消; +请求身份防止旧响应覆盖新搜索,分页合并按名称去重,相同 cursor 不再反复加载。 +精选与在线条目按 ID 和不含名称的连接定义去重。连接失败保留已匹配条目,明确区分 +安全连接、超时和网络不可达,并保留手动添加入口,不把失败伪装成“没有服务器”。 +请求默认遵循 macOS 系统代理;只有用户点击“直连重试”才为此页面的公开目录请求 +选择空代理字典的独立 URLSession,继续校验证书,并允许切回系统代理。路由不持久化, +不修改系统、Agent 或实际 MCP 服务器的网络配置,也不自动绕过用户的代理。 + +公开元数据 +的在线响应只保存在进程内存;精选目录属于只读编译输入。两者均无发行目录运行时写入,不影响签名及 Sparkle delta;资源复用 exclusion +将其登记为 `mcp-registry-metadata`,不存在可复制阶段。 ## 考虑过的备选方案 @@ -99,6 +202,13 @@ Windows 的 Agent 配置路径和 Tauri 侧文件访问模型不同,同步实 ## 验证 +- `LITHE_MCP_PROBE_INTEGRATION=1 ./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh --max-seconds 45 -- --filter McpArgumentRoundTripTests`(显式联网验收 Context7 的真实工具列表、原生大栈线程、取消前启动保护、精确参数、独立工具规则和过期文件保护;普通测试不依赖公网) +- `node .agents/skills/write-stable-tests/scripts/run-rust-tests-with-timing.mjs --manifest rust/Cargo.toml --package lithe-agent-host --report .artifacts/test-stability/mcp-tools-host.json`(本地 stdio、HTTP JSON/SSE 响应、分页、超大响应拒绝、虚拟超时及取消后进程回收) + +- `./scripts/test-macos.sh --filter "McpRegistryTests|McpServerLibraryTests"`:离线首页零网络请求、失败保留精选、显式选择路由和路由间分页隔离、精选与在线连接去重,Registry 桥接、分页、取消、旧请求淘汰,JSON 预览不写文件也不取得所有权;原生卡片浅色/深色下在 420 与 780 点宽度重排,截图写入 `.artifacts/mcp-discovery/` 供人工检查。 +- `LITHE_RUN_MCP_REGISTRY_INTEGRATION=1 ./scripts/test-macos.sh --filter McpRegistryIntegrationTests`:可选联网集成,通过生产 adapter 显式直连官方目录并分页;普通单元测试不依赖公网。 +- `cargo test --manifest-path rust/Cargo.toml -p lithe-core mcp::registry::`:精选目录无需网络且草稿固定版本,元数据夹具、失效记录、动态输入、危险链接和大小上限。 +- `node --test scripts/test-reuse-worktree-resources.mjs`:进程内目录元数据不得跨 worktree 复用。 - `cargo test --manifest-path rust/Cargo.toml -p lithe-core mcp::`(覆盖删除首项/中间项/末项、同行相邻项、值跨多行且下一个键同行的布局、单行与混合换行布局,JSON 输出重新解析,重复 JSON 键拒绝编辑;Codex 更新保留 `bearer_token_env_var`、`startup_timeout_sec`、未知键、建模键与值的注释、未变化的 args 数组内注释和非字符串 env 值,变量列表清空时也保留非字符串值;内联 `mcp_servers` 表可删除、可编辑;拒绝重复变量键;哈希忽略当前传输不写入的 command/args/env/url/headers 且与重解析一致;重命名保留专属字段) - `./scripts/test-macos.sh --filter McpServerLibraryTests`(覆盖计划后磁盘变化拒绝写入、文件在计划后出现拒绝创建、linked worktree 公共排除文件、关闭会话后仍能清理项目注册、项目切换后清理两个项目的注册、Git 排除失败不回滚账本、注销冲突写入错误横幅、重复变量键校验、重命名外部条目、按 Agent 开启时忽略传输不写入的字段、切换传输方式丢弃隐藏字段后仍保持所有权、重命名保留 Codex 字段、自然排序条目的所有权、两个窗口合并账本) - `./scripts/test-macos.sh --filter "McpWriteProtectionTests|FileStorageReplacementTests|McpFeatureBatchFailureTests"`(备份完成后外部保存被保留并返回 `changedOnDisk`,预检后出现的新文件不被覆盖;`MacFileStorage.replaceData` 拒绝过期快照、按匹配快照原子发布、拒绝覆盖已存在文件;`removeData` 只删除匹配快照的文件;失败/成功路径都不遗留临时文件;临时文件从创建起为 `0600`;回滚换出更新保存或无法恢复时保留并报告路径;不支持交换的卷拒绝写入和删除) diff --git a/.agents/notes/implemented/feature/2026-10-18-windows-agent-guidance-and-mode-sync.md b/.agents/notes/implemented/feature/2026-10-18-windows-agent-guidance-and-mode-sync.md index d6b8746415..6d0ae69dd4 100644 --- a/.agents/notes/implemented/feature/2026-10-18-windows-agent-guidance-and-mode-sync.md +++ b/.agents/notes/implemented/feature/2026-10-18-windows-agent-guidance-and-mode-sync.md @@ -4,7 +4,7 @@ ## 先说结论 -第 12 期把 macOS 已有的三项对话呈现接入 Windows:Agent 上报的执行计划、与回复分开的思考过程文本,以及 Agent 自行切换模式后的选择器同步。三项都只消费 ACP 上游事件,不在 Windows 侧推断或补造状态;没有计划就不渲染计划栏,没有思考文本就不会出现空的思考行。 +第 12 期把 macOS 已有的三项对话呈现接入 Windows:Agent 上报的执行计划、与回复分开的思考过程文本,以及 Agent 自行切换模式后的选择器同步。三项都只消费 ACP 上游事件,不在 Windows 侧推断或补造状态;没有计划就不渲染计划栏,没有思考或工具就不会出现空的过程入口。 ## 问题 @@ -13,8 +13,8 @@ ## 决策 - 思考文本使用独立的 `reasoning` 消息角色。收到 `agent_thought_chunk` 时先冲刷待合并的正文缓冲,再追加思考,保证思考与回复交替时顺序正确且文本不混合。 -- 思考行按 macOS 呈现为安静的一行摘要:流式时显示小号进度和“思考中…”,结束后显示“思考过程”;折叠时不显示正文预览。展开后是最大 240px 的 Markdown 卡片,流式时跟随到底部。 -- 搜索命中时思考行强制展开,搜索期间点击标题不会收起;清空搜索后回到手动偏好或流式默认状态。 +- 思考和工具、工具之前的过程说明按 macOS 合成每轮一个“思考过程”入口。流式时也默认收起,用户展开后才按原顺序展示全部过程,最终回复始终位于入口外。最后一条思考或工具之后的模型文字作为回复;用户消息和已记录轮次结束切断分组,过滤纯空白消息。展开的主过程区复用消息列表滚动,不设固定高度卡片。原始 reasoning 角色与导出不变,位置规则和流式限制见 [共享对话决策](../architecture/2026-09-25-shared-acp-agent-conversation.md)。 +- 搜索命中时思考行强制展开,搜索期间点击标题不会收起;清空搜索后回到手动偏好或默认折叠状态。 - 计划在每次 `plan` 更新时整体替换。空条目数组是合法清空;顶层结构非法时忽略整次更新,保留上一份计划;单条缺少内容或状态不在 `pending/in_progress/completed` 内时丢弃该条,不默认成待办。 - 计划栏固定在活动统计栏上方,折叠时显示单行“计划 1/3”和当前步骤;展开后最多 180px 滚动。已完成使用成功色勾选并加删除线,进行中在 Agent 响应时使用虚线圆环、轮次结束后使用半填充圆环,待办使用空心圆。 - `current_mode_update` 只同步 `category == "mode"` 的已有配置项,并且要求目标 id 已在该项的 choices 中;未知 id 不写入、不回发请求。 @@ -41,8 +41,8 @@ - `windows/tauri`: `bun run typecheck` 通过;`bun run build` 通过;`bun run lint` 通过,仅有既有警告。 - `src/features/agent` 套件 106 条通过,覆盖计划解析与进度、非法计划忽略、模式同步、思考与正文顺序、搜索分组及 Markdown 导出。 - 仓库门禁:功能矩阵 `--check`、矩阵变更门禁、`verify-agent-notes.mjs`、`verify-windows-boundaries.ps1`、插件隔离与构建产物扫描均通过。 -- 真机验收仍需在 Windows 上执行:真实 Agent 上报计划、思考流式展开与结束后收起、搜索保持展开、计划模式往返后选择器同步、中英文文案及深浅主题。 +- 真机验收仍需在 Windows 上执行:真实 Agent 上报计划、整轮过程默认折叠与手动展开、搜索保持展开、计划模式往返后选择器同步、中英文文案及深浅主题。 ## 适用范围 -Windows Tauri 前端的 Agent 对话呈现、ACP 事件归并与历史导出。ACP 协议、Rust host 转发和 macOS 的 `AgentPlanView`/`AgentThoughtRow` 未改动;macOS 继续作为交互参考。 +Windows Tauri 前端的 Agent 对话呈现、ACP 事件归并与历史导出。整轮过程折叠与 macOS 的 `AgentTranscriptView`/`AgentProcessGroupView` 同步;计划和模式继续沿用原有边界,ACP 协议及 Rust host 转发不变。macOS 继续作为交互参考。 diff --git a/.agents/notes/implemented/feature/2026-10-20-agent-skill-management.md b/.agents/notes/implemented/feature/2026-10-20-agent-skill-management.md new file mode 100644 index 0000000000..623f55f874 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-20-agent-skill-management.md @@ -0,0 +1,102 @@ +# Agent 笔记:技能文件管理与原生 Agent 加载边界 + +状态:已实现 + +## 先说结论 + +技能设置按“全局技能 / 项目技能”组织,Codex 和 Claude 作为列表中的来源标签和筛选项。用户可以查看正文、打开文件、定位目录、新建、导入及启停技能;这些操作使用各 Agent 的原生目录和配置,不维护 Lithe 自己的技能副本或启用状态。技能文件出现在列表中,并不代表当前会话已经加载或执行了它。 + +## 问题 + +输入框的 `$` 和 `/` 补全只能显示当前会话上报的命令,不能回答技能保存在哪里、属于哪个项目、如何添加配套脚本。直接把文件清单当成会话能力,会误导用户;若另存一份技能或增加只影响 UI 的开关,还会出现页面“已关闭”但原生 Agent 仍然读取文件的情况。 + +## 决策 + +### 范围和页面 + +页面放在设置的 AI 分组,Agents 页提供管理入口。主要分类只分全局和项目;项目默认取打开设置的工作区,可选择其他已打开项目,选择动作不切换工作台或会话。新建和导入默认使用正在查看的分类,并明确选择适用 Agent 和显示实际目标目录。 + +「技能」和「指令」是同一 AI 分组中的两个独立页面,必须同时保留入口与各自的功能模型。新增全局指令编辑器不能替换技能设置;预览多个尚未合并的功能时,需要使用包含两者的工作区构建,不能把切到另一开发分支造成的入口缺失误判为原生技能未加载。 + +信息结构参考 PI-Desktop `de57f05aa65b05c93a4113bf5d4db9723884b080` 的 [AgentSkillsPage](https://github.com/vastsa/PI-Desktop/blob/de57f05aa65b05c93a4113bf5d4db9723884b080/apps/desktop/src/components/settings/AgentSkillsPage.tsx) 和 [SkillEditorSheet](https://github.com/vastsa/PI-Desktop/blob/de57f05aa65b05c93a4113bf5d4db9723884b080/apps/desktop/src/components/settings/SkillEditorSheet.tsx)。只参考分类、搜索、紧凑列表与独立编辑面板的交互组织,不复制其代码、样式或状态存储。控件继续使用 Lithe 的设置分段、选择器、搜索框、按钮和菜单。主工作台、标题栏和项目标签不参与此变更。 + +### 发现事实仍由 Agent 拥有 + +Codex 复用已安装 CLI 的官方 `skills/list`,以所选工作区发出强制刷新,保留上游范围、启用状态和插件身份。本机 Codex 0.160.0 与固定适配器依赖的上游 rust-v0.156.1 公开协议均提供这些字段;不需要升级适配器,也不创建会话或发送提示词。生命周期复用既有 App Server 短进程及其有界读取、超时和进程树回收。 + +Claude 固定适配器把 SDK 的 `supportedCommands()` 转成 ACP 命令列表,但缺少可独立查询的完整技能文件路径和范围目录。因此设置页只列出所选项目 `.claude/skills` 和用户配置目录 `skills` 下的文件,并明确显示“目录清单”。用户配置目录服从 `CLAUDE_CONFIG_DIR`,未设置时使用 `~/.claude`。不仿制 Claude 的祖先目录、插件、策略与延迟加载规则;列表 enabled 只表示对应范围的本地配置可见性,不等于原生会话最终可用性。输入框继续仅使用会话实际上报的命令。 + +原生目录依据 [Codex 技能文档](https://developers.openai.com/codex/skills/) 与 [Claude 技能目录说明](https://code.claude.com/docs/en/skills#choose-where-skills-load)。正确示例:在项目分类为 Codex 创建技能,写到该项目 `.agents/skills//SKILL.md`。不要把 Claude 项目目录里的文件改名或移动到全局目录来模拟启停。 + +### 原生启停与配置作用范围 + +纯操作菜单通过共享呈现器保留设置窗口焦点,避免仍开启的原生开关仅因菜单打开而变灰;详见[共享下拉菜单焦点](../simplification/2026-09-30-macos-shared-dropdown-style.md)。每行使用既有原生小开关。点击后立即显示用户选择的状态,后台写入并回读原生配置,不在开关旁增加加载转圈。仅等待确认的技能行禁止重复操作,其他技能保留原生开关的启用状态和颜色;点击其他行时立即显示其选择,并按点击顺序串行执行原生写入与回读,不能并发交换整份清单而覆盖较新的确认。单行失败回滚该行,后续已排队的操作继续执行,错误保留至下一批操作;项目切换或页面离开同时清除未执行队列。不要用页面级忙碌状态禁用全部开关,否则关闭一个技能会让所有仍开启的技能也变灰,误导用户以为它们一起被关闭。用户选择暂存在功能模型,不能提前替换已确认的原生清单或显示“已保存”。确认成功后采用回读结果;失败恢复已确认状态并显示错误,提示刷新核对。未能解析配置时显示“状态未知”,不能把读取失败当作默认启用。项目切换或页面离开会清除临时选择、取消自有请求并拒绝迟到结果,不中断原有 Agent 对话。 + +Codex 使用官方 [skills/config/write](https://developers.openai.com/codex/app-server) 按 SKILL.md 路径设置 enabled。一次有界 App Server 交换先用所选工作区发现技能,拒绝未知路径,再写配置并强制读取列表验证最终状态。Codex 的此接口写入用户配置,因此项目技能的开关也影响所有使用相同路径的工作区;页面提示这一点,不将项目分类误称为独立项目开关。不直接重写 Codex 的 TOML 或插件文件。 + +Claude 使用官方 [skillOverrides](https://code.claude.com/docs/en/skills#override-skill-visibility-from-settings) 的 on/off;固定适配器对应的最低 CLI 2.1.280 已包含该能力,上游 CHANGELOG 从 2.1.129 明确支持。全局开关写 CLAUDE_CONFIG_DIR/settings.json,项目开关写所选项目 .claude/settings.local.json,不修改团队共享 settings.json 或 SKILL.md。该原生配置按名称生效,同名技能共享配置,项目及受管配置可以覆盖全局;界面帮助必须解释这些限制。全局行读取用户配置,项目行合并用户、该项目 settings.json、settings.local.json;这只是所列本地配置的视图,不声称复刻受管、祖先或已连接会话状态。name-only 和 user-invocable-only 仍显示开启,用户关闭再开启会明确改为 on;插件技能需使用 Claude 的插件管理,不通过改名搬目录实现开关。 + +Claude 配置仅合并目标 skillOverrides 键,保留其他 JSON 值与既有权限;无效 JSON、未知枚举、非普通文件、符号链接、超出 1 MiB 的配置均拒绝写入。配置目录内的持久锁文件用非阻塞文件锁串行化 Lithe 实例,写前重新比较原始字节以识别外部修改;临时文件同目录落盘并同步后原子替换,取消或失败回收临时文件。外部编辑器并不共享该锁,字节比较和替换间仍有极短的竞争窗口,不承诺跨所有编辑器的事务。锁与配置由用户目录/所选项目持有,不是可跨 worktree 复用的构建资源,不能写入 app bundle。 + +开关反馈只说明原生配置已保存。成功提示始终预留文字所需的布局空间,仅在原生确认后可见;开始下一次保存时隐藏文字但保留空间,同时从辅助功能树隐藏。不要按保存状态插入或删除整行提示,否则首次保存及连续切换都会让下方技能列表上下跳动。已经加载进会话的内容无法撤回,现有对话应重新连接以刷新命令和技能清单;不自动重连正在执行任务的 Agent。 + +### 文件写入和异步生命周期 + +共享 Core 只负责 `agent.skills` 的类型化分派、取消和错误信封;原生发现与文件操作在 `lithe-agent-host`。Swift 的服务桥离开主线程执行,功能模型保存列表和操作状态,视图只消费模型及显式的原生对话框/打开/定位动作。 + +技能服务与指令服务共用 `RustCoreCancellation`:进入服务和启动后台工作前检查取消,Core 同步发出 `requestStarted` 后补传更早的取消,再分派文件操作。只在取消处理器中发一次 `core.cancel` 不够,因为尚未注册的操作会返回未找到,独立后台任务随后仍可能创建、导入或改配置。功能模型在调用写入服务前也检查取消;安装进度继续通过同一事件回调转发,注册事件不被误当成进度。`AgentSkillsCancellationTests` 在 MainActor 不让出执行权即取消真实桥接任务,并在编码边界触发取消,验证独立项目的技能目录、原生配置和暂存状态均不变,后续合法操作仍能成功。 + +导入保留完整目录中的脚本、模板和文件权限,源目录只读。名称、YAML 文件头、UTF-8、目录深度、文件数和大小都有边界;拒绝导入中的符号链接和特殊文件,以及通过链接的目标容器。先写随机隐藏暂存目录,再以不覆盖模式发布;已有空目录也算冲突。失败或取消回收暂存目录,不改已有技能。查看正文仅返回有界文本,不执行文件中的命令或指令。 + +最终发布必须由操作系统原子地拒绝已有目标,不能只在准备暂存前检查目录是否存在。 +macOS 使用 `RENAME_EXCL`,Linux 使用 `RENAME_NOREPLACE`;Windows 使用 +`windows-sys` 的 `MoveFileExW`,只启用 `MOVEFILE_WRITE_THROUGH`,不启用替换和跨卷复制。 +UTF-16 路径保留 Unicode、空格和长路径前缀,内嵌 NUL 拒绝传入原生 API。 +不能把 Windows 的 `std::fs::rename` 当成不覆盖操作:[Rust 官方文档](https://doc.rust-lang.org/std/fs/fn.rename.html) +说明支持 `FileRenameInfoEx` 的文件系统可替换已有空目录,而 +[MoveFileExW 文档](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw) +提供显式控制替换的标志。Windows CI 曾在这个边界实际覆盖空目标,故回归必须保留失败 +断言;正确做法是让最后一次移动失败并保留源包和已有目标,不是增加另一次易竞争的存在检查。 +未实现排他发布的平台直接拒绝,不退回可能覆盖目标的普通重命名。 + +刷新独立查询两个 Agent,单方不可用不遮住另一方结果;同一项目刷新失败保留旧列表,切项目先清除旧数据。请求与写入捕获项目,迟到结果须核对版本;页面离开时取消自有任务。原生探测的取消必须进入已有交换清理路径,初始化卡住时也先终止并回收子进程,再销毁短生命周期的 Tokio 运行时;不能只丢弃读取 future,把子进程回收留给已经退出的事件循环。保存后刷新列表,是否被运行中的会话发现继续由原生 Agent 决定,不停止会话,也不承诺一次导入会让所有 Agent 都可用。 + +这些文件是用户明确创建或导入的工作资料,不是构建产物、下载缓存或可跨工作树复用资源。它们的生命周期由用户和原生 Agent 管理,写入位置仅为原生用户或所选项目目录;不写应用包、已安装适配器、签名资源或发行目录,不改变代码签名和 Sparkle 增量更新基线。没有新增可复用的构建资源,因此工作树资源清单不登记用户技能。 + +## 考虑过的备选方案 + +- **统一扫描并重建完整 Agent 技能目录**:Codex 已有官方接口,另做一套发现会遗漏插件和策略;Claude 的清单不够完整时应明确说明,而不是复制它的整个解析器。 +- **沿用 PI-Desktop 的独立启用状态和运行时合并**:PI 自己掌握执行器,Lithe 使用外部原生 Agent,自己的布尔值无法证明对方已停用。启停改为写原生配置,不保留独立开关状态;市场、删除或跨范围移动仍不提供。 +- **把所有技能存入 Lithe 数据目录**:会增加原生 CLI 看不到的第二份事实,也让其他工具与 IDE 的结果不一致,因此使用原生技能包目录。 + +## 后果 + +用户能够直接分清全局和项目技能,并通过来源标签理解适用 Agent;管理文件不干扰对话。代价是 Claude 设置清单明确不完整,原生 CLI 不可用时 Codex 的发现会显示错误,技能生效时间仍取决于原生会话。 + +Windows 已能使用共享 Core 与 Host 操作,但设置页面和真实 Windows 文件系统、进程清理尚未验收;功能矩阵保持 partial/pending。组件和受控协议测试不代替真实会话验收。 + +## 验证 + +- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh` +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentSkillsFeatureModelTests|AgentSkillsRustBridgeTests|AgentSkillsAppearanceTests|SettingsSidebarLayoutTests|SettingsSearchVocabularyTests'` +- `node .agents/skills/write-stable-tests/scripts/run-rust-tests-with-timing.mjs --manifest rust/Cargo.toml --package lithe-agent-host --report .artifacts/test-stability/agent-skills-rust.json` +- Windows 在原生 CI 中运行相同 Host 测试,覆盖暂存后出现的空目录、普通文件和非空目录都不覆盖,缺失的 Unicode 目标完整发布,以及 NUL 路径不按截断名称发布。这些文件系统用例不等于 Windows 设置页或真实 Agent 会话验收。 +- `./scripts/verify-shared-contracts.sh` +- `./scripts/verify-service-boundaries.sh` +- `./scripts/verify-runtime-bundle-immutability.sh` +- `./scripts/verify-platform-feature-matrix.sh` +- `./scripts/verify-agent-notes.sh` + +原生布局测试覆盖窄宽页面及深浅主题,检查菜单锚点落在可见宽度内;开关测试分别阻塞两次原生写入,对比首次点击前、关闭保存中、关闭确认后、再次开启保存中和开启确认后的全部原生开关位置,确保提示变化不移动技能行。可通过 `LITHE_AGENT_SKILLS_SCREENSHOTS` 输出审查截图。夹具验证原生发现与目录清单的区别、原生启用状态、Claude 本地配置可见性、作用范围和插件身份;临时项目通过真实 Core 桥验证创建、查看和冲突。受控假 App Server 验证发现只读,启停只执行发现、原生配置写入和读取核对,拒绝未知路径,均不发送创建会话或提示词请求。临时配置验证全局/项目隔离、其他字段及文件权限保留、无效配置、锁冲突、符号链接和取消清理;功能模型验证点击立即反馈但不提前报告已保存、失败回滚到已确认状态、写入串行和项目切换拒绝旧结果;原生控件回归验证保存期间当前开关立即关闭且不增加加载转圈,其他开启行的原生状态、交互能力与像素外观不变;功能模型控制两次写入的释放次序,验证同一 Agent 的连续保存不丢确认、单项失败后队列继续、重复点击去重,以及切项目/退出时未执行请求不再启动。窗口失去焦点时保留 macOS 原生非活动外观,滑块位置和实际原生配置不随窗口焦点变化。 + +## 适用范围 + +- `rust/lithe-agent-host/src/skills.rs` +- `rust/lithe-agent-host/src/skills/files.rs` +- `rust/lithe-agent-host/src/skills/settings.rs` +- `rust/lithe-core/src/agent/mod.rs` +- `macos/Sources/LitheCoreContracts/Agent/AgentSkills.swift` +- `macos/Sources/Lithe/Application/Features/AgentSkillsFeatureModel.swift` +- `macos/Sources/Lithe/Views/Agent/AgentSkillsSettingsView.swift` +- `macos/Sources/Lithe/Views/Agent/AgentSkillSheets.swift` +- `shared/fixtures/agent/skills-management-v1.json` diff --git a/.agents/notes/implemented/feature/2026-10-21-agent-global-instructions.md b/.agents/notes/implemented/feature/2026-10-21-agent-global-instructions.md new file mode 100644 index 0000000000..2f8fd0deb1 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-21-agent-global-instructions.md @@ -0,0 +1,93 @@ +# Agent 笔记:全局指令使用原生用户文件 + +状态:已实现 + +## 先说结论 + +macOS 设置的「AI → 指令」可以编辑 Codex、Claude Code 各自的全局指令。页面同时支持一份共享正文:选择 Agent 后,只把它同步到原生文件中的 Lithe 标记区块,保留用户自有内容。新对话由上游 Agent 加载;Lithe 不额外拼接提示词,也不改项目规则。页面显示实际路径,保存冲突或失败时保留草稿。 + +## 问题 + +Agent 已能继承用户的全局规则,但 Lithe 没有查看和编辑入口。只新增一个文本框并保存到 Lithe 偏好,会让界面显示与 Agent 实际收到的内容不一致;忽略 Codex override 文件,也会出现保存成功却不生效的问题。 + +## 决策 + +- 页面参考 Magpie 的紧凑卡片和 Agent 行:共享正文默认收起,Agent 行显示品牌图标、实际路径、自有非空行数与共享开关,点击展开完整原生编辑器。说明放在 ⓘ,不把长段落放到页面。使用现有设置卡片和原生多行编辑框。Codex、Claude 分别保留内存草稿,切换页面或 Agent 不自动保存。明确选择重新加载并确认放弃草稿后,成功读取才替换草稿。 +- 全局指令和技能管理的入口、功能模型、Core 命令分别保留,在同一 AI 分组中并列可用;预览构建必须包含两个功能后再交给用户。全局指令编辑器不负责加载、启停或复制技能。 +- 原生文件路径由 Agent Host(负责原生文件和进程的 Rust 适配层)解析,使用 Agent 启动时继承的 `CODEX_HOME`、`CLAUDE_CONFIG_DIR` 或平台用户目录。Codex 选择首个非空的 `AGENTS.override.md` / `AGENTS.md`,Claude 选择 `CLAUDE.md`;不支持的自定义 Agent 不猜路径。 +- Core 只分派带类型的请求和稳定错误;Swift 应用模型管理加载、保存、失败和草稿,视图不直接访问文件。原生路径仅作显示和保存校验,不成为跨平台持久标识。 +- 保存比较原文件的路径与摘要(字节内容的 SHA-256 标识),防止覆盖读取后发生的外部修改。临时文件在目标目录内创建,写完同步后原子替换;保留已有权限,拒绝覆盖只读文件,Unix 新文件为 `0600`、新目录为 `0700`。同进程保存使用不等待的锁;其他程序不共享锁,检查与替换之间仍存在很小的外部竞争窗口,不声称提供系统级事务。 +- 清空 override 后重新按原生规则读取,页面可能回到基础 `AGENTS.md`。正确做法是展示这个实际生效的来源;不要保留一个已被原生跳过的空 override 并声称已清空全部指令。 +- 文件只接受至多 1 MiB 的 UTF-8 普通文件,符号链接拒绝编辑。限制用于保护设置页资源;Codex 自身的上下文截断预算仍由原生配置决定,Lithe 不修改它。 +- 仅用户点击保存时写用户配置目录;不写发行 bundle、安装目录、代码签名资源或 Sparkle 增量更新基线。不新增下载、缓存或可跨工作树复用的生成资源,共享正文及同步锁作为用户状态登记到工作树资源排除清单。临时文件由单次保存拥有,成功和失败都清理,异常退出遗留只是一份随机命名临时文件,不会被 Agent 加载。 + +### 共享正文与原生区块 + +用户明确要求共享同步后,增加一份 `Shared.md` 作为已保存共享正文的来源。macOS +组合根通过 `FileStorage.applicationSupportDirectory()` 提供 `Lithe/AgentInstructions/`, +Agent Host 负责原子文件操作,Core 负责严格请求分派,功能模型负责草稿、排队和逐项反馈。 +这是用户配置,不是缓存,不存在版本/平台/架构/工具链身份,也不能跨工作树复制。 +`Shared.md` 和 `.sync.lock` 随用户资料保留;同目录暂存由单次保存拥有,完成或失败清理, +异常退出可能遗留随机命名暂存但 Agent 不读取。目录不从 bundle 或安装目录推导, +因此不影响代码签名与 Sparkle delta。资源复用脚本显式拒绝该用户状态。 + +同步不替换整份规则,而是插入/替换 `LITHE:SHARED-INSTRUCTIONS:BEGIN/END` 注释之间的正文。 +开关由实际文件区块推导,关闭只移除拥有的区块和它自己的换行,原始字节保持。 +非空 Codex override 仍是有效目标;不为了共享功能绕过上游优先级。正文中不能包含标记, +重复、损坏或不完整标记使开关成为未知并提示修复,不猜测删除范围。 + +共享源先保存,再依次更新已开启 Agent。每个文件单独原子替换;这不是多文件事务, +一项权限错误或摘要冲突不能回滚其他已完成项或丢掉共享正文。失败行保留原因, +实际区块与源不同显示待同步,用户显式重试。多个开关按点击顺序串行处理, +只锁定待处理行,其他行不因全局禁用变灰;取消拥有队列的页面任务后不启动后续写入。 +重新进入页面只回读实际状态,不自动同步,也不丢弃未保存草稿。 + +Swift 适配器在启动工作任务前检查取消,并通过 `RustCoreCancellation` 把注册前到达的 +取消保存到同步状态。Core 在登记操作后、分派命令前同步发出 `requestStarted`;回调在这个 +边界补传取消,因此快速保存也不会在注册空隙中漏掉取消。不要只调用一次尚未登记的 +`cancel`,也不要用定时重试保证短写入:写入可能在首次重试前完成。已完成的原子替换 +不会因稍后取消而撤销,跨文件保存仍只在每次写入之间观察取消。 + +正确做法:用户在 Codex 文件写了自有规则,开启共享后两者并存;关闭后自有规则逐字节保留。 +不要把关闭共享解释成关闭 Codex 或删除其整份 AGENTS.md,也不要在启动时自动覆盖外部修改。 + +布局与行为核对 [Magpie 指令页](https://github.com/yetone/magpie/blob/ccbc7278180e513bf95461c62eeb15729fc7cae7/internal/gui/assets/plugins/library/client.js#L1173) +的共享卡片、Agent 行和区块保留方式。本次独立实现,只支持 Lithe 已有原生文件能力的 Codex、Claude Code, +一份共享正文,不引入截图中其他 Agent、多个命名方案、导入私有规则或文件链接。 + +交互结构参考 [PI-Desktop 的全局指令页](https://github.com/vastsa/PI-Desktop/blob/a6f4ba121e524a167c42466c7b4debbb3cfd0744/apps/desktop/src/features/settings/agent-sections.tsx):路径、文本编辑和显式保存。没有复制其源码或视觉样式。原生规则依据 [Codex AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md) 和 [Claude Code memory](https://code.claude.com/docs/en/memory)。 + +## 考虑过的备选方案 + +### 把共享正文直接注入每次请求 + +会和原生文件加载产生两份提示词,并需要维护上下文去重和历史恢复语义。因此共享正文通过显式区块同步落到原生用户文件,由上游加载;不修改 Agent 的请求或注入链路。 + +### 保存时停止并重连所有项目 + +会打断正在进行的任务,且不能撤回已进入模型上下文的旧内容。因此只提示新建对话,不自动停止或篡改现有会话。项目规则优先级继续由上游 Agent 决定,不在页面声称全局指令能覆盖所有规则。 + +## 后果 + +用户可以直接管理 CLI 也会读取的全局文件,冲突能保留草稿并显式恢复。代价是符号链接配置需先由用户自行处理,外部编辑不会实时推送到页面,更新也不会改写现有对话。Windows 已可通过共享 Core 调用文件能力,尚无对应设置页和 Windows 实机验证;矩阵保留部分实现、待验证状态。 + +## 验证 + +- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh`。 +- `SharedInstructionsFeatureTests` 覆盖共享草稿、切换排队、逐项失败与页面取消;Host shared 测试用临时目录验证真实读写、关闭字节还原、override、损坏标记和跨文件部分成功。 +- `AgentInstructionsCancellationTests` 使用真实 Core 和隔离共享源,确定性控制进入适配器前取消、编码后注册前取消,确认文件及锁未创建、已有字节不变,下一次有效保存仍能完成;不写开发者原生指令文件。 +- 典型共享流程前后比较 app bundle 文件清单与哈希,用户目录不参与发行资源写入;`node --test scripts/test-reuse-worktree-resources.mjs` 确认共享指令用户状态不能复制。 +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentInstructionsFeatureModelTests|SettingsSidebarLayoutTests|SettingsSearchVocabularyTests|ContextMenuCoverageTests'`。 +- Agent Host 计时测试覆盖原生优先级、读取不创建文件、保存和清空、外部修改与路径变化、UTF-8/大小限制、符号链接拒绝、权限保留和临时文件清理。 +- Core 的 `global_instruction_contract_dispatches_and_rejects_unknown_agents_without_file_access` 验证共享 fixture 与严格请求边界。 +- 隔离临时用户目录,通过同一 Core 命令保存,再捕获真实 Codex CLI 与 Claude CLI 发出的模型请求;Codex 的保存、更新、override 保存、清空 override、清空基础文件,以及 Claude 的保存、更新、清空共八项均确认新增指令出现、旧指令消失。Codex 五项同时确认项目规则继续存在。响应来自本地受控服务,不是在线模型推理;测试进程和临时目录均回收,不修改用户实际配置。 +- `./scripts/verify-runtime-bundle-immutability.sh`、`./scripts/verify-service-boundaries.sh`、`./scripts/verify-shared-contracts.sh`、`./scripts/verify-agent-notes.sh`、`./scripts/verify-platform-feature-matrix.sh`。 + +## 适用范围 + +- `rust/lithe-agent-host/src/instructions.rs` +- `rust/lithe-agent-host/src/instructions/shared.rs` +- `rust/lithe-core/src/agent/instructions.rs` +- `macos/Sources/Lithe/Application/Features/AgentInstructionsFeatureModel.swift` +- `macos/Sources/Lithe/Views/Agent/AgentInstructionsSettingsView.swift` +- `shared/fixtures/agent/agent-instructions-v1.json` diff --git a/.agents/notes/implemented/feature/2026-10-21-declarative-native-plugin-ui.md b/.agents/notes/implemented/feature/2026-10-21-declarative-native-plugin-ui.md new file mode 100644 index 0000000000..599f2f787b --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-21-declarative-native-plugin-ui.md @@ -0,0 +1,186 @@ +# Agent 笔记:声明式原生插件树与动作 + +状态:已实现 + +## 先说结论 + +插件可以声明树、列表、输入框、下拉选择、复选框、工具栏按钮和右键菜单,由 macOS 使用 Lithe 共享控件显示。开发者只提供数据与函数,不必开发前端样式。本轮复用现有插件函数调用和跨项目生命周期,不增加后端执行系统;Windows 原生渲染留待后续独立实现和验证。 + +## 问题 + +文本窗口无法表达节点选择与业务动作;Web 页面则要求插件作者自己维护控件与样式。现有窗口注册、停靠、主题和函数调用已经具备基础能力,重复建设会带来两套状态和清理边界。 + +## 决策 + +在 ui 能力版本 3 下引入独立版本的原生声明,首版支持一棵树、一个列表或一个表单及其面板动作。共享 Core 同时负责初始声明与刷新数据的结构、所有权和预算校验(原生适配器不复制节点规则),应用功能模型负责选择、展开、数据替换和事件,视图只使用已有标题栏、工具栏按钮、菜单、行样式和滚动条。声明不能携带颜色、字体或脚本。树/列表的行容器在共享行背景外提供 Tree.horizontalInset 的左右留白,让悬停与选中保持同一外边界;文字内部仅保留层级缩进。键盘区域复用 litheFocusEffectHidden 关闭系统矩形焦点环,仍通过共享行的活动/非活动选中配色表达焦点,保留方向键和无障碍。 + +数据源和动作复用现有插件函数入口:它负责授权、工作区和插件范围、超时及取消。原生窗口使用与网页相同的会话登记表,插件停用、替换及关闭项目时同步取消请求。隐藏窗口也释放请求,重开从已有状态存储恢复选择和展开。发布包与插件包只读,状态只写平台提供的原有可写存储,不改变签名或 Sparkle 增量更新基线;没有新增可跨工作树复用的缓存资源。 + +正确做法:插件声明树和刷新动作,函数只返回稳定节点 ID 与标签。不要这样做:按插件名在工作台增加视图分支,或为了渲染树另建后端进程与生命周期。 + +## 考虑过的备选方案 + +- 要求所有插件提供 Web 页面:已有能力保留,但不满足免写样式的需求。 +- 执行第三方 Swift 界面代码:增加二进制兼容、签名和隔离问题,不采用。 +- 一次实现表单、任意组件组合与领域迁移:超出本轮范围,先验证最小完整路径。 + +## 后果 + +同一份树与动作声明可以直接使用 IDE 样式。代价是首版只允许每个面板匹配一个已检测构建实例,完整刷新最多 2000 个节点,尚无多实例选择器或按需加载子节点;这些限制在契约中明确,不能默默选择第一个实例。动作限定面板内,不能据此宣称全局独立命令或整个 #1209 已完成。 + +选择与展开按稳定 ID 保存;无效回复保留上一份有效节点并显示错误。入口必须保留稳定的非空宿主容器:不能依赖最初没有子视图的 Group 启动实际工作台中的出现回调。首次内容创建、后续重绘和清理均绑定容器,初始化失败必须显示原因,不能留下无标题的空白面板。 +同一停靠槽会复用相同 SwiftUI 渲染类型;内部会话必须以 contribution.id 为身份边界, +不能只按工作区标识。否则切换两个插件面板会沿用前一个面板的 State,释放回调却拿到 +后一个窗口 ID,遗留会话后再次打开就报重复登记。 +仅增加窗口身份仍不够:隐藏、重开和父容器销毁时,不能依赖 onDisappear 回调再读取 +可能已销毁的 State 存储。StateObject 会话所有者持有准确的 feature 与释放函数, +出现/消失时显式打开/关闭,销毁时在 MainActor 同步释放未结束的会话;登记表仍核对句柄身份, +旧释放不能关闭后来新建的会话。 +外层分隔布局改变结构时,新分支的面板可能先挂载,旧分支才消失;仅靠视图身份与 +销毁回调无法避免这个重叠。登记表允许同一插件同一窗口原子替换:先登记新句柄, +再关闭旧句柄,旧视图随后释放时仍按句柄身份检查。不同插件、网页句柄以及被停用的插件 +不能借此覆盖登记。样式不是插件作者的责任:标题栏 +复用已更新的公共 LitheToolWindowHeader(41pt/13pt Bold、纯文字、无图标和边框/底线),不由插件另画。完整挂载回归需要把插件视图嵌入父容器,不能只渲染已经创建好的内容面板。 + +已在真实 DEBUG 预览中通过磁盘安装的 Sample Build 和测试工作区验证:节点加载、展开、Inspect、右键点其他节点、方向键,以及隐藏重开恢复。真实产品浅深色、窗口失焦、停靠恢复和示例安装验收仍应独立记录,组件测试不代替原生产品验收。 + +真实 UI Lab 预览已验证:静态树、静态列表、空面板同时显示,左右面板分别隐藏重开, +动态树/列表与静态面板切换均正常,没有重复登记错误;标题均为纯文字,无标题图标、边框和底线。 +本轮相关195项 macOS回归通过。桌面工作区曾在 FSEventStreamCreate 监控父目录时阻塞, +因此这轮面板验收使用含同一 marker 的本地临时工作区;没有把该目录监控问题记为已修复。 + +工具栏动作只显示图标,title 用作悬停提示和无障碍名称;复用现有 LitheToolWindowHoverAction, +鼠标进入整个面板时显露,离开隐藏,键盘/无障碍聚焦仍可操作。动作可用既有宿主图标声明 +选择语义图标,原生动作不加载自定义包图片;未声明时 refresh 使用刷新,invoke 使用通用执行。 +不能按插件或动作 ID 猜测业务图标,也不能退回文字按钮。 +本轮52项宿主图标/悬停/原生面板回归与Core图标边界检查通过。实机已确认非悬停时工具栏隐藏; +鼠标坐标自动化返回 windowNotFoundAtPosition,未据此宣称实机进入/移出已通过,仍需用户验收。 + +### 表单只接声明,不复制已有公共控件 + +输入框复用 `litheSettingsTextField`,选择器复用 `LitheSettingsSelect`,复选框复用 +`LitheSettingsCheckbox`,后者仅补充 literal 标题入口,样式不变。新的 `form` 内容容纳 +至多32个字段和16个按钮,避免为了输入三个控件而引入任意布局/脚本/表单引擎。按钮外观仍使用已有公共 owner。 +控件 ID、值类型、选项、动作引用与预算由 Core 校验;宿主恢复已保存字段时还要剔除 +已删除字段、变更类型和失效选项,不能让旧状态绕过新声明。 + +输入每次编辑只存本地值;回车提交,选择器与复选框在改变后提交。onCommit 指向 +已有的非选择 invoke 动作,所有当前值以 values 通过现有函数通道传递,之后来源 +仍返回空 nodes。没有新后端接口;没有 onCommit 的字段仅本地保存。这样保留中文 +输入法编辑过程,避免每个按键都启动插件函数并禁用输入。关闭、停用、替换继续使用 +既有取消与晚到结果防护。树/列表旧函数不增加 values 参数,保留原参数形状。 + +本轮63项 macOS图标/原生面板/公共选择器/菜单覆盖回归通过(8.093s),新增实际 +挂载表单检查耗时46ms;19项声明式Core测试与打包后Node示例测试通过。 +已生成 NativeUILab 0.2.0,由用户手动导入;完整产品中的表单深浅色、输入法、 +弹出选择、禁用与跨工作区人工验收仍 pending,不能由挂载测试代替。 + +### 表单栅格 + +form 可声明1–12列及控件span,省略span占满一行,省略layout维持旧纵向布局。 +按声明顺序排放,剩余列不足时换行;Core拒绝越界跨度、非表单布局及自定义间距。 +宿主通过共享 `LitheFormGrid` 的原生 SwiftUI Layout 测量和放置,不发布逐控件几何 +Preference,也不因为缩放重建不同的视图分支。12pt间距与640pt单列阈值由 +LitheTheme.FormGrid持有,字段值和焦点继续由稳定ID和既有控件保存。 +验收插件0.2.1按用户要求将所有字段各占6列,每行两个完整字段(标题+控件)。 + +本轮24项macOS栅格/原生表单/公共选择器回归通过(0.918s),原生宽窄布局 +高度测量检查耗时55ms;20项声明式Core校验及打包后Node示例检查通过。 +0.2.1声明为每行两个字段,仍由用户手动导入验收;无layout的旧0.2.0声明 +保留单列,不能把仅更新宿主当作测试插件已更新。 + +### 未聚焦输入框的光标 + +表单已复用公共 `litheSettingsTextField`,但原公共修饰器未接入搜索输入框已有的 +`litheTextEditingCursor`,导致鼠标进入未聚焦字段时仍是箭头,点击后才由原生编辑器 +显示I形光标。现在由公共输入框修饰器在完整控件区域安装已有光标语义;禁用时仍为 +箭头,移出恢复箭头,不在插件或各页面补丁,也不改原生编辑、组合输入或提交行为。 + +本轮26项搜索输入/原生表单/选择器回归通过(2.483s),复用既有光标处理。 +组件回归不等同于实机鼠标悬停验收,最终未聚焦/禁用字段的光标仍由用户在新预览核对。 + +### 公共组件验收插件0.3.0 + +以现有通用表单公共组件为本轮完整范围:输入、搜索、多行输入、下拉、分段选择、 +复选框、数字步进器,配合标签信息提示、普通/主要按钮与栅格。搜索新增完整 +`LitheSearchField` 入口,内部组合已有原生输入、光标、焦点、图标和清空动作; +其余字段直接复用既有owner,补充literal标题入口,避免宿主误翻译插件提供的文字。 + +info直接使用LitheInfoTip的延迟、固定、关闭、键盘与辅助功能行为,不额外写Tooltip。 +按钮复用LitheActionButtonStyle;只增加内容宽度/填满容器的布局能力与宽度上限, +默认参数保留既有页面的尺寸与颜色,不向插件开放视觉参数。按钮与值字段分开声明, +不会成为State.values或函数参数里的假字段。执行仍使用现有action与provider桥接。 +多行输入保留Enter换行,以表单按钮提交;其他字段保留回车/修改提交的语义。 +Core对信息长度、字段类型、选项、数字范围、按钮引用、跨度及属性适用范围统一校验。 + +本轮验证:63项Swift公共控件/原生表单/栅格/选择器/样式覆盖回归通过(10.335s), +21项Core声明插件测试通过;源码及打包后Node验收通过。 +Core全量初跑有8项历史测试依赖master,而本机系统Git默认main;仅为测试进程 +设置init.defaultBranch=master后,1069项Core单测及Node/Java集成全通过(16.77s单测)。 +Swift桥接校验通过;Java测试显式使用已安装OpenJDK21,避开系统java启动桩。用户当前运行另一按钮分支 +预览,本轮保留其窗口;0.3.0插件需使用本分支新宿主导入,完整实机视觉验收仍待用户完成。 + +### 完整输入入口与0.3.1验收 + +原生表单、IDE设置和其他使用共享输入样式的页面,都通过完整LitheTextField / +LitheTextEditor调用,不再由调用方组合原生输入与样式修饰器。搜索框统一为 +LitheSearchField,附加动作和外部焦点保留;公共标签与字段间距由LitheFormField负责。 +字段和表单按钮仅禁用实际输入/动作,i说明可悬停、点击固定和键盘打开。 +InfoTip同时处理已有页面禁用整个父区块的情形,不把例外补丁留给插件渲染器。 +0.3.1给每种禁用字段和禁用按钮增加独立说明,方便逐项定位验收;协议不新增字段。 +本轮78项macOS回归通过(10.456s),新增完整入口检查373ms、原生输入深浅色 +两例126ms、实际声明式挂载和禁用提示打开131ms。源码和打包后Node验收均通过; +边界、契约、矩阵、笔记和bundle不可变静态检查通过。保留用户正在操作的预览, +0.3.1新插件由用户导入;完整人工视觉/键盘验收仍pending。 + +### 合并#1422后的文字按钮标准 + +声明式动作直接使用preview已合并的LitheActionButtonStyle,不携带本分支原来的 +fillsWidth、maximumWidth或borderInset扩展。公共按钮独立拥有主按钮蓝色及 +hover/按下/禁用状态、28pt高度、72pt最小宽、14pt水平内边距、字体和焦点外观。 +动作组使用公共6pt间距,横排不足时纵排;字段栅格继续拥有字段布局,不拉伸文字按钮。 + +实验包0.3.0/0.3.1的buttons.width与buttons.span兼容读取,但不再影响动作组布局。 +这样已有测试包能继续加载,同时不会把旧绘制规则带回公共按钮。新0.3.2示例省略 +旧布局提示,以普通/主要按钮的短/长文案、禁用、提示及慢动作验证统一样式。 +旧fixture保留fill提示,原生挂载回归验证实际按钮仍为28pt且不会填满栅格。 + +同步preview时保留新版设置分类/插件贡献页面、已安装插件启停分组/筛选,以及 +Git未跟踪目录摘要和可见行缓存;这些界面仍使用完整公共树/列表和输入入口。 + +本次同步后的验证:macOS 323项/56套件通过(10.561s),覆盖公共按钮、实际声明式挂载、输入/树列表、设置、Git及HTML预览;Rust Core全量1106项通过。UI Lab源码及打包后的SDK检查通过。完整产品人工视觉验收仍pending。 + +## 验证 + +- `./scripts/verify-shared-contracts.sh` +- `cargo test --manifest-path rust/lithe-core/Cargo.toml --lib native_ui_validates_version_ownership_actions_and_budgets` +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'PluginNativeUI|PluginWeb|WorkbenchContributionHostTests'` +- `LITHE_PLUGIN_SDK_ROOT= node --test macos/Tests/Fixtures/native-ui-lab/native-ui-lab.test.mjs` +- `./scripts/verify-service-boundaries.sh` +- `./scripts/verify-runtime-bundle-immutability.sh` +- `./scripts/verify-platform-feature-matrix.sh` + +## 适用范围 + +- `shared/contracts/plugins/v1/native-ui.schema.json` +- `rust/lithe-core/src/plugins/native_ui.rs` +- `macos/Sources/LitheModuleAPI/Plugins/PluginNativeUI.swift` +- `macos/Sources/Lithe/Application/Features/PluginNativeUIFeature.swift` +- `macos/Sources/Lithe/Views/Workbench/PluginNativeToolWindowView.swift` +- `macos/Tests/Fixtures/native-ui-lab/`(宿主验收 fixture,打包使用外部官方 SDK) + +## 2026-10-23 SDK 迁移后的验收归属 + +遵循 #1442,主仓库不恢复 SDK 及其端到端测试。UI Lab 作为 macOS 宿主的回归与人工验收 fixture 保留在 `macos/Tests/Fixtures/native-ui-lab/`;Core 继续验证其完整清单。打包和 Node 验收显式通过 `LITHE_PLUGIN_SDK_ROOT` 使用外部 lithe-plugin-docs 的官方 Node SDK,复制结果仅进入忽略的 `.artifacts/native-ui-lab/`,不写入已安装包或应用 bundle。插件开发者说明与 SDK 发版继续归 lithe-plugin-docs。 + +## 与 MCP 和 Agent 设置的公共输入入口兼容 + +MCP 命令行的新编辑行为继续由 MCP 功能层解析并校验引号,公共 `LitheTextField(axis: .vertical)` 仅负责允许换行和共享外观,调用方可限制可见行数。默认单行输入及密码框高度保持原规则。Agent 指令编辑器把外部布尔焦点传入完整公共编辑器,使 Command-S 仍对应当前编辑区;不在外层容器重装焦点或旧样式 modifier。新增的技能、指令和 MCP 页面都使用同一搜索、输入和多行编辑入口,避免上游合并后重新出现并行样式。 + +VCS Provider 的变更分组与文件行也通过 `LitheTreeList` 呈现,使用分组/文件两类稳定 ID 映射原有路径选择;包含勾选仍由业务控制,公共入口负责展开、选择、菜单、键盘及默认无障碍激活。新增文件激活回归应验证最终打开文件,而非仅改变选中状态。 + +## 公共树的外部改选与键盘导航 + +选择绑定是调用方发布当前选中的唯一入口。公共控件记录自身发起的选择,只有外部改选才重置键盘游标和范围锚点,避免画面已到新行而 Return 仍操作旧行;Shift 扩选与缩选保留原锚点,并直接处理 AppKit 的范围选择命令。普通不可选标题不进入上下键遍历,可展开分组仍保留键盘焦点和左右键语义。原生挂载回归验证外部 a→c 后 Return/方向键及 Shift 的最终对象,并在真实 Problems 视图检查 A 诊断→Down→Return 最终打开 B 文件的行列。 + +同步 preview 的会话历史时保留排序/行模型缓存和 Equatable 列表边界,公共树仅接管平铺可见行与交互;新建项目复用已抽出的 PluginFieldControls,侧栏与 Settings 共用公共树,不恢复旧样式 modifier。 diff --git a/.agents/notes/implemented/feature/2026-10-23-macos-agent-editor-large-history-and-claude-layout.md b/.agents/notes/implemented/feature/2026-10-23-macos-agent-editor-large-history-and-claude-layout.md new file mode 100644 index 0000000000..81bf112744 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-23-macos-agent-editor-large-history-and-claude-layout.md @@ -0,0 +1,60 @@ +# Agent 笔记:macOS 编辑区 Agent 对话的大历史加载与 Claude Desktop 式布局 + +状态:已实现 + +## 先说结论 + +打开一个很大的历史对话,慢的不是上游 Agent,而是 Lithe 自己:回放每来一条消息就把整份对话复制一遍并通知界面,随后又一次性排版全部消息。现在回放期间的消息先放进不对外发布的缓冲区,回放结束时只发布一次;对话视图只构建最近 48 项,更早的内容点“显示更早的 N 项”再展开。同时,编辑区(对话作为编辑器标签打开时)的界面向 Claude Desktop 靠拢:历史列表有置顶与日期分组和“…”菜单,输入框变成圆角卡片,审批模式和模型移到卡片下方的状态行,右侧详情栏改成可折叠分区并新增“更改”。右侧工具窗口里的 Agent 样式不变。 + +## 问题 + +用户反馈大历史对话打开很慢,期望无论多大都在 2 秒内加载完成。先量化了每一段的耗时: + +- 上游:直接对 Claude ACP 发 `session/load`,43 MB 的会话约 1 秒回放完;它只重放最近一段上下文,回放事件只有几百条。上游不是瓶颈。 +- Lithe 接收:`AgentConnectionModel.conversations` 是 `@Published` 字典。对它做 `conversations[id]?.x = …` 会先整体读出、修改、再整体写回,数组写时复制,所以每个回放事件都复制已有的全部消息,总成本随历史长度平方增长。调试构建下 10 800 个事件耗时 27 秒。 +- Lithe 排版:回放结束后 `LazyVStack` 仍要为底部跟随估算整个文档高度,300 轮的历史首次排版 12 秒,1 200 轮 31 秒。 + +## 决策 + +- 回放走专用缓冲区:`loadSession` 期间的 `append`/`upsertTool` 写入 `replayMessages` 和 `replayToolIndex`(普通存储属性,原地修改,工具更新 O(1)),`sessionLoaded` 时一次 `commitReplay` 发布。连接中断且没有旧副本时提交已收到的部分(与以前一致,仍不标记为完整历史);加载失败且有旧副本时丢弃缓冲区并恢复旧副本。子 Agent 状态更新要改消息时直接作用于缓冲区。没有活动回合时不再调用 `refreshTurnClock`,因为它同样会触发整体复制。 +- 对话视图窗口化:`AgentTranscriptWindow` 规定初始 48 项、每次展开 96 项。搜索时不窗口化;导航栏点到窗口之外的回合时先扩大窗口再滚动;展开更早内容后把滚动位置保持在原来的第一项。 +- 历史列表一次计算快照(排序和筛选原来每次渲染重复约 8 次),按“收藏 → 今天 → 昨天 → 过去 7 天 → 过去 30 天 → 更早 → 时间未知”分组;筛选或搜索时不分组。每行用 Agent 图标磁贴、标题、消息数与会话 ID(保留复制)、相对时间和“…”菜单(收藏、重命名、导出、复制会话 ID、移出历史/恢复)。选择、归档、已移除筛选、导出等原有能力全部保留。 +- 编辑区对话 UI:用户消息用中性灰圆角气泡;输入框改 16pt 圆角带轻阴影,卡片内只留“添加”和“发送”,审批模式在状态行左侧、模型在右侧,之后是订阅额度和上下文环。详情栏的上下文、更改、会话统计三个分区可单独折叠,状态保存在 `lithe.agent.editor.detailsCollapsed`;标题栏新增详情栏开关按钮,原菜单项保留。“更改”来自对话中的文件编辑记录,只显示已有数据。 +- 历史列表滚动与刷新:页面观察整个 `AgentConnectionModel`,流式输出每几十毫秒发布一次。`AgentHistoryRowsCache` 分三层记忆:排序结果(会话、标注、搜索、筛选不变就不重排,O(n log n) 只在必要时执行)、行数据(当前会话、已加载对话的消息条数、分钟数不变就复用)、版本号(列表 `==` 只比较版本号,O(1))。时间戳解析、本地化 `.lproj` 查找、`DateFormatter` 都做了缓存;`canExport` 改为打开菜单时对该行计算;行高固定为 56pt;行菜单只在悬停或当前行时挂载原生锚点。 +- 编辑区右侧详情栏改为 `AgentEditorDetailsColumn`:每个分区一张圆角卡片,标题后有 ▾ 可折叠;上下文卡显示已用百分比、容量、彩色进度条和"彩色方块 + 名称 + token + 占比"图例(无分解数据时显示"—");更改卡显示总增删行数和文件列表(文件名 + 所在目录),超过 5 个折叠到"显示更多"。工具窗口输入框打开的会话详情弹窗仍是原来的 `AgentEditorDetailsPanel`,样式不变。 +- 编辑区输入卡片左侧是 Agent、审批模式、模型,右侧是额度圆环(订阅时)与上下文圆环;回车发送,文件用拖入、粘贴或 @,回复进行中才显示停止按钮。上下文圆环始终显示,未上报时为空圈。审批模式的本地化规则见共享 Agent 对话 Note。 +- 详情栏负责任务、子代理和文件审查。详情栏被关闭或编辑区宽度不足以放下它时,输入框上方恢复显示活动摘要,避免这些入口消失。 +- Claude 订阅额度:Claude Code 在回复过程中产生 `rate_limit_event`,适配器以 `usage_update._meta["_claude/rateLimit"]` 转发,每条只描述一个窗口(`five_hour`、`seven_day`、`seven_day_opus`、`seven_day_sonnet`,`utilization` 为 0–1 比例,`resetsAt` 为 Unix 秒)。`AgentSubscriptionQuota` 按窗口类型累积,来源标为 `claude`:快照在窗口重置前一直有效(Codex 的“超过 2 分钟视为过期”只适用于主动轮询),悬停卡片显示“截至某时刻,仅在回复时更新”。不调用 Anthropic 接口、不轮询;overage 和未知窗口不显示。 +- 这些改动只作用于 `isEditorLayout`;工具窗口的 Agent 面板(CC GUI 样式)没有改变。 + +## 考虑过的备选方案 + +- 本地缓存转录快照,再次打开时先显示缓存:能把二次打开压到几十毫秒,但会让 Lithe 在磁盘上持有一份 Agent 对话内容,并需要失效策略。实测上游只要约 1 秒,Lithe 侧修复后总耗时不到 1 秒,所以没有引入。如果以后上游回放变慢,再单独评估。 +- 把回放解析移到后台线程:事件很少时收益不大,而且需要把 `[String: Any]` 越过 actor 边界;平方复制消除后主线程的接收成本已经可以忽略。 +- 对话视图改成真正的虚拟列表:改动面太大,会牵动底部跟随、导航锚点和文本选择;窗口化用最小代价把首次排版量与历史长度脱钩。 +- 在历史列表里加“固定”之外的新概念(置顶):Lithe 已有“收藏”,沿用收藏并把它显示在列表最前,避免两个相似概念。 + +## 后果 + +- 调试构建下,1 200 轮(10 800 个事件)的接收从 27 秒降到约 0.45 秒,首次排版从 31 秒降到约 0.3 秒;加上上游约 1 秒,仍在 2 秒内。 +- 代价:窗口之外的旧消息不会在首屏构建,滚到最顶部需要点一次“显示更早的 N 项”。回放期间看不到逐步出现的消息(原来也是等回放结束才显示)。 +- “消息数”只对已加载过的会话显示;Claude Desktop 的“N 条消息”需要上游列表提供计数,目前没有。 +- “分叉会话”和“归档会话”没有对应的上游能力,菜单里不放占位项。 + +## 验证 + +- 历史滚动基准(200 条会话,程序化逐帧滚动,调试构建):纯滚动 p95 从 8.6ms 降到 1.4ms;流式输出期间刷新 p50 从 14ms(25% 帧超过 16.7ms)降到 1.7ms(0 掉帧);标题变化 p95 从 34ms 降到 10ms。排序与分组在 200 / 2,000 / 20,000 条会话下每行成本分别为 14.9 / 5.5 / 4.2µs,确认没有平方级路径。 + +- `AgentLargeHistoryLoadTests`:1 200 轮回放整体只发布少量次数、顺序和工具合并正确;中断保留部分回放但不标记完整;失败恢复旧副本;窗口扩大规则;历史分组。 +- `./scripts/test-macos.sh --filter AgentHistoryTests`、`AgentConversationSelectionTests`、`AgentTranscriptScrollTests`、`ContextMenuCoverageTests` 等 Agent 相关套件。 +- 对 Claude ACP 的 `session/load` 探测方法:直接起 `claude-agent-acp`,发 `initialize` 与 `session/load`,统计事件数量和耗时。 + +## 适用范围 + +- `macos/Sources/LitheAgentConversationModule/Application/AgentConnectionModel.swift` +- `macos/Sources/Lithe/Views/Agent/AgentTranscriptView.swift` +- `macos/Sources/Lithe/Views/Agent/AgentHistoryView.swift` +- `macos/Sources/Lithe/Views/Agent/AgentComposerView.swift` +- `macos/Sources/Lithe/Views/Agent/AgentEditorConversationChrome.swift` + +Windows 的编辑区对话仍使用自己的布局,见 [Windows Agent 对话在编辑区标签与工具窗口之间的放置](2026-10-18-windows-agent-conversation-editor-placement.md)。 diff --git a/.agents/notes/implemented/feature/2026-10-23-plugin-project-generators.md b/.agents/notes/implemented/feature/2026-10-23-plugin-project-generators.md new file mode 100644 index 0000000000..a918c64220 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-23-plugin-project-generators.md @@ -0,0 +1,147 @@ +# Agent 笔记:插件注入的新建项目向导 + +状态:已实现 + +## 先说结论 + +「New Project」原来只是选一个文件夹再打开。现在它是参照 IDEA 的新建项目窗口:左栏分「新建项目」和「生成器」两组,右侧是所选条目的表单。窗口外框、名称、位置、「创建 Git 仓库」和「空项目」归宿主;其余条目由插件声明的**项目生成器**(Project Generator,插件声明的一种新建项目方式)注入。 + +契约上**没有升 `pluginAPIVersion`**,只在级别 3 新增能力 `projectGenerators: 1`。Java、Maven Archetype、Spring Boot 三个生成器**直接声明在 Java Support 里**,不另设模板插件:模板属于对应语言插件,用户不用多管一个插件。Java Support 停用时条目照样列出、变灰,并提示「启用」。 + +界面复用 #1395 设置窗口的原生 SwiftUI 组件。为此把设置侧栏行、插件字段控件和文件夹选择器抽成了共享组件,设置窗口和克隆仓库窗口改用它们。 + +## 问题 + +- **入口什么都没做。** 欢迎页和项目切换器的「New Project…」调用 `chooseProject(title: "New Project")`,与「打开」只差一个标题。 +- **生态知识没有地方放。** Cargo、Spring Initializr、Maven Archetype 各有自己的脚手架。写进宿主会违反「宿主不按插件名分支」;写进 Core 会重演 `execution/detectors/` 写死生态表的问题。 +- **基础环境要重复配置。** 建完 Java 项目后还得去设置里再选一次 JDK。 +- **能力版本的副作用。** [按能力分版本](../architecture/2026-10-17-plugin-capability-versions.md)要求 `requires` 恰好列出用到的能力,宿主缺任何一项就整个插件不可装。生成器放进语言插件后,不支持生成器的宿主连这一版语言支持一起看不到。Java Support 只在 macOS 分发、随同版本的 Lithe.app 内置,所以影响的只是「旧宿主拿不到新版 Java Support」;社区语言插件加生成器前要权衡这一点。 + +## 决策 + +### 1. 谁负责什么 + +| 部分 | 负责方 | 落点 | +| --- | --- | --- | +| 声明的校验、表单求值(派生默认值、显示条件、错误码)、生成计划、压缩包解压 | Rust Core | `rust/lithe-core/src/project_generators/`,命令 `project.*` | +| 契约与标准答案 | 共享契约 | `shared/contracts/plugins/v1/README.md` 的 Project generators 一节,`fixtures/project-generator.plugin.json`、`fixtures/project-plans.json` | +| 窗口、名称、位置、Git、空项目、启用插件提示 | macOS 宿主 | `NewProjectView`、`NewProjectFeatureModel` | +| 执行计划:临时目录、写文件、跑命令、下载、移动到位 | macOS 宿主 | `ProjectGenerationService`(编排)+ `MacProjectGenerationPlatform`(平台操作) | +| Java、Maven Archetype、Spring Boot 的字段、模板和步骤 | 插件 | `Plugins/mac/Official/JavaSupport/`(`plugin.json` 的 `projectGenerators`、`icons/`、`templates/`) | + +宿主里没有任何生成器 ID 分支。项目打开后由已有的 `build.detect` 按 markers 识别 Maven、Gradle,生成器不声明「按什么导入」。 + +### 2. 清单:`projectGenerators` 1 + +每个生成器只声明:展示(`displayName`、`section`、`icon`、`order`)、字段(`fields`)、需要的语言或构建系统(`requirements`)、步骤(`steps`)、创建后打开的文件(`openFiles`)。字段类型沿用插件设置的命名(`text`、`enum`、`boolean`、`multiSelect`),另加 `runtime`(宿主发现的 JDK)。字段只能读在它之前声明的字段,所以默认值一次求完,显示条件不会成环。跨字段的版本约束也按这个顺序声明:`text`、`enum` 字段的 `maximumVersion` 是读取前面字段的模板,Spring Boot 的目标 Java 写 `${field.jdk.major}`,Core 在表单和计划阶段都拒绝高于所选 JDK 的组合(`versionTooHigh`),宿主不用为生成器写分支。 + +步骤三种,按顺序在临时目录里执行: + +- `files`:复制包内模板目录。 +- `command`:调用脚手架(例如 `mvn archetype:generate`),可用 `collect` 把工具生成的子目录内容上移到项目根。 +- `archive`:拼 HTTPS 地址下载 zip 并解压(Spring Initializr)。 + +需要 `project.generate` 权限;`command` 必须写 `mayUseNetwork`,`archive` 一律算联网。每条 `command` 步骤整体(命令、参数模板、环境、`when`、`collect`、超时、联网标记)是安装确认里的一条 `projectGenerator` 程序;插件更新新增命令或改了其中任何一项,都会作为新程序要求用户重新确认。`files` 和 `archive` 不启动进程,由权限覆盖。 + +### 3. 模板语法 + +清单字符串用 `${…}`,与其他插件模板一致。**模板文件用 `{{ … }}`**,因为 POM、Gradle 脚本、properties 本身就含 `${project.version}` 这类文本,必须原样进入项目。文件里只有 `{{` 后紧跟 `project.` 或 `field.` 才算变量。 + +修饰符 `path`、`package`、`lower` 用 `|` 串接,例如 `${field.groupId|package|path}`。路径段以 `dot-` 开头会变成点文件(`dot-gitignore` → `.gitignore`),因为插件包扫描会跳过隐藏文件。 + +**带变量的路径写在步骤的 `target` 里,不要写进模板文件名**:仓库要在 Windows 上检出,文件名里的 `|` 在 Windows 上非法。 + +### 4. 生成过程 + +1. Core 用 `project.generationPlan` 校验表单,展开所有文件、argv 和 URL。 +2. 宿主在目标父目录下建 `..lithe-creating-<随机>` 临时目录,按步骤执行。压缩包由 Core 的 `project.extractArchive` 解压:拒绝目录外路径、符号链接、覆盖已有文件,并限制实际写出的字节数。 +3. 全部成功才原子移动到最终位置;目标已存在就拒绝。失败或取消时删除临时目录。 +4. 所选 JDK 通过 `ProjectRuntimeService.presetJavaHome` 写成新项目的 JDK;勾选 Git 时用 `git.initialize` 建仓库;`openFiles` 写进新项目的工作区会话,项目在哪个窗口打开都会恢复这些标签。 + +压缩包边下载边计数,超过 256 MiB 立即取消,不先占满磁盘;服务器拒绝请求时最多读 64 KiB 的错误说明。 + +插件包、app bundle 只读;写入只发生在用户选的目录和一个临时下载文件里,不影响代码签名和 Sparkle delta。 + +### 5. 启用状态与插件提示 + +- macOS 的插件启用状态来自它的模块。只有生成器的插件原本没有模块,会永远显示为停用。所以描述符为生成器合成一个 `ModuleID.projectGeneratorsExtension(pluginID)` 模块:应用级、不启动任何东西,只控制条目是否可用。 +- `project.generatorCatalog` 接收全部已安装插件和启用状态,为每个 requirement 列出提供它的插件。宿主据此提示:有提供者但已停用,显示「启用」;没有提供者,显示「在插件市场中查找」。requirement 从不阻止创建。 +- 生成器自己的插件停用时,条目照样列出但变灰,必须先启用才能创建。 +- requirement 的提供者就是生成器自己的插件时(Java Support 的 Java 模板需要 Java 语言),不再单独提示:上一条的「启用」已经覆盖它,避免同一个插件出现两次。 + +### 6. 界面复用 + +| 共享组件 | 来源 | 使用方 | +| --- | --- | --- | +| `LitheSidebarRow`、`LitheSidebarHeading` | 设置窗口侧栏 | 设置窗口、新建项目 | +| `PluginMultiSelectField`(支持分组标题)、`PluginTextSettingField`、`PluginFieldOption` | `PluginSettingsSection` 的私有控件 | 插件设置、新建项目 | +| `LitheDirectoryPicker` | 克隆仓库窗口 | 克隆仓库、新建项目 | +| `LitheSettingsSelect`、`LitheSettingsSegmentedControl`、`LitheSettingsCheckbox`、`LitheInfoTip` | 原本就共享 | 新建项目 | + +左栏固定宽度,不用每帧 `GeometryReader`,符合[可调整面板性能边界](../architecture/2026-09-13-resizable-ui-performance-boundaries.md)。插件提供的标题和说明走 `LocalizedStringKey`,与插件设置一样可以在宿主的 `Localizable.strings` 里翻译。 + +### 正确做法 + +- 新增一个 Cargo 生成器:在 Cargo 插件里声明一条 `command` 步骤调用 `cargo init --vcs none`,`requirements` 写 `{"buildSystemID": "cargo"}`,`requires` 加 `projectGenerators: 1`。宿主不用改。 +- 需要按包名放文件:模板目录里放 `Main.java`,步骤写 `"target": "src/main/java/${field.groupId|package|path}"`。 + +### 不要这样做 + +- 在宿主里写 `if generator.id == "spring-boot"` 拼下载地址。生态知识必须留在插件声明里,否则社区插件无法参与。 +- 在 POM 模板里写 `${field.groupId}`:文件里的 `${…}` 会原样保留,应写 `{{ field.groupId }}`。 +- 直接在用户选的目录里跑脚手架。失败会留下半个项目,还可能改到已有文件。 +- 在 `projectGenerators` 里加描述运行中进程的字段(例如「支持依赖搜索」)。这属于以后 Provider 协议协商的内容。 + +## 考虑过的备选方案 + +### 独立的内置模板插件 Java Project Templates + +最有吸引力的地方:停用 Java Support 不影响模板,`requires` 不用让语言插件多依赖一个能力。最初按这个方案实现,后来没有采用:用户把模板看作 Java 支持的一部分,多一个只有模板的内置插件反而难以理解,打包、校验脚本和资源清单也要多维护一份。停用后条目变灰并提示启用,已经能解决「停用后向导无从提示」的顾虑;契约仍允许只有生成器的插件,社区插件可以这样做,宿主为它合成模块。 + +### 引入「可选能力」 + +最有吸引力的地方:语言插件加生成器后,旧宿主仍能装它,只是不显示条目。没有采用:`requires` 属于基础字段,改它要升 `pluginAPIVersion`;Java Support 与宿主同版本发行,暂时不需要。 + +### 生成器只用后端进程 + +最有吸引力的地方:一种机制覆盖所有情况,Spring Initializr 一开始就能拿到动态的版本和依赖列表。没有采用:多数生成器只是一条命令或几个模板,让作者写后端进程门槛太高;宿主的后端调用目前只服务构建 Provider,进程生命周期和临时目录权限都要另外设计。动态数据留给以后的 `projectGeneratorProviders` 能力,在 `initialize` 里协商 `litheProject` 协议。 + +### 用插件网页(`ui` 2)画表单 + +最有吸引力的地方:宿主几乎不用改。没有采用:插件界面原生优先,网页只作为可选方式;名称、位置、校验和提交需要宿主统一控制。 + +### 宿主自己解压(`ditto`) + +最有吸引力的地方:少一个 Core 命令。没有采用:压缩包来自用户可修改的服务器,属于不可信输入;放在 Core 里,路径穿越、符号链接、覆盖和大小上限的检查只写一次,Windows 接入时直接复用。 + +## 后果 + +- 收益:社区插件只靠声明就能加新建项目条目;表单规则和计划在 Core,两个平台给出同样的答案;失败不留残留。 +- 收益:设置窗口、克隆窗口和向导共用侧栏行、字段控件和文件夹选择器,样式不会各自漂移。 +- 代价:Spring Boot 的版本和依赖列表写在插件里,会随时间过时,要靠插件发版更新;动态列表要等 Provider 能力。 +- 代价:Java Support 的 `requires` 多了 `projectGenerators: 1`,不支持生成器的宿主装不了这一版;停用 Java Support 时模板一起停用。 +- 暂不支持:JavaFX、Quarkus、Micronaut、Jakarta EE 等其他 Java 生成器;Windows 宿主(不声明 `projectGenerators`,需要它的插件在 Windows 市场中隐藏)。 + +## 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p lithe-core project_generators +./scripts/verify-shared-contracts.sh +./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'ProjectGenerationTests|PluginManagerTests' +./scripts/verify-official-plugins.sh +./scripts/verify-platform-feature-matrix.sh +``` + +## 适用范围 + +- `rust/lithe-core/src/project_generators/` +- `rust/lithe-core/src/plugins/declarative.rs` +- `rust/lithe-core/src/plugins/capabilities.rs` +- `shared/contracts/plugins/v1/` +- `Plugins/mac/Official/JavaSupport/` +- `macos/Sources/Lithe/Views/Workspace/NewProjectView.swift` +- `macos/Sources/Lithe/Application/Features/NewProjectFeatureModel.swift` +- `macos/Sources/Lithe/Services/ProjectGeneration/` +- `macos/Sources/Lithe/Platform/MacOS/ProjectGeneration/` +- `macos/Sources/Lithe/Views/Components/LitheSidebarList.swift` +- `macos/Sources/Lithe/Views/Components/PluginFieldControls.swift` diff --git a/.agents/notes/implemented/feature/2026-10-23-windows-editor-cursor-history.md b/.agents/notes/implemented/feature/2026-10-23-windows-editor-cursor-history.md new file mode 100644 index 0000000000..81ba8e1088 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-23-windows-editor-cursor-history.md @@ -0,0 +1,64 @@ +# Agent 笔记:Windows 编辑器优先回退误移动的光标 + +状态:已实现 + +## 先说结论 + +Windows 编辑器误按方向键后,第一次 Ctrl+Z 恢复最近一次键盘移动前的位置,保留刚输入的代码;再按一次才撤销代码。 +光标和文本仍使用 Monaco 各自的历史,Lithe 只判断本次撤销该调用哪种动作。 +另提供 Ctrl+U 回退、Ctrl+Shift+U 重做光标,命令面板和快捷键设置均可找到这两个独立动作。 + +## 问题 + +用户编辑后误按向下箭头,希望恢复光标,却通过文本撤销丢失刚才的修改。 +现有导航历史只记录显式跳转和鼠标手势,不能替代逐次方向键移动与多选区的恢复。 +Monaco 原生提供光标历史,但原来的 Ctrl+Z 只调用文本撤销,也缺少可发现的产品命令入口。 + +## 决策 + +复用固定 Monaco 0.55.1 的 `cursorUndo` / `cursorRedo`。 +上游实现位于 `src/vs/editor/contrib/cursorUndo/browser/cursorUndo.ts`,由每个编辑视图保存最多 50 个光标、选区和滚动状态;文本修改或切换模型后清空,避免旧坐标误用。 +Windows 的 `createCursorAwareUndo` 仅保存待执行动作类型,不保存坐标或另一份历史。 +同一文档版本中的键盘选区变化启用一次光标撤销;文本变化、模型切换和鼠标或 API 移动会取消它。 +光标恢复后,Ctrl+Shift+Z 或 Ctrl+Y 可以恢复该次移动;若先继续 Ctrl+Z 撤销文本,则之后重做文本。 +连续多次方向键移动也只优先恢复最近一次,避免用户一直无法撤销代码。 + +策略随活动视图适配器安装、卸载,切换文件或分屏不沿用其他视图的待执行状态。 +只读与预览视图允许恢复光标,仍禁止修改文本。 +独立光标命令也经过活动编辑适配器的同一策略执行:Ctrl+U 实际恢复选区后,普通重做应恢复光标;Ctrl+Shift+U 消费这次恢复状态。空光标历史不抢占文本重做。其他平台仍使用共享 `runEditorCommand` 的原生动作映射。 + +Windows 注册稳定命令 ID `editor.cursorUndo` / `editor.cursorRedo`,供快捷键设置和命令面板使用。 +默认快捷键仅在编辑区生效,普通输入框、查找输入框和终端沿用原有处理。 +正确做法是“输入代码 → 方向键移动 → Ctrl+Z 恢复光标 → Ctrl+Z 撤销输入”。 +不要在文本撤销栈或工作区跳转历史中插入每次光标移动。 + +## 考虑过的备选方案 + +- 只暴露独立 Ctrl+U 不满足用户明确选择的 Ctrl+Z 行为,未采用。 +- 让连续 Ctrl+Z 逐个耗尽所有光标移动,会推迟文本撤销,未采用;仅优先消费最近一次移动。 +- 将每次方向键移动加入工作区跳转历史,会混合跨文件导航与视图内多选区,并重复实现 Monaco 的边界处理,未采用。 +- 自建光标栈需要跟随文档版本、只读、多光标与视图销毁同步,上游能力已经完整覆盖,未采用。 + +## 后果 + +用户可以独立恢复光标,也能使用重做恢复刚才的移动。 +光标历史属于视图,不跨文件、分屏共享,不写入会话或发行目录。 +编辑内容后历史清空是上游的版本保护行为,并非长期位置书签。 +此行为改变 Windows 的文本撤销入口,键盘导航后需多按一次才能撤销文本。 +macOS 已加载原生 Monaco 动作;本次的 Ctrl+Z 策略和产品命令入口仅接入 Windows。 + +## 验证 + +- 快捷键集成测试覆盖默认回退/重做与文本撤销分别分发、自定义快捷键和查找输入框隔离。 +- 策略测试覆盖一次光标恢复后进入文本撤销、对应重做、历史失效、只读、视图隔离和监听器释放。 +- 真实 Monaco 验证输入后移动、回退、重做、选区和滚动恢复、空栈不改文本、编辑与切换模型清空历史以及不同视图隔离。 +- 运行 Windows Frontend 计时检查、Windows 构建、功能矩阵校验、Agent Notes 校验和发行目录只读检查;浏览器探针不能替代 Windows WebView2 与 macOS 原生按键验收。 + +## 适用范围 + +- `frontend/editor/src/editor-commands.ts` +- `windows/tauri/src/features/editor/engines/monaco/cursor-aware-undo.ts` +- `windows/tauri/src/features/editor/components/monaco-editor.tsx` +- `windows/tauri/src/features/keymaps` +- `windows/tauri/src/features/command-palette/constants/navigation-actions.tsx` +- `windows/tauri/src/i18n/locale.ts` diff --git a/.agents/notes/implemented/feature/2026-10-24-agent-hooks.md b/.agents/notes/implemented/feature/2026-10-24-agent-hooks.md new file mode 100644 index 0000000000..a6bf190a5f --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-24-agent-hooks.md @@ -0,0 +1,166 @@ +# Agent 笔记:Hook 资源库与原生执行边界 + +状态:已实现 + +## 先说结论 + +用户在 AI > Hooks 中保存一份命令,再选择让 Codex、Claude Code 使用它。 +Lithe 管理资源库并显式写入原生配置;何时运行、Shell、标准输入、超时和 +命令信任都由原生 Agent 决定。写入成功不代表命令已被批准或执行, +尤其 Codex 仍需在原生 `/hooks` 中确认。 + +## 问题 + +通知、格式化等命令需要在 Agent 的特定事件运行。用户分别维护不同配置 +容易出错,但在 Lithe 中再建一套命令执行器,会与 CLI 的事件、权限、 +取消和会话恢复产生两份事实。修改已有配置还可能误删用户 Hook,或者 +把 Codex 的索引信任状态移到另一个命令上。 + +## 决策 + +### 配置复用原生能力 + +交互参考 [Magpie Hook 管理](https://github.com/yetone/magpie/blob/dd1e8822d916f8aeb085db4db93fc3d79e2263ed/internal/library/hooks.go): +命名、描述、事件、命令、可选超时和 Agent 选择,独立实现,不复制其代码。 +首批仅实现两个已有 Agent 的五种共同事件,不给尚未支持的 Agent 放虚假开关。 +工具筛选映射到各自的上游工具名,不通过猜测 ACP 显示文本触发命令。 + +原生语义核对 [Codex 0.160.0 Hook discovery](https://github.com/openai/codex/blob/rust-v0.160.0/codex-rs/hooks/src/engine/discovery.rs) +与 [Claude Code Hooks](https://code.claude.com/docs/en/hooks)。Codex 写入 +`CODEX_HOME/hooks.json`,Claude 写入 `CLAUDE_CONFIG_DIR/settings.json`, +未配置环境变量时沿用原生用户目录。Hook 名称和描述只保存在 Lithe 资源库。 +不自动修改禁用 Hook 的用户开关、信任记录或当前对话,也不添加后台同步。 + +### 编辑器布局 + +按用户提供的新 Hook 参考页,macOS 与 Windows 编辑器采用左侧标签、右侧 +表单的两列布局,标题和底部动作由分隔线独立出来。描述使用较大的多行 +输入区,命令保留紧凑的多行代码输入,避免长命令或换行在界面调整中丢失。 +创建与编辑分别显示「新 Hook」和「编辑 Hook」,主按钮也区分添加与保存。 + +Agent 使用带原有品牌标志的选择按钮及「全部」动作,只展示已支持的两个 +Agent;全不选时仍由已有校验禁止保存。macOS 复用 MCP 资源库原有的选择 +按钮外观,抽为 `AgentSelectionChip`,不再分别维护两套样式。说明继续遵守 +macOS 设置的 ⓘ 规则,不复制参考页的控件下方说明段落。描述字段通过共享 +文本编辑器显式选择正文字体,其他调用者继续使用原有等宽字体与控件样式。 + +搜索按名称、描述和命令筛选。页面必须根据筛选后的列表显示空状态:有 +搜索词而无结果时提示「没有匹配的 Hook」,无搜索词且资源库为空时提示 +「还没有 Hook」。不能只检查资源库总数,否则已有内容的无匹配搜索会留下 +空白页面。读取尚未完成或失败时不冒充空资源库,搜索本身也不触发配置写入。 + +### 所有权与失败恢复 + +所有权记录表示 Lithe 在哪个文件、事件、数组位置写入了什么完整条目。 +只有该位置仍精确匹配记录,Lithe 才允许更新或移除它。外部编辑过的托管 +条目会阻止同步,提示用户恢复或移除原生条目;不猜测删除范围或追加重复命令。 +恰好相同的用户自有条目也不自动认领。 + +Codex 按组和处理器索引标识 Hook。删除后用空处理器组保留位置,新增命令 +只追加,保持其他组索引不变,不替用户迁移信任。修改命令也不转移旧信任。 +例如先有用户组 A、后有 Lithe 组 B,移除 B 只清空 B 的处理器,不移动 A。 +不要把数组压缩当成无害整理:这会让后续组的 trust key 指向不同命令。 + +资源库先保存用户期望,再对每个目标单独同步。写原生文件前,先在资源库 +记录旧、新两种条目的所有权;原生写入成功后才确认新版本。进程在任何 +一步中断,下一次显式同步都能根据实际原生字节恢复,不丢失已写条目。 +删除失败的 Hook 保留「等待移除」条目。两个目标不是多文件事务, +单项失败继续其他项并显示错误,不把失败目标显示为已同步。 + +### 存储与平台边界 + +平台 storage adapter 提供 Application Support 中的 `Lithe/AgentHooks/`; +Windows 使用平台 app-data 根目录下的 `AgentHooks/`。该目录拥有资源库、 +写入前所有权日志和空 `.sync.lock`。它们都是用户状态,没有可复用的构建 +身份,禁止跨 worktree 复制。读取页面不创建目录;只有显式操作才写入。 +共用 instruction 文件 adapter 提供 1 MiB 有界 UTF-8 读取、普通文件校验、 +摘要和路径冲突保护、原子替换及暂存清理。非阻塞 OS 文件锁协调多进程。 + +原生 JSON 的未知字段和已有条目保留语义值,写入时规范化格式;非法 JSON、 +重复键或非对象配置拒绝覆盖。原生配置内容可能含密钥,不进入公开快照、 +日志或设置页响应。Hook 库本身包含用户命令,不能作为构建产物发布。 +运行时不写 bundle 或安装目录,不改变签名与 Sparkle delta 源基线。 + +Swift 功能模型负责草稿与 UI 状态,Rust Core 负责严格命令分派,Agent Host +负责原生文件。Windows 通过现有 `platform_invoke` 使用同一个共享命令, +不增加平台专属 Hook 执行器。 + +## 考虑过的备选方案 + +- 监听 Lithe 的 ACP 事件后自己启动 Shell:无法完整复刻上游工具和信任 + 语义,会漏掉 CLI 外的会话或重复执行,因此保留原生执行所有权。 +- 直接编辑两份 JSON,不保留资源库:无法共享描述、暂停后恢复命令, + 也无法在部分失败和中断后可靠判断哪些条目归 Lithe 所有。 +- 删除组后重新编号并迁移 Codex trust:需要修改另一个敏感配置文件, + 增加跨文件中断窗口,因此使用保留数组位置的空组。 + +## 后果 + +用户可以统一管理两个 Agent 的原生 Hook,并明确看到部分失败、待同步和 +等待移除状态。代价是 Codex 的原生信任步骤仍需用户完成;空组会保留在 +原生配置中,外部编辑过的托管条目需要先在原生文件中处理。Windows +设置页已实现,目标平台运行验收仍须单独完成。 + +## 验证 + +- Host Hook 测试使用独立临时目录,覆盖读不创建、事件/筛选/超时、保留 + 自有条目、trust 索引不变、只读/符号链接、冲突、部分失败、取消与日志恢复。 +- Core `hook_contract_dispatch_is_strict_before_accessing_native_files` 校验 + 严格请求;Swift 与 TypeScript 读取同一个跨平台 fixture。 +- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh`。 +- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'AgentHooks|SettingsSidebarLayoutTests|SettingsSearchVocabularyTests|ContextMenuCoverageTests'`。 +- `./scripts/verify-runtime-bundle-immutability.sh`、`./scripts/verify-service-boundaries.sh`、 + `./scripts/verify-shared-contracts.sh`、`./scripts/verify-agent-notes.sh`、 + `./scripts/verify-platform-feature-matrix.sh`。 +- `node --test scripts/test-reuse-worktree-resources.mjs`。 +- `./scripts/verify-rust-core.sh` 全量通过,含 1,157 项 Core 单元测试、C ABI、 + 链接符号和 Git/Swift 桥接;更新后的实际 macOS 开发预览已确认 AI > Hooks 入口。 +- 本机隔离用户目录 smoke:Codex 0.160.0 通过原生 `hooks/list` 识别生成 + 的配置,初始保持未信任,临时目录中显式设置原生信任后首轮触发 + SessionStart;Claude Code 2.1.310 同样触发 SessionStart,均确认 JSON + 标准输入和会话目录正确。删除后的空组通过 Codex 原生解析。 +- 上述流程前后比较已运行预览 bundle 的 1,382 个文件,SHA-256 全部不变; + 临时 CLI 进程树和配置目录已回收,结果保存在本次 + `.artifacts/test-stability/hooks-native-smoke.json`,不发布为构建资源。 +- macOS 真实端到端验收:在原生设置页创建、搜索、编辑、停用、重新启用和 + 删除 Hook,再用 Codex 0.160.0、Claude Code 2.1.310 以及 Lithe 已安装的 + Codex ACP 1.13.1、Claude ACP 0.81.2 完成真实模型对话和文件修改。 + 四条运行路径均实际触发五种事件,Shell/文件编辑筛选、JSON 标准输入和 + 工作目录正确;修改只执行新命令,停用与删除后不再执行,重新启用后恢复。 + 两种原生客户端的 1 秒超时均终止 3 秒测试命令,模型回合继续正常完成。 +- 共 10 个原生 CLI 会话、4 个 ACP 会话,捕获 86 次事件执行。还实际验证 + Claude 改写设置后,页面显示冲突,重新加载并重试恢复同步。ACP、删除与 + 超时流程前后的 1,382 个 bundle 文件哈希不变。测试命令、临时凭据副本、 + 工作区和进程已清理,保留普通开发预览;结果与脱敏输入样本在本次 + `.artifacts/test-stability/hooks-real-e2e.json` 和 + `.artifacts/test-stability/hooks-real-payload-samples.json`,不提交生成物。 +- 本机 51 项 Swift 设置/外观/导航测试及 9 项 Bun 契约/界面测试通过; + Windows 重新加载请求不会携带仅供写入使用的快照字段,回归测试同时 + 确认保存请求仍保留用户看过的资源库与原生文件快照。 + Host 计时 lane 为 231 通过、39 项 opt-in 跳过,其中新增 Hook 用例 14 项。 + Windows 实机运行验收仍待完成,不能用 macOS、配置文件或组件测试替代 + Windows 的运行证据。 +- 参考页布局调整后,78 项 Swift 计时测试覆盖编辑器列对齐、多行描述与 + 命令输入、MCP 资源库及共享设置控件,中英文深浅色截图已检查;9 项 + 前端计时测试与类型检查通过,包含 Agent 全不选禁用保存、全部恢复、 + 创建/编辑标题和取消草稿。实际开发预览验证了空白表单、名称初始焦点、 + 多行描述、Agent 选择和工具事件筛选;测试草稿取消后未写入原生配置。 +- Review 修复后的 11 项 frontend 计时测试及类型检查通过。新增中英文 + 界面回归覆盖名称/命令不区分大小写、中文描述匹配、无匹配提示、清空 + 搜索恢复列表,以及重新加载后的空资源库提示;搜索期间没有写入请求。 +- Review 视觉复核重新采集 macOS 原生列表/编辑器和 Windows React 的 + 中英文深浅色截图。Windows 截图来自 macOS Chromium 与固定 IPC fixture, + 不能替代目标平台验收。MCP 按 preview `19042e87c6fc` 原控件与当前共享 + 控件分别渲染选中/未选中状态,两种主题的 PNG 字节与像素均完全一致。 + 本地对照页为 `.artifacts/test-stability/hooks-review-ui.html`;临时采集源、 + 截图和报告保留在 `.artifacts/`,不进入产品提交。 + +## 适用范围 + +- `rust/lithe-agent-host/src/hooks.rs` +- `rust/lithe-agent-host/src/hooks/storage.rs` +- `rust/lithe-core/src/agent/hooks.rs` +- `macos/Sources/Lithe/Views/Agent/AgentHooksSettingsView.swift` +- `macos/Sources/Lithe/Application/Features/AgentHooksFeatureModel.swift` +- `windows/tauri/src/features/settings/components/tabs/agent-hooks-settings.tsx` +- `shared/fixtures/agent/hooks-management-v1.json` diff --git a/.agents/notes/implemented/feature/2026-10-24-agent-subscription-multi-account.md b/.agents/notes/implemented/feature/2026-10-24-agent-subscription-multi-account.md new file mode 100644 index 0000000000..b99f737f7f --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-24-agent-subscription-multi-account.md @@ -0,0 +1,77 @@ +# Agent 笔记:Claude 与 Codex 官方订阅多账号与手动切换 + +状态:已实现 + +## 先说结论 + +Claude 与 Codex 的官方订阅都可以保存多个账号。每个账号对应官方 CLI 的一个独立配置目录, +启动适配器时通过环境变量指向它:Claude 用 `CLAUDE_CONFIG_DIR`,Codex 用 `CODEX_HOME`; +登录、令牌保存和刷新都由官方 `claude auth login --claudeai` / `codex login` 完成。Lithe 只保存「显示名 + 目录路径 + 是否首选」, +以及适配器最近一次推送的邮箱、套餐和用量,不读取、复制、导入或刷新任何凭据。 +切换账号只能由用户手动触发,额度耗尽时不会自动换号。 + +## 问题 + +用户有工作和个人两个 Claude 订阅时,只能在终端 `/logout` 再 `/login`,再回到 Lithe 重连; +两个账号的会话历史混在一起,也看不到各自的用量。搬运或改写凭据文件最容易触发账号风控, +所以不能让 Lithe 代替 CLI 管理登录。 + +## 决策 + +- **目录即账号。** 新账号的空目录建在 Application Support 的 `AgentAccounts//`, + 不在 app bundle 或安装目录内,不影响 Sparkle 增量更新。默认账号不创建目录,沿用 CLI 自己的 + 默认位置,已有用户无需迁移。 +- **Host 只透传目录。** `AgentLaunch.claude_config_dir` 仅在 Claude 订阅模式下生效, + `codex_home` 仅在 Codex 订阅模式下生效,都必须是绝对路径;相对路径会按适配器工作目录解析, + 可能悄悄选中另一个账号,所以直接报错。Codex 的额度查询和已归档历史会各启动一个 App Server, + 它们同样带上这个目录,否则会读到默认账号。 +- **登录在后台运行,不弹终端。** 「添加另一个账号」创建目录后,通过进程运行器在登录 shell 里执行官方登录命令, + 环境里只多一个目录变量;浏览器由 CLI 自己打开。卡片显示「正在等待浏览器登录…」和取消按钮, + 取消或关闭设置页会终止命令,5 分钟未完成按超时处理。使用登录 shell 是为了拿到用户自己的 PATH, + 不去猜 CLI 安装位置。最初的方案是在内置终端里运行,但会打断用户当前的工作区,所以改为后台。 +- **切换即重启。** 设为首选会停止当前项目的 Claude 连接,再用新目录启动。正在回复、新建会话、 + 等待授权或有待处理的权限请求时拒绝切换,行为与切换供应商一致。 +- **邮箱与套餐来自适配器。** 适配器初始化时推送 `_auth/status_update`,Host 在确认已登录后把其中的 + 账号转成 `account` 事件。此前该事件只给 Codex 使用,Claude 的邮箱和套餐没有送到界面。Codex 的用量 + 仍由 Host 已有的额度查询提供。 +- **用量只是快照。** 账号卡片显示 5 小时和 7 天窗口的已用比例、重置时间、配速,以及 + 「截至 <时间>,本次未主动刷新」。数据来自 `usage_update._meta["_claude/rateLimit"]`,Lithe 不调用 + Anthropic 接口,也不轮询。配速等于已用百分点减去窗口已过去的百分点:正值表示消耗快于匀速。 + 窗口已重置后的旧数据不再显示重置时间和配速,也不会显示成健康的绿色。 +- **标注按账号分开。** 收藏、重命名和隐藏的存储身份由 Agent id 扩展为 `#<账号 UUID>`, + 默认账号保持原身份,原有标注原样可用。切换账号后重新读取标注。 +- **移除只忘记。** 移除账号只删除 Lithe 里的记录,不删除目录:那里面是官方 CLI 的登录状态, + Lithe 不应该删除或检查。 + +## 考虑过的备选方案 + +- **导入凭据文件或读取钥匙串:** 搬运凭据,风控风险最高,也超出原生支持。拒绝。 +- **改写默认 `~/.claude` 的登录态来「始终登录首选账号」:** 需要改写 CLI 默认凭据,不是原生行为。拒绝。 +- **Lithe 自己请求用量接口:** 需要读取 OAuth 令牌。拒绝;只显示适配器推送的快照。 +- **额度耗尽时自动轮换账号:** 可能被视为绕过限额。拒绝,只允许手动切换。 +- **「整窗 ≈ tokens / 金额」和曲线图:** 需要统计 `~/.claude` 下的会话记录,不属于官方推送的数据。本次不做。 + +## 后果 + +多账号完全依赖官方 CLI 的目录隔离。macOS 上不同 `CLAUDE_CONFIG_DIR` 的登录是否在钥匙串中互不覆盖, +需要用临时目录登录第二个账号后,只用 `claude auth status`(Codex 用 `codex login status`)确认原账号仍然登录,不读取凭据; +这一项尚未在真机验证,功能矩阵保持「待验证」。Windows 的 Host 已接受 `claudeConfigDir` 和 `codexHome`, +设置页和登录入口尚未接入。第三方产品提供 claude.ai 登录需要 Anthropic 事先批准,本功能只启动用户自己 +安装并登录的官方 Claude Code,是否对外宣传由维护者决定。 + +## 验证 + +- `cargo test -p lithe-agent-host --lib`:目录透传、相对路径拒绝、已登录账号随事件发出。 +- `AgentAccountTests`:配速计算、账号仅保存路径、观察值按账号保留、每个 Agent 只收到自己的账号目录。 +- `./scripts/verify-platform-feature-matrix.sh`、`node scripts/generate-i18n.mjs --check`。 + +## 适用范围 + +- `rust/lithe-agent-host/src/claude_subscription.rs` +- `rust/lithe-agent-host/src/subscription.rs` +- `rust/lithe-agent-host/src/codex_history.rs` +- `macos/Sources/Lithe/Models/Settings/AgentAccounts.swift` +- `macos/Sources/Lithe/Models/AppModel/AppModel+AgentAccounts.swift` +- `macos/Sources/Lithe/Views/Agent/AgentAccountsSection.swift` +- `macos/Sources/Lithe/Platform/MacOS/Agent/MacAgentAccountSignIn.swift` +- `macos/Sources/LitheAgentConversationModule/Application/AgentHistoryFeatureModel.swift` diff --git a/.agents/notes/implemented/feature/2026-10-24-agent-transcript-images-and-task-notifications.md b/.agents/notes/implemented/feature/2026-10-24-agent-transcript-images-and-task-notifications.md new file mode 100644 index 0000000000..7795ef6453 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-24-agent-transcript-images-and-task-notifications.md @@ -0,0 +1,168 @@ +# Agent 笔记:对话中的图片缩略图、放大查看与后台任务通知 + +状态:已实现 + +## 先说结论 + +工具读取或截取图片时,对应的工具行(以及整组工具的标题行)右侧显示缩略图: +最多三张,其余用 `+N` 表示,悬停出大图预览,点击进入统一的放大查看器。用户消息里附带的图片也用同一套缩略图和查看器。 +Claude Code 发出的 `` 不再原样显示为用户气泡,而是一行状态文字。 +图片数据只在内存中以受限大小保存,原图由视图层按需异步解码。 + +## 问题 + +ACP(Agent Client Protocol,Agent 与客户端之间的协议)的工具结果可以携带 `image` 内容块, +但原先解析时只保留了类型名,图片本身被丢弃,用户看不到 Agent 读到的截图。 +用户发送的图片附件只以 `📎 路径` 文字出现,无法预览。Claude Code 的后台任务完成通知是协议标记, +却被当成用户输入显示。 + +## 决策 + +- `AgentToolDetails.images` 与 `AgentConversationMessage.images` 保存 `AgentImage`。内联图片解码后 + 不超过 10 MB、每条消息最多 24 张,超限直接丢弃,不进入会话状态。 + `AgentImage` 按内容指纹(SHA-256,硬件加速)或路径判等,转录行的相等比较不会去比较像素。 + 同一条消息、同一组工具里 id 相同的图片用 `uniquedByID` 去重,`ForEach` 和查看器翻页都要求 id 唯一。 + 内联数据必须能被 ImageIO 识别,否则不生成缩略图,工具输出里对应条目退回文字显示。 +- 只提供文件路径的 `read` 工具调用(例如读取 `.png`)直接显示该文件,且文件必须存在。其他类型的工具不会因为位置是图片就当作“查看”。 +- 一次更新如果没有图片,不会清掉之前已收到的图片,避免最终状态更新把缩略图抹掉。 +- 缩略图在视图层用 ImageIO 在后台线程按需生成并放入 `NSCache`(限制总像素成本和数量,文件缓存键含修改时间,行滚出后解码任务随之取消,解码失败显示占位图标);历史回放只保存数据,不解码。 + 悬停预览复用 `workbenchHoverCard`,放大查看器是转录区域上的覆盖层,状态放在 + `AgentImageViewerState`,行只持有引用,打开或关闭不会重绘全部行。 +- 放大查看器仍是转录区域上的覆盖层,在其上补充捏合缩放(相对适应窗口 25%–800%)、拖动、双击、 + 百分比和 ⌘+/⌘−/⌘0,拖动范围限制在画面边缘内;←/→ 翻页只在查看器持有焦点时生效,不抢输入框的光标键;侧栏很窄时可放大查看。曾试过改用独立窗口复用编辑器的 `MediaViewerView`, + 因为偏离了既有交互而放弃。 +- 折叠的“思考过程”标题行也显示图片缩略图,否则默认折叠时用户看不到工具返回的图片。 +- 用户消息里的 `[@名字](file:///路径)`、裸 `file://` 地址、`📎 路径` 和 Claude Code 的 + `[Image: source: …]` 指向图片文件时显示为缩略图;`[Image: original …]` 尺寸说明丢弃。缩略图按引用在原文中的位置排序;超过 24 张的引用从文字中移除并显示 `+N`。 + 用户消息的解析结果按消息缓存,悬停和流式刷新不会重复扫描全文。 +- Claude Code 写进用户消息的特殊格式由 `AgentClaudeCodeMarkup` 识别:斜杠命令、本地命令输出、 + Shell 转义、中断提示、压缩续接摘要、技能展开和宿主上下文显示为状态行或折叠块; + ``、`` 等只给模型看的块只在消息开头或结尾时移除(Claude Code 就是加在那里), + 夹在用户文字中间的同名标签原样保留。只有整条消息都是这些标记时才整体替换,用户自己输入的相似文字不会被吞掉。 + 格式清单来自对本机真实会话的标记名称统计,新增格式在这里补充。 +- `` 只有在整条用户消息完全由通知块组成时才解析,用户自己输入的相同文字不会被吞掉。 + 这是协议标记的呈现,不按 Agent 名称在宿主里分支。 + +### 后台任务通知并入后续过程 + +后台命令的完成通知会唤醒 Agent 再跑一轮。连续几次后台命令会让对话里反复出现“通知行 + 思考过程”。 +现在成功的通知(`AgentTranscriptItem.isQuietNotification`)不单独成行,而是进入随后一轮的折叠过程区,展开后按原顺序显示。 +失败、被终止的通知,以及后面没有任何思考或工具的通知,仍作为状态行保留,避免用户错过。 +Claude Code 为 shell 任务生成的英文摘要(`Background command "…" completed (exit code 0)`)按界面语言显示。 + +### 恢复会话时补全压缩前的历史 + +Claude Agent SDK 的 `getSessionMessages` 只返回当前 parent 链;压缩边界(`compact_boundary`)没有 parent, +因此适配器回放从最后一个边界开始,更早的记录虽然还在 `<配置目录>/projects/*/<会话 id>.jsonl`,却不会到达客户端, +对话里看起来像“前文丢了”。Rust 宿主(`claude_earlier_history.rs`)在 `session/load` 之前只读取该文件中最后一个边界之前的行, +转成与回放一致的 ACP 更新(用户、回复、思考、工具调用和结果),先于适配器的回放发出, +所以每次压缩的摘要也会在原位置显示为“对话已从摘要继续”。 + +这是对“宿主不读取转录文件”约定的有限例外,边界如下: + +- 只读,只打开 `projects/*/<会话 id>.jsonl`;账号目录里的登录信息和其他文件不触碰;会话 id 只接受字母、数字、`-`、`_`。 +- 配置目录按所选 Claude 账号解析(账号目录、`CLAUDE_CONFIG_DIR`、`~/.claude`),不写入任何文件,不缓存。 +- 超过 128 MiB 的记录文件不读取;最多发出 30000 条更新;工具输入输出每段截断到 4000 字符;子代理侧链和 `isMeta` 行跳过。 +- 用户消息里的内联 base64 截图随所在消息回放(带同一个 `messageId`,macOS 并入原气泡);`url` 来源和空数据跳过, + 超过客户端 10 MB 解码上限(base64 约 13.3 MB)的不发送,每条消息的数量与解码由 macOS `AgentImage` 限制。其他图片不回放。 +- 文件格式由 Claude Code 拥有。解析失败或格式变化时只是不显示更早的历史,不影响适配器自身的回放。 +- Codex 的历史仍由其官方 App Server 提供,不受此例外影响。 +- 4000 字符的截断只用于工具详情预览。Claude Desktop 的卡片、计划和回复由工具输入生成 + (`wrap_up`、`update_plan`、`ccd_*`、`visualize` 等),这些输入按 256K 字符保留;否则压缩前的可视化会被 + 截坏,任务提示也只会把截断后的内容填进新对话。 + +不要这样做:不要把前文合并进适配器已回放的片段,也不要改写或重新压缩 Claude 自己的记录文件。 + +任务进行中排队发送的提示(含后台任务通知)在记录文件里只存成 `attachment` 行(`queued_command`), +没有对应的用户行,所以前文转换把它当作用户消息回放,并用该行的 `uuid` 区分相邻消息。 + +### 兼容 Claude Desktop 开始的会话 + +Claude Desktop 给 Claude Code 挂了自己的 `ccd_turn` MCP 服务,模型在那里几乎不直接输出文字, +而是用 `mcp__ccd_turn__wrap_up` 工具交出每轮的总结(`recap`)和提问(`ask`),由 Desktop 渲染成卡片。 +在 Lithe 里恢复这类会话时,整段对话只剩折叠的工具调用,看起来像历史没有回放。 + +宿主(`claude_desktop.rs`)把 `wrap_up` 的工具调用改写成一条 Agent 回复(总结与提问各一段), +并吞掉该调用后续的 `tool_call_update`。改写同时作用于适配器的回放和上面的前文转换,Windows 共用同一宿主。 +Lithe 自己不提供 Desktop 的 MCP 服务,所以这些只影响从 Desktop 带过来的历史;CLI 会话不受影响。 + +Desktop 的其他机制同样在宿主里识别,结果分两层交给客户端: + +1. **可读的工具行**:每个 Desktop 工具(`ccd_*`、`visualize`、`terminal`、`Claude_Browser`、 + `computer-use`、`scheduled-tasks`,以及按 UUID 命名的 claude.ai 连接器)都换成英文标题和合适的 + `kind`,例如 “Chapter: …”“Computer use: app screenshot”“Connector: read”。不认识卡片的客户端 + (目前是 Windows)也不会再看到 `mcp__ccd_session__mark_chapter` 这类原名。打开窗格、在编辑器打开、 + 在 Finder 显示的调用改成 `read` 并带 `locations`,点击可以打开文件。 +2. **卡片描述**:有交互含义的调用在 `_meta.lithe.claudeDesktop` 里附一张卡片,macOS 原生绘制: + 章节(`mark_chapter`)显示为分隔线;建议回复(`wrap_up.actions`,宿主另发一个 `#next` 调用) + 显示为按钮,只在它是最后一条消息且不在回复中时出现;建议任务、启动会话、移交(`spawn_task`、 + `start_session`、`hand_off_to_session`)显示为任务卡片,撤回(`dismiss_task`)或启动的结果回来后, + 宿主补发一次 `tool_call_update` 改写原卡片状态;PR 绑定、监控、自动合并显示为带链接的一行; + 切换或申请目录显示为一行说明;`show_widget` 显示为可展开的可视化卡片。助手文字里的 + `URL` 由宿主改成 Markdown 链接。 + +卡片只在调用成功后单独显示。历史里很多 Desktop 调用被自动模式分类器拒绝,那些调用在 Desktop 里也没有发生, +所以留在折叠过程里作为失败的工具行。建议回复和任务按钮只把文字填进输入框(任务会先新建对话), +不会自动发送,用户仍然决定是否发出。 + +可视化卡片展开后才创建 `WKWebView`:非持久数据存储,CSP(内容安全策略)`default-src 'none'` +禁止一切网络加载,只允许内联脚本和样式,导航全部取消,链接也不外开。原因:页面脚本可以自己 +`a.click()`,WebKit 把它和用户点击一样报告为 `.linkActivated`,外开会让页面把内容拼进 URL 发往任意地址, +取消导航也撤不回已经调用的 `NSWorkspace.open`。`sendPrompt` 只填输入框,而且只在用户于页面内点击或按键 +之后 2 秒内生效(宿主监听真实输入事件,页面脚本伪造不了),否则页面一展开就能覆盖草稿、抢走焦点。宿主补上 Desktop 文档里的主题变量(`--text-primary`、`--surface-1` 等), +颜色来自当前主题。依赖 CDN 脚本的图表(例如 Chart.js)因此不会渲染,这是有意的取舍: +不让 Agent 写的页面从会话里向外发请求。 + +用户消息里的 ``(可带属性)和 `[Cross-session idle notice]` 等 Desktop 通知 +作为折叠的上下文块显示,不当成用户说的话。 + +`update_plan` 有整表(`items`)和增量(`done`/`start`/`advance`)两种写法。宿主按会话记住当前列表, +把每次调用改写成一条完整的 ACP `plan` 更新;增量调用之前没有整表时原样保留为工具调用。 +Swift 在加载历史期间把每份计划快照记成一条不带文字的 Agent 消息,转录里在两段折叠过程之间显示成清单, +导出 Markdown 时写成任务列表,不计入消息数。折叠的过程标题后面附上工具摘要(如“运行了 3 条命令”), +没有文字回复的段落也能看出做了什么。 + +不要这样做:不要在 Swift 或 Windows 前端按工具名再做一份改写,也不要读取或模拟 Desktop 的 MCP 服务。 +卡片字段增加时,先在宿主和 `shared/contracts/application-boundary.md` 里定义,再让客户端解析; +客户端遇到不认识的卡片类型必须退回普通工具行。 + +### 不要这样做 + +- 不要把图片 `Data` 放进 `Equatable` 的逐字节比较,也不要在主线程解码原图。 +- 不要为放大查看另起一套弹窗样式或使用 `.popover`;悬停预览与查看器共用本 Note 中的入口。 + +## 考虑过的备选方案 + +### 把图片写到缓存目录再引用路径 + +好处是内存占用更小。但应用层不能直接做文件系统写入,需要新增平台存储适配;在单张和总量都有上限的前提下, +先在内存中保留,需要时再评估。 + +### 保留“复制”按钮并在工具行内渲染 + +issue #1462 指出的多余复制按钮来自不属于最终回复的过程文字。#1464 已把这类文字收进默认折叠的过程区, +空白回复也不再生成行,因此本次没有再改动复制按钮的显示规则。 + +## 后果 + +- 收益:Agent 看过的图片可以直接在对话里确认;用户附件可点击放大。 +- 代价:长会话里的内联图片占用内存,最坏情况每条消息 24 × 10 MB。 +- 需要重新评估:出现明显内存压力,或需要 Windows 端对齐时。 + +## 验证 + +- `./scripts/test-macos.sh --filter AgentImageTests` +- `./scripts/test-macos.sh --filter AgentDesktopCardTests` +- `cargo test -p lithe-agent-host claude_earlier_history` +- `cargo test -p lithe-agent-host claude_desktop` +- `./scripts/verify-agent-notes.sh` + +## 适用范围 + +- `macos/Sources/LitheAgentConversationModule/Application/AgentImage.swift` +- `macos/Sources/Lithe/Views/Agent/AgentImageViews.swift` +- `macos/Sources/Lithe/Views/Agent/AgentToolGroupView.swift` +- `rust/lithe-agent-host/src/claude_earlier_history.rs` +- `rust/lithe-agent-host/src/claude_desktop.rs` +- `macos/Sources/LitheAgentConversationModule/Application/AgentDesktopCard.swift` +- `macos/Sources/Lithe/Views/Agent/AgentDesktopCardView.swift` diff --git a/.agents/notes/implemented/feature/2026-10-24-remote-ssh-connections.md b/.agents/notes/implemented/feature/2026-10-24-remote-ssh-connections.md new file mode 100644 index 0000000000..17e368ab4c --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-24-remote-ssh-connections.md @@ -0,0 +1,93 @@ +# Agent 笔记:远程开发的 SSH 连接配置、测试与单连接启动 + +状态:已实现 + +## 先说结论 + +欢迎页的“远程开发”按钮会打开一个连接窗口:左侧是保存的 SSH 连接,右侧是表单、测试连接和主机信息。连接列表存在偏好设置里,密码和私钥口令只存在单独的钥匙串服务 `remote` 里,从不发给 Rust Core。测试、浏览和打开都通过系统 OpenSSH 完成,OpenSSH 提出的每个问题(密码、口令、验证码、未知主机指纹)都弹窗问用户。连接主机时只用**一条** SSH 连接完成探测、安装和启动远端服务,所以需要密码的主机只认证一次。 + +## 问题 + +远程开发需要一个地方告诉 Lithe“连哪台机器、怎么登录”,并且在打开项目前确认这台机器能用。难点有三个: + +- 认证方式很多:`~/.ssh/config` 别名、SSH Agent、私钥文件、密码、2FA。Lithe 不应该自己实现 SSH,也不能把密码放进命令行或环境变量。 +- 一次“连接”要做好几件事(探测平台、检查已安装的服务、上传、启动)。每件事各开一条 SSH 连接,用密码登录的主机就会被连续问好几次。 +- 连接信息里有秘密,必须和普通设置分开存放。 + +## 决策 + +### 连接配置放在哪里 + +- 连接模型 `RemoteSSHConnection` 只包含会改变目标或认证方式的字段,以及用户起的名字、选中的远程文件夹和上次检查到的主机信息。它存在偏好设置键 `lithe.remote.ssh-connections.v1` 下,由 `RemoteConnectionStore` 负责读写。 +- 勾选“记住到钥匙串”时,密码或私钥口令存在钥匙串服务 `remote`、键 `remote.ssh.<连接 id>.secret` 下。取消勾选、删除连接或改成不需要秘密的认证方式时,保存会顺带删除这个条目。 +- 窗口像 IDEA 的运行配置一样,把整张列表当作一份工作副本来编辑:点“保存”才写入,关窗口不保存。唯一例外是“上次检查到的主机信息”:它不是用户的修改,已经保存过的连接会立即单独写回,不会把表单里其他没保存的修改一起写进去。 + +### 认证怎么问 + +- Rust Core 用 `lithe-git-host` 的 AskPass(由 Lithe 自己充当 `SSH_ASKPASS` 程序、通过带令牌的回环 socket 把问题转给主程序)会话启动 `ssh`,和 Git 共用同一套机制。每个问题变成一个 `authentication` 事件,界面用 `remote.authRespond` 回答一次。 +- 记住的秘密只在**第一次**、并且提问内容明确是在要它时自动填入:密码认证的提问里要有 `password`,私钥认证的要有 `passphrase`。验证码、主机指纹确认,以及任何重复的提问都交给用户,所以记错的密码不会被悄悄重复发送。 +- 认证方式只翻译成 OpenSSH 选项:私钥对应 `IdentityFile` + `IdentitiesOnly=yes`,密码对应 `PreferredAuthentications=keyboard-interactive,password`,等等。另外始终加上 `BatchMode=no`,避免用户配置里的 `BatchMode yes` 把提问吞掉。连接里设置的选项排在传输链默认的保活选项前面,因为 OpenSSH 对每个选项只认第一次出现的值。 + +### 单连接启动 + +`lithe_remote_client::launch` 在远端跑一段单行 POSIX `sh` 脚本: + +1. 先输出主机详细信息和平台标记块; +2. 客户端按平台选出随 App 附带的服务,把“版本 SHA-256 大小 目录名”作为一行写回; +3. 远端已有报告相同哈希的服务就直接复用,否则从同一条流里读取刚好那么多字节,写到临时文件名后再改名,并再次校验; +4. 最后 `exec` 启动服务,协议会话接着在同一条流上开始。 + +安装目录和 `Bootstrapper` 一致(`~/.lithe-server/bin/<版本>-<哈希前 16 位>`),两条路径装的服务可以互相复用。脚本要穿过 SSH 跳转,所以不含 `'`、`\`、`!` 和换行。 + +### 随 App 附带的远端服务 + +`scripts/build-remote-servers.sh` 用 Rust 自带的 `rust-lld` 构建两个 Linux musl 静态版,输出到 `rust/target/remote-servers/`,同时写一个 `manifest.json` 记录服务版本。打包时复制到 `Contents/Resources/RemoteServers/<目标>/lithe-remote-server`。 + +- 这是只读的固定输入:运行时只读取它并上传给主机,不往这个目录写任何东西,所以不影响代码签名和 Sparkle 增量包。 +- Linux ELF 文件对 `codesign` 来说只是资源。macOS 版服务是 Mach-O,需要 Developer ID 签名和公证,所以暂时不附带;远程 Mac 会显示“暂不支持这个平台”。 +- 构建输出在每个 worktree 自己的 Cargo 目标目录里,从源码重新构建,不跨 worktree 复用。 + +### 主机信息与 IP 地区 + +探测脚本额外输出 `key=value` 格式的主机名、发行版、内核、CPU、内存、磁盘和运行时长;原始输出在客户端解析,这样脚本里不需要那些不能穿过 SSH 的字符。主机地址用本机系统解析器解析。**只有用户点击“查询地区”时**,才会把这个地址通过 HTTPS 发给 ipinfo.io;内网和保留地址在本地判断,不会发出去。 + +## 考虑过的备选方案 + +### 用 OpenSSH 连接复用(ControlMaster)避免重复认证 + +最吸引人的地方是不需要新脚本。不采用的原因是:执行 ssh 的进程运行器会在 ssh 退出后结束整个进程组,复用主进程可能被一起结束;`ControlPath` 的长度和权限在不同系统上也各有坑。单连接启动不依赖这些行为。 + +### 在 Lithe 里自己实现 SSH 并保存密码 + +见 `.agents/notes/proposed/architecture/2026-10-24-remote-development-workspace-host.md` 中“内置 SSH 库”一节:会丢掉用户的 SSH 配置、ProxyJump 和各种 Agent。现在由 OpenSSH 负责认证,Lithe 只负责回答它提出的问题。 + +### 自动查询 IP 地区 + +会在用户不知情时把服务器地址发给第三方,所以改成手动触发,并在 ⓘ 里写明会发给谁。 + +## 后果 + +- 新增 Core 命令 `remote.probe`、`remote.connect`、`remote.readDir`、`remote.readFile`、`remote.writeFile`、`remote.disconnect`、`remote.authRespond`,契约见 `shared/contracts/rust-core-api.md` 的“Remote connections”一节。 +- 三个打包 workflow 需要安装 `x86_64-unknown-linux-musl` 和 `aarch64-unknown-linux-musl` 目标。 +- 连接打开后由 Core 持有,直到 `remote.disconnect` 或 App 退出;断线不会自动重连:Core 返回 `connection_closed`,传输层首次发现时通知所属窗口,弹出远程开发窗口并预选原连接,用户重新认证后恢复同一个远程文件夹,工作区随之重新加载。关闭项目会释放连接。 + +## 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p lithe-remote-client +cargo test --manifest-path rust/Cargo.toml -p lithe-remote-server --test server +cargo test --manifest-path rust/Cargo.toml -p lithe-core remote:: +./scripts/verify-runtime-bundle-immutability.sh +``` + +`lithe-remote-server` 的 `one_connection_installs_once_and_starts_the_server` 通过本机传输验证“首次上传、再次复用、两次都能建立会话”。`lithe-core` 的远程测试用假 `ssh` 覆盖选项映射、失败分类、平台不支持和主机信息解析。 + +## 适用范围 + +- `macos/Sources/Lithe/Views/Workspace/RemoteConnectionsView.swift` +- `macos/Sources/Lithe/Application/Features/RemoteConnectionsFeatureModel.swift` +- `macos/Sources/Lithe/Services/Remote/RemoteConnectionStore.swift` +- `macos/Sources/Lithe/Platform/MacOS/Remote/` +- `rust/lithe-core/src/remote/` +- `rust/lithe-remote-client/src/launch.rs` +- `scripts/build-remote-servers.sh` diff --git a/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md b/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md index 6e9ed0e893..f1a4f16269 100644 --- a/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md +++ b/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md @@ -131,6 +131,15 @@ macOS 槽位约十分钟。只改 Rust 时 Swift 测试 job 仍会启动,但 桥接验证,不跑完整单元测试。本地 `scripts/verify-rust-core.sh` 依次运行两半, 入口不变。 +纯 Rust 通道的 Agent Host(Agent 进程和协议宿主)使用计时 runner 默认的 +20 分钟总预算,包含冷编译、测试枚举和执行。运行 `39939156503` 在持续编译 +Agent Host 及 ACP/MCP 协议依赖时,触发了原先的两分钟总限额,测试尚未开始。 +因此不再把轻量 Git Host 的两分钟预算套给 Agent Host;Git Host 仍保留原预算。 +每项 Agent Host 测试仍受 15 秒看门狗约束,整个 CI job 仍限制为 30 分钟。 +代价是异常冷编译可以占用更长时间,不能据此放宽单测试或重试失败断言。 +验证时使用空 Cargo target 目录,并检查报告中的编译耗时和每项测试预算, +不能用本机暖缓存的快速通过代替冷编译证据。 + 插件通道的冷编译与测试执行分别计时。运行 `39741361768` 中,Rust Core 和 Swift 测试编译共用的 20 分钟步骤到期时,Swift 仍在持续编译完整 Lithe 目标,尚未开始 测试;后续 PR 运行复用了这一进行中的任务,因此源任务超时也使后续插件门禁失败。 @@ -139,6 +148,13 @@ macOS 槽位约十分钟。只改 Rust 时 Swift 测试 job 仍会启动,但 没有提高测试执行预算,也不把源任务失败或未生成测试报告当成通过。代价是异常 冷编译最多占用更久的 runner;不能用整个 job 的超时替代单测试的有界等待。 +Swift 测试通道使用同样的分离方式。运行 `39943871840` 中,`scripts/test-macos.sh list` +在 20 分钟上限到期时仍停在 1406/1456 个编译任务;被复用的失败任务又使 macOS 门禁 +失败。`swift-tests` 的编译步骤现为 40 分钟、整个 job 80 分钟,与插件通道一致; +单元、插件和数据库测试仍使用 960 秒执行预算和 17 分钟外层步骤。构建失败时跳过合并 +报告生成,避免把同一个构建失败重复计成“未生成测试报告”;构建成功后报告缺失仍然 +使该步骤失败,不能用跳过报告掩盖测试执行问题。 + 此前在旧 runner 上通过 Swiftly 安装独立工具链失败,测试未能启动。因此继续 使用完整 Xcode,让 Swift、链接器和 SDK 保持匹配。Xcode 27/SDK 27 只作为 开发者本机的兼容路径,不进入 CI 基线;`MacOS13SDKCompatibility.h` 仅在 @@ -164,10 +180,14 @@ macOS runner(GitHub 托管的 Mac 构建机)按组织共享,Free 计划同 5 个 macOS job,超出的 job 只能排队。排队不是配额耗尽,也不是滥用信号, 但会拖慢所有 PR 的反馈。因此: -- 主 Swift 测试和数据库 Swift 测试共用 macOS CI 的 **Swift tests** job,只执行 +- 主 Swift 测试和 Rust Core 桥接检查共用 macOS CI 的 **Swift tests** job,只执行 一次 `./scripts/test-macos.sh list` 编译,再按分类结果运行各自的过滤测试。 - 过去数据库 job 单独编译约 10 分钟,只为运行十几秒测试。Database CI 只保留 - Linux 上的 Rust 数据库测试和 gate。 + 过去独立的数据库 job 单独编译约 10 分钟,只为运行十几秒测试;数据库改为插件 + 后,这条 lane 和 Database CI 已一起删除。 +- 远程工作区的三个 crate(`lithe-remote-*`)走独立的 Remote Workspace CI,只在 + Linux 上运行。Linux runner 能执行服务的 Linux 适配器,同时交叉检查 macOS 目标, + 所以不需要为它申请 macOS runner;这些 crate 暂未被产品链接,改动它们也不会触发 + macOS 或 Windows lane。 - 插件测试保留在独立的 macOS Plugin CI,只有分类器选中插件 lane 时才申请 macOS runner。多数 PR 不改插件,独立 job 时整条 lane 直接跳过;合进共用 job 则需要为少数插件 PR 额外拉长主 Swift job,收益更小。 @@ -189,6 +209,9 @@ macOS runner(GitHub 托管的 Mac 构建机)按组织共享,Free 计划同 Windows lane,只保留 Linux 上的分类、diff、Rust 数据库、注释和元数据检查; PR 标记为 ready for review 时,macOS CI、Windows CI 和 macOS Plugin CI 重新 触发完整分类。 +- 独立共享 i18n 校验同样遵循 Draft 限制:Draft 只展开 Linux 生成器检查, + ready PR、preview 推送和手动运行展开三系统矩阵。工作流监听 + `ready_for_review`,并取消同一 PR 的旧 i18n 校验,避免重复占用 runner。 正确做法:新增 Swift 专项测试时,在共用 **Swift tests** job 里加一个按分类 结果执行的过滤步骤。不要这样做:新建一个 macOS job 再跑一遍 @@ -218,6 +241,16 @@ runner 上可能出现断言都通过、计时工具仍因超过十秒而失败 持久化观察和退出信号仍保持原有短期限,单测试仍受 15 秒计时上限约束。 正确做法是等待明确的就绪事件,不通过固定睡眠同步,也不修改产品进程行为。 +Windows 的真实 Git 集成测试按实测成本设置单例预算,普通用例仍为 15 秒。 +双仓库分别确认推送目标的用例会初始化两个工作仓库与两个裸远端,先检查 +三种确认状态,再提交并推送两个仓库;同一提交在真实 JDT 任务中用时 12.3 秒, +在 SharedRust 任务中触发了 15 秒进程期限。本机跟踪 287 次 Git 进程全部退出, +没有单次 Git 内部操作超过 0.6 秒,但完整测试耗时 49.9 秒,30 秒预算也不足。 +两个任务都仅给这个用例 60 秒, +保留每次 Git 请求的期限、进程树清理和整套测试期限。正确做法是给已测量的 +外部进程成本配置精确用例预算;不要统一放宽所有测试或用清缓存重编译掩盖 +测试超时。具体预算与证据维护在 `write-stable-tests` 的 Windows/Rust 指引中。 + CI 缓存 Cargo fingerprints、build script outputs 和依赖 outputs,不缓存 最终可执行文件。缓存覆盖 `rust/target/macos` 的 Rust Core 和 `rust/target` 的数据库辅助 crate;缓存键必须包含运行器架构、编译器、Xcode/SDK/macOS @@ -323,11 +356,11 @@ macOS CI 失败来修复。 插件过滤测试和官方插件打包验证串进主 Swift job,延长包含插件改动的 PR 的 主反馈路径,因此插件 CI 保持独立并依赖分类器跳过。 -### 用 workflow 的 `paths` 过滤跳过插件或数据库 CI +### 用 workflow 的 `paths` 过滤跳过插件 CI -`Plugin CI gate` 和 `Database CI gate` 是 `preview` ruleset 的必需检查。 -workflow 被 `paths` 过滤掉后不会上报状态,PR 会一直等待这个检查,因此继续 -使用“Linux 分类 job + gate”的方式:未选中时 gate 在 Linux 上几秒内通过。 +gate 检查一旦被设为 `preview` ruleset 的必需检查,workflow 被 `paths` 过滤掉后 +就不会上报状态,PR 会一直等待这个检查,因此继续使用“Linux 分类 job + gate”的 +方式:未选中时 gate 在 Linux 上几秒内通过。 ### 为所有 CI 任务强制上传完整安装包 @@ -370,7 +403,7 @@ Swift 测试已经编译完整 Lithe 目标。再生成两个 DMG 会在普通 ## 验证 -- `actionlint .github/workflows/ci-macos.yml .github/workflows/ci-windows.yml .github/workflows/ci-database.yml .github/workflows/ci-plugins.yml` +- `actionlint .github/workflows/ci-macos.yml .github/workflows/ci-windows.yml .github/workflows/ci-plugins.yml .github/workflows/ci-remote.yml` - `./scripts/test-classify-ci-changes.sh`:包含 Draft PR 关闭 runner lane、保留 Linux lane,以及 macOS Java 不触发 real-JDT、共享引擎和 Windows Java 仍触发的正反例。 - `node --test scripts/ci-workflow-gates.test.mjs scripts/plan-ci-lane-reuse.test.mjs scripts/verify-reused-ci-jobs.test.mjs`:从实际工作流执行 gate shell,验证取消条件、分类/测试失败、lane 覆盖及复用来源的失败传播。 - `node --test scripts/cancel-closed-pr-runs.test.mjs`:用离线 GitHub CLI 替身执行关闭清理,验证分页历史运行、来源隔离、关闭时间、取消竞争和 API 失败传播。 @@ -396,7 +429,7 @@ Swift 测试已经编译完整 Lithe 目标。再生成两个 DMG 会在普通 - `docs/ci-builds.md` - `.github/workflows/ci-macos.yml` - `.github/workflows/ci-windows.yml` -- `.github/workflows/ci-database.yml` +- `.github/workflows/ci-remote.yml` - `.github/workflows/ci-plugins.yml` - `scripts/classify-ci-changes.sh` - `scripts/install-windows-frontend-dependencies.ps1` diff --git a/.agents/notes/implemented/simplification/2026-09-30-macos-shared-dropdown-style.md b/.agents/notes/implemented/simplification/2026-09-30-macos-shared-dropdown-style.md index 138124e2aa..47f6b9ef18 100644 --- a/.agents/notes/implemented/simplification/2026-09-30-macos-shared-dropdown-style.md +++ b/.agents/notes/implemented/simplification/2026-09-30-macos-shared-dropdown-style.md @@ -25,6 +25,16 @@ Project/Dependencies 标题与 Terminal shell/动作入口同样使用 `LitheMen - 产品下拉框直接显示透明面板,`animationBehavior = .none`,不使用 SwiftUI `Menu`、菜单式 `Picker` 或系统 `NSPopover` 的弹跳展开,也不添加缩放或弹簧过渡。新增或修改产品下拉框时必须走共享组件;AI 的强制入口规则位于 `develop-lithe` Skill。 - 调用方只提交条目、动作、可用条件和定位信息。例如 Git Log 设置调用 `show(items:at:appearance:locale:)`;不要重新增加样式布尔值或局部绘制另一套菜单。 +### 操作菜单保留所属窗口的激活外观 + +技能开关暴露了共享动作菜单的焦点问题:打开三个点时菜单成为 key window(接收键盘事件的窗口),设置窗口失去该身份,AppKit 把仍开启的 NSSwitch 画灰。不能用修改技能 enabled、另画开关或强制整页活动状态修饰这个显示问题。 + +对照 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade` 的 `platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java`:它区分弹窗是否能够聚焦与是否主动请求焦点(myFocusable/myRequestFocus)。Lithe 的纯操作菜单没有文本编辑器,在所属窗口已经接收键盘事件时保留其 key 身份,自己以不可成为 key 的子面板显示;由共享事件监听把该窗口的方向键、Return 和 Esc 交给菜单。无法确认有效所属窗口时保留旧的聚焦回退。可搜索的自定义下拉仍自己接收焦点,输入法和搜索字段路径不改。 + +若操作菜单来自另一个可搜索面板,父监听必须先让子菜单处理按键,避免一次方向键被父搜索列表抢走。未处理的输入或快捷键先关闭动作菜单再传回父窗口。所属窗口失焦或应用切走时关闭动作菜单;关闭、锚点拆除及失败清理时移除所有事件和通知监听。原生开关仍由真实 isEnabled 决定忙碌禁用,菜单不触发任何技能配置写入。 + +`AgentSkillsAppearanceTests` 检查保留父窗口焦点、原生开关值/可用性与绘制不变,以及真正保存期间仍被禁用。`LitheActionMenuFocusTests` 检查键盘和原响应者、嵌套面板分派、Esc、失焦和应用切换清理;仍需真实应用打开菜单核对颜色,后台测试进程的灰色基线不能单独证明实际视觉已修复。 + Monaco 的原生菜单选中经过异步 WebKit 消息回调,已经失去浏览器的用户手势授权。 复制、剪切、粘贴因此由 WKWebView 原生 responder 命令执行,继续触发 Monaco 自己的 DOM 剪贴板监听和撤销逻辑;返回 `handled` 后前端不能再次执行动作。 @@ -370,7 +380,7 @@ Commit 文件树参考同一 Community revision 的 `ChangesTreeCellRenderer`、 `#70AEFF`、新增 `#73BD79`、删除 `#6F737A`、冲突 `#DE6A66`、未跟踪 `#E88F89`。 使用既有文件类型图标映射,不再用铅笔/加号色块代替类型图标。 -`GitChangeInclusionCheckbox` 复用上游 expUI 三态 SVG,24pt 画布中包含 16pt 方框, +`LitheTreeCheckbox`(原 GitChangeInclusionCheckbox)复用上游 expUI 三态 SVG,24pt 画布中包含 16pt 方框, 分别保留开/关/部分选中、禁用、焦点状态。深色原图不改,浅色按同 revision 的 `Checkbox ColorPalette` 生成颜色变体,图形路径不改;出处记录在图标目录 NOTICE。 这是构建期固定资源,只通过既有 bundle 图标解析器读取,不增加运行时下载或写入。 @@ -392,7 +402,9 @@ Commit 文件树参考同一 Community revision 的 `ChangesTreeCellRenderer`、 New UI 行高 32、左侧 empty border 为 13,`ErrorBorder` 仅在错误/警告时绘制, 不能套用设置页的常规输入框 chrome。`focusedNewItemFieldBlendsIntoPopup` 在 深浅模式下渲染真实新建文件/目录内容,聚焦后检查输入区域与弹窗背景一致。 -新建文件/目录继续使用 `lithePopupNameField`;Commit 变更列表不消费轻量名称框。 +数据库的 Move → To Folder 目的地列表使用 `LitheDropdownPopover.centersMenu`,由现有菜单 presenter 在所属窗口居中并限制到屏幕内;调用者只提供菜单项,不复制列表样式或键盘处理。 + +新建文件/目录和数据库新建/重命名分组共用 `LitheNamePopup`(`macos/Sources/Lithe/Views/Components/LitheNamePopup.swift`),内部继续使用 `lithePopupNameField`;保留项目区的 340×78 普通状态布局,验证错误时自然增高;数据库名称弹窗通过共享 `LitheCenteredPopup.resizesToContent` 显式开启内容尺寸跟随,其他弹窗保持原有尺寸策略。`ProjectItemNameDialogContent` 只映射标题与动作,不能为数据库复制另一份名称框。Commit 变更列表不消费轻量名称框。 Commit 新建、编辑变更列表依据 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade` 的 `NewChangelistDialog.java`、`NewEditChangelistPanel.kt`、 `DarculaEditorTextFieldBorder.java` 和 Islands 明暗主题,使用原生带标题栏对话框。 @@ -1075,12 +1087,16 @@ GitFeatureModel 写入及确认流程;只读历史/保存记录模式不提供 ### 普通选中行与窗口焦点 -项目树原先固定使用灰色选中底色,其余列表也有只用活动色或只用灰色的入口。 -现在普通树和列表都通过 `litheSelectionRow(isSelected:isFocused:)` 绘制选中与 -悬停表面。单行树由 `litheTreeRow` 转交;多行列表直接调用该入口,保留原行高、 -内边距、字体和圆角参数。只有“区域有焦点”且“窗口接收键盘输入”同时成立才显示 -主题活动色;失去任一条件都显示灰色。窗口是否活动由共享样式读取 -`controlActiveState == .key`,不能让每个新调用方自行决定是否检查。 +普通树和列表现在通过完整控件 `LitheTreeList` 提交数据、选择绑定、层级和业务动作。 +外侧留白、字体、行高、悬停、活动/失焦选择、焦点环处理、方向键、范围选择、回车动作、 +滚动定位和可访问性动作由控件负责。递归业务树可以提交 `LitheTreeListNode`,控件只读取 +已展开的子节点并形成直接的惰性行;异步加载、取消和插件动作仍归各业务数据拥有者。 +旧 `litheTreeRow` / `litheSelectionRow` 业务扩展已经删除,不能继续手工拼装样式。 +`LitheTreeListRowSurface` 仅是组件内部绘制和像素测试的实现,业务代码不得直接使用。 + +选中色只有在列表区域有焦点且窗口接收键盘输入时才使用活动色,否则使用失焦色。 +单行默认使用 `LitheTheme.Tree`;多行保留内容高度。项目树的用户行高设置属于已有产品 +选项,由原生交互适配器传入,不能因为共用控件而把 20–32pt 设置强制改成 24pt。 颜色依据 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade` 的 `platform/platform-resources/src/themes/islands/ManyIslandsDark.theme.json` 与 @@ -1088,8 +1104,10 @@ GitFeatureModel 写入及确认流程;只读历史/保存记录模式不提供 `selection-bg-inactive`。Lithe 默认主题的深色活动/失焦色为 `#2A4371/#33353B`, 浅色为 `#D0DFFE/#E9EAEE`;其他主题沿用既有主题定义,不逐页面复制颜色。 -区域焦点沿用 `LitheToolWindowActivityTracker`,应安装在列表所在区域,不能 -装在整个工作台后让多个列表同时变蓝。观察器按实际被编辑的输入框判断焦点, +区域焦点由完整控件的 SwiftUI FocusState 和已有 `LitheToolWindowActivityTracker` 共同确认。 +键盘代理视图(KeyViewProxy)的可见矩形可能为空,此时必须保留 FocusState 的真实键盘归属, +不能仅靠矩形相交把已取得键盘的列表判为失焦。观察器由完整控件安装在列表区域,不能 +由调用方装在整个工作台后让多个列表同时变蓝。观察器按实际被编辑的输入框判断焦点, 不能只比较 macOS 共用的 field editor(窗口内多个输入框共用的文本编辑器); 鼠标回调、焦点变化和卸载共用事件版本;延迟点击只有版本仍最新、窗口未变且 当前 focus owner 未变时才能发布点击状态,否则按当前 responder 重算。 @@ -1097,9 +1115,14 @@ GitFeatureModel 写入及确认流程;只读历史/保存记录模式不提供 区域均在下一次主队列回调发布。挂载、卸载及窗口通知不得同步写 SwiftUI Binding, 否则真实 NSHostingView 销毁期间会与 GraphHost.invalidate 发生独占访问冲突并 触发 SIGABRT。旧挂载或较早点击的回调不能覆盖新焦点。 +原生项目树可消费方向键而暂时保留旧 SwiftUI 键盘代理,因此控件在有效的区域外鼠标回调中 +释放自身 FocusState,避免两棵树同时显示活动选中。不能为解决颜色而抢回异步编辑器焦点。 + +双向滚动的短项目树必须由公共视口提供最小宽高并顶部对齐;否则 SwiftUI 会把短内容 +垂直居中。尺寸来自唯一的视口 GeometryReader,不给每行增加几何测量。 -项目树是已有键盘路由的特殊消费者:`ProjectTreeKeyboardCommands` 发布 -键盘归属变化,颜色与实际消费方向键的区域一致。通过窗口 didUpdate 及时识别 +项目树通过 `LitheTreeListNativeInteraction` 接入已有的 `ProjectTreeKeyboardCommands`: +完整控件接收键盘归属变化,颜色与实际消费方向键的区域一致。通过窗口 didUpdate 及时识别 新获得焦点的文本框,搜索框尚未输入字符时树就失焦;其他编辑器仍保留首次按键 建立基线的异步聚焦处理。直接套用普通区域观察器会在 文件打开后的 WebKit 异步聚焦中误判。惰性树内容的相等判断必须包含焦点, @@ -1113,24 +1136,40 @@ SwiftUI 消费者包括 Project、Commit、Maven、Git Log 分支/比较/Rebase 输入接收视图)判断,并在宿主窗口状态改变时重绘。Shelf 的原位展开行沿用 共享树色与窗口判断,不替换其已有展开 renderer(负责行绘制的组件)。 -正确做法是给已有多行历史行调用 -`litheSelectionRow(isSelected: selected, isFocused: historyListActive)`,再在 -历史列表区域安装已有观察器。不要写 `.background(selected ? -LitheTheme.selection : .clear)`;这种写法无法在窗口失焦时切换灰色。 -不要把只读详情或相邻列表的活动状态传给该历史行。 +正确做法是调用 `LitheTreeList(entries, selection: binding, multiline: true) { entry in ... }`, +只在行内容里保留图标、文字和业务按钮。不要另外加 ScrollView、外侧 padding、焦点观察器、 +选中色或方向键处理。选择变化与主点击/回车动作有独立回调,避免方向键移动时误打开编辑器。 + +Git 提交文件树和 Worktree 列表仍使用原生视口绘制,经 `LitheTreeList(native:)` 进入; +它们调用同一 `LitheTreeListAppearance` 和 `LitheTreeListNavigation`,不再各自维护背景留白 +或方向键边界规则。原生列表使用真实第一响应者与 key window 判断焦点,不由页面传布尔值。 +项目树保留拖放、剪贴板和同级全选语义,但单选/范围选择与方向键决策复用相同纯函数。 +Search Everywhere 使用输入框驱动的结果模式,保留搜索输入和左右切换分类,不能把文本焦点 +抢到结果列表。Shelf 的悬停展开由 `LitheTreeListOverflowRow` 保留,展开窗口也读取控件内的 +选择与焦点状态,调用方不再自己绘制另一套背景。 + +本轮还核对 Community `idea/262.9437.185` 的 +`platform/platform-impl/src/com/intellij/ide/ui/laf/darcula/ui/DarculaTreeUI.java` 和 +`platform/util/ui/src/com/intellij/util/ui/tree/WideSelectionTreeUI.java`:树的整行绘制、 +焦点/非焦点选择与层级导航属于控件自身。Lithe 复用已有 Tree 主题尺寸和原生事件路由, +没有引入另一套配色或复制 Swing(Java 桌面组件)的实现。 工具窗口标签、菜单高亮、活动栏、数据库单元格、编辑器文本选择、状态徽章、 复选框包含状态和选定设置值有各自语义与主题,不强制与普通行的 RGB 相同。 -它们继续使用各自共享样式;没有持久选择的 Dependencies、普通 Search 结果 -也不会因此新增选择功能。Windows 本次未修改;其文件树已有 +它们继续使用各自共享样式;Dependencies、普通 Search 结果现在也使用完整控件的行与键盘入口, +只读内容和编辑器本身仍保留各自语义。Windows 本次未修改;其文件树已有 `data-tree-focused` 在 `--selected` 与 `--border` 之间切换,不能把 macOS 组件验证当作 Windows 实机验收。 -统一入口减少遗漏窗口判断和重复颜色的机会,代价是每个独立列表仍需明确提供 -区域焦点状态。没有把所有页面强制改成 24pt 树行,也没有新增第二套焦点系统。 +完整入口使业务代码不再负责补焦点修饰符或行表面;代价是必须提交稳定行标识和可见层级。 +不能把多行列表、表格或提交图谱强制变成 24pt 树行。`LitheTreeListTests` 的覆盖检查禁止 +业务代码再次调用旧修饰符或直接使用组件内部的行表面。 回归通过 `LitheTreeRowStyleTests` 检查真实绘制的深浅色、区域活动/失焦与 窗口 key/inactive 状态,以及 24pt 树和 40pt 多行尺寸; +`LitheTreeListTests` 检查统一导航、多选与锚点、惰性节点、完整挂载的辅助功能选择和行内按钮; +`LitheTreeListViewportTests` 检查短原生树顶部对齐以及区域外鼠标回调的版本与卸载失效; +`DependencyTreePresentationTests` 检查取消及旧加载结果不能覆盖新的展示状态; `ProjectTreeSelectionTests` 检查方向键和多选; `EditorTabOrderFeatureModelTests.tabActivityFollowsNativeKeyboardFocusAcrossRegions` 检查原生区域及共用输入编辑器之间的焦点转移、卸载失效和重新挂载同步; @@ -1144,6 +1183,8 @@ SwiftUI State 宿主销毁和主队列清理,不能只靠手写 Binding 的组 完整应用验收与组件回归分别记录, 不由编译或单一页面截图推断整个产品已验证。 +本轮 302 项相关 macOS 回归通过;深色完整预览中,项目树恢复顶部排列, +点击项目树时插件树失焦变灰,插件树方向键展开/折叠与 Inspect 动作通过。 深色新预览已通过真实鼠标验证 Settings 分类:点击分类显示主题色,点击右侧 内容变灰,重新点击恢复。完整应用全部入口、浅色/非默认主题和跨窗口切换仍 保留 pending;共享像素与原生焦点回归不替代这些实机验收。 @@ -1207,7 +1248,11 @@ Debug 空状态标题栏使用 Git Log / Terminal 已有的 41pt 工具窗口规 13pt Bold 标题、左 12pt / 右 8pt 留白、两侧各 2pt 的 22pt 图标按钮, `expui/general/moreVertical.svg` 与 `expui/general/hide.svg` 原始 16pt 资源。 底线复用 `LitheToolWindowHeaderDivider` 的主题边框颜色、1pt 高度和两侧 1pt 内缩, -以内侧 overlay 绘制,不能再叠加普通 divider,也不使用旧 `LitheToolWindowHeader` 的 30pt 度量。 +以内侧 overlay 绘制,不能再叠加普通 divider。 +公共 `LitheToolWindowHeader` 已同步使用 41pt、13pt Bold、左12pt/右8pt留白。 +按用户明确的统一规则,普通面板标题只显示文字,不显示图标、外框或底部分隔线; +标题栏 API 不再接受图标参数,调用方也不得补 `LitheToolWindowHeaderDivider`。 +插件及普通标题栏调用公共入口即可,不再保留其原有30pt规则。 标题栏源码依据同一 Community revision 的 `platform/platform-impl/src/com/intellij/toolWindow/ToolWindowHeader.kt`:New UI 高度走 @@ -1876,3 +1921,91 @@ JDT Run marker 的测试元数据,不根据文件名包含 Test 推断。深 ### 2026-10-17 软换行覆盖按钮光标 软换行按钮已改为 WebKit 文档内的标准 HTML 按钮,与 Monaco 正文使用同一套光标系统;旧的 `EditorSoftWrapToggle` 原生覆盖层及手动光标回调已删除,不再使用 `onContinuousHover` 强制设置箭头。按钮尺寸和绘制参数仍来自共享图标按钮样式,悬停使用小手,正文与空白编辑区保留 I 形光标。可见性、禁用和点击继续通过现有 configure 与同源消息处理。原因和真实 WebKit 验证见[Monaco 可行性与原生桥接 Note](../architecture/2026-09-15-macos-monaco-feasibility-probe.md)的软换行按钮决策;独立组件测试不代替完整应用悬停验收。 + + +### 2026-10-23 数据库范围列表的外部点击提交 + +`litheDropdown` 可显式传入 `onAccept`,由共享 `LitheContextMenuPresenter` 在外部点击或失去键盘焦点时调用一次再关闭。数据库范围列表据此对齐 IDEA 的 Enter/点击外部应用、Esc 取消;Enter 复用已有 `litheDropdownKeyHandler`。程序化关闭、锚点卸载和 Esc 只关闭,不提交;未传入回调的菜单保留既有行为。回调在调用前清空,避免关闭窗口引发再次失焦而重复保存。深浅主题数据库宿主回归覆盖 Enter、Esc、父窗口重新获得焦点及实际持久化结果。 + + +数据库范围列表复用共享树样式,不使用普通菜单行。Git 原有的 IDEA 三态复选框提升为 `LitheTreeCheckbox`,仍消费原 `commit/checkBox*.svg` 的完整深浅/焦点/禁用状态,Git 调用显式传入提交语义,Run 浏览器沿用批量选择语义;数据库通过可选标题组成完整可点击树行。`LitheSettingsCheckbox` 保持设置页原行为,不增加菜单专用变体。范围列表使用 `litheTreeRow` 与共享树边距,底部说明复用弹层背景;整个选择面板局部禁用小手,入口徽标保留小手且没有悬停提示。数据库宿主回归在库名右侧空白发送真实鼠标事件,Git 原资源检查继续覆盖全部状态。 + +### 公共树的展开入口补齐(2026-10-21) + +项目树与声明式插件树共用 `LitheTreeDisclosureIcon`,由它持有原始 chevron SVG、 +16pt 图标和 Tree 图标槽;调用方不再复制资源路径与对齐。公共树的可选中分支 +双击名称默认切换展开,叶子保留激活动作;回车使用相同的分支优先规则。 +不可选分组保留单击展开,连续双击不应撤销第一次展开。Project 原生指针 +适配器继续负责双击与拖放,公共层不重复派发。语言测试树删除了重复折叠回调。 +用 `LitheTreeListTests` 覆盖展开/收起与叶子激活;本轮42项树/列表/插件/Project +回归通过,新增激活检查耗时1ms。打包预览实机验证动态树名称双击展开和再次收起、 +静态树名称双击收起;预览保留给用户继续验收。1379项包文件/符号链接清单与哈希未变。 + +### 声明式工具栏颜色与原生动作尺寸(2026-10-21) + +`litheToolbarIconButton` 直接传入 `ActionToolbar.icon`,不再要求每个标签重复 +foregroundStyle。通用 `litheIconButton` 保留原来的文字颜色,避免影响其他用途。 +声明式 `WorkbenchContributionIcon` 按 role 选资源:活动栏保留20pt,已有16pt +动作版本的 run/debug/settings/vcs 使用原生16pt SVG;不靠缩小活动栏图标改变描边。 +包内品牌图标仍遵守 original/template 契约,菜单与主工具栏保留各自的颜色上下文。 + +核对 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade` 的 +`platform/icons/src/expui/toolwindows/run.svg` 与现有资源字节一致; +`platform/platform-resources/src/themes/islands/ManyIslandsDark.theme.json` +将 CED0D6 映射到 icon-default-stroke/gray-120(C3C5CB),与现有 ActionToolbar token 一致。 +深浅主题用真实 SwiftUI 按钮像素检查无调用方颜色修饰时的工具栏色, +并检查动作明暗 SVG 均为16pt,防止再次取20pt活动栏图标。 + +模板动作必须使用空心/描边素材,不能把带深色内部填充的彩色 run/debug SVG 套成单色, +否则内层底色也变为前景而得到实心图形。run 使用已有16pt空心版本,debug 使用已有 +`debug_stroke.svg` 模板版本;后者无单独暗色文件,两种主题都由统一 token 着色。 + +本轮15项图标/悬停/树行样式测试通过(2.836s),新增按钮着色像素检查覆盖深浅主题, +新增资源检查拒绝把深色内部填充素材当模板图标。实际预览确认面板内显露工具栏, +不将组件深浅主题检查等同于完整工作台深浅主题人工验收。 + +## 完整输入组件入口(2026-10-22) + +搜索只保留LitheSearchField,移除LitheSettingsSearchField的第二份实现。 +原设置搜索的28pt外框改为现有共享搜索样式的36pt完整占位;选择器弹窗高度 +同步取完整搜索组件高度,避免搜索和列表相互挤压。所有原生产品搜索配方 +调用迁入此入口,原有筛选、替换、匹配导航等语义动作以附加内容保留, +布尔和枚举焦点仍由原页面控制。多行Agent任务编辑不是搜索框,保留其原生编辑器。 + +普通共享表单输入通过LitheTextField、LitheTextEditor调用。原修饰器成为文件内 +私有实现,业务页面和声明式均不能漏装外框、焦点或光标处理。密码仍用 +原生SecureField,不改为明文模拟;外部焦点、回车、原生文本编辑和撤销保留。 +已有专用表格单元格、提交消息和编辑器查找等仍保留各自的交互所有者。 + +LitheFormField复用原公共标签;禁用仅作用于编辑内容。LitheInfoTip在组件内部 +恢复可用环境,让现有页面即使禁用整个区块,也可用鼠标/键盘查看说明。 +这只开放说明,不开放值写入或插件动作;声明式Feature仍拒绝禁用字段编辑。 +LitheTextInputTests检查旧入口重新出现,并验证实际原生编辑器、清空与焦点; +PluginNativeUITests验证禁用说明可点击打开真正的公共弹窗。 + +## 公共树的右键与无障碍动作边界(#1423审查修复) + +右键选择只定位菜单目标,不代表打开内容。数据库连接/表的打开行为迁到明确的 +点击或激活路径,避免仅右键另一张表再取消菜单,就切换活动表并清空当前草稿。 +方向键和右键仍可改变树的选中行,但不触发数据库工作区导航。 + +没有实际条目的节点菜单不安装外层右键捕获视图,保留运行配置和依赖内容内已有 +业务菜单。菜单构造只生成描述;显示时仍重新获取条目,保留选择后的可用状态。 +单选和多选节点适配器共用同一判断,不在运行/依赖调用方分别补例外。 + +无障碍默认按下先选中,再执行与Return相同的激活语义:分支展开/折叠,叶节点 +调用激活动作,未提供激活动作时使用点击动作。节点适配器遵守相同回退。 +普通键盘移动保持仅选择;行内独立按钮仍执行自身动作,不打开整个结果行。 +原生回归覆盖数据库三类未应用编辑、菜单Escape取消、行内业务菜单,以及 +无障碍动作最终打开的编辑器文档。 + +本轮使用仓库固定Swift6.3.3运行89项/9套件回归通过(10.718s)。新增数据库 +右键草稿保留、嵌套菜单单/多选两种适配器,以及无障碍打开文档的回归均有 +独立计时;没有运行新的产品预览,也没有改动插件协议或按钮统一方案。 + +## 声明式文字按钮接入#1422标准 + +声明式插件与产品文字动作使用同一个LitheActionButtonStyle,删除插件分支对该样式 +增加的拉伸、最大宽和外圈留白参数。按钮组按公共ActionButton.spacing横排, +空间不足时纵排。旧实验声明的width/span保留解析兼容,新示例不再声明这些布局提示; +不得为了兼容旧包重新绘制fill按钮或恢复输入焦点色作为主按钮颜色。 diff --git a/.agents/notes/proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md b/.agents/notes/proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md index deaa09ca09..95e8f1015b 100644 --- a/.agents/notes/proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md +++ b/.agents/notes/proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md @@ -82,7 +82,6 @@ Services 接入容器、Spring Boot、数据库和本地服务。后续统一设 - `macos/Sources/LitheApplicationKernel/` - `macos/Sources/Lithe/Platform/MacOS/MacServiceContainer.swift` - `macos/Sources/Lithe/Views/Workbench/` -- `macos/Sources/Lithe/Views/Database/` - `windows/` ## 跟踪任务 diff --git a/.agents/notes/proposed/architecture/2026-10-17-build-system-provider-api.md b/.agents/notes/proposed/architecture/2026-10-17-build-system-provider-api.md index 826eb303a2..ef72a2c797 100644 --- a/.agents/notes/proposed/architecture/2026-10-17-build-system-provider-api.md +++ b/.agents/notes/proposed/architecture/2026-10-17-build-system-provider-api.md @@ -164,7 +164,7 @@ BSP 本身也是 JSON-RPC 上的进程外构建协议,生命周期与 LSP 几 1. **P0 契约骨架(Core 已完成)**:API 3 的 `buildSystems`、13 个 `build.*` 命令、Cargo/Go/Gradle/Maven 四个 fixture 和 `build-plans.json` 标准答案。 2. **P1 通用工具窗口与运行配置**:两端实现第 2 节的宿主能力和 Build 工具窗口;`runConfig.generate` 接入构建模型,先迁移 Cargo;把 npm、just、Make、Composer 等现有探测器改写成内置声明;定下 `buildSystems.test` 与 `executionProviders.test` 在同一工程里谁优先。 -3. **P2 进程型 Provider 与 Gradle**:`provider` 声明、启动计划和回复解码的 Core 部分已完成(`gradle-provider`、`mix-provider` fixture 和标准答案)。macOS 宿主部分(协商、发送 `lithe/build/*`、进程生命周期)已完成,剩下 Windows 宿主。Gradle 先用插件包内的 init script 输出 `litheBuildModel`(fixture 已验证这条路)关闭 Issue #10,再评估改用 Tooling API 的 Provider 以获得结构化进度和测试事件;Go、Cargo 的依赖树走模型命令档。 +3. **P2 进程型 Provider 与 Gradle**:`provider` 声明、启动计划和回复解码的 Core 部分已完成(`build-provider`、`build-provider-language-server` fixture 和标准答案)。macOS 宿主部分(协商、发送 `lithe/build/*`、进程生命周期)已完成,剩下 Windows 宿主。Gradle 先用插件包内的 init script 输出 `litheBuildModel`(fixture 已验证这条路)关闭 Issue #10,再评估改用 Tooling API 的 Provider 以获得结构化进度和测试事件;Go、Cargo 的依赖树走模型命令档。 4. **P3 BSP**:有实际需求(Scala、Bazel、SwiftPM)时,在同一传输层上加入 `bsp/2` 协议。 5. **P4 Maven 收编**:官方插件 `dev.lithe.maven` 先以 `hostIntegration: maven` 进入同一领域模型,由通用工具窗口渲染,Windows 随后用同一组 fixture 切换。最后改为插件自带的 Provider,从 Core 删除 `maven` 集成。逐项对照见下一节。 @@ -179,7 +179,7 @@ BSP 本身也是 JSON-RPC 上的进程外构建协议,生命周期与 LSP 几 除特别标注外,各项都在 P4 迁移。 -Core 侧已经具备的部分:fixture `maven-build.plugin.json`(插件 ID 为 `dev.lithe.fixture.maven-build`)声明了 Wrapper、`MAVEN_HOME` 和 `PATH` 三类候选,Profile、`settings.xml`、本地仓库、离线和跳过测试五个选项,`clean`、`compile`、`package`、`install` 和自由输入的 goal,统一后的单测模板,以及诊断正则;`maven` 集成负责 reactor 模型、依赖树和 Surefire 结果。表里“迁移做法”剩下的主要是宿主工作、正式的 `dev.lithe.maven` 清单,以及删除旧命令。 +Core 侧已经具备的部分:fixture `build-host-integration.plugin.json`(插件 ID 为 `dev.lithe.fixture.build-host-integration`)声明了 Wrapper、`MAVEN_HOME` 和 `PATH` 三类候选,Profile、`settings.xml`、本地仓库、离线和跳过测试五个选项,`clean`、`compile`、`package`、`install` 和自由输入的 goal,统一后的单测模板,以及诊断正则;`maven` 集成负责 reactor 模型、依赖树和 Surefire 结果。表里“迁移做法”剩下的主要是宿主工作、正式的 `dev.lithe.maven` 清单,以及删除旧命令。 #### 工程模型 @@ -317,7 +317,7 @@ Core 契约部分的验收已在已实现的 Note 里完成。剩下的工作按 - 安装一个第三方 Gradle 构建插件后,它注入的每个运行任务各自生成一条运行配置,名字包含 project 路径和任务名,满足 Issue #10 的验收条件,Core 和宿主都不改代码。 - 在未信任的工作区打开项目时,没有任何 `command` 档进程被启动;Wrapper 只在用户操作后运行。 - 只有 `buildSystems: [1]` 的宿主可安装模板构建插件,但市场、清单校验和安装审查必须拒绝自有与共享 Provider;宿主真正实现 Provider 后才加入 `buildProviders: [1]`。已有 `declarative_plugins` 和 `marketplace_plugins` 回归覆盖这一边界,后续宿主实现要保留这些断言。 -- `gradle-provider` 这类自有 Provider 只在用户操作或工作区信任后启动,并登记进进程账本;`mix-provider` 这类共享写法不启动第二个进程,在工作区信任前不发送 `lithe/build/*`。 +- `build-provider` 这类自有 Provider 只在用户操作或工作区信任后启动,并登记进进程账本;`build-provider-language-server` 这类共享写法不启动第二个进程,在工作区信任前不发送 `lithe/build/*`。 - 任务通过现有执行链路运行,具备取消、期限和输出上限;禁用插件会停止它登记的守护进程,用户在终端启动的同一工具不受影响。 - `junitXml` 和 `lineMatcher` 解码出的诊断与测试结果能跳转到编辑器位置。 - 迁移过程中 Maven 工具窗口的生命周期、Profile、依赖树和执行目标行为不变;`build.*` 与 `maven.*` 并行期间,两者对同一组 Maven fixture 的结果一致。 diff --git a/.agents/notes/proposed/architecture/2026-10-22-unified-vcs-provider-model.md b/.agents/notes/proposed/architecture/2026-10-22-unified-vcs-provider-model.md index f21db215d2..79bf7fd3f9 100644 --- a/.agents/notes/proposed/architecture/2026-10-22-unified-vcs-provider-model.md +++ b/.agents/notes/proposed/architecture/2026-10-22-unified-vcs-provider-model.md @@ -6,9 +6,11 @@ Lithe 现在只认识 Git:Rust Core、macOS 和 Windows 的变更列表、提交面板、项目树文件颜色、编辑器行标记(gutter)都直接使用 Git 类型。[Issue #827](https://github.com/1lck/Lithe-IDEA/issues/827) 要求支持 SVN,以后还可能有 Mercurial、Perforce 等其他 VCS(Version Control System,版本控制系统)。 -提案照搬[构建系统插件契约](../../implemented/architecture/2026-10-17-build-system-plugin-contract.md)已经验证过的做法:Rust Core 定义一份与具体 VCS 无关的统一模型和 `vcs.*` 命令,每种 VCS 是一个 Provider(回答“这个目录现在是什么状态、怎样提交、怎样回滚”的实现)。Git 是内置 Provider,只在 Core 里加一层转换,现有 `git.*` 代码和契约不动;SVN 作为官方插件,以插件进程的形式接入,后续其他 VCS 走同一条路。 +提案照搬[构建系统插件契约](../../implemented/architecture/2026-10-17-build-system-plugin-contract.md)已经验证过的做法:Rust Core 定义一份与具体 VCS 无关的统一模型和 `vcs.*` 命令,每种 VCS 是一个 Provider(回答“这个目录现在是什么状态、怎样提交、怎样回滚”的实现)。SVN 作为第一个插件 Provider,后端用官方 Node SDK 编写,以插件进程的形式接入,后续其他 VCS 走同一条路。2026-10-23 决定 SVN 插件不放进主仓库,而是像 Cargo 插件一样做成独立仓库的社区插件([lithe-plugin-svn](https://github.com/Lithe-IDEA/lithe-plugin-svn));主仓库只保留与具体 VCS 无关的 Core、契约、SDK 和宿主。 -对日常开发的影响:在统一层落地之前,新的 Git 功能照常写在 Git 模块里;但凡是“任何 VCS 都会有”的界面(文件状态颜色、gutter、提交、回滚、文件历史、Annotate),不要再新增对 Git 类型的直接依赖,等统一层就绪后改读 `vcs.*`。 +2026-10-22 评审决定:**Git 暂不接入统一层**。现有 Git 代码约 8 万行(Rust、Swift、TypeScript 合计,不含测试),两端还有重复的确定性逻辑,直接“包一层”会把这些债一起带进统一层。所以第一期统一层只服务插件 Provider,Git 保持现有 `git.*` 和 Git 模块不变;Git 的精简、迁移和改成内置官方插件另起提案(第 10 节)。 + +对日常开发的影响:新的 Git 功能照常写在 Git 模块里;但凡是“任何 VCS 都会有”的界面(文件状态颜色、gutter、提交、回滚、文件历史、Annotate),不要再新增对 Git 类型的直接依赖,以免 Git 迁移时多一处要拆。 ## 问题 @@ -34,11 +36,11 @@ Lithe 现在只认识 Git:Rust Core、macOS 和 Windows 的变更列表、提 | 层 | 位置(计划) | 负责 | 不负责 | | --- | --- | --- | --- | -| Rust Core 的 `vcs` 模块 | `rust/lithe-core/src/vcs/`(新) | 统一模型、`vcs.*` 命令、Provider 注册表(Registry,按 VCS ID 登记和查找实现的目录)、根目录识别、插件回复的解码和校验 | 启动进程、弹窗、存储 | -| 宿主 | macOS 新的 VCS 工作区模块;Windows 的 VCS 状态 store | 根映射与用户覆盖、文件监听触发刷新、Changelist、Shelve、凭据交互、Provider 进程生命周期、项目控制台 | 解析任何 VCS 的输出 | -| Provider | Git:Core 内置;其他 VCS:插件进程 | 回答状态、基线内容、历史、Annotate、提交、回滚、更新等事实 | 界面和跨根聚合 | +| Rust Core 的 `vcs` 模块 | `rust/lithe-core/src/vcs/`(新) | 统一模型、`vcs.*` 命令、根目录识别、请求计划、插件回复的解码和校验 | 启动进程、弹窗、存储 | +| 宿主 | macOS 新的 VCS 工作区模块;Windows 的 VCS 状态 store | 根映射与用户覆盖、文件监听触发刷新、凭据交互、Provider 进程生命周期、项目控制台 | 解析任何 VCS 的输出 | +| Provider | 插件进程(第一期只有 SVN);Git 暂不接入 | 回答状态、基线内容、历史、Annotate、提交、回滚、更新等事实 | 界面和跨根聚合 | -内置 Git Provider 在 Core 内部直接调用现有 `git::*` 函数,再把结果转换成统一形状。`git.*` 命令保持原样,现有 Git 界面和老调用方不受影响。插件 Provider 的回复先由 Core 校验(路径必须在根内、排序稳定、数量和大小有上限),再转换成和 Git 完全相同的形状。所以宿主拿到的 `vcs.status` 结果不区分来自哪个 Provider。 +插件 Provider 的回复先由 Core 校验(路径必须在根内、排序稳定、数量和大小有上限),再转换成统一形状。所以宿主拿到的结果不区分来自哪个 Provider,以后 Git 接入时也只需要产出同一形状。 ### 2. 统一模型只收所有 VCS 都成立的概念 @@ -58,7 +60,7 @@ Lithe 现在只认识 Git:Rust Core、macOS 和 Windows 的变更列表、提 ### 3. 界面看能力,不看 VCS 的名字 -每个 Provider 声明自己支持哪些能力,界面只按能力出现入口。必备能力是状态、基线内容、提交和回滚;其余都可选:历史、文件历史、Annotate、更新、推送、暂存区(staging)、分支、冲突处理、文件操作跟踪、锁、属性、清理等。 +每个 Provider 声明自己支持哪些能力,界面只按能力出现入口。必备能力是状态、基线内容、提交和回滚;协议版本 1 的可选能力是历史、文件历史、Annotate、更新、推送、暂存区(staging)、文件操作跟踪、冲突处理和清理。分支、锁、属性、忽略规则等到有第二个 Provider 需要时,再按能力版本加入。 集中式与分布式 VCS 的差别也表达成能力: @@ -90,31 +92,79 @@ Git Log 图、Rebase、Worktree、Stash、Push 和 Fetch 对话框、历史改 - 进程启动后,宿主在 `initialize` 里通过 `capabilities.experimental.litheVcs` 协商版本,Provider 在回复里给出自己的能力集。能力集描述的是运行中的进程,所以不写进清单,这遵守[声明式插件按能力分版本](../../implemented/architecture/2026-10-17-plugin-capability-versions.md)第 4 节的规则。 - 请求方法统一以 `lithe/vcs/` 开头。宿主不按 Provider ID 写分支;进程按“插件 ID + VCS ID + 工作区”隔离,生命周期规则与构建 Provider 相同。 -字段和方法的草案见 [VCS Provider 契约草案](../../../../shared/contracts/vcs-provider-v1.md),示例数据见 [`shared/fixtures/vcs/model-v1.json`](../../../../shared/fixtures/vcs/model-v1.json)。草案在 P1 落地前都可以修改,任何 Core 命令、清单校验和宿主能力表都还不接受这些名字。 +字段和方法见 [VCS Provider 契约](../../../../shared/contracts/vcs-provider-v1.md),示例数据见 [`shared/fixtures/vcs/model-v1.json`](../../../../shared/fixtures/vcs/model-v1.json)。Core 已按契约实现(见第 9 节的进度),但还没有宿主声明 `vcsProviders`,所以插件市场不会列出这类插件;宿主接入前契约仍可以修改,改动要同时更新 fixtures 和 Core 测试。 + +### 7. SVN 作为社区插件 -### 7. SVN 作为官方插件 +SVN 插件在独立仓库维护和发布(见第 9 节 P2),调用用户系统里的 `svn` 命令行,解析它稳定的 `--xml` 输出。IDEA 的 svn4idea 也是这样做的(`plugins/svn4idea/src/org/jetbrains/idea/svn/status/CmdStatusClient.kt` 使用 `svn status --xml`)。这和 Lithe 使用系统 Git 的方式一致,不需要打包 JVM 或跨平台编译 libsvn。 -SVN 插件调用用户系统里的 `svn` 命令行,解析它稳定的 `--xml` 输出。IDEA 的 svn4idea 也是这样做的(`plugins/svn4idea/src/org/jetbrains/idea/svn/status/CmdStatusClient.kt` 使用 `svn status --xml`)。这和 Lithe 使用系统 Git 的方式一致,不需要打包 JVM 或跨平台编译 libsvn。 +后端进程用官方 Node SDK([lithe-plugin-docs](https://github.com/Lithe-IDEA/lithe-plugin-docs) 的 `sdk/node`)编写,以 `runtime` 方式启动(`tool: node`)。SDK 已经负责线协议、协商、取消、子进程清理和错误映射,Cargo 社区插件也走这条路;同一份后端在 macOS 和 Windows 上都能运行,插件包里没有原生二进制,不涉及按架构编译和 Gatekeeper。代价是用户除了 `svn` 还需要 Node 20.3 以上,安装审查和缺少运行时的提示要说清楚。SDK 新增 `litheVcs` 支持,和 `litheBuild` 并列;2026-10-23 起 SDK 不在主仓库,这部分改动在 lithe-plugin-docs 提交(见[插件契约、SDK 与开发者资料的发布](../../implemented/architecture/2026-10-23-plugin-contract-publishing.md))。 -凭据规则沿用[Git 执行与项目控制台](../../implemented/architecture/2026-09-12-git-execution-and-project-console.md):SVN 自己的认证缓存优先回答;需要交互时,Provider 通过协议向宿主发出认证挑战,宿主复用现有认证界面。命令始终带 `--non-interactive`,密码通过 `--password-from-stdin` 传入,不出现在命令参数、控制台和日志里,Lithe 也不保存密码。 +凭据规则沿用[Git 执行与项目控制台](../../implemented/architecture/2026-09-12-git-execution-and-project-console.md):命令始终带 `--non-interactive`,SVN 自己的认证缓存优先回答。第一版只依赖这份缓存,认证失败时返回 `vcs.authentication.failed`,宿主提示原因;交互式认证(宿主弹窗收集用户名和密码,经 `--password-from-stdin` 传给 `svn`)在宿主阶段实现。密码不出现在命令参数、控制台和日志里,Lithe 也不保存密码。 ### 8. 宿主接手与 VCS 无关的状态 -- **根映射**:一个工作区可以同时有 Git 和 SVN 根。文件归属最深的那个根;同一目录同时出现两种标记时,要求用户在设置里选择,不能猜。IDEA 用 `VcsDirectoryMapping` 保存这种映射。 -- **Changelist 和 Shelve**:从 `LitheGitModule` 上移到宿主的 VCS 层,存储键从“Git 仓库路径”改为“VCS 根 ID”,迁移时继续读取旧格式。IDEA 的 `ChangeListManagerImpl` 同样在 `platform/vcs-impl`,不属于任何一种 VCS。 +- **根映射**:一个工作区可以同时有 Git 和 SVN 根。文件归属最深的那个根;同一目录同时出现两种标记时,要求用户在设置里选择,不能猜。IDEA 用 `VcsDirectoryMapping` 保存这种映射。Git 暂不接入统一层,但 Core 识别根时仍认得 `.git` 标记,这样嵌套和歧义的判断对 Git 也成立,Git 根继续交给 Git 模块。 +- **Changelist 和 Shelve**:长期应从 `LitheGitModule` 上移到宿主的 VCS 层(IDEA 的 `ChangeListManagerImpl` 在 `platform/vcs-impl`,不属于任何一种 VCS)。这一步和 Git 迁移一起做;第一期 SVN 根的变更不分组,也不支持 Shelve。 - **监听刷新**:[单仓库 Git 状态监听提案](../bug-fix/2026-09-13-single-repository-git-status-observation.md)里要求的监听范围,扩展成“每个 Provider 声明的元数据目录”,例如 SVN 的 `.svn/wc.db`。 -- **项目控制台**:现有 Git 执行事件多带一个 VCS ID 字段,插件 Provider 用通知上报实际执行的命令和输出。展示、折叠和脱敏规则不变。 +- **项目控制台**:现有 Git 执行事件多带一个 VCS ID 字段。插件 Provider 用 `lithe/vcs/execution` 通知上报实际执行的命令和输出,Core 把它转成会话日志事件交给宿主。展示、折叠和脱敏规则不变。 ### 9. 分阶段落地 +2026-10-22 起按“先底层、再 SVN、Git 最后”的顺序: + | 阶段 | 内容 | 完成标志 | | --- | --- | --- | | P0 | 本提案、契约草案和 fixtures | 评审通过 | -| P1 | Core `vcs` 模块:类型、注册表、根识别;内置 Git Provider 的读路径(状态、基线内容、历史、文件历史、Annotate) | 同一批测试仓库上 `vcs.*` 与 `git.*` 的结果等价 | -| P2 | macOS 只读的通用界面改读 `vcs.*`:项目树颜色、gutter、状态栏 | Git 项目的行为和外观没有变化 | -| P3 | 插件 `vcsProviders` 能力、`litheVcs` 协商、宿主 Provider 会话(复用构建 Provider 的会话代码);Changelist/Shelve 上移;通用提交面板 | 测试用的假 Provider 插件跑通完整流程 | -| P4 | SVN 官方插件 MVP | 见验收标准 | -| P5 | Windows 按同一契约和 fixtures 接入 | Windows 通过同一组 fixtures | +| P1 | Core:插件清单 `vcsProviders` 能力、根识别、请求计划与插件回复解码、`litheVcs` 协商和 `lsp.vcsRequest`;Node SDK 支持 `litheVcs` | fixtures 和用 SDK 写的假 Provider 经真实 Core 会话跑通 | +| P2 | SVN 社区插件(独立仓库,Node SDK 后端) | 插件仓库里录制的 `svn --xml` 样例解析测试,以及用固定版本 Core 在本地 `file://` 仓库上跑的端到端测试 | +| P3 | macOS 宿主:Provider 会话(复用构建 Provider 的会话代码)、项目树颜色和 gutter 合并展示插件根、通用“版本控制”工具窗口(变更、提交、回滚、更新)、文件操作跟踪、交互式认证 | 见验收标准 | +| P4 | Windows 按同一契约和 fixtures 接入 | Windows 通过同一组 fixtures | +| 之后 | Git 精简并接入统一层,见第 10 节 | 另起提案 | + +P1 已完成(2026-10-22): + +- 插件清单:API 级别 3 的 `vcsProviders`、`process.vcs` 权限和 `onVcsRoot:` 激活事件;`git` 是保留 ID。安装审查列出 Provider 进程,SVN 这类工具路径另放在 `vcsTool` 字段,不覆盖运行时的 `tool`。 +- 根识别 `vcs.detectRoots`:只读目录项,不读文件、不启动进程;工作区自己没有标记时向上查祖先目录,所以“工作区开在 SVN 工作副本的子目录里”也能识别。新增 `vcs.ownerOf`,让各宿主用同一条“最深根”规则给文件找归属,不必各写一份。 +- 会话:`litheVcs` 只提供给插件启动的会话;`lsp.vcsRequest` 按操作名生成 `lithe/vcs/*` 请求,未协商的能力在写出前就拒绝;Provider 只能使用契约列出的错误码,其余一律变成 `vcs.provider.failed`。`lithe/vcs/authenticate` 在宿主实现交互式认证前按未知请求回绝。 +- Node SDK:`backend.vcs(id, handlers, traits)` 按声明的处理函数协商能力,VCS 处理函数里的 `context.run` 会自动发出 `lithe/vcs/execution` 执行报告(只带命令行、退出码、耗时和截断后的 stderr,不带 stdout)。示例 `sdk/node/examples/sample-vcs` 是一个把修订存在 JSON 文件里的玩具 VCS,端到端场景经真实 Core 会话跑通它的协商和全部 11 种操作。SDK、示例和端到端场景随 SDK 迁到 lithe-plugin-docs,在那里用 `scripts/sdk-e2e.mjs --ref <本分支>` 运行;主仓库只保留契约、fixtures 和 Core 测试。 + +P2 已完成(2026-10-23),同日改为社区插件: + +- 插件在独立仓库 [lithe-plugin-svn](https://github.com/Lithe-IDEA/lithe-plugin-svn),ID 是 `io.github.lithe-idea.svn-support`,`vendor.signatureRequirement` 为 `none`。仓库结构照搬 [Cargo 插件](../../implemented/feature/2026-10-22-cargo-build-plugin.md):`sdk.lock.json` 按上游提交和逐文件 SHA-256 固定 Node SDK,打包前校验;安装包发在插件仓库的 GitHub Releases,Lithe Store 索引记录下载地址和 SHA-256。 +- 插件仓库自己的 CI 按 `sdk.lock.json` 的同一个宿主提交检出 Core,经公开 JSON 接口在 `svnadmin create` 建立的 `file://` 仓库上跑状态、提交、更新、冲突、回滚、移动、历史和 Annotate。svn 怎样映射到每个操作、svn 错误码怎样映射到契约错误码,都写在插件仓库的 README 里,主仓库的契约只留一个指向。 +- 主仓库不再有 SVN 专属的代码和测试,CI 也不用装 svn。它保留通用的部分:契约、fixtures 和 Core 测试,以及宿主测试用的 `shared/contracts/plugins/v1/fixtures/vcs-provider.plugin.json`(只有清单,没有实现)。样例 Provider `sample-vcs` 和它的端到端场景随 SDK 在 lithe-plugin-docs。 + +正确做法:主仓库改 VCS 契约或 Core 时,用 fixtures 和 Core 测试验证,合并前在 lithe-plugin-docs 用 `scripts/sdk-e2e.mjs --ref <分支>` 跑样例 Provider;SVN 插件升级 `sdk.lock.json` 后在自己的 CI 里验证。 +不要这样做:在主仓库测试里引用隔壁的 SVN 插件 checkout,或者为 SVN 在宿主代码里加按名字判断的分支。 + +宿主需要知道的 SVN 行为(插件 README 有完整说明): + +- `head` 是根目录自己的工作修订(`svn info` 的结果)。提交后根目录停在旧修订,要 `update` 才前进,这是 SVN 的混合修订语义,宿主不要拿它判断“提交没成功”。 +- svn 每个写命令结束前会等约 1 秒,保证之后的修改时间戳不同,宿主不要指望写操作更快完成。 +- 第一版 `status` 每次查整个根,契约 v1 也没有按路径查询的参数。增量查询要等量出大工作副本的实际耗时后再加,要加就按能力版本加参数。 + +P3 第一部分已完成(2026-10-23),在 macOS 上: + +- 宿主在 `PluginManifest.hostCapabilities` 声明 `vcsProviders: [1]`,声明了 VCS Provider 的插件从这时起能安装。宿主读清单时给这类插件生成一个 `<插件 ID>.vcs-providers` 模块,只承载插件的启用状态,默认关闭,做法和构建系统插件的 `.build-systems` 模块一样。安装审查给三种 Provider 程序(插件内、系统已装、运行时启动)显示启动命令、`vcsTool` 里的客户端命令,以及 `mayUseNetwork` 声明的“可能连接仓库服务器”,让用户批准前知道插件会运行哪个 `svn`。客户端命令和 Provider 自己的运行时一样经构建工具发现查找,找不到时报契约的 `vcs.tool.unavailable`,工具窗口据此先显示安装提示。 +- Provider 会话按窗口创建:启动时已安装的插件声明了 `vcsProviders`,`MacServiceContainer` 才建 `VcsProvidersFeatureModel`。启动后再安装的插件要重启才生效,工具窗口这时只显示“重启 Lithe 后使用”。这样做是因为服务在窗口组合时一次建好,运行中插桩会话要改动整个组合根,收益只是省一次重启。 +- 每个 Provider 贡献一个左侧工具窗口,宿主只认贡献上的 `vcsProviderPluginID`、`vcsProviderID` 两个可见性键,代码里没有任何 VCS 或插件的名字(与构建系统窗口同一套注册方式)。Provider 不声明图标,派生的窗口和命令统一用宿主的版本控制图标(`git` 宿主图标,即 `vcs@20x20.svg`),不用通用工具窗口图标。第一次读状态时 Provider 进程才启动,可能要几秒,变更列表这时显示“正在读取更改…”,不留空白。变更行在辅助功能里是一个合并元素,默认动作是选中,另有“显示差异/打开文件”和“包含在提交中”两个命名动作,否则辅助功能只能切换复选框。窗口按冲突、变更、未版本化文件分组:默认只勾选“变更”组,未版本化文件要用户勾选才提交,冲突文件不能勾选,要先“标记为已解决”;移动的文件提交时连同原路径一起提交,否则 SVN 会只提交一半。按钮是否出现看 Provider 协商的能力,不看 VCS 名字。 +- 与基线比较做成窗口级的弹出面板(`VcsProvidersFeatureModel.presentedDiff`,由 `RootView` 显示),不挂在工具窗口里,因为项目树右键的“显示差异”在工具窗口隐藏时也要能用。比较复用本地历史的 `LocalHistoryDiffBuilder` 和 Git 的 `DiffPaneView`;超过 2 万行时退成左右两栏纯文本,避免逐行比较卡住界面。有没有基线由 `VcsRootState.comparesWithBase` 一处判断,工具窗口双击、辅助功能动作和项目树右键共用:新增、未版本化、忽略的文件没有基线,双击直接打开;复制的文件和复制来源比较。 +- 项目树颜色:插件根的状态通过一个只观察 `VcsProvidersFeatureModel` 的包装视图传给项目树,项目树本身仍是 `.equatable()`,刷新状态不重绘整棵树。同一文件 Git 和插件都有状态时用 Git 的,因为第一期 Git 不在统一层,嵌套时以 Git 的显示为准,不让两套颜色互相覆盖。 +- 文件操作跟踪:工作区模块只多一个注入的 `moveTrackedItem` 回调,不认识任何 VCS。项目树里的重命名和移动先交给拥有该文件的 Provider(`svn move`);Provider 不跟踪这个文件或拒绝时,退回原来的磁盘移动。源路径和目标路径都要用 Core 的 `vcs.ownerOf` 判断归属,两端都属于同一个插件根才交给 Provider:嵌套的 Git 仓库、还没选 VCS 的歧义目录都不属于外层工作副本。只按“最深的插件根”判断会把移进嵌套 Git 仓库的文件也交给 `svn move`。等待 Core 判断归属期间如果开始了别的操作,移动直接退回磁盘移动,和操作开始前就在运行时一样,不报“等正在运行的操作结束”。 +- 刷新:工作区监听和项目树一样隐藏 VCS 元数据目录。内置隐藏列表里有 `.git` 和 `.svn`,与 IDEA 默认的忽略列表一致;不隐藏的话,`.svn/pristine` 里每个文件的基线副本会进入项目树、搜索、本地历史和索引。所以每个插件根单独监听自己的标记目录,例如 `.svn`,或工作区开在工作副本子目录里时的 `../.svn`。只监听 `metadataPaths` 所在的标记目录,不监听整个外层工作副本。工作文件的变化仍走工作区事件,两路事件走同一条过滤和合并逻辑,合并后再刷新受影响的根。元数据目录里只有 Provider 声明的 `metadataPaths`(SVN 是 `.svn/wc.db`)会触发刷新。宿主自己的请求执行期间和结束后的短暂时间内忽略元数据变化,否则每次提交、甚至每次读状态都会再刷新一遍;代价是命令行在这段时间里的改动,要等下一次刷新才显示。元数据监听丢了事件,或标记目录消失时,按标记变化处理,重新识别根。标记目录隐藏后,工作区事件里看不到新出现的 `.svn`。所以新建的条目和它所在的目录如果不是已知的根,就在后台线程查一次有没有标记,有才重新识别,例如在工作区里 checkout 了新工作副本,或 update 拉下了 external。 +- 生命周期:关闭工作区、切换工作区,以及原生项目窗口关闭(`shutdownProjectSession`,这时工作区可能被设置窗口保留而不关闭)都会在第一次挂起前清空 `VcsProvidersFeatureModel`:取消识别和刷新任务,停止该工作区启动中和运行中的 Provider 进程,停掉元数据监听。窗口关闭走 `shutdown()`,之后这个窗口的模型不再加载任何工作区:关闭时还在进行的工作区扫描随后调用 `loadVcsProviders` 也不会把 Provider 重新拉起来;关闭的会话模型不会被复用,所以不需要恢复入口。 +- 歧义目录的选择按工作区存在应用偏好里(`Vcs.RootMappings.<工作区摘要>`),不写工作区,也不写 app bundle。识别时目录不再歧义,就丢掉旧选择。 + +P3 剩下的部分:编辑器 gutter 与 Annotate、交互式认证(`lithe/vcs/authenticate` 仍按未知请求回绝)、把 `lithe/vcs/execution` 执行报告接到项目控制台,以及在 Lithe Store 上架 SVN 插件。上架要等带 `vcsProviders` 能力的 Lithe 版本发布后再做,否则旧版本用户会在市场里看到装不上的插件;P1 合入后,插件仓库的 `sdk.lock.json` 也要改指向 `preview` 上的提交。 + +### 10. Git 什么时候、怎样接入 + +Git 接入不在本提案的范围内,这里只记下评审时已经确认的方向,供以后的提案使用: + +- **先精简再接入。** 提交图投影在 Core 里已有一份(`git/graph.rs` 的 `project_graph`),但生产代码没有调用,macOS 和 Windows 又各写了一份布局;执行日志、Changelist、状态模型等确定性逻辑也是两端各一份。接入时按切片迁移,每一片两端同时切换,并在同一个 PR 里删掉旧路径。 +- **Git 改成随应用内置的官方插件**,和 SVN 走同一个注册入口;通用界面不再直接依赖 `LitheGitModule`。是否搬到独立进程是传输方式问题,可以单独决定。 +- **平台层功能先做在统一层**:三方合并工具、行级历史、Changelist、Shelf 在 IDEA 里都属于平台而不属于 git4idea,补齐时直接给所有 VCS 用。 ## 考虑过的备选方案 @@ -126,13 +176,25 @@ SVN 插件调用用户系统里的 `svn` 命令行,解析它稳定的 `--xml` 界面只需要面对一套 API,看起来最“统一”。但 Rebase、Worktree、暂存区这些概念在 SVN 里不存在,要么让通用层变成 Git 的翻版,要么为了照顾最弱的 VCS 削掉 Git 的能力。IDEA 也没有这样做,因此只统一跨 VCS 成立的概念。 +### 第一期就把 Git 包成内置 Provider + +最初的方案是在 Core 里给现有 `git::*` 加一层转换,让 Git 和插件同时产出统一形状。评审时发现这样会把两端重复的逻辑和 Git 专属的结果类型原样带进统一层,以后再精简反而更难,因此改为第一期不接 Git,先用 SVN 把底层跑通(第 10 节)。 + ### Git 也改成插件进程 -这样内置和插件完全对称。但 Git 是 Lithe 的核心体验,状态读取在热路径上,认证、取消和执行事件链路也刚稳定;搬到进程外对用户没有收益,迁移风险却很大。内置 Provider 和插件 Provider 共用 Core 里同一套转换和校验,结果形状已经对称,因此不搬。 +这样内置和插件完全对称。但 Git 是 Lithe 的核心体验,状态读取在热路径上,认证、取消和执行事件链路也刚稳定;第一期不动 Git。以后 Git 改成内置官方插件时,进程内还是进程外只是传输方式,可以单独决定。 + +### SVN 后端用 Rust 写成原生可执行文件 + +不需要用户安装 Node,启动也更快。但插件包要为每个平台和架构各编译一份、单独签名,macOS 上下载来的原生可执行文件还要处理 Gatekeeper 隔离;而 Node SDK 已经实现协议、取消和子进程清理,社区构建插件也在用。SVN 后端的工作主要是调用 `svn` 和解析 XML,不是性能热点,因此选 Node SDK。 ### SVN 内置到宿主 -首发更快,也不需要先做插件能力。但插件 VCS 这条链路要等到第三种 VCS 出现才会被真实验证,而且宿主里会出现 SVN 专属代码。用 SVN 作为第一个插件 Provider,可以在真实需求下验证整条插件链路,因此选择官方插件。 +首发更快,也不需要先做插件能力。但插件 VCS 这条链路要等到第三种 VCS 出现才会被真实验证,而且宿主里会出现 SVN 专属代码。用 SVN 作为第一个插件 Provider,可以在真实需求下验证整条插件链路,因此做成插件。 + +### SVN 作为官方插件留在主仓库 + +P2 的第一版就是这样:插件放在 `Plugins/mac/Official/SvnSupport/`,用 Lithe 的发布者私钥签名,进官方插件目录。好处是主仓库改 Core 时,SVN 端到端测试会一起跑。但插件每次修改都要走主仓库的 PR、CI 和官方签名发布,和 Lithe 的发版节奏绑在一起,主仓库 CI 还得装 svn;Go、PHP、Cargo 插件已经因为同样的原因搬到了独立仓库。改为社区插件后,插件可以独立发布,信任来自 Lithe Store 的审核、SHA-256 和安装确认框;契约回归由主仓库的样例 Provider 覆盖,SVN 自己的回归由插件仓库 CI 用固定版本 Core 覆盖。因此不采用。 ### 用 SVNKit 或 libsvn 嵌入 SVN @@ -144,28 +206,34 @@ SVN 插件调用用户系统里的 `svn` 命令行,解析它稳定的 `--xml` ## 验收标准 -- P1:在同一批 Git 测试仓库上,`vcs.status`、基线内容、历史、文件历史和 Annotate 与对应 `git.*` 结果等价;大仓库上 `vcs.status` 比 `git.status` 只多出转换开销,不额外启动进程。 -- P2:macOS 项目树颜色、gutter 和状态栏改读统一模型后,Git 项目的现有测试和截图检查全部通过,行为没有变化。 -- P3:一个只用于测试的假 VCS Provider 插件完成安装审查、协商、状态、提交、回滚和进程清理;停用或卸载插件后进程退出,它的根从界面消失;未协商成功的进程收不到 `lithe/vcs/*` 请求。 -- P4:SVN 官方插件在本地 `svnadmin create` 建立的 `file://` 仓库上完成状态、提交、更新、回滚、add/move/delete 跟踪、Annotate、文件历史和冲突显示;密码不出现在进程参数、控制台和日志中。 +- P1:清单校验接受和拒绝的情况都有 fixture;根识别覆盖嵌套、歧义和 Git worktree 文件标记;插件回复的校验用例全部通过;用 Node SDK 写的假 Provider 经真实 Core 会话完成协商和每种请求,未协商成功的进程收不到 `lithe/vcs/*` 请求。 +- P2(在插件仓库 CI 中运行):SVN 插件对录制的 `svn status/info/log/blame --xml` 样例解析正确;在本地 `svnadmin create` 建立的 `file://` 仓库上完成状态、提交、更新、回滚、add/move/delete、Annotate、文件历史和冲突显示。 +- P3:macOS 上安装 SVN 插件后完成安装审查、根识别、文件颜色、gutter、提交、回滚、更新和进程清理;停用或卸载插件后进程退出,它的根从界面消失;Git 项目的行为没有变化;密码不出现在进程参数、控制台和日志中。 - 每个阶段都运行对应的 Core、契约、Note 和平台验证脚本;Windows 的结论只来自 Windows 上的实际运行。 ## 风险 -- **Git 仍在快速变化。** Issue 评论里提到 Git 还没稳定。缓解办法:统一层只包装稳定的概念,`git.*` 不冻结,Git 专属功能继续在 Git 模块里演进。 -- **状态读取是热路径。** 内置 Git Provider 在 Core 内直接转换,不额外起进程。SVN 在大工作副本上 `svn status` 较慢,由 Provider 按变化路径做增量查询,宿主合并刷新事件。 -- **Changelist 和 Shelve 的存储迁移。** 改变存储键可能丢失用户已有分组。迁移时保留读取旧格式,并用旧数据做回归测试。 +- **统一层只被一种 VCS 验证过。** 第一期只有 SVN 用统一层,模型里可能藏着 SVN 的偏见。缓解办法:模型只收第 2 节表里 Git 和 SVN 都成立的概念,Git 专属的暂存维度已经作为可选能力进入 fixtures;Git 接入时如果发现缺口,按能力版本加字段。 +- **Git 仍在快速变化。** Issue 评论里提到 Git 还没稳定。第一期不碰 Git,`git.*` 不冻结。 +- **状态读取是热路径。** SVN 在大工作副本上 `svn status` 较慢。第一版每次查整个根,宿主先合并刷新事件、限制刷新频率;实测太慢时再按能力版本加按路径的增量查询。 - **混合根和嵌套根。** Git 仓库里嵌套 SVN 工作副本时,文件必须只归属一个根,否则会重复显示或重复提交。识别规则和用户映射都要有 fixture 覆盖。 - **插件进程常驻的成本。** 只在识别到对应根时启动,关闭工作区即回收,规则与构建 Provider 相同。 -- **回退方式。** 每个阶段都只新增路径。P2 切换后保留旧读取路径一个版本,出问题可以切回。 +- **回退方式。** 每个阶段都只新增路径,停用 SVN 插件即回到现状。 ## 适用范围 +- `rust/lithe-core/src/vcs/` +- `rust/lithe-core/src/plugins/` +- `rust/lithe-core/src/lsp/interface/` +- [SVN 插件仓库](https://github.com/Lithe-IDEA/lithe-plugin-svn) - `rust/lithe-core/src/git/` - `rust/lithe-git-host/` - `macos/Sources/LitheGitModule/` - `macos/Sources/Lithe/Views/Git/` - `macos/Sources/Lithe/Views/Workspace/` +- `macos/Sources/Lithe/Views/Vcs/` +- `macos/Sources/Lithe/Platform/MacOS/Vcs/` +- `macos/Sources/Lithe/Application/Features/VcsProvidersFeatureModel.swift` - `macos/Sources/Lithe/Views/Editor/` - `windows/tauri/src/features/git/` - `shared/contracts/plugins/v1/` diff --git a/.agents/notes/proposed/architecture/2026-10-24-java-support-independent-distribution.md b/.agents/notes/proposed/architecture/2026-10-24-java-support-independent-distribution.md new file mode 100644 index 0000000000..f0e75919d6 --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-10-24-java-support-independent-distribution.md @@ -0,0 +1,226 @@ +# Agent 笔记:Java Support 独立分发与内置包迁移 + +状态:提议中 + +## 先说结论 + +Java Support 已经是声明式插件,JDT LS、JDK、Lombok 和调试、测试扩展都由插件包拥有,本提案不重复做插件化。建议下一步把它从“随 Lithe.app 发行”改成“官方签名、独立发布、按需安装”,让不使用 Java 的用户不再下载这部分资源。先完成可信安装、旧版本兼容和老用户迁移,再从主安装包移除资源;现有 `jdt` 宿主集成与 Java 运行、测试、调试实现保持当前边界。 + +## 问题 + +### 已完成什么,尚未完成什么 + +以下事实以 `preview@53f7052a463eaf1a4c548663aca4afd1e381224a` 为评估基线。 + +当前边界由[Java 语言服务器是一个声明式插件](../../implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md)规定,不能把以下已实现内容重新列为开发任务: + +| 部分 | 当前事实 | +| --- | --- | +| 插件身份 | `dev.lithe.plugin.java-support`,清单位于 `Plugins/mac/Official/JavaSupport/plugin.json` | +| 插件资源 | 拥有 JDT LS、Lombok、Java Debug、Java Test、语言服务器使用的 JDK,以及项目生成器的图标和模板 | +| 启动与状态 | 按清单启动;JDT 工作区索引和 Equinox 配置区写到平台缓存,安装目录只读 | +| 宿主集成 | 插件声明 `hostIntegration: jdt`,启用宿主现有 Java 工程、运行、测试和调试能力 | +| 安装方式 | 随 macOS 应用发行、默认启用;卸载只登记移除标记,重启后停止注册,不能释放 bundle 中的文件 | +| 分发缺口 | 没有 Java Support 的官方下载条目,仍要随主应用下载和升级 | +| Windows | 仍使用自己的 JDT/JDK 打包与启动路径,尚未按这份插件声明接入 | + +已有笔记估算 Java 包约 400 MB,实际收益应在实施时按架构重新测量。不能把未压缩资源大小直接当成 DMG 下载量、增量更新流量或卸载后的净释放空间。 + +### 独立分发存在几个实际阻碍 + +1. **信任方式不同。** Java 当前声明 `sameTeamAsHost`,含义是由宿主 app 的代码签名封住资源;`MacOfficialPluginSignatureVerifier` 会拒绝位于宿主 bundle 外的这种包。下载版需要 `publisherPackage` 发布者签名,不能只把同一目录压缩上传。 +2. **发布工具还不能原样发布 Java。** `scripts/official-plugin-releases.mjs` 当前按 API 1、单个下载项生成索引;Java 已使用 API 3,需要准确保留 `requires`,并按架构记录不同下载包和校验值。 +3. **旧宿主不一定能安装新版。** 旧版官方目录将 Java 的签名策略固定为 `sameTeamAsHost`。即使旧宿主认识 API 3 和清单的全部能力,也不代表它能接受下载版的新信任方式。 +4. **首次离线体验会变化。** 当前实现明确以“首次启动离线可用”为随包发行的理由。改为按需安装后,未安装 Java 插件的新用户需要联网,或提前准备可信磁盘包。这是本提案建议改变的产品行为,尚未落地。 +5. **语言服务器 JDK 同时参与现有运行时发现。** 资源移出主包时,需要验证项目运行时选择,防止运行或调试落到失效的旧插件路径。 + +## 提案 + +### 一、目标与范围 + +首个交付只完成独立分发,不要求把所有 Java 专属逻辑从宿主删除。 + +| 位置 | 首个交付后的责任 | +| --- | --- | +| Java Support 插件 | 完整语言服务器资源、自带 JDK、清单、设置声明、现有项目生成器、组装与发布配置 | +| Lithe 宿主 | 官方目录、兼容过滤、安装审查、启停、更新、回滚、进程生命周期,以及现有 `jdt` 集成 | +| Rust Core | 现有清单校验、LSP(语言服务器协议)会话、JDT 适配及运行/测试/调试的共享契约 | +| 平台存储适配器 | 插件版本目录、下载临时文件、安装记录、日志、索引和可写配置区 | + +沿用当前插件 ID,不用新 ID 让用户重新配置 Java。下载版采用新插件版本,不得覆盖已发行 `1.0.0` 的内容;精确版本号在实施和发布时确定。 + +本轮不拆 Spring/MyBatis,不把 Maven/Gradle 构建模型搬进 Java 插件,不设计新的 Java 语义分析器,也不顺带把 Run/Test/Debug 改为公共扩展点。后者是独立的架构任务,继续由[公共扩展契约提案](2026-10-13-public-extension-contract-and-provider-api.md)跟踪。 + +### 二、分发与源码位置 + +独立分发不要求独立仓库。首期继续在 `Plugins/mac/Official/JavaSupport/` 维护源码,复用主仓库的官方插件构建和发布流程,以插件自己的版本、标签及下载包发布;改插件不再要求用户更新整个 Lithe 安装包。 + +- macOS 先发布 `darwin-arm64`、`darwin-x64` 两个包,使用同一插件版本和能力声明,各有自己的 ZIP 和 SHA-256。通用架构的 Lithe 根据实际运行架构选择包,不能依靠文件名猜测架构。 +- 官方插件目录和官方索引保留在主仓库。下载 URL 必须属于索引登记的插件发布仓库,沿用当前索引约束。 +- 主仓库继续拥有现有上游锁定清单、组装、资源完整性和真实 JDT 回归,不因默认应用精简而删除测试或 Windows 仍消费的资源输入。 +- 迁入独立官方仓库是后续可选任务,拟用名 `Lithe-IDEA/lithe-plugin-java`,不作为首期验收前提。届时一次性迁移插件源码及专属测试,不同时保留两份可编辑源码;宿主兼容性测试用固定提交与校验值准备输入,禁止两个仓库各自静默升级一份。 + +继续复用现有 JDT LS、Java Debug、Java Test 和 Lombok 组合,首轮迁移不同时升级它们。Java 的符号、源码根、classpath、可运行入口和测试发现仍由现有上游引擎提供,避免把分发改造变成语言后端重写。 + +### 三、先解决信任和兼容,再公开下载 + +发布者签名(Ed25519,对插件完整文件树的签名)与 macOS 原生可执行文件的启动信任是两件事,需要分别验证。 + +- 下载版清单和宿主官方目录一致要求 `publisherPackage`,签名必须覆盖所有最终资源;代码签名、资源规范化等会改变字节的构建步骤全部在包签名之前完成。 +- 旧内置包只能在宿主自身签名封装的 bundle 内按旧策略读取;用户目录里的同名、同版本包不能因此获得旧信任。过渡期允许两种来源时,必须明确检查来源、清单和版本,不能把 Java 的全局校验放宽成“任意一种签名都可以”。 +- 实测下载包里的 JDK 可执行文件及动态库的签名、解压后的执行权限、隔离属性和系统启动限制;沿用或调整原生签名、公证方式的选择由这轮证据确定,不能靠启动时改写包或移除系统保护来通过。 +- 发布前确定旧宿主的兼容过滤方案。当前索引没有可直接填写的最低宿主版本字段,不能虚构字段,也不能仅凭 `pluginAPIVersion: 3` 判定可安装。若现有能力表不足以表达信任边界变化,先通过通用契约变更补齐拒绝机制,再上架新包。 +- 如需新增兼容或信任能力,契约在主仓库随 Core 修改,合并后按[两仓库同步规则](../../implemented/architecture/2026-10-23-plugin-contract-publishing.md)发布到 `lithe-plugin-docs`;未来独立插件仓库不能直接修改契约副本。 + +首期在主仓库受保护的官方插件发布流程中完成签名,不引入跨仓库私钥分发。私钥不进入插件、源码、构建缓存或日志;本提案不要求读取或转移私钥。未来迁移独立仓库时,另行确定发布权限和签名流程。 + +### 四、安装、升级和老用户迁移 + +建议新安装的 Lithe 默认不含 Java 资源。打开 Java 项目、Java 文件或进入相应新建项目入口时,显示明确的安装入口;下载在用户确认安装审查后开始,保留从磁盘安装的离线路径。 + +老用户采用先具备安装能力、再移除内置资源的顺序: + +1. **过渡宿主仍带内置包。** 新宿主先实现新信任策略、官方源和下载安装;下载失败或取消时继续使用现有内置包。 +2. **安装可信下载版。** 从已发布签名包安装到用户目录,经验证和重启后切换。不要复制旧 bundle 中的包再让客户端自行签名,也不要尝试修改已安装 app。 +3. **单一生效来源。** 同一 ID 只能有一个生效版本:已验证的用户安装版本优先,过渡宿主中的内置包只作为尚未完成切换时的来源。选择规则由通用包管理器执行,避免两个 Java 所有者或两个 JDT 会话同时注册。 +4. **保留用户选择。** 已启用、已禁用和已卸载分别迁移;旧 `.removed-bundled-plugins.json` 标记不能被当作重新启用的授权。插件设置、项目 JDK 和已有运行配置不重置。 +5. **再发布精简主包。** 只有下载、离线安装、迁移和失败恢复都验证后,才删除主包组装步骤。延后安装的用户届时仍可编辑普通文本,但 Java 语言服务、测试和调试需要完成插件安装;产品提示要说明这一变化。 + +不能假设每位用户都安装过过渡版本。直接从旧版升级到精简主包时,也要识别旧设置、卸载标记和失效资源路径,提供明确安装入口并保留用户选择;网络不可用时保留配置和可重试状态,不把“升级成功”显示成“Java 已可用”。 + +更新沿用宿主现有重启边界:活动会话绑定实际使用的插件版本,切换和卸载前释放本插件资源,进程退出后才能删除该版本目录。新包验证或启动失败时保留可恢复的已验证旧版本;回滚也必须重新执行签名和宿主兼容检查。 + +插件路径变化可能触发 JDT 索引重建。配置、项目运行时选择和 `.lithe/run/` 中的稳定配置 ID 保持有效;缓存按新版本指纹重新生成,不把旧索引跨版本复制作为优化。 + +### 五、语言服务器 JDK 与项目 JDK 保持当前区别 + +插件自带 JDK 用于启动 JDT LS;项目运行和调试继续使用现有同一条项目运行时选择链。保留当前明确的插件运行时发现规则,迁移时检查它返回的新安装路径,不能另外给调试写一条兜底路径。 + +必须覆盖用户指定 JDK、项目默认 JDK、未安装其他 JDK、无效路径,以及插件升级或卸载后的情况。选择链失败时明确提示,不能偷偷改用语言服务器进程的 JVM。约束以[Java 调试与运行使用同一个 JDK](../../implemented/bug-fix/2026-10-14-java-debug-uses-the-run-jdk.md)为准。 + +### 六、Windows 单独验收 + +Windows 接入依赖声明式插件宿主及其官方签名、下载管理、`jdt` 集成等实际能力,不能把宿主 PR 合并等同于 Java 插件已经可用。 + +当前 Java 清单还要求 `settings: 2`、`projectGenerators: 1`。只有 Windows 能执行清单中每项必需能力,才能公开 Windows 下载;不能悄悄忽略生成器,或在同一索引版本下发出 `requires` 不同的包。若需要先交付 Java 语言服务,先另立生成器拆包方案并定义依赖和迁移,本提案不默认拆包。 + +Windows 插件包使用自己的原生运行时、入口和存储适配器,复用同一资源版本与契约语义。macOS 先完成分发不受 Windows 阻塞;矩阵保留两端各自的真实实现和验证状态。 + +### 七、资源生命周期与工作树复用 + +| 资源 | 所有者与位置 | 清理和复用规则 | +| --- | --- | --- | +| 插件安装版本 | 平台应用数据中的插件版本目录 | 运行时只读;卸载或保留版本策略负责删除,不跨工作树共享 | +| 下载与安装 staging(尚未发布的临时目录) | 安装操作的平台临时目录 | 失败、取消后清理;校验通过后原子发布,不跨工作树共享 | +| JDT 索引、Equinox 可写配置、日志 | 宿主现有平台缓存与状态适配器 | 由集成和会话负责,不复制到安装包,不跨工作树共享 | +| JDT/JDK 构建下载缓存 | 构建准备脚本 | 有版本、平台、架构和来源校验时才允许按现有资源路由复用 | + +移除旧路径或增加新生成物时,同步更新 `scripts/worktree-resources.json`、`scripts/reuse-worktree-resources.mjs` 的校验路由、对应测试,以及 `docs/ci-builds.md` 的独立工作树章节。安装包和可变状态没有可靠复用身份时继续隔离。 + +运行时不向 app bundle、Windows 安装目录或插件安装版本写入内容,保持代码签名及 Sparkle 增量更新的源基线。典型安装、启动、运行、测试、调试和卸载流程都要比较发行目录与已安装插件版本的文件清单和哈希。 + +### 八、分期与退出条件 + +| 阶段 | 交付内容 | 进入下一阶段前必须证明 | +| --- | --- | --- | +| 0:确认基线 | 盘点两种 macOS 架构的实际资源、压缩体积、JDK 消费者和离线行为;记录上游版本与许可证 | 明确本轮只改变分发;列出需保留的用户工作流和资源消费者 | +| 1:可信下载安装 | 信任迁移、旧宿主兼容拒绝、真实 API/能力索引、多架构发布、签名包安装;保留内置包 | 干净环境可构建;下载包能在对应架构实际启动;正式构建拒绝伪造、损坏和未签名包 | +| 2:老用户切换 | 验证下载版选择、重启切换、设置迁移、失败恢复,以及跳过过渡版的升级路径 | 同一 ID 只有一个生效版本;启用、禁用、卸载选择均正确保留;迁移不依赖用户逐版升级 | +| 3:移除内置资源 | 删除默认应用组装中的 Java 包,更新推荐安装、离线说明、资源清单与矩阵 | 精简应用不含 JDT/JDK 副本;安装插件后完整 Java 工作流可用 | +| 后续:独立仓库 | 可选迁移插件源码、专属测试和发布来源,固定宿主测试资源输入 | 无第二份可编辑源码;用户无需重新安装或重置设置 | +| 后续:Windows 对齐 | 补齐必需宿主能力、平台包和真实 Windows 回归 | 同一版本能力声明可执行,Windows 安装目录哈希不变 | + +首期按三个实现 PR 推进:可信下载安装、老用户切换、移除内置资源,阶段 0 的基线记录随第一项交付。完成阶段 3 即可验收 macOS 独立分发,不等待独立仓库或 Windows;Windows 另行验收前不声明双端完成。每项实现从最新 `preview` 创建独立分支,本地验证后先提交 Draft PR;不通过反复推送试探完整 CI。 + +### 正确做法与反例 + +正确做法:升级 Java Support 时,由插件流水线固定并校验上游输入,生成对应架构的最终签名包,宿主验证后在用户目录安装;重启后现有 `jdt` 集成读取新包声明的资源。 + +不要这样做:把旧内置目录直接上传为下载包,继续声明 `sameTeamAsHost`;下载后往 `Lithe.app/Contents/Resources` 解压;为了让旧宿主接受下载版而取消官方 ID 的签名检查。这些做法分别破坏独立来源信任、发行基线和官方身份边界。 + +## 考虑过的备选方案 + +### 保持随应用发行 + +首次离线体验最完整,迁移成本最低;但不使用 Java 的用户持续承担资源和更新成本,卸载不能释放空间。若产品仍坚持“全新安装立即离线开发 Java”,应保留当前方案,不宣称同时达成精简默认包。 + +### 只把 JDK 改成宿主共享下载 + +可以减少插件包大小,但会重新形成宿主拥有 Java 专属资源的路径,增加运行时版本、卸载和回滚的双重管理。首轮保持自带 JDK,用户项目仍可使用自己的 JDK。 + +### 同时迁移 Java 全部 Run/Test/Debug 逻辑 + +最终可降低宿主对 Java 原始协议的了解,但需要新的领域 Provider 契约、SDK 支持和完整工作流验证。独立分发本身不依赖这些能力,本轮同时推进会扩大回归面,另行规划。 + +### 换用社区插件 ID + +发布可以更独立,但会改变官方身份、用户配置和迁移路径,还不能使用现有受信任 `jdt` 集成。保留官方 ID 和官方签名,通过明确的来源迁移解决问题。 + +## 验收标准 + +- 全新安装的精简 macOS 应用不含 Java Support 资源、JDT LS 或备用 JDK 副本;移除前后分别记录应用大小与下载大小。 +- 两种 macOS 架构均能从官方源或磁盘安装可信包;缺包、取消、离线、错误架构、校验失败及旧宿主不兼容都有明确结果。 +- 安装后真实完成补全、诊断、跳转、Java 主类运行、Spring Boot 运行、测试发现与执行,以及工具栏断点调试;不能只验证启动器或接口调用。 +- 插件语言服务器 JDK 与用户程序 JDK 的选择符合现有规则,显式无效配置不会静默回退。 +- 老用户的设置和启用选择保留,失败安装不破坏过渡期可用的旧包;更新、回滚、禁用、卸载没有重复会话和残留进程。 +- 用户跳过过渡版本直接升级时,配置和卸载选择仍保留;离线和下载失败可重试,不错误显示 Java 已就绪。 +- 项目生成器及其 Maven/Gradle 依赖提示与迁移前一致;停用 Java 插件后相应贡献按当前契约撤销。 +- 应用发行目录和插件安装版本在典型运行流程前后清单、哈希不变;缓存与临时目录由明确 owner 清理。 +- Windows 独立完成相同契约的产品验收前,功能矩阵不标记双端完成。 + +## 验证 + +本提案只修改决策文档,当前执行: + +```bash +./scripts/verify-agent-notes.sh +git diff --check +``` + +后续实现按改动选择以下现有入口,并按 `write-stable-tests` Skill 编写和运行有界测试;下面是计划要求,不表示本次已经执行: + +```bash +./scripts/verify-shared-contracts.sh +./scripts/verify-official-plugins.sh +./scripts/verify-macos-package.sh +./scripts/verify-runtime-bundle-immutability.sh +node scripts/test-reuse-worktree-resources.mjs +node scripts/verify-java-semantic-ownership.mjs +./scripts/verify-service-boundaries.sh +./scripts/verify-windows-boundaries.sh +./scripts/verify-platform-feature-matrix.sh +``` + +实现时新增覆盖官方索引真实 API/能力与多下载项、过渡期来源选择、签名降级拒绝、用户选择迁移及版本回滚的回归;复用现有 Java 入口、真实 JDT、运行配置、测试和调试集成验证。修改平台实现后,在提交和 PR 前通过相对实际基线的矩阵变更门禁;所有本次验证启动的应用和子进程在交付前关闭并确认无残留。 + +## 风险 + +- **首次离线行为改变。** 精简默认包与当前离线承诺不能同时保留;本提案建议通过明确安装入口和磁盘包支持离线准备,实施前在产品说明中确认这一改变。 +- **旧版兼容与信任策略耦合。** 直接翻转官方目录策略会使旧内置包失效;直接发布新清单会让旧宿主错误展示可安装。阶段 1 未解决前不得上架。 +- **独立更新仍受宿主能力限制。** Java 原始协议适配仍在宿主,插件不能任意升级上游版本;每次升级要在声明兼容的宿主上验证,不能宣传为与宿主版本完全无关。 +- **独立仓库资源来源漂移。** 主仓库真实 JDT 测试和 Windows 仍消费上游输入时,需要固定关系和升级责任,避免插件与宿主测试各验证不同版本。 +- **索引重建与磁盘峰值。** 安装期间旧包、新包、归档和新索引可能同时占空间;在下载前检查空间,按版本使用和回滚策略回收,不能删除仍被进程使用的目录。 +- **Windows 能力缺口。** 声明式宿主基础落地不等于支持当前 Java 清单;能力未实现时保持不可安装,而不是绕过兼容过滤。 + +## 适用范围 + +- `Plugins/mac/Official/JavaSupport/` +- `Plugins/marketplace/` +- `scripts/build-official-plugins.sh` +- `scripts/official-plugin-releases.mjs` +- `scripts/package-app.sh` +- `scripts/verify-macos-package.sh` +- `scripts/worktree-resources.json` +- `scripts/reuse-worktree-resources.mjs` +- `docs/ci-builds.md` +- `third_party/jdtls/manifest.json` +- `third_party/jdk/manifest.json` +- `macos/Sources/LitheModuleAPI/Catalog/BuiltInModuleCatalog.swift` +- `macos/Sources/Lithe/Platform/MacOS/Plugins/` +- `macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift` +- `macos/Sources/LithePluginPackageSigning/` +- `rust/lithe-core/src/lsp/languages/` +- `shared/contracts/plugins/v1/` +- `shared/platform-feature-matrix/features/java-support-plugin.json` +- `windows/tauri/src/extensions/` +- `windows/tauri/src-tauri/src/language_tools.rs` diff --git a/.agents/notes/proposed/architecture/2026-10-24-remote-development-workspace-host.md b/.agents/notes/proposed/architecture/2026-10-24-remote-development-workspace-host.md new file mode 100644 index 0000000000..33fa3f4e73 --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-10-24-remote-development-workspace-host.md @@ -0,0 +1,279 @@ +# Agent 笔记:远程开发——传输链与远端工作区服务 + +状态:提议中 + +## 先说结论 + +Lithe 要支持“完整的远程开发”:项目放在 SSH 主机、Docker 容器(包括 Dev Container)或远程 Mac 上,本地只负责界面,文件读写、终端、构建、运行、调试、LSP、Git 和 Agent 都在远端执行。做法是把远端能力全部放进一个独立的远端服务 `lithe-remote-server`。SSH、`docker exec` 这些连接方式只叫传输链(transport chain,从本机到工作区所在位置的一串跳转),它们只负责“把服务启动起来、接通标准输入输出”,不实现任何功能。Linux 和 macOS 远端的差异放在服务内部的平台适配器里,并且以能力描述(capabilities,服务在握手时报告的主机事实)的形式告诉客户端。客户端只看能力,不写“如果远端是 Mac 就怎样”的分支。 + +这不是插件能完成的事情,它是宿主的核心能力。插件以后只需要声明“我的后端跑在本地界面侧,还是跑在远端工作区侧”。目前已经落地的是 Rust 后端:协议、远端服务(文件、目录遍历、文件监听、进程、终端、登录环境、隧道和端口)、断线后不中断的常驻会话、传输链、远端安装和客户端 API。产品界面暂不接入,Windows 暂不考虑。 + +## 问题 + +现在所有能力都默认“项目就在本机”: + +- macOS 应用里有一百多处直接用 `FileManager.default`,Rust Core 里也有大量 `std::fs` 和 `Command::new`。它们读写的都是本机路径,没有一层能替换成“远端实现”的接口。 +- 唯一的远端能力是数据库的 SSH 隧道(`rust/lithe-db-sidecar` 里的 `open_ssh_tunnel`),它只服务数据库,不能复用来打开远程项目。 +- Windows 端的 WSL 工作区是另一套独立实现,没有和“远程”共用抽象。 + +如果不先定下边界,后面每个功能都会各自加一个“远程分支”:文件树用一种 SSH 命令,终端用另一种,构建再拼一条 `ssh host 'mvn …'`。这样做的结果是 Docker 和远程 Mac 都要各写一遍,路径和引号的处理散落在各处,连接断开、进程清理、凭据这些问题也没有一个统一的负责者。 + +## 提案 + +### 整体分层 + +```text +本地 Lithe(界面、设置、钥匙串、本地历史) + │ 工作区后端接口(文件 / 进程 / 终端 / 监听 / 搜索 / 端口 / 插件后端) + ├── 本地实现(现在的代码,迁移到接口之后) + └── 远程实现 ── lithe-remote-protocol ── 传输链 ── lithe-remote-server + ├─ 本机子进程(测试用) + ├─ ssh 主机 + ├─ docker/podman exec 容器 + └─ ssh 主机 → 主机上的容器 +``` + +三个 Rust crate,职责分开: + +| crate | 负责什么 | 不负责什么 | +| --- | --- | --- | +| `rust/lithe-remote-protocol` | 帧格式、消息信封、方法名、参数类型、错误码 | 不做任何 I/O 决策 | +| `rust/lithe-remote-server` | 远端的全部能力和 Linux/macOS 适配器 | 不知道自己是被 SSH 还是 Docker 启动的 | +| `rust/lithe-remote-client` | 传输链拼命令、远端安装校验、会话收发 | 不实现文件或进程语义 | + +### 传输链只做“执行一条命令” + +每一跳只需要做到“在下一跳上执行命令,并接通标准输入输出”。 + +- SSH 跳转使用系统的 OpenSSH(`ssh -T -e none -- <目标> <命令>`)。用户的 `~/.ssh/config`、ProxyJump、硬件密钥、1Password 这类 agent 都自然生效,Lithe 不解析也不复制这些配置。 +- 容器跳转使用 `docker exec -i` 或 `podman exec -i`。OrbStack、Colima 等兼容 Docker 的工具只需要换可执行文件路径。 +- 链可以叠加:`[ssh devbox, docker api]` 就是“先 SSH 到 devbox,再进入它上面的 api 容器”,不需要在 devbox 上额外装任何东西。 +- 空链就是本机子进程。CI 用它跑完整个远程栈,不需要 SSH 环境。 + +SSH 会把远端命令交给远端用户的登录 shell 当作一个字符串解析,而 sh、bash、zsh、fish、csh 的转义规则互不相同(例如 fish 在单引号里也会处理 `\`)。所以穿过 SSH 的参数一律用单引号包起来,并且**直接拒绝**任何 shell 在单引号里还会特殊处理的字符:`'`、`\`、`!` 和控制字符。这些参数都由 Lithe 自己生成(安装路径、容器名),拒绝比转义更可靠。 + +唯一的例外是内层 SSH 跳转生成的命令行。链里有两个 SSH 跳转时(例如 `[ssh 跳板机, ssh 开发机]`),内层生成的命令本身带单引号,外层还要再引用一次。这时把单引号写成 `'"'"'`:先结束单引号,在双引号里放一个单引号,再重新开始单引号。上面五种 shell 读回来都是原样的。因为内层命令只由通过了校验的参数拼成,不含 `\`、`!` 和控制字符,这个例外不会放宽对原始参数的限制。 + +链的身份字符串(例如 `ssh:devbox/docker:api`)用作最近项目、按主机存储的键,它不包含保活参数这类不改变目标的设置。 + +### 远端服务的启动和安装 + +1. **探测**:通过链执行 `sh -c`,在标记行之间输出 `uname -s`、`uname -m` 和 `$HOME`。远端 shell 启动文件打印的欢迎信息会被忽略。 +2. **选择构建产物**:架构以远端报告为准。Apple 芯片的 Mac 上跑 amd64 镜像时,需要的是 x86_64 的服务。Linux 产物使用 musl 静态链接,所以同一个二进制能跑在各种发行版和精简镜像上。 +3. **校验已安装版本**:执行 `<路径> --identity`,服务对自己的可执行文件计算 SHA-256 并输出。只有哈希和本地产物完全一致时才复用。这样不依赖远端有没有 `sha256sum` 或 `shasum`。 +4. **上传**:总是由本地通过同一条链上传,不让远端自己下载,因为很多服务器和容器不能访问外网。上传先写到临时文件名,再改名为正式文件,配合 `umask 077`,同一台机器上的其他用户无法替换它。 +5. **安装位置**:默认是 `$HOME/.lithe-server/bin/<版本>-<哈希前 16 位>/lithe-remote-server`,不同版本并存,互不覆盖。 + +本地这一侧,服务产物属于应用的打包资源,运行时只读取,不写入 app bundle。实现打包时还要遵守已发布程序包的运行时只读规则,并登记到 worktree 资源清单。 + +### 远端服务必须保持轻量 + +远端服务会在第一次连接时上传到每一台主机、每一个容器,很多时候链路很慢,容器里的资源也有限。所以它必须是一个 Rust 编译的原生二进制:不依赖远端的 glibc、Node.js、Python 或任何运行时,复制过去就能运行。作为对比,基于 Node.js 的远程服务通常有几十 MB,还要在远端解压一整套运行时。 + +实测(stripped release,opt-level `s` + LTO): + +| 目标 | 只有文件和进程时 | 全部后端能力 | 链接方式 | +| --- | --- | --- | --- | +| `x86_64-unknown-linux-musl` | 约 760 KiB | 约 1.15 MiB | 静态(static-pie) | +| `aarch64-unknown-linux-musl` | 约 680 KiB | 约 1.0 MiB | 静态 | +| `aarch64-apple-darwin` | 约 610 KiB | — | 只依赖系统库和系统框架 | + +空闲时常驻内存约 2 MB。依赖仍然只有 `libc`、`serde`、`serde_json`、`base64`、`sha2`。inotify 和 FSEvents 都直接调用系统接口,没有引入 `notify` 这类封装 crate。 + +用 `panic = "abort"` 构建还能再省约 9%(约 110 KiB),但没有采用。原因是会话里任何一个线程 panic 都会让整个常驻进程退出,连带用户所有的终端和构建一起结束。现在的 unwind 模式下,出问题的只是那一个线程,锁中毒(poisoned lock,持锁线程 panic 后锁被标记)也会被恢复。 + +Linux 静态版用 Rust 自带的 `rust-lld` 链接,在 macOS 和 Linux 上都能直接构建,不需要 Docker、zig 或额外的交叉工具链。M1 打包时直接沿用这个做法。 + +开发者需要遵守的动作: + +- `scripts/verify-remote-workspace.sh --static` 会构建两个 Linux 静态版,超过 1.5 MiB 就失败。要提高上限,必须先在本 Note 里写明实测原因。 +- 给服务加依赖前先量体积。异步运行时(如 tokio)、HTTP 库、正则库这类大依赖,只有在实测证明标准库线程做不到时才考虑。 +- 功能留在客户端能做的,就不要放进服务,例如解析项目结构、计算 diff、渲染。服务只提供主机必须提供的能力:文件、进程、终端、监听、端口。 +- 不要在服务里内嵌语言服务器、JDK 这类大资源。它们按插件单独上传,按需安装。 + +### 协议的几条硬规则 + +- **流前标记(preamble)**:服务先写一行 `LITHE-REMOTE-PROTOCOL/1`,客户端丢弃它之前的所有字节,最多 64 KiB。这样登录横幅不会破坏协议流;如果远端根本没有服务,被丢弃的输出(例如 `not found`)会作为诊断信息返回给用户。 +- **帧格式**:4 字节大端长度加 UTF-8 JSON,单帧不超过 16 MiB。单次文件传输不超过 8 MiB,更大的文件分段读取。 +- **路径**:协议里的路径是远端主机上的绝对路径。这一层是文件系统和进程的适配器,工具链、终端和调试器都需要真实的主机路径;以工作区为根的相对标识在这一层之上构造。 +- **错误**:错误码稳定(例如 `NOT_FOUND`、`CONFLICT`、`DIRECTORY_NOT_EMPTY`),客户端按错误码分支。`message` 只给人看,OS 错误号放在 `details` 里。 +- **兼容**:不兼容的修改才提升协议版本号。新增方法通过 `features` 列表声明,客户端先查列表再调用。 +- **只走标准输入输出**:服务不监听任何 TCP 端口,所以共享主机上的其他用户连不进来。端口转发以后也走这条流。 + +### 文件和进程的语义 + +- **保存是原子的**:内容先写到同一目录下的临时文件,再改名覆盖目标,读者和崩溃都不会看到写了一半的文件。写入会穿过符号链接,链接本身保留。代价是文件换了 inode,硬链接会断开,这和其他编辑器的安全写入(safe write)模式一样。 +- **防止覆盖别人的修改**:写入时可以带上上次读到的大小和修改时间。不一致就返回 `CONFLICT`,不带版本时默认不覆盖已存在的文件。只是“创建”的写入(不带版本、也不允许覆盖)用不覆盖的改名提交,所以两个请求同时创建同一个文件时只有一个成功,另一个返回 `ALREADY_EXISTS`;提交前的存在性检查挡不住这种并发。 +- **临时文件不泄露内容**:覆盖已有文件时,临时文件创建时的权限不超过原文件(umask 只会再去掉权限),写完再恢复原文件的权限。否则保存一个只有自己能读(`0600`)的文件时,其他用户可以趁临时文件还是 `0644` 打开它,之后再收紧权限也收不回已经打开的文件描述符。 +- **不覆盖的重命名**:Linux 用 `renameat2(RENAME_NOREPLACE)`,macOS 用 `renamex_np(RENAME_EXCL)`,文件系统不支持时才退回“先检查再改名”。 +- **按会话结束进程**:每个子进程都通过 `setsid` 成为新会话的首进程。结束进程时,信号发给这个会话里的所有进程,包括 shell 作业控制(job control)另外建出来的进程组。这样 Maven、Gradle 这类构建工具的 worker 不会残留;dash 不会像 bash 那样把挂断信号转发给后台作业,所以这一步必须由服务来做。`process.exit`、`terminal.exit` 只表示首进程退出;会话里还有成员(例如后台启动的守护进程)时,服务继续持有这个会话,`process.kill`、关闭终端和会话结束时的清理仍然能结束它们,`server.state` 用 `leaderExited` 标出这种进程。会话里没有任何成员后才删除记录,这时再 kill 返回 `UNKNOWN_RESOURCE`。POSIX 不会在进程组或会话还存在时复用这个编号,所以向保留的编号发信号不会误伤其他进程。 +- **输出顺序**:输出分块作为事件发送,退出事件一定排在该进程的所有输出之后。孙进程如果继承了管道、一直不关,最多再等 500 毫秒,之后的输出会被丢弃。 +- **背压**:发送队列有上限,客户端读得慢时,服务就停止读取子进程的输出,内存不会无限增长。 +- **会话结束时的清理**:`--stdio` 会话在连接断开时结束;常驻会话在客户端调用 `server.shutdown`、或空闲超时后结束。结束时,服务先向所有进程和终端发送 `SIGTERM` 或 `SIGHUP`,2 秒后对仍有成员的发送 `SIGKILL`;判断是否结束看的是会话里是否还有进程,而不是首进程是否退出。之后服务最多再等 5 秒,让进行中的请求完成再退出,否则被中断的写入会在用户文件旁边留下临时文件。 +- **客户端请求超时**:客户端由一个单独的写线程负责发送,请求只是把帧放进有上限的队列。对端停止读取时只会卡住写线程,每个请求仍然在自己的超时时间结束,也不会挡住后面的请求;超时时还没发出的帧直接丢弃。 + +### 平台适配器(Linux / macOS) + +所有平台差异都放在 `rust/lithe-remote-server/src/platform/` 里,并通过能力描述告诉客户端: + +| 差异 | Linux | macOS | 客户端看到的能力 | +| --- | --- | --- | --- | +| 服务被强杀后的子进程 | `PR_SET_PDEATHSIG`,内核负责杀掉进程组长 | 没有对应机制,只能在服务正常退出时清理 | `process.orphanProtection` | +| 文件名是否区分大小写 | 用“换大小写再查同一个 inode”来探测 | `pathconf(_PC_CASE_SENSITIVE)`,失败时用同样的探测 | `fs.volumeInfo.caseSensitive`(按卷) | +| 不覆盖的重命名 | `renameat2` | `renamex_np` | 行为一致,不暴露 | +| 账户信息 | 先读环境变量,再查 passwd | 同左 | `userName`、`homeDirectory`、`defaultShell` | +| 文件监听 | inotify,逐个目录注册 | FSEvents,一个流监听整棵树 | `watch.backend`、`watch.directoryLimit` | +| 会话里的进程 | 读 `/proc//stat` | `proc_listallpids` 加 `getsid` | 行为一致,不暴露 | +| 正在监听的端口 | 读 `/proc/net/tcp{,6}`(容器里就是容器自己的网络) | 系统自带的 `lsof` | `ports.listening` | +| Unix socket 对端的用户 | `SO_PEERCRED` | `getpeereid` | 行为一致,不暴露 | + +`PR_SET_PDEATHSIG` 有一个容易踩的坑:它跟随**创建子进程的那个线程**,而不是整个进程。线程一退出,子进程就会被杀。连接线程和工作线程都会来来去去,所以服务里所有子进程都交给一个专门的、和会话同生命周期的创建线程(`spawner.rs`)来创建。 + +FSEvents 的另一个坑:同一个文件的标志位是累积的。一个在监听期间新建的文件,之后每次保存都还会带着“已创建”标志。macOS 适配器会记住已经报告过“新建”的路径,之后的事件按“修改”报告。 + +inotify 的坑:目录是逐个注册的,新建目录要在收到创建事件后再注册,而注册之前已经写进去的文件不会产生事件。所以新目录注册时,会把里面已有的内容补报为“新建”。到达 `fs.inotify.max_user_watches` 上限时,发送 `watch.error`(`LIMIT_REACHED`),已注册的部分继续工作。 + +现在就要为远程 Mac 预留、但第一步还没实现的部分: + +- 通过 SSH 访问 `~/Documents`、`~/Desktop` 受 TCC(macOS 的隐私权限系统)限制。需要识别出这类错误,并提示用户在“远程登录”里开启完全磁盘访问。 +- SSH 会话里钥匙串通常是锁着的,`git` 的 `osxkeychain` 会失败。后续由凭据回传解决。 +- 服务被 `SIGKILL` 时的孤儿进程,需要一个基于 kqueue 的守护者。 + +### 会话和连接分开:断线不丢终端和构建 + +进程、终端、文件监听这些状态都挂在会话(`session.rs`)上,不挂在连接上。 + +- 传输链实际执行的是 `lithe-remote-server --connect `。它只是一个代理:连上 `` 对应的常驻进程(daemon),常驻进程不存在就先启动一个,然后在标准输入输出和 Unix socket 之间转发字节。 +- SSH 断开时,被结束的只有这个代理。常驻进程通过 `setsid` 脱离了 SSH 会话,收不到挂断信号,终端和构建会继续运行。 +- **事件按序号确认,不按“写进 socket”算送达**:每个事件带递增的 `seq`。服务端保留客户端还没确认(`server.ack`,客户端每收到 256 个事件确认一次)的事件,最多 8 MiB。只算“写进 socket”是不够的:连接断开时,事件可能还在发送队列里或内核缓冲区里,这些都会丢。第一版就是这样丢事件的,代码审查时发现。 +- **先订阅、再续传**:握手之后服务端不会马上发事件,要等客户端调用 `server.resume`。重连的客户端先用 `server.state` 找回仍在运行的终端和进程,订阅好,再用上一次连接最后收到的 `seq` 调用 `resume`。这之后的事件按顺序各到达一次,而且进入对应的订阅,不会落进通用通道。第一版在握手响应之后立即重放,重连后接上的终端就收不到断开期间的输出。保留的事件超出上限时,`resume` 返回 `truncated`,客户端据此重新读取状态。 +- 一个会话同一时间只有一个客户端,新连接会顶替旧连接。网络切换后,旧连接常常半死不活、还没被发现,顶替能让重连立刻生效。 +- 连接结束时,常驻进程必须关掉这个连接 socket 的**所有**描述符,包括为“顶替”保留的那一份。只调用 `shutdown` 不够:在 BSD/macOS 上,对端半关闭之后 `shutdown` 会返回 `ENOTCONN`,什么都不做。开发时就出过这个问题:代理一直收不到结束信号,每次断开都会在远端留下一个卡住的代理进程。现在由 `the_proxy_exits_by_itself_when_its_client_disconnects` 测试防止回归。 +- 隧道和目录遍历属于打开它们的连接,连接断开就结束。本地的 socket 已经不在了,它们留着没有意义。 +- **关闭期间不可达**:常驻进程收到 `server.shutdown` 后,先删除 socket 再回复,之后才去结束进程(最长约 13 秒)。紧接着重连的客户端会启动一个新的常驻进程,它会等旧进程释放锁,而不是直接退出。第一版会连到正在关闭的旧会话,或者新常驻进程拿不到锁就静默退出,用户看到的都是连接失败。 +- 没有客户端的会话会在空闲超时(客户端在握手时指定,默认 10 分钟,最多 24 小时)后结束,避免被遗忘的会话一直占着共享主机。 +- 安全:socket、锁文件和日志都放在只有用户自己能访问的目录里,常驻进程还会校验对端的用户 id。socket 路径有约 104 字节的长度限制,macOS 的临时目录本身就很长,所以超长时退回到 `/tmp/lithe-server-/`,同样要求是私有目录。 +- 并发启动:两个代理同时发现没有常驻进程时,会各启动一个。锁文件(`flock`)决定由谁负责,另一个直接退出。 +- socket 名字里带服务版本号。客户端升级后会启动新的常驻进程,不会连到旧版本;旧的那个空闲超时后自己结束。 + +### 资源 id 由客户端分配 + +进程、终端、监听、隧道、遍历的 id 都由客户端指定,在会话内唯一。这样做有两个原因: + +1. 客户端可以在发请求之前先订阅这个 id 的事件(`RemoteSession::subscribe`),最早的几条输出不会因为请求结果还没返回而被漏掉。 +2. 重连后 id 继续有效。握手结果里的 `nextResourceId` 让重启后的客户端接着编号,不会和仍在运行的资源冲突。 + +所有事件都叫 `.`,并带 `Id`。客户端的读线程据此把事件分发给对应的订阅;没有订阅的事件进入通用通道。 + +### 终端、登录环境和端口 + +- **终端**:用伪终端(PTY)运行,被启动的程序成为新会话的首进程,并把这个终端设为控制终端,和 SSH 登录时完全一样。所以作业控制、`Ctrl-C`、窗口大小变化(内核会发 `SIGWINCH`)都符合用户习惯。默认程序是用户的登录 shell,按 POSIX 惯例在 `argv[0]` 前加 `-`,sh、bash、zsh、fish、csh 都认。关闭终端相当于关掉终端窗口:向它的整个会话发 `SIGHUP`,2 秒后还在的发 `SIGKILL`。 +- **登录环境**:经 SSH 或 `docker exec` 执行的命令,拿到的环境往往很少,nvm、SDKMAN、Homebrew 加进 `PATH` 的路径都不在里面。服务会用用户自己的 shell 以交互式登录 shell 启动一次,让它运行 `lithe-remote-server --print-environment`,读取它最终得到的环境,并在会话内缓存。用服务自己的二进制输出环境,可以避开 GNU 和 BSD `env` 的参数差异;每次尝试最多等 10 秒,profile 卡住(例如启动 tmux)也不会挂住服务;交互式失败就退回非交互登录 shell,再失败就用服务自己的环境,并在 `diagnostics` 里说明原因。`process.spawn` 指定 `environment: login` 就会用这个环境作为基础。 +- **复制**:`fs.copy` 拒绝把文件复制到它自己身上(同一路径、经过符号链接的任一方向、或者是硬链接),也拒绝在解析链接之后把目录复制进它自己。覆盖写入会在读取源文件之前先截断目标,第一版因此会把源文件清空。socket、FIFO 和设备文件从不复制:作为复制对象本身时返回 `UNSUPPORTED`,在目录树里时跳过并计数,因为打开 FIFO 读取可能永远阻塞。 +- **端口**:隧道把远端的 TCP 端口或 Unix socket(Docker、数据库的 socket)接到协议流里。客户端的 `PortForward` 在本地监听,每个连接开一条隧道,所以浏览器访问 `127.0.0.1:<本地端口>` 就能到达只监听在远端回环地址上的服务。本地客户端发完请求后半关闭时(HTTP/1.0、`nc -N`),转发用 `tunnel.shutdownWrite`:先把排队的数据写完,再只关闭写入端,远端的响应照常返回;`tunnel.close` 只用于中止。`ports.listening` 列出远端正在监听的端口,为以后的自动转发做准备。 + +### 后续阶段 + +1. **M0(已完成)**:协议、服务(文件、进程)、传输链、远端安装、会话,以及端到端测试。 +2. **M1(后端已完成)**:目录遍历、文件监听(inotify/FSEvents)、终端(PTY)、登录环境解析、隧道与端口转发、监听端口列表、文件复制/真实路径/权限、常驻会话与断线重连、客户端强类型 API。剩下的是 musl/darwin 产物的构建和打包(Linux 用 `rust-lld` 静态链接,并登记 worktree 资源清单),这一步会改动 macOS 应用的构建流程,单独提交。 +3. **M2**:macOS 应用中的工作区后端接口,把现有的直接调用迁到本地实现,再接入“打开远程文件夹”。这一步的接口设计另写 Note。 +4. **M3**:Git(凭据请求回传到本地,复用现有的 askpass 入口)、运行和构建、端口自动转发(HTML 预览访问转发后的地址)。 +5. **M4**:插件声明运行位置和支持的平台,插件包在本地验签后上传,远端再校验哈希;LSP 和调试适配器在远端运行。 +6. **M5**:Dev Container。按 `devcontainer.json` 规范构建和启动容器时复用官方的 `@devcontainers/cli`,Lithe 只负责用容器跳转连进去。精简镜像里没有 `sh` 时,改用 `docker cp` 上传。Agent 在远端运行。 + +### SSH 连接配置界面(M2 参考) + +M2 接入“打开远程文件夹”时,SSH 连接的配置界面参考用户提供的一个现成 SSH 客户端的连接表单:名称、颜色、主机、端口、用户名、认证方式(密码、私钥、SSH Agent、无认证、自动按顺序尝试)、私钥路径或私钥内容、私钥口令、sudo 凭据、2FA 自动应答,以及收在“显示高级选项”后面的只读、SSH 超时、SSH 保活、环境变量、远程命令等。字段落到核心时按下面的原则处理,具体界面另写 Note: + +- **能直接映射到现有跳转的**:主机、用户名对应 `SshHop.destination`,端口对应 `port`,私钥路径、超时、保活对应 `-o IdentityFile=`、`ConnectTimeout`、`ServerAliveInterval` 这类 `options`。只填了一个 `~/.ssh/config` 里的别名时,其余字段留空,交给系统 OpenSSH。 +- **需要把交互回传到界面的**:密码、私钥口令、2FA 验证码都是 OpenSSH 在连接时提出的问题,应通过 `SSH_ASKPASS` 回传到 Lithe 弹窗(和 M3 Git 凭据共用一个 askpass 入口),不要把密码拼进命令行或环境变量。是否以及怎样保存到钥匙串,另行决定。 +- **私钥内容**:粘贴的私钥不能写进 app bundle 或工作区。保存位置、生命周期和权限要在界面 Note 里单独说明,并优先只支持私钥路径和 SSH Agent。 +- **与远程工作区无关的**:sudo 白名单、终端远程命令、自动应答脚本属于终端类产品的功能,不进第一版远程开发;名称、颜色只属于界面。只读开关是客户端策略,由工作区后端拒绝写操作,不需要服务配合。 +- 表单的说明文字按设置界面的现有约定收进 ⓘ,不平铺在每个字段下面。 + +### 正确做法 + +- 远端需要一个新能力(例如文件监听)时,在协议里加方法,在服务里实现,在 `features` 里声明;Linux 和 macOS 的差异写在 `platform/`。 +- 客户端需要区分平台行为时,读能力描述。例如比较路径时看 `caseSensitive`,不看 `os`。 +- 新增一种连接方式(例如 Kubernetes `kubectl exec`)时,只加一种跳转,不碰服务。 + +### 不要这样做 + +- 不要在功能代码里拼 `ssh host 'mvn package'`。所有远端执行都走 `process.spawn`。 +- 不要为了“远程 Mac”在客户端写 `if os == macos`。缺少的事实应该补进能力描述。 +- 不要让服务监听 TCP 端口,也不要把服务装到其他用户可写的目录。 +- 不要在 SSH 跳转后面传入用户提供的任意字符串(例如带 `'` 的路径)。把它放进协议请求里,不要放在命令行里。 + +## 考虑过的备选方案 + +### 把远程文件系统挂载到本地(SSHFS / SFTP) + +最吸引人的地方是现有代码几乎不用改,把远程目录当成本地目录用就行。不采用的原因是:LSP 索引、全文搜索、Maven/Gradle 构建都要大量读小文件,走网络会慢到不可用;而且构建和运行会发生在本地,而不是项目所在的机器上,在 Docker 和异构架构下结果甚至是错的。 + +### 远端跑整个 IDE,本地只显示画面(JetBrains Gateway 模式) + +最吸引人的地方是远端和本地行为天然一致。不采用的原因是:Lithe 的界面是原生 SwiftUI/AppKit,没法拆成远端渲染、本地显示;远端也不一定有图形环境。 + +### 内置 SSH 库(如 russh),不依赖系统 ssh + +最吸引人的地方是可以完全控制连接复用和认证流程,Windows 上也不受 OpenSSH 版本限制。不采用的原因是:用户的 SSH 配置、ProxyJump、硬件密钥、各种 agent 和公司的 Kerberos/证书方案都要重新实现,“无缝兼容”反而做不到;系统 OpenSSH 是成熟的上游工具,按仓库规则应该优先复用。将来确实需要时,可以把它作为另一种跳转加入,不影响服务和协议。 + +### 让远端自己下载服务 + +最吸引人的地方是本地不用上传几 MB 的文件。不采用的原因是:大量开发机和容器不能访问外网,而且远端下载需要 `curl` 或 `wget`、证书链和代理配置,失败面更大。本地上传对所有链都成立。 + +### 每个功能各自发 SSH 命令,不做远端服务 + +最吸引人的地方是不用维护远端二进制。不采用的原因是:每次操作都要新建 SSH 会话,延迟很高;进程清理、输出顺序、原子写入都没法保证;Docker 和 Mac 也要各写一份。 + +### 服务用 tokio 异步运行时 + +最吸引人的地方是 PTY、隧道这类大量并发 I/O 写起来更顺。没有采用的原因是:加上终端和隧道之后,标准库线程加有界通道依然够用,每个终端或隧道只多两三个线程;musl 静态产物更小,背压和顺序也更好推理。只有在实测出线程数成为瓶颈(例如同时转发成百上千个连接)时才重新评估,协议本身不受影响。 + +### 资源 id 由服务分配 + +最吸引人的地方是客户端不用管 id 冲突。没有采用的原因是:客户端只有在请求返回之后才知道 id,而这时进程的第一批输出可能已经到了,只能先缓存所有“未认领”的事件;重连后,重启过的客户端也没法和仍在运行的资源对上号。由客户端分配 id,再配合 `nextResourceId` 和 `RESOURCE_ID_IN_USE`,这两个问题都没有了。 + +### 用 `notify` crate 做文件监听 + +最吸引人的地方是成熟、跨平台。没有采用的原因是:它会带来一串依赖(FSEvents 绑定、crossbeam 等),而服务需要的只是“递归监听 + 排除目录 + 合并批次”这一小部分;更关键的是,FSEvents 的累积标志和 inotify 新目录补报这两个语义问题,无论用不用它都要自己处理。直接调用系统接口,代码量相近,体积更小,行为完全可控。 + +### 断线后靠客户端重新执行,而不是常驻会话 + +最吸引人的地方是服务可以保持无状态。没有采用的原因是:构建、测试、长时间运行的服务、终端里的交互程序都无法“重新执行”而不丢状态;Wi-Fi 切换或笔记本合盖是远程开发里最常见的情况,用户期望回来时一切还在。 + +## 验收标准 + +- `cargo test -p lithe-remote-protocol -p lithe-remote-client -p lithe-remote-server` 在 macOS 和 Linux 上通过。端到端测试经本机传输启动真实的服务二进制,按功能分成几个测试文件: + - 代码审查发现的问题都配了回归测试,并通过“临时撤掉修复、确认测试失败”验证过:复制到自身、半关闭请求的响应、断线后不丢不重的续传、空会话及时清理。常驻进程关闭期间重连的那个测试,在修复之前就失败过。 + - `tests/server.rs`:握手和能力描述、未握手前的请求和协议版本不匹配、原子写入和冲突检测、并发创建同一文件只有一个成功、断开时正在进行的写入完成且不留临时文件、对端停止读取时请求按时超时、分段读取大文件、穿过符号链接写入、目录操作的错误码、复制时保留链接和权限并拒绝复制到自身内部、复制文件到它自己(含链接和硬链接)被拒绝且文件完好、复制时跳过特殊文件、登录横幅被忽略、服务缺失时返回诊断信息、上传一次之后复用已校验的二进制。 + - `tests/processes.rs`:输出先于退出事件、标准输入转发、整个会话的进程一起结束、首进程退出后 kill 和断线清理仍能结束会话成员、正在使用的 id 不能复用、`--stdio` 会话断开后子进程被清理、登录环境包含 profile 导出的变量。 + - `tests/terminals.rs`:命令运行在控制终端上、调整窗口大小后程序看到的尺寸跟着变、关闭终端会挂断 shell 和它的后台作业。 + - `tests/watch_and_walk.rs`:新建、修改、删除都能报告;一次建出的整棵目录树完整报告,新目录也会被继续监听;排除的目录不报告;监听的根目录被删除时监听结束;遍历按名字顺序、不跟随链接、排除目录、限制条数。 + - `tests/tunnels.rs`:通过隧道访问 TCP 端口和 Unix socket、连接已关闭的端口返回稳定的错误码、本地端口转发的完整链路、半关闭的请求仍能收到响应、能列出本地监听的端口。 + - 单元测试:覆盖受限文件时临时文件不放宽权限(`fs.rs`),多个 SSH 跳转嵌套时参数原样到达 sh、bash、zsh、csh、fish(`transport.rs`,本机装了的都会测)。 + - `tests/persistence.rs`:断线期间进程继续运行且输出被重放、断线后续传时每个事件恰好到达一次、重连后先订阅再续传能接上终端并收到断开期间的输出、旧常驻进程关闭期间重连会得到新会话、空会话不需要额外请求就会被清理、空闲会话超时结束、两个代理并发启动时只有一个常驻进程、已订阅的事件不会进入通用通道。 +- 服务能检查通过所有远端目标(Linux musl 两种架构、macOS 两种架构);两个 Linux 静态 release 版用 `rust-lld` 构建成功,并且都不超过 1.5 MiB 的体积上限。 +- Linux CI lane(`.github/workflows/ci-remote.yml`)只在远程相关路径变化时运行,不占用 macOS 和 Windows runner。 +- 后续阶段的验收标准在各自的 PR 和 Note 中补充,例如 M1 要在真实的 SSH 主机、Docker 容器和远程 Mac 上各验证一次完整流程。 + +## 风险 + +- **macOS 远端的孤儿进程**:服务被 `SIGKILL` 时,macOS 上的子进程会继续运行。能力描述里如实报告了这一点(`serverShutdown`),实现会话保持时再补 kqueue 守护者。 +- **非 UTF-8 文件名**:这一版协议无法表达非 UTF-8 文件名,列目录时会计数并跳过,不会静默丢弃。如果用户确实遇到,再加一种字节编码的名字字段。 +- **环境变量**:登录环境靠运行用户自己的 shell 得到。profile 写得很特别时(比如要求输入、或者每次都会执行 `exec tmux`),会退回非交互登录 shell 或服务自己的环境,并在 `diagnostics` 里说明。界面接入时需要把这个说明展示给用户。 +- **只有 `sh` 的假设**:探测和上传依赖远端有 POSIX `sh`。distroless 这类精简镜像需要 M5 的 `docker cp` 路径。 +- **一个会话只有一个客户端**:新连接会顶替旧连接。如果以后同一个工作区要同时开两个窗口,客户端需要用不同的 key,或者在协议里加入多客户端。 +- **保留的事件有上限**:断开期间输出很多的构建,超过 8 MiB 未确认的部分会丢失,`server.resume` 会返回 `truncated`。如果用户需要完整日志,应该让构建写日志文件,而不是依赖事件流。 +- **确认的是“交给客户端库”**:客户端库把事件分发到订阅通道时就算收到,`last_event_seq()` 也按这个计数。应用丢弃了还没读完的句柄,那部分事件不会再补发;需要更精细控制时,按每个事件自带的 `seq` 决定从哪里续传。 +- **按会话结束进程时的 pid 复用**:信号按会话 id(等于首进程的 pid)发送。会话还有成员时,这个编号不会被复用;会话一空就必须尽快忘掉它。服务每 500 毫秒检查一次只为剩余成员保留的会话,`kill` 和关闭终端在发信号前也会再检查一次。剩下的风险只在“会话刚空、检查还没跑”的这几百毫秒里,而且还需要恰好在这段时间里分配到同一个 pid。检查一次扫描整张进程表,判断所有待查的会话。 +- **测试会碰到用户自己的配置**:交互式 shell 会读写用户真实的 `~/.bash_history`。开发时就发生过一次:终端测试把测试命令写进了开发者的历史记录,之后已经清理。现在所有测试终端都设置 `HISTFILE=/dev/null`;登录环境测试使用临时的 `HOME` 和固定的 `/bin/sh`。以后新增的测试也必须这样隔离。 +- **回退方式**:三个 crate 目前没有被产品代码引用。如果方向需要调整,可以整体删除,不影响现有功能。 + +## 适用范围 + +- `rust/lithe-remote-protocol/` +- `rust/lithe-remote-server/` +- `rust/lithe-remote-client/` +- `shared/contracts/remote-workspace-protocol-v1.md` +- `.github/workflows/ci-remote.yml` diff --git a/.agents/skills/develop-lithe/SKILL.md b/.agents/skills/develop-lithe/SKILL.md index 4fe60eca0f..80353f17a0 100644 --- a/.agents/skills/develop-lithe/SKILL.md +++ b/.agents/skills/develop-lithe/SKILL.md @@ -80,7 +80,6 @@ individual upstream commands were called. | `Plugins/win/` | Windows-owned plugin packages | | `frontend/editor/` | Shared Monaco presentation, tokenization, and editor model lifecycle; no platform APIs | | `shared/` | Cross-platform contracts and fixtures, not compiled implementation | -| `sdk/` | Official plugin backend SDKs and their samples; they implement `shared/contracts/plugins/` and never define protocol | | `infra/` | Repository-level development and validation infrastructure | | `third_party/` | Upstream code; leave unchanged unless the task explicitly targets it | @@ -243,8 +242,8 @@ the existing stack can reasonably avoid. bounds in `LitheDropdownMetrics`; Project measures content width and Branch uses the Community New UI 375pt baseline. Do not copy the top-bar Project switcher's previous fixed width or create another search-field style: - searchable popup inputs use `LitheSearchTextField` + `litheSearchField` in - `macos/Sources/Lithe/Theme/LitheTheme.swift`, as Git Log does. + searchable popup inputs use the complete `LitheSearchField` owner in + `macos/Sources/Lithe/Views/Components/LitheSearchField.swift`, as Git Log does. Width is role-specific, not globally equal: action menus measure localized titles, icons, shortcuts and checks through the presenter; settings selectors retain their shared trigger/content measurement. Do not add per-caller magic @@ -269,11 +268,20 @@ the existing stack can reasonably avoid. System dialogs, the macOS application menu, editor completion/caret popups and hover documentation are separate interactions; this rule does not replace them with product dropdowns. -- For search inputs use `LitheSearchTextField` and `litheSearchField`; preserve - native IME composition and the I-beam cursor before focus. Do not patch each - feature's placeholder, border or hover cursor separately. - Their owner is `macos/Sources/Lithe/Theme/LitheTheme.swift`; read its metrics - rather than copying height, inset, radius, font, placeholder or focus colors. +- Search controls use the complete `LitheSearchField` owner, including external + boolean/enum focus and semantic accessories. Its internal native editor and + chrome reuse `LitheSearchTextField` / `litheSearchField`; ordinary callers must + not assemble that recipe. Multiline agent task composition remains a distinct + editor using the native editing primitive with `axis: .vertical`. +- Ordinary shared single-line/secure and multiline form inputs use `LitheTextField` + and `LitheTextEditor` in `Views/Components/LitheTextInput.swift`. They own native + editing, focus, cursor, chrome and optional labels; `LitheFormField` owns label + spacing and input-only disabled state. The old `litheSettingsTextField` / + `litheSettingsTextEditor` recipes and `LitheSettingsSearchField` entry are retired. + Existing specialized table/editor/commit-dialog renderers retain their owners. + `LitheInfoTip` remains interactive even inside a disabled field or section. + Preserve native IME composition and the I-beam cursor before focus. Read shared + metrics rather than copying height, inset, radius, font, placeholder or focus colors. A binding that is still empty during marked text must not restore the prompt. Multiline commit text keeps `CommitMessageEditor` in `macos/Sources/Lithe/Views/Git/CommitMessageEditor.swift`; do not substitute a @@ -289,33 +297,40 @@ the existing stack can reasonably avoid. feedback and default arrow cursor. Its 22pt hit area is not a rule for every button: main-toolbar switches and other existing controls retain their own shared metrics. Pointer and I-beam behavior are explicit control semantics. -- Tree rows use `litheTreeRow` and `LitheTheme.Tree` in `LitheTheme.swift` for - the existing single-line tree style: font, row height, hover and focused versus - inactive selection. Read shared indentation/icon metrics; do not add local - row padding, selection colors or per-row separator lines. Preserve disclosure, - multiselect, context actions and accessibility. Native trees keep their - existing renderer and consume the same tokens; do not replace them with - SwiftUI rows. Multiline project-menu entries, tables and commit graph rows - keep their own existing shared renderer/metrics rather than using tree rows. -- Persistent tree/list selection uses `litheSelectionRow(isSelected:isFocused:)` - in `LitheTheme.swift`; `litheTreeRow` delegates to it. The shared renderer - paints `LitheTheme.Tree.focusedSelection` only when the owning region is - focused **and** `controlActiveState == .key`; otherwise it paints - `Tree.inactiveSelection`. Multiline lists keep their geometry and use this - renderer directly. Never paint persistent selection with fixed `selection`, - `subtleSelection`, `settingsSelection`, accent opacity, or local RGB values. -- Observe focus at the selected list/tree region with the existing - `LitheToolWindowActivityTracker`, not at the entire workbench; neighboring - lists and detail editors must not activate each other's selections. Project - uses `ProjectTreeKeyboardCommands`' ownership callback because its file-open - flow can asynchronously focus WebKit. Include focus in custom `Equatable` - comparisons. Native renderers keep the same theme tokens, check key-window - and responder/region ownership, and invalidate drawing when window focus - changes even if selection/data did not change. +- Ordinary tool-window title bars use `LitheToolWindowHeader`. It owns the current + 41pt height, 13pt Bold title, outer insets and action spacing. Panel titles are + text-only: no title icon, outline or bottom divider, and no appended + `LitheToolWindowHeaderDivider`. A real session-tab + bar keeps its existing tab semantics; do not imitate tabs for a static title. +- Ordinary trees and persistent lists use the complete `LitheTreeList` owner in + `macos/Sources/Lithe/Views/Components/LitheTreeList.swift`. Callers provide stable + row IDs, selection, hierarchy and business actions. The owner installs gutters, + scrolling, focus observation/effect suppression, hover/selection rendering, + keyboard navigation, range selection and accessibility. Recursive business trees + may provide `LitheTreeListNode`; hidden children must not be materialized. + Do not recreate a ScrollView/row-style/focus-modifier recipe at the call site. + The former `litheTreeRow` and `litheSelectionRow` extensions are retired; + `LitheTreeListRowSurface` is internal rendering, never a business entry point. +- Native viewport renderers use `LitheTreeList(native:)` and the same + `LitheTreeListAppearance` / `LitheTreeListNavigation` policy. Keep their renderer + and virtualization; do not replace native trees with one SwiftUI view per row. + Project's `LitheTreeListNativeInteraction` retains drag/clipboard and asynchronous + editor-focus ownership, while using shared selection/navigation decisions. + Preserve the existing configurable Project row height. Other rows use Tree + metrics; multiline lists retain their content geometry. +- Selection changes and primary click/activation are separate actions. Moving the + keyboard cursor must not accidentally activate an editor or run a task. + Search Everywhere retains its search-input-driven focus and category shortcuts; + tables, commit graphs, menus and editor completion retain their own owners. + Native list focus comes from the first responder and key window, not a page's + manually supplied boolean. Verify neighboring fields, inactive windows, dark/light + rendering, multiselect, keyboard bounds, context targets, expansion, and scrolling. + Run `LitheTreeListTests`, `LitheTreeRowStyleTests`, the affected native regressions + and the real product workflow. The source coverage test rejects retired recipes. - Keep selection distinct from checkbox inclusion, chosen settings values, status badges, activity-rail buttons, menu highlight, tool-window tabs and editor text selection. Those retain their owning styles; tool-window tabs - use `LitheToolWindowTabStyle`, tables use `DatabaseGrid` tokens. Do not add + use `LitheToolWindowTabStyle`. Do not add persistent selection to a view that does not already have it. Verify dark/light focused/inactive rows, focus moving to a neighboring list or editor, whole window deactivation and reactivation, keyboard navigation, and shared field @@ -381,7 +396,7 @@ The decision and owning components are recorded in #### Rust Core comments Apply the following comment standard to first-party code under -`rust/lithe-core/`. It does not require comment coverage in the database helpers, +`rust/lithe-core/`. It does not require comment coverage in other helper crates, Windows/Tauri Rust crates, generated code, or third-party sources. - Write comments in English and keep them accurate when behavior changes. diff --git a/.agents/skills/submit-lithe-issue/SKILL.md b/.agents/skills/submit-lithe-issue/SKILL.md index d03a30d871..9e4702b530 100644 --- a/.agents/skills/submit-lithe-issue/SKILL.md +++ b/.agents/skills/submit-lithe-issue/SKILL.md @@ -3,7 +3,8 @@ name: submit-lithe-issue description: >- 用户要求为 Lithe 报告问题、提出功能建议、整理 Issue 草稿或提交 GitHub Issue 时使用。 根据用户描述追问缺失信息,采集并脱敏必要诊断数据,先查重和初步诊断, - 再严格遵循仓库最新 Issue 模板及 #394 提交须知;禁止用自由正文、API 或表单标记标签绕过 gh 模板。 + 再严格遵循仓库最新 Issue 模板及 #394 提交须知;gh 不支持 YAML 表单时,按表单字段逐项 + 生成正文用 gh 提交,禁止空白正文、API 直建或手工添加表单来源标签。 --- # 提交 Lithe Issue @@ -36,8 +37,8 @@ description: >- 有相关补充时再读取评论。 2. 目标仓库默认分支的 `.github/ISSUE_TEMPLATE/config.yml`,以及将使用的 Issue 模板全文。先通过 `gh api` 确认默认分支,不把本地开发分支当作线上规则。 -3. 仓库现有标签和 `gh` 实际可用的原生模板。具体命令与兼容性规则见 - [gh 模板提交规则](references/gh-template-submission.md)。 +3. 仓库现有标签和 `gh` 实际可用的原生模板。具体命令、表单正文格式与兼容性 + 规则见 [gh 模板提交规则](references/gh-template-submission.md)。 默认目标为 `1lck/Lithe-IDEA`;用户指定其他仓库时以用户指示为准。请求前确认 目标身份和仓库重定向,所有 GitHub 命令都显式指定目标仓库。 @@ -161,8 +162,9 @@ Bug 至少需要以下实质信息,即使某些字段在 YAML 中是可选的 - 保留模板标题前缀,用“触发条件 + 具体异常/诉求”写完整标题,避免“有 Bug”、 “优化一下”等无信息标题。 -- 按 `body` 顺序保留所有输入字段;Markdown 草稿使用 `### ` - 对应当前字段原名,不翻译、合并、改名、删节或另起一套顶层结构。静态说明不 +- 按 `body` 顺序保留所有输入字段;正文使用 `### ` 对应当前 + 字段原名,逐字复制,不翻译、合并、改名、删节或另起一套顶层结构。gh 提交时 + 这份正文就是表单内容,Bot 依据这些标题识别必填字段并打分派标签。静态说明不 冒充用户回答;证据、推测和关联链接放在模板已有的适当字段中。 - 下拉框只使用模板允许的准确选项;复选框逐条保留原文,只勾选已完成事项。 所有必填字段和必选检查项均须真实完成,不能用“待补充”“未知”或占位符充数。 @@ -193,7 +195,7 @@ Bug 模板没有跨平台选项时,按实际复现平台填写并在对应字 - **已提交**:给出真实 Issue 链接、所用模板、关键环境与证据、重要限制。 - **已有重复**:给出原 Issue 链接及可补充内容,不谎称创建了新报告。 - **仅草稿/等待补充**:给出完整草稿、缺失字段和具体待回答的问题。 -- **gh 不支持当前表单**:给出按字段整理好的脱敏草稿和带 `template=` 的官方 - 表单链接,明确“尚未提交”,说明无法继续的具体模板能力限制。 +- **无法用 gh 提交**(没有登录态、没有创建权限或网络受限):给出按字段整理好的 + 脱敏正文和带 `template=` 的官方表单链接,明确“尚未提交”及具体原因。 不能把打开页面、生成草稿、上传附件或命令部分成功描述为“已完整提交”。 diff --git a/.agents/skills/submit-lithe-issue/references/gh-template-submission.md b/.agents/skills/submit-lithe-issue/references/gh-template-submission.md index 061389bf22..afe636a096 100644 --- a/.agents/skills/submit-lithe-issue/references/gh-template-submission.md +++ b/.agents/skills/submit-lithe-issue/references/gh-template-submission.md @@ -1,7 +1,8 @@ # gh 模板提交规则 -此文件是 `submit-lithe-issue` 的强制提交规则。必须同时满足“正文严格符合当前 -模板”和“通过真实模板入口创建”,仅把正文写得像模板不算通过。 +此文件是 `submit-lithe-issue` 的强制提交规则。无论走哪条路径,提交的正文都必须 +严格符合当前模板:字段标题、顺序、下拉选项和检查项与线上表单一致,必填字段 +真实填写。gh 能原生选择模板时走路径一,不能时走路径二。 ## 读取线上模板与 gh 能力 @@ -50,47 +51,66 @@ gh issue create --repo 1lck/Lithe-IDEA \ 标题前缀来自选定模板,功能建议不能照用 `[Bug]`。需要标签时仅使用已核实的 类型、平台、模块和优先级标签;模板来源标记必须由真实模板流程产生。 -没有 TTY、编辑器或可操作的模板交互时,交付草稿及具体限制。不能为了无交互 -执行而退回自由正文命令,也不能把 `yes`、空答案或脚本伪装成用户完成检查。 +没有 TTY、编辑器或可操作的模板交互时,按路径二的正文格式生成完整正文后用 +`--body-file` 提交;不能退回不符合模板的自由正文,也不能把 `yes`、空答案或脚本 +伪装成用户完成检查。 -## 路径二:gh 不支持仓库当前表单 +## 路径二:gh 不支持仓库当前表单,按表单字段提交正文 -本 Skill 创建时实测(2026-10-17,`gh 2.102.0`): +GitHub GraphQL `repository.issueTemplates` 只返回 Markdown 模板(`.md`),不返回 +YAML Issue Forms(`.yml`/`.yaml`)。`gh` 的 `--template` 依赖这个接口,所以仓库只有 +YAML 表单时,`gh` 列不出任何模板,反复重试 `--template` 也没有用。这是 gh 的已知 +限制([cli/cli#5865](https://github.com/cli/cli/issues/5865),自 2022-06 未解决; +[cli/cli#11681](https://github.com/cli/cli/issues/11681) 维护者确认只支持 Markdown +模板),不是仓库配置错误。2026-10-24 对比 facebook/react、rust-lang/rust、golang/go +时规律一致:同目录下的 `.md` 被返回,`.yml`/`.yaml` 全部缺席。 -- 仓库使用 `bug_report.yml` 与 `feature_request.yml` 两种 YAML Issue Forms, - `blank_issues_enabled: false`。 -- GitHub GraphQL `repository.issueTemplates` 返回空列表,当前 `gh` 无法从中 - 选择这两种表单。不能通过反复重试 `--template` 解决。 -- `gh issue create` 明确禁止 `--template` 与 `--body` 或 `--body-file` 同时使用。 - 删掉 `--template` 再传完整正文会绕过原生模板流程,即使章节看起来一致也禁止。 +仓库的 Issue 门禁(见 `.agents/notes/implemented/process/2026-10-01-issue-form-enforcement.md`) +接受两种表单证明:`issue-form:*` 标签,或正文包含某个表单全部必填字段的 +`### 字段名`。按表单逐字段生成正文,维护者拿到的信息与网页表单相同,Bot 也会 +按正文里的优先级、平台、模块和标题前缀自动打分派标签。因此原生模板不可用时, +用 `gh issue create --body-file` 提交按表单生成的正文。 -这些是当时的能力快照,后续任务必须重新检测,不能把它当成永久限制。 +### 正文格式 -在不支持时,完成查重、追问、采集、脱敏和逐字段草稿后,交付选定模板的 -GitHub 官方表单入口,由用户在该表单中填写并完成提交: +完全模仿 GitHub 渲染表单提交结果的格式,按选定模板 `body` 中输入字段的顺序生成: -- [Bug report / 问题报告](https://github.com/1lck/Lithe-IDEA/issues/new?template=bug_report.yml) -- [Feature request / 功能建议](https://github.com/1lck/Lithe-IDEA/issues/new?template=feature_request.yml) +- 每个 `input`、`textarea`、`dropdown`、`checkboxes` 字段对应一段 + `### `,标题逐字复制线上模板,包括中英双语和斜杠、空格。 + `markdown` 类型的静态说明不写入正文。 +- `dropdown` 只写一个模板 `options` 中逐字存在的选项;多选下拉用逗号分隔。 +- `checkboxes` 逐条保留原文,写成 `- [x]