From 370ff35d1d671511ae66297dd84ae834b3a295ce Mon Sep 17 00:00:00 2001 From: "frank.wang" Date: Thu, 29 Oct 2026 20:10:20 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(windows):=20=E6=96=87=E4=BB=B6=E6=A0=91?= =?UTF-8?q?=E5=8D=95=E5=87=BB=E4=BB=85=E9=80=89=E4=B8=AD=E3=80=81=E5=8F=8C?= =?UTF-8?q?=E5=87=BB=E6=89=93=E5=BC=80=EF=BC=8C=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20Delete=20=E9=94=AE=E5=88=A0=E9=99=A4=EF=BC=88=E5=A7=8B?= =?UTF-8?q?=E7=BB=88=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 单击文件只选中并高亮,不再打开或新建预览页签;双击或 Enter 才打开,与单击目录行为一致 - 选中后按 Delete 弹出删除确认框,多选时一次确认并逐项删除; 即使“删除前确认”设置关闭也始终确认,工作区根目录与带修饰键的 Delete 不触发 - 补充选择与 Delete 键的组件测试,并更新跨平台功能矩阵说明 --- .../features/file-explorer.json | 4 +- .../file-explorer-selection.test.tsx | 86 ++++++++++++++++++- .../components/file-explorer-tree.tsx | 36 +++++--- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/shared/platform-feature-matrix/features/file-explorer.json b/shared/platform-feature-matrix/features/file-explorer.json index ada77eef2..e1be3093a 100644 --- a/shared/platform-feature-matrix/features/file-explorer.json +++ b/shared/platform-feature-matrix/features/file-explorer.json @@ -56,6 +56,6 @@ "verificationStatus": "pending" }, "owner": "Workspace", - "verification": "浏览目录、展开/折叠、打开资源并在文件变更后刷新树。 macOS 文件树、标签页与面包屑共用 TOML 文件图标:亮色使用 IDEA New UI 官方浅底蓝色 T,深色使用深底蓝色 T;检查明暗主题切换后选择对应 SVG,保留原始 16pt 几何与颜色。 点击目录名称后上下键按可见行移动选择,右键展开目录并再次右键进入首个子项,左键折叠或返回显示父项;双击目录名称展开/折叠,单击与修饰键多选保持独立。验证压缩 Java 包、列表首尾、Shift 上下范围选择与自动滚动;方向键不送到编辑器,点击编辑器或转移到搜索输入框后恢复其按键处理。 Windows Project 文件树滚轮与 Git Log 共用 Community Windows 默认 200ms 惯性过渡;核对连续同向、反向、Shift 横向滚动、顶底边界、键盘定位与滑块拖动取消惯性、卸载帧清理和减少动态效果偏好;虚拟树保持可见区呈现,真实 WebView2 实机验收 pending。 macOS 项目文件和目录选中行共用 LitheTheme.Tree 的主题聚焦色与灰色失焦色;点击编辑器、其他工具窗口后失焦,键盘转移到搜索输入框尚未输入时失焦,重新点击树恢复,整个窗口失焦时保持灰色。深浅主题、非默认主题、多选、上下键和右键菜单均需核对;组件像素和焦点回归不能替代完整应用验收。 Windows 在默认及舒适密度下比较 Project 与 Dependencies 树正文,再修改界面字号确认同步;深浅主题检查刷新图标为循环箭头,点击仍刷新目录。 Windows 打开深层文件并折叠祖先,点击定位当前文件,检查展开后滚动到目标行;滚动离开后重复点击同一文件仍定位;编辑器切换页签不得让树自动滚动。延迟目录加载完成后再滚动,旧请求完成不改变新位置;树内搜索和 Dependencies 视图下定位应回到完整 Project 树。 Windows 搜索首次命中和 Enter 切换后复制/剪切应使用当前命中;同时选择父目录、后代与其他文件后右键后代,复制/删除仍处理整个选择且父子只操作一次。原生移动等待期间关闭 Project 或切换工作区,成功项同步原工作区并停止后续项。受控 WSL/远程/POSIX 路径用例保留大小写不同的文件与目录,Windows 本地/普通 UNC 同名目标继续拒绝覆盖;这些用例不代表已开放 WSL/远程功能,真实 WebView2 工作流仍 pending。 Windows 编辑器保持 README 打开时点击目录名称,通过真实 FileExplorerPane、侧栏与缓冲区 store 验证路径同步往返后目录仍被选中,复制/剪切、键盘焦点和 Shift 范围锚点仍属于目录;实际切换编辑文件时单选跟随,明确多选保留。 Windows 批量拖动分别在目录预检、原生移动和原工作区同步等待期间切换 A→B→A,确认旧批次不再派发剩余项,已完成项仍同步原工作区;普通重渲染继续当前批次,旧移动失败不清除新拖动或显示过期错误。 macOS 打开含 .claude/worktrees 等嵌套 Git linked worktree 与 .artifacts 的仓库:项目树、文件列表与搜索索引不包含这些目录,子模块和位于主检出之外的工作树仍显示;嵌套工作树内的文件变化不触发刷新。外部新增/删除普通文件时树原地增删、不重新扫描整个工作区,新目录、压缩包结构变化时回退完整快照;定位当前文件按路径逐级查找,侧栏渲染不再遍历全部文件。 macOS 项目树按需列目录:快照只返回文件清单,目录展开时才列出下一层;检查首次打开大仓库只显示根层,展开、折叠后再展开、源码根下压缩 Java 包与全量构建一致;外部改动未展开目录下的文件不改变树,展开后可见;重新扫描后已展开目录保持展开并显示最新内容;定位当前文件会逐级加载祖先后滚动到目标行;新增或删除文件后先展开目录再处理监听事件,确认搜索等文件库存仍同步;压缩包重列期间发生其它文件增删,完成后库存和树都保留最新结果。 macOS 项目树同级排序对齐 IDEA 项目视图:目录先于文件,名称按自然顺序(数字按数值、忽略大小写、'-' 与 '_' 相邻);打开含 docs/releases/v0.1.4.md、v0.1.12.md、v0.1.14.md 的仓库,确认 v0.1.4 在 v0.1.12 之前,外部新建 v0.1.5.md 后原地插入到二者之间,与重新扫描的顺序一致。 外部构建产生超过 512 条 target/build 事件且混有源码变化时,只转发有效事件,不触发输出洪峰导致的整项目重扫;显式文件/生成源码监听、项目根名为 build、跨排除边界重命名均保留,系统丢失事件始终触发恢复重扫。 Windows 同时粘贴同名文件不覆盖彼此;剪切期间再次复制相同或不同内容,旧粘贴完成不得清除新剪贴板。", - "notes": "Windows 补齐 Ctrl/Shift 多选、批量复制/剪切/删除、已选集合拖动、Project/Dependencies 切换、定位当前文件、刷新和隐藏。目录名称单击选择,双击或箭头展开;长名称水平滚动。目录标记按工作区持久化,并随移动、重命名和删除迁移;本地 Windows/UNC 按不区分大小写处理,POSIX、WSL 和远程标识保留大小写并隔离工作区。批量移动预检父子重复及重名目标,原生发布拒绝覆盖;真实 WebView2 的拖动与焦点验收 pending。 Windows 树正文统一使用 ui-text-sm(默认 13px),随界面字号设置变化,舒适密度不单独放大 Dependencies 等树行;Project 刷新按钮使用 IDEA refresh.svg。手动定位使用有代次的展开后滚动请求;编辑器切换页签不再自动跟随滚动(autoRevealActiveFileInFileTree 设置已退役);定位清除树内搜索并切回 Project,保持虚拟列表。 搜索导航同步选择与锚点,右键成员判断保留原始选择集合,批量目标再去除父子重复;已完成移动不因面板卸载或工作区切换而漏掉原工作区同步。路径去重与拖动预检按文件系统大小写语义比较,包含 WSL UNC 别名。编辑器活动文件变化时单选同步高亮、复制/剪切目标及范围锚点,明确多选保留;使用同步 Hook 的真实编辑器目标判断切换,选择目录引发的共享侧栏路径往返不改变选择;真实面板与 store 回归覆盖此链路。 批量移动按每次工作区访问识别生命周期,相同根路径的再次访问不能复活旧批次;预检、移动、同步和失败回调共用该守卫。 macOS 快照跳过嵌套在自身仓库中的 linked worktree(.git 文件指向 <主仓库>/.git/worktrees/*)并内置隐藏 .artifacts;项目树由 workspace.listDirectories 按需逐层列出,children 为 nil 表示目录未加载;平铺文件清单 projectFiles 仍完整保留供搜索等功能使用。 macOS 项目树同级排序由 Rust Core 的 visible_children 与 Swift 的 NaturalNameOrder 共同实现 IDEA FileNameComparator(共享 fixture file-name-order-v1.json);快照 files 与搜索索引按 ChildOrder::Traversal 保持原有的小写排序(Windows 快速打开取前 100 项依赖它),只有 workspace.listDirectories 使用自然顺序。Windows 项目文件树仍使用 localeCompare,Git 文件树已使用同源的 compareGitFileNames,Windows 项目树对齐待后续处理。" + "verification": "浏览目录、展开/折叠、打开资源并在文件变更后刷新树。 macOS 文件树、标签页与面包屑共用 TOML 文件图标:亮色使用 IDEA New UI 官方浅底蓝色 T,深色使用深底蓝色 T;检查明暗主题切换后选择对应 SVG,保留原始 16pt 几何与颜色。 点击目录名称后上下键按可见行移动选择,右键展开目录并再次右键进入首个子项,左键折叠或返回显示父项;双击目录名称展开/折叠,单击与修饰键多选保持独立。验证压缩 Java 包、列表首尾、Shift 上下范围选择与自动滚动;方向键不送到编辑器,点击编辑器或转移到搜索输入框后恢复其按键处理。 Windows Project 文件树滚轮与 Git Log 共用 Community Windows 默认 200ms 惯性过渡;核对连续同向、反向、Shift 横向滚动、顶底边界、键盘定位与滑块拖动取消惯性、卸载帧清理和减少动态效果偏好;虚拟树保持可见区呈现,真实 WebView2 实机验收 pending。 macOS 项目文件和目录选中行共用 LitheTheme.Tree 的主题聚焦色与灰色失焦色;点击编辑器、其他工具窗口后失焦,键盘转移到搜索输入框尚未输入时失焦,重新点击树恢复,整个窗口失焦时保持灰色。深浅主题、非默认主题、多选、上下键和右键菜单均需核对;组件像素和焦点回归不能替代完整应用验收。 Windows 在默认及舒适密度下比较 Project 与 Dependencies 树正文,再修改界面字号确认同步;深浅主题检查刷新图标为循环箭头,点击仍刷新目录。 Windows 打开深层文件并折叠祖先,点击定位当前文件,检查展开后滚动到目标行;滚动离开后重复点击同一文件仍定位;编辑器切换页签不得让树自动滚动。延迟目录加载完成后再滚动,旧请求完成不改变新位置;树内搜索和 Dependencies 视图下定位应回到完整 Project 树。 Windows 搜索首次命中和 Enter 切换后复制/剪切应使用当前命中;同时选择父目录、后代与其他文件后右键后代,复制/删除仍处理整个选择且父子只操作一次。原生移动等待期间关闭 Project 或切换工作区,成功项同步原工作区并停止后续项。受控 WSL/远程/POSIX 路径用例保留大小写不同的文件与目录,Windows 本地/普通 UNC 同名目标继续拒绝覆盖;这些用例不代表已开放 WSL/远程功能,真实 WebView2 工作流仍 pending。 Windows 编辑器保持 README 打开时点击目录名称,通过真实 FileExplorerPane、侧栏与缓冲区 store 验证路径同步往返后目录仍被选中,复制/剪切、键盘焦点和 Shift 范围锚点仍属于目录;实际切换编辑文件时单选跟随,明确多选保留。 Windows 批量拖动分别在目录预检、原生移动和原工作区同步等待期间切换 A→B→A,确认旧批次不再派发剩余项,已完成项仍同步原工作区;普通重渲染继续当前批次,旧移动失败不清除新拖动或显示过期错误。 macOS 打开含 .claude/worktrees 等嵌套 Git linked worktree 与 .artifacts 的仓库:项目树、文件列表与搜索索引不包含这些目录,子模块和位于主检出之外的工作树仍显示;嵌套工作树内的文件变化不触发刷新。外部新增/删除普通文件时树原地增删、不重新扫描整个工作区,新目录、压缩包结构变化时回退完整快照;定位当前文件按路径逐级查找,侧栏渲染不再遍历全部文件。 macOS 项目树按需列目录:快照只返回文件清单,目录展开时才列出下一层;检查首次打开大仓库只显示根层,展开、折叠后再展开、源码根下压缩 Java 包与全量构建一致;外部改动未展开目录下的文件不改变树,展开后可见;重新扫描后已展开目录保持展开并显示最新内容;定位当前文件会逐级加载祖先后滚动到目标行;新增或删除文件后先展开目录再处理监听事件,确认搜索等文件库存仍同步;压缩包重列期间发生其它文件增删,完成后库存和树都保留最新结果。 macOS 项目树同级排序对齐 IDEA 项目视图:目录先于文件,名称按自然顺序(数字按数值、忽略大小写、'-' 与 '_' 相邻);打开含 docs/releases/v0.1.4.md、v0.1.12.md、v0.1.14.md 的仓库,确认 v0.1.4 在 v0.1.12 之前,外部新建 v0.1.5.md 后原地插入到二者之间,与重新扫描的顺序一致。 外部构建产生超过 512 条 target/build 事件且混有源码变化时,只转发有效事件,不触发输出洪峰导致的整项目重扫;显式文件/生成源码监听、项目根名为 build、跨排除边界重命名均保留,系统丢失事件始终触发恢复重扫。 Windows 同时粘贴同名文件不覆盖彼此;剪切期间再次复制相同或不同内容,旧粘贴完成不得清除新剪贴板。 Windows 文件树单击文件只选中并高亮,不打开也不新建预览页签;双击或按 Enter 才打开,单击目录同样只选中。 Windows 文件树选中文件、目录或多选后按 Delete(无修饰键)总是弹出删除确认,即使“删除前确认”设置关闭;取消不删除,确认后按选择集合逐项删除,工作区根目录以及 Shift/Ctrl/Alt+Delete 不触发。", + "notes": "Windows 补齐 Ctrl/Shift 多选、批量复制/剪切/删除、已选集合拖动、Project/Dependencies 切换、定位当前文件、刷新和隐藏。目录名称单击选择,双击或箭头展开;长名称水平滚动。目录标记按工作区持久化,并随移动、重命名和删除迁移;本地 Windows/UNC 按不区分大小写处理,POSIX、WSL 和远程标识保留大小写并隔离工作区。批量移动预检父子重复及重名目标,原生发布拒绝覆盖;真实 WebView2 的拖动与焦点验收 pending。 Windows 树正文统一使用 ui-text-sm(默认 13px),随界面字号设置变化,舒适密度不单独放大 Dependencies 等树行;Project 刷新按钮使用 IDEA refresh.svg。手动定位使用有代次的展开后滚动请求;编辑器切换页签不再自动跟随滚动(autoRevealActiveFileInFileTree 设置已退役);定位清除树内搜索并切回 Project,保持虚拟列表。 搜索导航同步选择与锚点,右键成员判断保留原始选择集合,批量目标再去除父子重复;已完成移动不因面板卸载或工作区切换而漏掉原工作区同步。路径去重与拖动预检按文件系统大小写语义比较,包含 WSL UNC 别名。编辑器活动文件变化时单选同步高亮、复制/剪切目标及范围锚点,明确多选保留;使用同步 Hook 的真实编辑器目标判断切换,选择目录引发的共享侧栏路径往返不改变选择;真实面板与 store 回归覆盖此链路。 批量移动按每次工作区访问识别生命周期,相同根路径的再次访问不能复活旧批次;预检、移动、同步和失败回调共用该守卫。 macOS 快照跳过嵌套在自身仓库中的 linked worktree(.git 文件指向 <主仓库>/.git/worktrees/*)并内置隐藏 .artifacts;项目树由 workspace.listDirectories 按需逐层列出,children 为 nil 表示目录未加载;平铺文件清单 projectFiles 仍完整保留供搜索等功能使用。 macOS 项目树同级排序由 Rust Core 的 visible_children 与 Swift 的 NaturalNameOrder 共同实现 IDEA FileNameComparator(共享 fixture file-name-order-v1.json);快照 files 与搜索索引按 ChildOrder::Traversal 保持原有的小写排序(Windows 快速打开取前 100 项依赖它),只有 workspace.listDirectories 使用自然顺序。Windows 项目文件树仍使用 localeCompare,Git 文件树已使用同源的 compareGitFileNames,Windows 项目树对齐待后续处理。 Windows 文件树支持 Delete 键删除(始终二次确认),macOS 文件树暂无键盘删除,仅右键“移到废纸篓”。" } diff --git a/windows/tauri/src/features/file-explorer/components/file-explorer-selection.test.tsx b/windows/tauri/src/features/file-explorer/components/file-explorer-selection.test.tsx index 8019e3657..bcaba21ff 100644 --- a/windows/tauri/src/features/file-explorer/components/file-explorer-selection.test.tsx +++ b/windows/tauri/src/features/file-explorer/components/file-explorer-selection.test.tsx @@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, mock, spyOn, test } from "bun:test"; import { enableMapSet } from "immer"; import { mockIPC, clearMocks } from "@tauri-apps/api/mocks"; import { act, forwardRef, type ComponentProps } from "react"; +import { createPortal } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import * as debounce from "use-debounce"; import * as searchApi from "@/features/file-search/lib/file-search-api"; @@ -21,6 +22,7 @@ import { openFocusedFile } from "@/features/keymaps/hooks/use-file-open-target"; import { registerDefaultKeymaps } from "@/features/keymaps/defaults/register-defaults"; import { keymapRegistry } from "@/features/keymaps/utils/registry"; import { FileExplorerPane } from "./file-explorer-pane"; +import * as dialog from "@/ui/dialog"; import * as sidebar from "@/ui/sidebar"; const originalSidebar = { ...sidebar }; @@ -215,17 +217,38 @@ async function click(path: string, additive = false, range = false) { ), ); } -async function key(target: HTMLElement, value: string, ctrlKey = false) { +async function key(target: HTMLElement, value: string, ctrlKey = false, shiftKey = false) { await act(async () => target.dispatchEvent( new KeyboardEvent("keydown", { bubbles: true, key: value, ctrlKey, + shiftKey, }), ), ); } +// The native dialog's portal and focus lifecycle is outside this keyboard-routing regression. +function mockDialogFrame() { + spies.push( + spyOn(dialog, "default").mockImplementation(({ title, children, footer }) => + // Like the real dialog, render outside the tree DOM so its clicks are not tree clicks. + createPortal( +
+

{title}

+ {children} + {footer} +
, + document.body, + ), + ), + ); +} +const dialogButton = (label: string) => + [...document.querySelectorAll("button")].find( + (button) => button.textContent?.trim() === label, + ); async function menuAction(label: string) { await act(async () => row(child).dispatchEvent(new window.MouseEvent("contextmenu", { bubbles: true })), @@ -389,3 +412,64 @@ test("following an editor tab also moves the anchor for the next range selection expect(row(matches[1].path).getAttribute("aria-selected")).toBe("true"); expect(row(child).getAttribute("aria-selected")).toBe("false"); }); + +test("Delete key always asks before deleting, even when the context menu skips confirmation", async () => { + mockDialogFrame(); + const tree = container.querySelector('[role="tree"]')!; + await click(readme); + await key(tree, "Delete"); + expect(deleted).toEqual([]); + expect(document.body.textContent).toContain("Delete File"); + + await act(async () => dialogButton("Cancel")!.click()); + expect(deleted).toEqual([]); + expect(document.body.textContent).not.toContain("Delete File"); + + await key(tree, "Delete"); + await act(async () => dialogButton("Delete")!.click()); + expect(deleted).toEqual([readme]); +}); + +test("Delete key confirms and deletes the whole multi-selection", async () => { + mockDialogFrame(); + const tree = container.querySelector('[role="tree"]')!; + await click(directory); + await click(readme, true); + await key(tree, "Delete"); + expect(deleted).toEqual([]); + expect(document.body.textContent).toContain("Delete 2 selected items?"); + await act(async () => dialogButton("Delete")!.click()); + expect(deleted).toEqual([directory, readme]); +}); + +test("Delete key ignores the workspace root and modified presses", async () => { + mockDialogFrame(); + const tree = container.querySelector('[role="tree"]')!; + await click(workspace); + await key(tree, "Delete"); + expect(dialogButton("Delete")).toBeUndefined(); + + await click(readme); + await key(tree, "Delete", false, true); + await key(tree, "Delete", true); + expect(dialogButton("Delete")).toBeUndefined(); + expect(deleted).toEqual([]); +}); + +test("single click only selects a file; double click opens it", async () => { + const open = mock(async (_path: string, _isDir: boolean) => {}); + const select = mock(async (path: string) => path); + await act(async () => + useFileSystemStore.setState({ handleFileOpen: open, handleFileSelect: select }), + ); + await click(readme); + expect(row(readme).getAttribute("aria-selected")).toBe("true"); + expect(select).not.toHaveBeenCalled(); + expect(open).not.toHaveBeenCalled(); + + await act(async () => + row(readme).dispatchEvent(new window.MouseEvent("dblclick", { bubbles: true })), + ); + expect(open).toHaveBeenCalledTimes(1); + expect(open).toHaveBeenLastCalledWith(readme, false); +}); diff --git a/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx b/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx index b751219e0..ee61f0443 100644 --- a/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx +++ b/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx @@ -1087,19 +1087,11 @@ function FileExplorerTreeComponent({ } selectPath(t.path, { additive: e.ctrlKey || e.metaKey, range: e.shiftKey }); if (e.ctrlKey || e.metaKey || e.shiftKey) return; - if (!t.isDir) { - fileOpenBenchmark.ensureStarted(t.path, "explorer-click"); - fileOpenBenchmark.mark(t.path, "explorer-click"); - } - if (t.isDir) { - setFocusedPath(t.path); - updateActivePath?.(t.path); - } else { - setFocusedPath(t.path); - void Promise.resolve(onFileSelect(t.path, false)); - } + // A single click only selects; opening a file is the double-click (or Enter) action. + setFocusedPath(t.path); + updateActivePath?.(t.path); }, - [onFileSelect, toggleDirectory, updateActivePath, pathToFile, selectPath], + [toggleDirectory, updateActivePath, pathToFile, selectPath], ); const handleContainerDoubleClick = useCallback( @@ -1433,6 +1425,26 @@ function FileExplorerTreeComponent({ onRenamePath?.(current.path); break; } + case "Delete": { + // Shift+Delete means "permanent delete" on Windows; this tree has no such action. + if (!current || mod || e.altKey || e.shiftKey) break; + const entries = selectedEntries.length + ? selectedEntries + : workspaceRootPaths.includes(current.path) + ? [] + : [{ path: current.path, is_dir: !!isDir }]; + const [first] = entries; + if (!first) break; + e.preventDefault(); + // A stray key press must never delete on its own, so this path always asks first, + // even when "confirm before delete" is off for the context menu. + setDeleteCandidate( + entries.length === 1 + ? { path: first.path, isDir: first.is_dir } + : { path: first.path, isDir: first.is_dir, entries }, + ); + break; + } } }} onDragOver={(e) => { From 8e059e947515597831a92321ee5f307691b05aac Mon Sep 17 00:00:00 2001 From: "frank.wang" Date: Thu, 29 Oct 2026 20:27:29 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(windows):=20Git=20Log=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=97=E8=A1=A8=E9=80=89=E4=B8=AD=E8=A1=8C=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=20Diff=20=E9=A2=84=E8=A7=88=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E4=B8=80/=E4=B8=8B=E4=B8=80=E6=96=87=E4=BB=B6=E5=AF=BC?= =?UTF-8?q?=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Commit files 列表监听预览标签页当前显示的文件并同步选中行; 仅当预览属于当前所选提交或范围时才同步,避免过期预览改变选中 - 同步选中时自动展开被折叠的父目录,并仅在纵向滚动到可见位置,不改变横向滚动 - 抽出 commitFilePreviewHash 与 selectedCommitFilePath,发布预览与观察预览共用同一标识 - 补充选中跟随与折叠目录自动展开的组件测试,并更新跨平台功能矩阵说明 --- .../features/git-history.json | 5 +- .../components/log/git-commit-file-tree.tsx | 24 +++- .../log/git-commit-inspector.test.tsx | 121 +++++++++++++++++- .../components/log/git-commit-inspector.tsx | 28 ++++ .../git/hooks/use-commit-file-preview.ts | 6 +- .../git/utils/commit-file-diff-navigation.ts | 6 + .../git/utils/git-commit-selection-diff.ts | 7 + 7 files changed, 190 insertions(+), 7 deletions(-) diff --git a/shared/platform-feature-matrix/features/git-history.json b/shared/platform-feature-matrix/features/git-history.json index a8a691ef0..fed51141d 100644 --- a/shared/platform-feature-matrix/features/git-history.json +++ b/shared/platform-feature-matrix/features/git-history.json @@ -57,6 +57,9 @@ "windows/tauri/src/features/git/components/log/git-commit-details.test.tsx", "windows/tauri/src/features/git/hooks/use-git-diff-actions.test.tsx", "windows/tauri/src/features/git/hooks/use-commit-file-preview.ts", + "windows/tauri/src/features/git/components/log/git-commit-inspector.tsx", + "windows/tauri/src/features/git/components/log/git-commit-inspector.test.tsx", + "windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx", "windows/tauri/src/features/git/hooks/use-commit-file-preview.test.tsx", "windows/tauri/src/features/git/components/diff/commit-file-diff-preview.tsx", "windows/tauri/src/features/git/components/diff/commit-file-diff-toolbar.test.tsx", @@ -128,6 +131,6 @@ "verificationStatus": "pending" }, "owner": "Git", - "verification": "分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;macOS Git Log 工具窗口标签悬停有反馈,点击窗口内外时选中标签背景在活动色与非活动色之间切换,标签文字保持相同颜色与常规字重,按下时不变暗;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端使用 Community 完整颜色 ID 及明暗主题 RGB 规则;macOS Git Log 不再显示单独的 Fetch/Compare/Checkout/Cherry-pick 横向操作栏,分支树与竖向工具栏分离;检查 Fetch、分支比较、分支右键 Checkout、提交右键 Cherry-pick 和 Fetch 设置仍可用,分支搜索可筛选嵌套路径,并隐藏无匹配项的分组、显示无结果提示;定位按钮滚动到当前所选分支的首个可见提交而不重新加载历史;Git Log 的分支和提交搜索框复用 全局唯一的 LitheSearchFieldStyle,对照 IDEA SearchFieldWithExtension,核对 13pt 常规提示文字、Islands 明暗控件底色与边框色、36pt 外框内的 30pt 可见高度、3pt 外层留白、1pt 常态内描边、向外扩 1pt 的 2pt 聚焦描边及 4pt 圆角;图标与文字间距为 2pt,输入框清除按钮复用已有 16pt IDEA SVG 和 20pt 按钮区域,按上游预留清除图标宽度,移除分支搜索框重复外层水平内边距,提交搜索框按 15 列字体度量和 150pt 最小宽度布局,Search 与 Database 搜索输入框使用相同共享样式;竖栏更新按钮采用 IDEA ExpUI 的明暗 update 图标,按钮与侧边栏图标悬停使用共用工具提示样式,提示的明暗背景、文字色、1pt 内描边、圆角及内边距对齐 IDEA HelpTooltip;隐藏与显示分支使用 IDEA ExpUI 的 chevronLeft/chevronRight 明暗图标;竖栏宽 36pt,展开态按钮为 22pt 且四周保留工具栏内边距,展开态分支树左边框复用与 Commit 标题下边框同源的 1pt 分隔线;收缩态竖栏常态复用展开态侧栏背景,展开与收缩保留提交图谱和详情视图实例、滚动状态及分支宽度,仅切换分支面板与分隔条的占用宽度,按钮占满竖栏,深色 hover 使用 Islands Dark 的 #FFFFFF17,浅色 hover 使用 ExpUI Gray12,不在右侧显示 Show Branches 浮层提示,图标在顶部留 8pt 并使用常规小字号文字;展开后恢复分支树宽度。 使用原生 NSHostingView 渲染验证共享 LitheSearchTextField 的占位文字在深浅主题均为 IDEA Component.infoForeground 对应的 #73767C、13pt 常规字重;空输入框单独绘制占位文字,避免 SwiftUI 原生 prompt 替换其颜色与字重,有内容时占位文字消失并保留原生编辑和主文字色。 Git Log 分支树按 IDEA Community Islands 普通密度核对 24pt 行高、无行间额外间距、13pt 常规文字、12pt 次要计数与 4/12pt 树边距;展开箭头、路径文件夹、普通分支、当前分支和标签使用上游 16pt 明暗 SVG,Local/Remote/Tags 标题不附加图标,当前分支不显示额外蓝色勾选;HEAD 与当前分支是两个互斥选中节点,点击分支、HEAD、清除筛选和切换仓库时核对选中状态;深浅主题核对树行焦点/失焦选中色及 hover,保留多仓库名称、圆点、数量和只读菜单,移除分支行仓库色条;Windows 多仓库分支树的仓库行与 macOS 一致,仓库行使用中性色文件夹图标,并只在图标与名称之间显示 8px 仓库色点,仓库名保持普通文字色,不出现整组着色卡片底色、左侧色条或彩色名称,截断名称通过共享工具提示显示全名,行悬停为箭头光标;确认搜索、分组折叠、右键操作、缓存行比较和滚动状态正常。 Git Log 分支树右侧分隔线与 Commit 标题下边框共用 LitheTheme.toolWindowBorder 和 1pt 厚度,深色为 #26282C、浅色为 #E9EAEE;悬停、拖动开始/移动/结束均保持同色,不高亮,保留原生调整光标、命中区域、拖动尺寸约束及提交宽度行为。 Git Log 中间栏按 IDEA Community FilterComponent、VcsLogGraphTable 和 PaintParameters 核对四个筛选器的 13pt 常规文字、4pt 内边距、16pt 箭头和仅文字悬停反馈;工具栏使用 16pt IDEA 明暗 SVG、22pt 点击区域和共享提示,保留动作及禁用条件;提交列表为 26pt 连续行,图谱尺寸同比缩放,作者与日期同为 13pt 主文字色,日期列按打包字体度量,核对深浅主题行 hover、焦点/失焦选中色及选中覆盖 hover,筛选、分页、箭头命中、上下键导航和右键操作不回归。 分支竖栏设置齿轮复用 LitheIconButtonStyle 的 22pt 点击区域、4pt 圆角和主题 hover/按下背景;点击使用 LitheContextMenuPresenter 的共享操作菜单背景、1pt 边框、行 hover 与键盘导航,保留 Fetch Options 对话框、Show Worktree Repositories 勾选和无工作树时禁用行为,点击外部或按 Esc 关闭菜单。 macOS 下拉菜单仅保留 Project 同源的一套全局共享样式,移除 settingsStyle 切换;Project、Git Log 设置、Terminal、欢迎页及右键子菜单统一使用 8pt 外框圆角、1pt 内描边、24pt 行高、12.5pt 文字、6pt 外留白和 8pt 行内边距,设置选择下拉框共享同一外框与尺寸定义; Git Log 顶部按 IDEA 恢复 Branch: 已选分支名称,保留 Branch 筛选、清除和菜单内勾选;Branch、User、Date、Paths 按 IDEA FilterComponent 保留 4pt 内边距、3pt 箭头间距和 13pt 常规文字,四项文字 hover 为白色且不增加底色。Date 使用共享菜单呈现器,Branch/User 的可搜索列表与分组子菜单使用共享 LitheDropdownRowStyle、24pt 行高和共享菜单外框,Paths 输入面板复用共享菜单外框及搜索输入框样式,保留搜索、选项选中、路径 Apply/Cancel/Clear 和时间范围计算。 Branch/User/Paths 原生弹窗锚点不参与鼠标命中,核对按钮与文字收到 hover/点击、菜单正确弹出且关闭后可再次打开;Date 保持共享菜单呈现器。共享下拉外框裁剪内部内容,明暗主题下 Branch/User 搜索条不能覆盖圆角;Paths 输入框复用共享输入样式并显示正确焦点边框,Clear/Cancel/Apply 复用共享次按钮/主按钮,保留路径应用和清除行为。Branch、User、Paths 的可搜索内容同样使用 LitheContextMenuPresenter 的透明无边框面板,去掉 NSPopover 系统外框,复用 Project 共享背景、1pt 边框和圆角;分组展开与搜索动态调整弹窗尺寸,Esc、点击外部和切换菜单正确关闭。选择分支后清除筛选,确认 Branch 恢复默认标题且关闭按钮消失;关闭按钮保留 16pt closeSmall 明暗 SVG、22pt hover 区域及 4pt 圆角。展开分支树左侧固定线、中间分隔线和右侧详情拖动线均使用 toolWindowBorder 的 1pt 边框,右侧 hover 与拖动不高亮。右侧详情展示图标使用 IDEA previewVertically 明暗 SVG,装饰开关使用 show 眼睛而非 hide 工具窗口减号;所有工具栏展示图标复用 22pt、4pt 圆角的共享 hover 背景,保留原动作与提示。深浅主题渲染核对背景和边框,保留可选图标、勾选、快捷键、禁用、子菜单和键盘导航。 Git Log 提交标题、作者和日期使用 Inter 13 Normal,引用标签 12pt;核对至少 26pt 的字体度量行高、含 leading 的整数基线、长标题与中文/emoji 截断,以及选中合并提交的文字颜色。 所选分支存在时 Log 标签显示 Log: 分支短名(包含 codex/ 路径),选择另一分支立即更新;清除分支筛选后恢复 Log,保留本地化标题、原始分支名和原标签选择/焦点行为。 macOS 标题起点按缩放图谱列距、图文间距和 2pt 文字内边距计算,为对齐参考截图至少预留六列,密集行继续扩宽,不改变图谱节点、连线和箭头命中;在 SwiftUI/AppKit 两条渲染路径核对未选中的合并提交标题、作者、日期统一使用合并灰色,选中后恢复普通文字色,合并判定仅依赖至少两个父节点。 Git Log 右侧文件树使用共享 24pt 行高、19pt 层级缩进、常规字重、文件类型图标和 IDEA FileStatus 文件名颜色,无逐行横线及工具栏重复总数;核对文件夹折叠、文件打开 diff、长名称 hover 展开与横向滚动,拖动宽度不重建行内容。提交详情使用 14pt 外边距、10pt 内间距、等宽 bold 标题、同段作者邮箱时间与引用标签,长内容可滚动;文件树/详情之间分隔线复用共享边框,hover/拖动不高亮。左侧分支树从 180pt 专用下限改为共享 30pt 下限,独立 36pt 竖栏保持原宽,窄树裁剪后可拖回原宽,收缩/展开不重建图谱。 图谱圆点、连线、箭头按 SimpleGraphCellPainter/ PaintUtil 的 FLOOR/ODD 物理像素规则对齐;在 1×/2× 原生位图验证圆点与线宽,在真实 NSHostingView 检查对齐后的上下斜向箭头尖端及内侧描边可导航,切换屏幕只重绘且绘制/命中坐标一致。 时间列复用 Inter 13 常规字体的等宽数字与固定 AM/PM 区域,按应用语言使用英文 12h AM/PM 或中文 24h 格式;核对午夜、正午、未知日期回退、行起点一致及切换语言后的原生日期缓存失效,列表两条渲染路径和提交详情使用相同格式。 连续上下拖动 Git Log,包含上方长 Diff:原生列表只调整可见区域,保持文档身份、滚动位置、Shift/Command 多选、焦点色、键盘与箭头导航、分页和辅助功能;收缩再恢复后列表宽高正确。GitGraphInteractionTests 使用 300 条提交、80 次原生尺寸变化和共享分隔条事件验证,局部新旧路径计时作为诊断,不代替目标工作区帧率验证。 共享分隔容器按已有宽高直接分配两个面板与分隔条的确定矩形,避免 HStack/VStack 最小及理想尺寸探测;splitPaneBounds 覆盖横纵方向、leading/trailing、折叠、子视图身份和有限尺寸,达到尺寸限制后的相同结果不重复写状态。 通过键盘或程序化切换焦点时 Git 标签活动态双向更新;挂载或重新显示 Git Log 不主动抢走编辑器焦点。 在包含展开分支树的 Git Log 中连续拖动外层高度和内部宽度:共享分隔器只在拖动开始/结束传播环境状态,嵌套容器继承祖先状态;拖动期间共享行悬停不更新、提示不登记几何锚点、右键覆盖层暂停,松开后恢复菜单和提示,行内容及滚动状态不重挂载。69 行原生分隔条事件回归检查覆盖层暂停/恢复及内容身份;未悬停控件在平时同样不登记提示锚点。用目标工作区 Time Profiler/Animation Hitches 对照 Terminal 核对持续更新成本及起止开销,split.drag 整次手势时长不能当作单帧延迟;本次仅前端共享交互装饰调整,不修改后端及默认详情高度行为。 中文输入法组词期间,即使绑定文本仍为空,共享 LitheSearchTextField 占位文字立即隐藏;取消组词或删空后恢复,确认中文后仍隐藏。原生 setMarkedText 回归覆盖明暗主题和两个输入框互不影响;保留焦点、键盘提交、搜索/过滤语义及占位字号颜色。原生背景观察视图卸载和移出窗口时清理通知,匹配当前窗口的字段编辑器及所属输入区域,不修改业务绑定。 四个筛选弹窗均以各自按钮左下角为定位锚点,Date 不依赖鼠标点击坐标;空间不足时限制在屏幕可见范围内。原生面板回归核对操作菜单/可搜索内容相同锚点下的左上角、无展开动画和 Esc 关闭回调。共享搜索字段在未聚焦时悬停也显示文本编辑光标,离开或禁用时恢复箭头。 Git Console 左侧六个现有动作使用 Community 原始 16pt 明暗 SVG;22pt 背景、竖向 2/1 按钮留白、5/7 外边距和默认箭头光标。核对查找、软换行、跟随、停止、清空、复制的原有动作与禁用状态,组件资源检查不替代完整应用验收。 按 Community CurrentBranchHighlighter 标出当前本地分支可达的所有父提交,包含合并父节点;深色 #1D2336、浅色 #EDF3FF,选中背景优先,仅已加载图谱可证明的祖先被标记,当前分支/HEAD 单独筛选时不重复染色。引用默认显示并按 GitRefManager/SimpleRefGroup 紧凑合组:本地与上游同提交显示 remote & branch,HEAD/本地/远端/标签使用官方黄/绿/紫/灰轮廓及叠层,标签单独存在时默认只显示图标;长名称按列宽和 22 字符下限缩写,原生可见区 tooltip 保留所有完整名称,滚动后按当前鼠标位置解析。明暗原生渲染检查颜色、宽度、选择优先级及完整名称;GitLogReferenceResizeTests 在真实 GitLogView 中加载 1000 个分支并拖动外层高度,检查可见右键覆盖层数量和挂载生命周期,计时仅作诊断。最新正式预览已进入该工作树,观察全部引用下的深色背景、叠层缩写标签和多次上边缘拖动后的布局恢复;用户随后在本机明确确认“现在不卡顿了”,本次报告的高度拖动卡顿通过用户实机验收;组件计时和混合工作区采样不用于推断唯一根因或量化通用帧延迟,其他主题/悬浮等视觉项继续按各自范围验收。 在 Windows Git Log 选中带多行正文的提交,确认右侧详情在标题下方完整显示正文(保留空行与缩进),复制提交信息和打开提交 Diff 时也包含正文;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端使用 Community 完整颜色 ID 及明暗主题 RGB 规则。 读取提交正文期间切换仓库、重新打开另一提交或关闭面板,确认迟到结果不会打开旧 Diff 或覆盖新请求的加载状态。 Windows 另验证:单击提交后编辑区预览该提交第一个文件的 diff;单击或双击 Commit files 中的文件只显示该文件的 diff(不含提交说明,也不显示文件列表),并复用同一个预览标签;Commit files 树中过长的文件或目录名不折叠为省略号,区域随最宽行横向展开,可通过横向滚动条或触控板侧滑左右滑动查看完整名称(与源代码管理变更树一致);Git Log 刚打开、尚未点击提交时不会自动打开 diff;日期列将有 UTC 偏移的提交转换到本机时区后显示相对时间(刚刚、N 分钟前、今天/昨天);英文绝对日期和时间通过 Windows 原生接口遵循系统短日期/时间及用户自定义格式,中文使用中文地区格式,未知偏移仍显示绝对日期;验证美式月/日/年、英式日/月/年、自定义分隔符、12/24 小时制、跨时区相对时间和迟到原生结果保护,真实 WebView2 产品验收为 pending;鼠标移入行时刷新相对时间;作者/日期列可在行内列边缘拖动调整宽度并在重启后保留。 非连续多选提交修改同一路径时,预览保留每个提交的 diff 和提交标识,可通过文件导航分别查看;延迟请求返回前取消选择、隐藏 Git Log、切换仓库或卸载工具窗,确认旧请求不会重新打开或激活预览标签。 Windows 端提交历史仅通过 Git Log 工具窗浏览:源代码管理侧栏入口已更名为 Commit 面板,其 History 标签已移除,面板固定显示变更列表与提交区。命令面板中的 Git: Show History 入口同步移除。 Windows Commit Files 单文件预览使用独立页面:上下箭头导航差异,下箭头在当前文件最后一处后继续进入下一文件并在比较完成后定位第一处差异,整个文件集合末尾禁用且不循环;图片、二进制和无文本差异文件也允许下箭头继续,比较未就绪时禁用以防连续点击跳过文件。左右箭头直接切换当前比较集合的上一文件/下一文件;标签使用 IDEA 的 Repository Diff: 文件名并跟随导航更新,不附加提交 hash。Jump to Source 按最后聚焦侧的补丁位置打开预览所属仓库的文件,重命名使用新路径、删除行映射邻近保留行、已删除文件禁用源跳转;模型更新期间禁用差异动作。保留并排/统一视图、空白显示、Monaco 查找、图片和二进制呈现,普通全量提交比较仍使用多文件页面。真实 WebView2 内核及源文件打开还需目标产品运行验收。 Windows 单文件历史 Diff 外观验证:并排视图使用独立外侧 12px 滚动轨道、8px 滑块和概览,通常的统一视图保留 Monaco 原生 Diff 组件,整文件新增使用真实新行流的独立只读控件,深浅主题检查常态/悬停/拖动色、圆角及边框;实际拖动任一并排滑块后按差异边界同步对应行且模型身份保持。工具栏使用现有 Button,核对 40px 高度和 22px 图标按钮;保留用户字体与字号,默认行距按 14px 字号对应 22px 并随缩放,自定义行距不覆盖。词级高亮开关复用现有 Monaco 装饰能力,关闭只隐藏字符差异、保留整行差异,切换文件保留词级和空白开关。旧/新版本标题显示真实第一父提交、范围基线/目标或非连续提交各自身份,根提交旧侧为空树,重命名显示旧/新路径;短 hash 和截断路径的 tooltip 保留全文。并排标题列宽由独立布局所有者更新 CSS,拖动分隔条时标题跟随且不重建模型;统一视图标题纵向排列。以上浏览器真实组件验证不替代目标 WebView2 产品验收。 Windows 单文件 Diff 滚动条回归:两侧横纵滚动条默认显示,鼠标移到工具栏后仍可见,不依赖悬停;差异概览不绘制重复的宽矩形视口指示,并排由独立外侧轨道负责点击、拖动和滚轮,统一视图保留原生委托。明暗主题、并排/统一视图及长文本双向溢出下检查唯一可见滑块、并排横向同像素偏移同步、纵向对应行同步和模型身份。 历史 Diff 两侧滚动轨道与代码编辑器概览画布背景透明,鼠标不悬停时滑块仍显示;关闭代码编辑器重复概览通道,但保留 Diff 专用差异色条。横向轨道不额外增加内容高度,检查代码能显示在滑块后、底部内容与滚动位置正确,拖动命中保留,并排两侧横向同像素偏移同步、纵向对应行同步。 Windows 并排单文件 Diff 使用两个独立只读 Monaco 代码控件,取消 DiffEditor 的补空白对齐。左右横向滚动按同一像素偏移双向同步,较短侧由 Monaco 钳制到真实内容边界且不反向拉回主动侧;纵向原生代码区、外侧滑块拖动/点击、轨道键盘、滚轮和查找以视口三分之一处为锚点,按 Git 差异块起止边界同步对应行并保留行内偏移,短侧在不等长块内钳制、共同上下文恢复对齐。同步有重入与微小方向保护;双模型替换、布局、字体配置和显式上下导航期间暂停同步。新增旧侧/删除新侧画横跨可见代码区的双像素零行边界,修改区画上下单像素边界;两列行号背景为直边,曲线只在两列之间弯曲,实际偏移改变端点形状。检查文件开头/末尾零行、三态、明暗主题和窄列,代码不补空白、不扩展横向范围;连接面覆盖两列历史行号背景,行号选择和源映射正确。左右外侧各显示透明常显纵向滑块与增删改概览,横向保留原生滚动条,滚动范围截止真实最长代码行。验证深浅主题、折叠展开、Ctrl/Cmd+F 搜索折叠文本、词级开关和预算、统一视图及跨文件首处定位、连续拖动和窄窗口非负尺寸。高频帧二分筛选可见差异,缓存像素边界及概览,不扫描全文、不更新 React;卸载和迟到语言准备清理所有帧、监听、观察器、拖动、两个编辑器和模型。浏览器生产组件验证使用平台记录器,真实 WebView2 产品验收仍为 pending。 外侧轨道预留独立空间,不显示代码;左轨道距面板边缘、右轨道距代码视口各留 4px 间隙,窄列随可用宽度缩减,右侧文字与轨道不重叠;并排和统一预览关闭缩进及括号引导线,保留语法颜色;中央行号缩小边距和曲线间隙,整数布局边界及上下半像素描边消除与代码区的垂直接缝。滚轮在代码、行号和轨道统一加快到两倍,拖动仍映射各侧真实进度,纵向同步对应行;方向键按用户行高移动。有词级结果时修改正文底色按 40% 修改色与 60% 页面背景混合,完整修改色突出替换词的两侧,纯新增和纯删除词分别使用完整增删色,语法前景色和文字透明度保持;关闭词级或超限保留整行底色。验证三态与开头末尾、双向对应行同步、明暗主题、轨道无遮挡、宽窄字体布局和预算回退。 单文件并排及统一视图隐藏补丁 @@ 标题,但保留真正以 @@ 开头的源码、行 ID、源行映射和原始补丁;默认多文件与暂存投影不变。Islands Dark 继承 Darcula 背景,Islands Light 使用 Light 方案,独立差异轨道标记与连接、代码边界共用对应差异背景色。底部保留五个用户实际行高的 padding,模型不增加假行;从任意侧滚到底部验证共同末行对齐与五行留白,字号/行距变化同步更新。 Windows 单文件 Diff 首次打开、左右切文件和同路径新比较均在模型/比较准备完成后定位第一处改动;定位只消费一次,调整词级、空白、字号或主题保持用户当前位置。统一视图的原生 Monaco 增删行、字符与行号栏使用该预览相同的 Islands 背景变量,移除默认字符边框;新增整文件应为深色 #294436 或浅色 #BEE6BE,删除为 #484A4A 或 #D6D6D6,语法前景色不变且其他编辑器主题不被覆盖。 整文件新增采用单流只读代码控件,隐藏不存在的旧侧与连接带,新增背景覆盖行号、代码和底部留白,避免空旧模型的灰色删除行及外圈;新增仅显示新版本、删除仅显示旧版本的一组 hash 与路径。并排两侧及新增单流轨道的增删改标记与对应内容背景共用颜色;原生统一预览关闭 renderIndicators,历史行号旁不显示 +/-。这组新增显示修复按用户要求延后运行验证,提交或 PR 前核对新增文件、普通修改、删除、深浅主题、查找、源跳转与底部留白。 Windows Git Log 单文件和 Commit 文件 Diff 的词级高亮、显示空白符、统一/并排切换均在左侧,最右侧统计当前文件差异,区分无差异/单数/复数;Commit 勾选、部分包含、取消及刷新后 included 数同步变化,全部包含仍显示数量;加载、切换文件及图片/二进制不显示文本差异统计。核对中英文、明暗主题、窄窗口和按钮选中/禁用状态。 Windows 单文件 Diff 上箭头在首处继续进入上一文件最后一处,下箭头在末处进入下一文件首处,集合首尾不循环、加载中禁用;Commit 文件列表随跨文件导航更新选中行并展开祖先、滚动到可见位置,保留编辑器焦点和 include 状态;同文件刷新、历史 Diff、无关仓库和迟到读取不扰动列表。 Windows Git Log 筛选后基于完整已加载历史投影可见祖先:隐藏中间提交以虚线连接,直接父子保持实线,无关分支不误连;合并路径去重并保留永久颜色,未加载父提交经隐藏节点传播,分页补齐后解除缺失标记;清空筛选恢复原图,核对文本、作者、分支名称三种范围、空结果与选择行为。算法和真实表格组件回归通过不代替 WebView2 原生验收,原生验证保持 pending。 Windows 未筛选图与筛选图共用上下半边打印元素:普通浏览的跨行泳道换列必须在共同中点连续连接,合流入边保留各自颜色到提交圆点;核对原始 IDEA 200 条历史输出的列号、方向、终止和颜色身份;截图/原生交互验收仍为 pending,组件输出和 golden 对照不代替 WebView2 验收。 Windows Git Graph 外观:核对明暗主题真实 IDEA RGB、实心节点、直线与筛选虚线、终止箭头及至少六列标题预留;默认 26px 行高,用户字号变化时虚拟行与 SVG 同步扩高;在 100%/125%/150%/200%/300% 缩放及跨屏 DPI 变化下核对奇数物理像素和相邻半边连续性,普通视口调整不重建历史图。箭头能跳到已加载且可见的父/子提交,未加载父节点没有可点击目标;浏览器生产组件验证不代替目标 WebView2 运行验收,原生验证保持 pending。 Windows 历史排序和图导航:在交错分支仓库核对首批/分页/回退及全引用上下文均使用日期顺序,切换分支保持可证明的永久颜色和基础顺序;上下文最多 5000 条,覆盖不完整或哈希重复时保留可见页并回退。核对 upstream/backup 等非 origin 远程引用分类及本地含斜杠分支;背景上下文不阻塞 50 条可见页,分页不取消或重读上下文,切仓库/刷新/卸载取消旧操作且关闭迟到游标。长边开关展开/收起不改变提交、筛选、选择和游标;验证 30/1000 行边界。点击或用 Enter/空格激活上下箭头,选择并滚动到屏幕外端点,挂载后键盘焦点随目标;不误选源行、不误开源行 Diff,保持普通选择/多选/双击/右键和原预览入口。明暗主题及 1x/1.25x/2x 检查箭头尖端与命中范围,真实 WebView2 运行验收保持 pending。 图谱颜色必须跟随 ThemeRegistry 根 data-theme-type 切换;不依赖页面存在 dark class。复用真实 Lithe 明暗主题检查同一已挂载节点与连线的颜色变化、布局不因主题重算,结合主题回归和浏览器真实切换验证。 Windows 分页稳定性:选中顶部提交后向下滚动触发连续分页,确认滚动位置不跳回已选提交,Commit Files 不清空或重读,已选文件及树折叠保持;更新引用、背景上下文和相同 hash 的元数据同样不导航。定位分支头通过独立请求仍能定位已经选中的头提交一次;新选择或切仓库仍读取新文件并丢弃旧结果。组件回归与真实浏览器虚拟列表/IntersectionObserver 验证通过,目标 WebView2 实机验收保持 pending。 Windows Git Log 标题栏不显示固定 Read-only/只读文字,关闭按钮保持靠右,刷新、设置及隐藏操作正常。 Windows Git Log 单个提交右键 New Tag… 输入非空且无空白的标签名,创建指向右键提交完整 hash 的轻量标签并刷新引用和历史;取消不写入,失败保留名称可重试,双击/Enter 不重复创建,多选和写操作期间禁用。仓库或工作区切换关闭旧弹窗,旧请求迟到不得刷新或关闭新弹窗。 Windows 提交区域点击后上下箭头更新可见提交选择和详情,焦点不随虚拟行移除而丢失;筛选、首尾边界、Home/End、Shift 范围选择和 Enter 正常,菜单与输入框保留自己的方向键。 Windows Git Log 鼠标或上下键选择提交后,异步 Diff 自动定位第一处改动但保持提交列表键盘焦点;并排与统一预览均不调用初始 focus。双击/Enter 打开完整 Diff、主动差异跳转及跨文件导航仍进入编辑器。 Windows Git Log 提交右键 New Tag 使用与 Tags 节点同源的 TagIcon(沿用现有标签轮廓图案),核对 16px 图标、菜单对齐和禁用反馈;新补图标的实机视觉确认待完成。 Windows 提交列表菜单键和 Shift+F10 打开当前选中提交的真实菜单;关闭后列表继续上下键导航,菜单操作仍针对所选提交。New Tag 在 tag.gpgSign=true 下生成轻量标签,不运行签名器或消息编辑器,不修改配置。 Windows 提交行高亮和紧凑引用组:在全部引用和其他分支视图核对当前检出分支全父祖先背景,当前分支/HEAD 视图关闭重复高亮;合并提交的说明、作者、日期未选中时弱化、选中后恢复,选中背景优先于分支并区分列表/文件树/窗口焦点。核对明暗主题固定基准色、真实 upstream 合并名称、游离 HEAD、每种最多两个叠加图标、仅标签图标和完整汇总悬停/无障碍提示。宽窄列及字号变化时按至少 22 字符缩略规则布局,只有一个视口尺寸观察器,列拖动不逐行读取宽度,卸载清理;分页不丢滚动/选择,成功后不重读 Commit Files;文件读取失败时 Commit Files 面板显示重试入口,重新选择同一提交或刷新历史也会重试一次,长边导航和新建标签原入口保持。组件与浏览器验证通过,目标 WebView2 实机验收 pending。 Windows Git Log 分支树和提交列表的普通滚轮使用 Community Windows 默认 200ms 和 (0.5,0.505,0.5,1) 曲线;连续同向累积、反向从当前位置重启。核对两处列表的中间帧、Shift 横向滚动、分页保持、顶底边界、键盘定位和滑块拖动取消惯性、关闭工具窗清理帧以及减少动态效果偏好。确定性动画与 DOM 回归通过,真实 WebView2 设备手感验收保持 pending。 Windows graph 标题不再固定预留六列:建议宽度取可见图边数的加权均值与标准差,最多以六列作统一基线,每行按节点与斜边中点扩展,密集行不把其他简单行扩成最大列宽。验证单线/少分支、罕见密集合并、超过六列、筛选、长边开关、引用标签可用宽度及字号/DPI,保持图谱颜色/连线/导航不变。 Windows Git Log 的 Commit Files 文件树沿用 IDEA Windows 默认 200ms 滚轮动画;在文件行、竖向/横向滚动条区域连续滚动、反向及 Shift 横向滚动,确认共用真实视口的动画目标,长文件名保持自然宽度;按下滚动条取消未完成惯性,减少动态效果时直接滚动,切换提交及卸载不残留帧。其它 ScrollArea 默认行为不变。真实 WebView2 交互验收 pending。 Windows Git Log 右键 New Tag 打开弹窗后焦点直接位于标签名输入框,无需点击即可输入,Enter 提交、Esc 关闭。 macOS Git Log 空状态:在非 Git 项目检查明暗主题、窄窗口和键盘操作:13pt 常规文字按 IDEA 左对齐,整组位于内容区上方三分之一处;显示 To track changes to code、GitHub/GitLab 官网链接、Initialize Git repository…、Open Git Settings 和带原始 ContextHelp 图标的 Version control integration(打开 Git 官方文档)。初始化仍先确认,不自动暂存或提交;设置打开既有 Git 设置。未提交仓库、无匹配提交、加载失败及重试保持原动作,所有空状态操作采用文字按钮。 非 Git 项目空状态隐藏 Log/Worktrees/Console 标签、添加按钮、分支树与动作条、筛选/搜索工具栏及提交详情,提示占满工具窗内容区;GitHub、GitLab、初始化、设置及帮助链接悬停仅给当前文字加主题强调色下划线并显示手形,离开后恢复;初始化或切换至真实仓库后恢复原有三栏布局,真实仓库无匹配提交仍保留筛选入口。 空状态字体与间距对照 Community c7f91397:按钮标签显式使用 Inter 13pt Regular;行首破折号按首行基线对齐,21pt 行高加 2pt 间隔,帮助图标间隔 2pt;灰字与链接分别沿 Component.infoForeground 和 Islands Link.activeForeground 核对明暗主题,悬停下划线仍使用当前主题强调色。 macOS 右侧提交详情的分支标签按可见区域懒加载,全部引用仍可滚动查看。重开面板初始化时恢复匹配的提交图和分支树,第一次布局即可读取全部已缓存数据,不依赖出现后的异步任务回填。重开面板复用未变化的分支树,单仓库不重复构建分组副本,多仓库活动根复用已有三类行;引用、仓库、工作树开关、折叠状态改变后拒绝旧树。原生绘制与无障碍日期文本复用缓存,语言改变后刷新。 macOS 大量引用首屏:同提交放置 1,500 个本地分支和 1,500 个远程引用,核对真实 upstream 优先、同名远程自然排序回退、完整提示及每类图标上限;反复切换 Terminal/Git Log,数据版本相同应复用已准备图,改变分支/筛选/图显示选项后不得复用旧图。运行 GitGraphInteractionTests 的 manyReferenceGroups 和 presentationCache;组件耗时仅诊断,整机卡顿以相同数据实测为准。 macOS Git Log 及终端按需创建后由工作台持有实际原生宿主;隐藏再重开及移动停靠位置复用控件。运行 GitLogReferenceResizeTests,验证同一工作台隐藏再重开时,固定原生表面上的提交文档和滚动容器对象身份不变,恢复可见性且尺寸正确,工作台释放后宿主释放;核对语言、主题、搜索、分支操作和终端会话。 Git Log 与终端同时停靠时,从外层横向和纵向分隔条中央及向面板两侧偏移 4pt 处,经窗口发送按下、拖动、松开,确认完整 10pt 命中区仍能提交尺寸、宿主身份保持,面板中央正常交互。;macOS 保留宿主圆角需在明暗主题、侧栏顶部与移动后、项目辉光改变、渐变开关及背景图切换时,与原工作台树四角像素一致(WorkbenchSurfaceBackgroundTests.retainedPaneCornersMatchWorkbenchGradient)。 Windows HEAD 按 Community HeadNodePainter 核对外圈、背景间隔和中心实点:26px 行高、100% 缩放时三层直径为 13/9/5px,125%/150%/200%/300% 按物理像素 FLOOR/ODD 对齐;深浅主题、悬停、当前分支及活动/非活动选择均透出真实行底色,穿过 HEAD 的连线不侵入间隔。普通节点及分叉、筛选、长边展开保持;同边双向终止元素键唯一。目标 WebView2 实机验收保持 pending。 macOS 在明暗主题的窄 Git Log 工具栏中选择长用户名、分支和路径,确认名称与选中值以同一行尾部截断,文字不重叠,清除按钮和重新选择仍可用。 macOS Git Log 接近底部时自动追加较早提交,正常浏览不出现必须点击的加载更多按钮;单一页面最多发起一次边缘请求,追加保留选择和原生视口偏移。无新提交或缺失后续游标时停止并释放游标;失败暂停自动读取,显式刷新重开历史而不重用失败游标。运行 GitGraphInteractionTests.automaticHistoryPaging 与 GitModuleTests.continuousHistoryPagingStopsOnDuplicatePagesAndRefreshesAfterFailure,真实大仓库连续滚动和 Windows WebView2 验收仍 pending。 macOS 深层历史后台刷新:文件状态变化不重开首批,元数据刷新保留已加载窗口和后续页占位,未变结果不重建提交图;外部新提交按可见 hash 与行内偏移保持原生视口,按新行位移补齐旧尾部,保留尾部选择。当前仓库引用只读取一次,多仓库仍独立加载。后台刷新打断自动分页后,即使历史相同也能继续读取新游标;失败仍暂停自动重试。运行 GitModuleTests.backgroundRefreshRetainsDeepHistoryAndSelectionWhileExternalCommitsStillAppear、引用聚合回归及 GitGraphInteractionTests.backgroundHistoryRefreshAnchorsVisibleCommitInsteadOfSelectedCommit/automaticHistoryPaging;真实大仓库长时间滚动仍待用户验收,原生进程 cleanup 超时未独立复现,不将减少重复查询等同于修复所有进程失败。 macOS #1542:显示保留的 Git Log/终端时,横纵分隔条中央及两侧偏移 4pt 仍经窗口事件完成拖动且只提交一次,宿主身份不变;根命中查询深度始终为 1,面板内部连续 100 次查询不产生额外根查询,鼠标移动不回查祖先 SwiftUI。验证禁用、收起(含嵌套分隔条)、恢复、卸载、跨窗口与登记表隔离、实时移动/裁剪、隐藏/透明和弱引用释放;保留宿主内部仍正常命中,外层边缘优先。Diff 两端收起后在普通与保留侧栏下均能经窗口拖回,不触发外层拖动。运行 GitLogReferenceResizeTests、SplitHandleHitTestTests 和 DiffScrollSynchronizationTests;旧实现先出现嵌套命中断言失败,新实现通过。较新 macOS 原生组件回归不替代 macOS 13 Intel 原报告崩溃验收,目标环境验证保持 pending。", + "verification": "分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;macOS Git Log 工具窗口标签悬停有反馈,点击窗口内外时选中标签背景在活动色与非活动色之间切换,标签文字保持相同颜色与常规字重,按下时不变暗;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端使用 Community 完整颜色 ID 及明暗主题 RGB 规则;macOS Git Log 不再显示单独的 Fetch/Compare/Checkout/Cherry-pick 横向操作栏,分支树与竖向工具栏分离;检查 Fetch、分支比较、分支右键 Checkout、提交右键 Cherry-pick 和 Fetch 设置仍可用,分支搜索可筛选嵌套路径,并隐藏无匹配项的分组、显示无结果提示;定位按钮滚动到当前所选分支的首个可见提交而不重新加载历史;Git Log 的分支和提交搜索框复用 全局唯一的 LitheSearchFieldStyle,对照 IDEA SearchFieldWithExtension,核对 13pt 常规提示文字、Islands 明暗控件底色与边框色、36pt 外框内的 30pt 可见高度、3pt 外层留白、1pt 常态内描边、向外扩 1pt 的 2pt 聚焦描边及 4pt 圆角;图标与文字间距为 2pt,输入框清除按钮复用已有 16pt IDEA SVG 和 20pt 按钮区域,按上游预留清除图标宽度,移除分支搜索框重复外层水平内边距,提交搜索框按 15 列字体度量和 150pt 最小宽度布局,Search 与 Database 搜索输入框使用相同共享样式;竖栏更新按钮采用 IDEA ExpUI 的明暗 update 图标,按钮与侧边栏图标悬停使用共用工具提示样式,提示的明暗背景、文字色、1pt 内描边、圆角及内边距对齐 IDEA HelpTooltip;隐藏与显示分支使用 IDEA ExpUI 的 chevronLeft/chevronRight 明暗图标;竖栏宽 36pt,展开态按钮为 22pt 且四周保留工具栏内边距,展开态分支树左边框复用与 Commit 标题下边框同源的 1pt 分隔线;收缩态竖栏常态复用展开态侧栏背景,展开与收缩保留提交图谱和详情视图实例、滚动状态及分支宽度,仅切换分支面板与分隔条的占用宽度,按钮占满竖栏,深色 hover 使用 Islands Dark 的 #FFFFFF17,浅色 hover 使用 ExpUI Gray12,不在右侧显示 Show Branches 浮层提示,图标在顶部留 8pt 并使用常规小字号文字;展开后恢复分支树宽度。 使用原生 NSHostingView 渲染验证共享 LitheSearchTextField 的占位文字在深浅主题均为 IDEA Component.infoForeground 对应的 #73767C、13pt 常规字重;空输入框单独绘制占位文字,避免 SwiftUI 原生 prompt 替换其颜色与字重,有内容时占位文字消失并保留原生编辑和主文字色。 Git Log 分支树按 IDEA Community Islands 普通密度核对 24pt 行高、无行间额外间距、13pt 常规文字、12pt 次要计数与 4/12pt 树边距;展开箭头、路径文件夹、普通分支、当前分支和标签使用上游 16pt 明暗 SVG,Local/Remote/Tags 标题不附加图标,当前分支不显示额外蓝色勾选;HEAD 与当前分支是两个互斥选中节点,点击分支、HEAD、清除筛选和切换仓库时核对选中状态;深浅主题核对树行焦点/失焦选中色及 hover,保留多仓库名称、圆点、数量和只读菜单,移除分支行仓库色条;Windows 多仓库分支树的仓库行与 macOS 一致,仓库行使用中性色文件夹图标,并只在图标与名称之间显示 8px 仓库色点,仓库名保持普通文字色,不出现整组着色卡片底色、左侧色条或彩色名称,截断名称通过共享工具提示显示全名,行悬停为箭头光标;确认搜索、分组折叠、右键操作、缓存行比较和滚动状态正常。 Git Log 分支树右侧分隔线与 Commit 标题下边框共用 LitheTheme.toolWindowBorder 和 1pt 厚度,深色为 #26282C、浅色为 #E9EAEE;悬停、拖动开始/移动/结束均保持同色,不高亮,保留原生调整光标、命中区域、拖动尺寸约束及提交宽度行为。 Git Log 中间栏按 IDEA Community FilterComponent、VcsLogGraphTable 和 PaintParameters 核对四个筛选器的 13pt 常规文字、4pt 内边距、16pt 箭头和仅文字悬停反馈;工具栏使用 16pt IDEA 明暗 SVG、22pt 点击区域和共享提示,保留动作及禁用条件;提交列表为 26pt 连续行,图谱尺寸同比缩放,作者与日期同为 13pt 主文字色,日期列按打包字体度量,核对深浅主题行 hover、焦点/失焦选中色及选中覆盖 hover,筛选、分页、箭头命中、上下键导航和右键操作不回归。 分支竖栏设置齿轮复用 LitheIconButtonStyle 的 22pt 点击区域、4pt 圆角和主题 hover/按下背景;点击使用 LitheContextMenuPresenter 的共享操作菜单背景、1pt 边框、行 hover 与键盘导航,保留 Fetch Options 对话框、Show Worktree Repositories 勾选和无工作树时禁用行为,点击外部或按 Esc 关闭菜单。 macOS 下拉菜单仅保留 Project 同源的一套全局共享样式,移除 settingsStyle 切换;Project、Git Log 设置、Terminal、欢迎页及右键子菜单统一使用 8pt 外框圆角、1pt 内描边、24pt 行高、12.5pt 文字、6pt 外留白和 8pt 行内边距,设置选择下拉框共享同一外框与尺寸定义; Git Log 顶部按 IDEA 恢复 Branch: 已选分支名称,保留 Branch 筛选、清除和菜单内勾选;Branch、User、Date、Paths 按 IDEA FilterComponent 保留 4pt 内边距、3pt 箭头间距和 13pt 常规文字,四项文字 hover 为白色且不增加底色。Date 使用共享菜单呈现器,Branch/User 的可搜索列表与分组子菜单使用共享 LitheDropdownRowStyle、24pt 行高和共享菜单外框,Paths 输入面板复用共享菜单外框及搜索输入框样式,保留搜索、选项选中、路径 Apply/Cancel/Clear 和时间范围计算。 Branch/User/Paths 原生弹窗锚点不参与鼠标命中,核对按钮与文字收到 hover/点击、菜单正确弹出且关闭后可再次打开;Date 保持共享菜单呈现器。共享下拉外框裁剪内部内容,明暗主题下 Branch/User 搜索条不能覆盖圆角;Paths 输入框复用共享输入样式并显示正确焦点边框,Clear/Cancel/Apply 复用共享次按钮/主按钮,保留路径应用和清除行为。Branch、User、Paths 的可搜索内容同样使用 LitheContextMenuPresenter 的透明无边框面板,去掉 NSPopover 系统外框,复用 Project 共享背景、1pt 边框和圆角;分组展开与搜索动态调整弹窗尺寸,Esc、点击外部和切换菜单正确关闭。选择分支后清除筛选,确认 Branch 恢复默认标题且关闭按钮消失;关闭按钮保留 16pt closeSmall 明暗 SVG、22pt hover 区域及 4pt 圆角。展开分支树左侧固定线、中间分隔线和右侧详情拖动线均使用 toolWindowBorder 的 1pt 边框,右侧 hover 与拖动不高亮。右侧详情展示图标使用 IDEA previewVertically 明暗 SVG,装饰开关使用 show 眼睛而非 hide 工具窗口减号;所有工具栏展示图标复用 22pt、4pt 圆角的共享 hover 背景,保留原动作与提示。深浅主题渲染核对背景和边框,保留可选图标、勾选、快捷键、禁用、子菜单和键盘导航。 Git Log 提交标题、作者和日期使用 Inter 13 Normal,引用标签 12pt;核对至少 26pt 的字体度量行高、含 leading 的整数基线、长标题与中文/emoji 截断,以及选中合并提交的文字颜色。 所选分支存在时 Log 标签显示 Log: 分支短名(包含 codex/ 路径),选择另一分支立即更新;清除分支筛选后恢复 Log,保留本地化标题、原始分支名和原标签选择/焦点行为。 macOS 标题起点按缩放图谱列距、图文间距和 2pt 文字内边距计算,为对齐参考截图至少预留六列,密集行继续扩宽,不改变图谱节点、连线和箭头命中;在 SwiftUI/AppKit 两条渲染路径核对未选中的合并提交标题、作者、日期统一使用合并灰色,选中后恢复普通文字色,合并判定仅依赖至少两个父节点。 Git Log 右侧文件树使用共享 24pt 行高、19pt 层级缩进、常规字重、文件类型图标和 IDEA FileStatus 文件名颜色,无逐行横线及工具栏重复总数;核对文件夹折叠、文件打开 diff、长名称 hover 展开与横向滚动,拖动宽度不重建行内容。提交详情使用 14pt 外边距、10pt 内间距、等宽 bold 标题、同段作者邮箱时间与引用标签,长内容可滚动;文件树/详情之间分隔线复用共享边框,hover/拖动不高亮。左侧分支树从 180pt 专用下限改为共享 30pt 下限,独立 36pt 竖栏保持原宽,窄树裁剪后可拖回原宽,收缩/展开不重建图谱。 图谱圆点、连线、箭头按 SimpleGraphCellPainter/ PaintUtil 的 FLOOR/ODD 物理像素规则对齐;在 1×/2× 原生位图验证圆点与线宽,在真实 NSHostingView 检查对齐后的上下斜向箭头尖端及内侧描边可导航,切换屏幕只重绘且绘制/命中坐标一致。 时间列复用 Inter 13 常规字体的等宽数字与固定 AM/PM 区域,按应用语言使用英文 12h AM/PM 或中文 24h 格式;核对午夜、正午、未知日期回退、行起点一致及切换语言后的原生日期缓存失效,列表两条渲染路径和提交详情使用相同格式。 连续上下拖动 Git Log,包含上方长 Diff:原生列表只调整可见区域,保持文档身份、滚动位置、Shift/Command 多选、焦点色、键盘与箭头导航、分页和辅助功能;收缩再恢复后列表宽高正确。GitGraphInteractionTests 使用 300 条提交、80 次原生尺寸变化和共享分隔条事件验证,局部新旧路径计时作为诊断,不代替目标工作区帧率验证。 共享分隔容器按已有宽高直接分配两个面板与分隔条的确定矩形,避免 HStack/VStack 最小及理想尺寸探测;splitPaneBounds 覆盖横纵方向、leading/trailing、折叠、子视图身份和有限尺寸,达到尺寸限制后的相同结果不重复写状态。 通过键盘或程序化切换焦点时 Git 标签活动态双向更新;挂载或重新显示 Git Log 不主动抢走编辑器焦点。 在包含展开分支树的 Git Log 中连续拖动外层高度和内部宽度:共享分隔器只在拖动开始/结束传播环境状态,嵌套容器继承祖先状态;拖动期间共享行悬停不更新、提示不登记几何锚点、右键覆盖层暂停,松开后恢复菜单和提示,行内容及滚动状态不重挂载。69 行原生分隔条事件回归检查覆盖层暂停/恢复及内容身份;未悬停控件在平时同样不登记提示锚点。用目标工作区 Time Profiler/Animation Hitches 对照 Terminal 核对持续更新成本及起止开销,split.drag 整次手势时长不能当作单帧延迟;本次仅前端共享交互装饰调整,不修改后端及默认详情高度行为。 中文输入法组词期间,即使绑定文本仍为空,共享 LitheSearchTextField 占位文字立即隐藏;取消组词或删空后恢复,确认中文后仍隐藏。原生 setMarkedText 回归覆盖明暗主题和两个输入框互不影响;保留焦点、键盘提交、搜索/过滤语义及占位字号颜色。原生背景观察视图卸载和移出窗口时清理通知,匹配当前窗口的字段编辑器及所属输入区域,不修改业务绑定。 四个筛选弹窗均以各自按钮左下角为定位锚点,Date 不依赖鼠标点击坐标;空间不足时限制在屏幕可见范围内。原生面板回归核对操作菜单/可搜索内容相同锚点下的左上角、无展开动画和 Esc 关闭回调。共享搜索字段在未聚焦时悬停也显示文本编辑光标,离开或禁用时恢复箭头。 Git Console 左侧六个现有动作使用 Community 原始 16pt 明暗 SVG;22pt 背景、竖向 2/1 按钮留白、5/7 外边距和默认箭头光标。核对查找、软换行、跟随、停止、清空、复制的原有动作与禁用状态,组件资源检查不替代完整应用验收。 按 Community CurrentBranchHighlighter 标出当前本地分支可达的所有父提交,包含合并父节点;深色 #1D2336、浅色 #EDF3FF,选中背景优先,仅已加载图谱可证明的祖先被标记,当前分支/HEAD 单独筛选时不重复染色。引用默认显示并按 GitRefManager/SimpleRefGroup 紧凑合组:本地与上游同提交显示 remote & branch,HEAD/本地/远端/标签使用官方黄/绿/紫/灰轮廓及叠层,标签单独存在时默认只显示图标;长名称按列宽和 22 字符下限缩写,原生可见区 tooltip 保留所有完整名称,滚动后按当前鼠标位置解析。明暗原生渲染检查颜色、宽度、选择优先级及完整名称;GitLogReferenceResizeTests 在真实 GitLogView 中加载 1000 个分支并拖动外层高度,检查可见右键覆盖层数量和挂载生命周期,计时仅作诊断。最新正式预览已进入该工作树,观察全部引用下的深色背景、叠层缩写标签和多次上边缘拖动后的布局恢复;用户随后在本机明确确认“现在不卡顿了”,本次报告的高度拖动卡顿通过用户实机验收;组件计时和混合工作区采样不用于推断唯一根因或量化通用帧延迟,其他主题/悬浮等视觉项继续按各自范围验收。 在 Windows Git Log 选中带多行正文的提交,确认右侧详情在标题下方完整显示正文(保留空行与缩进),复制提交信息和打开提交 Diff 时也包含正文;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端使用 Community 完整颜色 ID 及明暗主题 RGB 规则。 读取提交正文期间切换仓库、重新打开另一提交或关闭面板,确认迟到结果不会打开旧 Diff 或覆盖新请求的加载状态。 Windows 另验证:单击提交后编辑区预览该提交第一个文件的 diff;单击或双击 Commit files 中的文件只显示该文件的 diff(不含提交说明,也不显示文件列表),并复用同一个预览标签;Commit files 树中过长的文件或目录名不折叠为省略号,区域随最宽行横向展开,可通过横向滚动条或触控板侧滑左右滑动查看完整名称(与源代码管理变更树一致);Git Log 刚打开、尚未点击提交时不会自动打开 diff;日期列将有 UTC 偏移的提交转换到本机时区后显示相对时间(刚刚、N 分钟前、今天/昨天);英文绝对日期和时间通过 Windows 原生接口遵循系统短日期/时间及用户自定义格式,中文使用中文地区格式,未知偏移仍显示绝对日期;验证美式月/日/年、英式日/月/年、自定义分隔符、12/24 小时制、跨时区相对时间和迟到原生结果保护,真实 WebView2 产品验收为 pending;鼠标移入行时刷新相对时间;作者/日期列可在行内列边缘拖动调整宽度并在重启后保留。 非连续多选提交修改同一路径时,预览保留每个提交的 diff 和提交标识,可通过文件导航分别查看;延迟请求返回前取消选择、隐藏 Git Log、切换仓库或卸载工具窗,确认旧请求不会重新打开或激活预览标签。 Windows 端提交历史仅通过 Git Log 工具窗浏览:源代码管理侧栏入口已更名为 Commit 面板,其 History 标签已移除,面板固定显示变更列表与提交区。命令面板中的 Git: Show History 入口同步移除。 Windows Commit Files 单文件预览使用独立页面:上下箭头导航差异,下箭头在当前文件最后一处后继续进入下一文件并在比较完成后定位第一处差异,整个文件集合末尾禁用且不循环;图片、二进制和无文本差异文件也允许下箭头继续,比较未就绪时禁用以防连续点击跳过文件。左右箭头直接切换当前比较集合的上一文件/下一文件;标签使用 IDEA 的 Repository Diff: 文件名并跟随导航更新,不附加提交 hash。Jump to Source 按最后聚焦侧的补丁位置打开预览所属仓库的文件,重命名使用新路径、删除行映射邻近保留行、已删除文件禁用源跳转;模型更新期间禁用差异动作。保留并排/统一视图、空白显示、Monaco 查找、图片和二进制呈现,普通全量提交比较仍使用多文件页面。真实 WebView2 内核及源文件打开还需目标产品运行验收。 Windows 单文件历史 Diff 外观验证:并排视图使用独立外侧 12px 滚动轨道、8px 滑块和概览,通常的统一视图保留 Monaco 原生 Diff 组件,整文件新增使用真实新行流的独立只读控件,深浅主题检查常态/悬停/拖动色、圆角及边框;实际拖动任一并排滑块后按差异边界同步对应行且模型身份保持。工具栏使用现有 Button,核对 40px 高度和 22px 图标按钮;保留用户字体与字号,默认行距按 14px 字号对应 22px 并随缩放,自定义行距不覆盖。词级高亮开关复用现有 Monaco 装饰能力,关闭只隐藏字符差异、保留整行差异,切换文件保留词级和空白开关。旧/新版本标题显示真实第一父提交、范围基线/目标或非连续提交各自身份,根提交旧侧为空树,重命名显示旧/新路径;短 hash 和截断路径的 tooltip 保留全文。并排标题列宽由独立布局所有者更新 CSS,拖动分隔条时标题跟随且不重建模型;统一视图标题纵向排列。以上浏览器真实组件验证不替代目标 WebView2 产品验收。 Windows 单文件 Diff 滚动条回归:两侧横纵滚动条默认显示,鼠标移到工具栏后仍可见,不依赖悬停;差异概览不绘制重复的宽矩形视口指示,并排由独立外侧轨道负责点击、拖动和滚轮,统一视图保留原生委托。明暗主题、并排/统一视图及长文本双向溢出下检查唯一可见滑块、并排横向同像素偏移同步、纵向对应行同步和模型身份。 历史 Diff 两侧滚动轨道与代码编辑器概览画布背景透明,鼠标不悬停时滑块仍显示;关闭代码编辑器重复概览通道,但保留 Diff 专用差异色条。横向轨道不额外增加内容高度,检查代码能显示在滑块后、底部内容与滚动位置正确,拖动命中保留,并排两侧横向同像素偏移同步、纵向对应行同步。 Windows 并排单文件 Diff 使用两个独立只读 Monaco 代码控件,取消 DiffEditor 的补空白对齐。左右横向滚动按同一像素偏移双向同步,较短侧由 Monaco 钳制到真实内容边界且不反向拉回主动侧;纵向原生代码区、外侧滑块拖动/点击、轨道键盘、滚轮和查找以视口三分之一处为锚点,按 Git 差异块起止边界同步对应行并保留行内偏移,短侧在不等长块内钳制、共同上下文恢复对齐。同步有重入与微小方向保护;双模型替换、布局、字体配置和显式上下导航期间暂停同步。新增旧侧/删除新侧画横跨可见代码区的双像素零行边界,修改区画上下单像素边界;两列行号背景为直边,曲线只在两列之间弯曲,实际偏移改变端点形状。检查文件开头/末尾零行、三态、明暗主题和窄列,代码不补空白、不扩展横向范围;连接面覆盖两列历史行号背景,行号选择和源映射正确。左右外侧各显示透明常显纵向滑块与增删改概览,横向保留原生滚动条,滚动范围截止真实最长代码行。验证深浅主题、折叠展开、Ctrl/Cmd+F 搜索折叠文本、词级开关和预算、统一视图及跨文件首处定位、连续拖动和窄窗口非负尺寸。高频帧二分筛选可见差异,缓存像素边界及概览,不扫描全文、不更新 React;卸载和迟到语言准备清理所有帧、监听、观察器、拖动、两个编辑器和模型。浏览器生产组件验证使用平台记录器,真实 WebView2 产品验收仍为 pending。 外侧轨道预留独立空间,不显示代码;左轨道距面板边缘、右轨道距代码视口各留 4px 间隙,窄列随可用宽度缩减,右侧文字与轨道不重叠;并排和统一预览关闭缩进及括号引导线,保留语法颜色;中央行号缩小边距和曲线间隙,整数布局边界及上下半像素描边消除与代码区的垂直接缝。滚轮在代码、行号和轨道统一加快到两倍,拖动仍映射各侧真实进度,纵向同步对应行;方向键按用户行高移动。有词级结果时修改正文底色按 40% 修改色与 60% 页面背景混合,完整修改色突出替换词的两侧,纯新增和纯删除词分别使用完整增删色,语法前景色和文字透明度保持;关闭词级或超限保留整行底色。验证三态与开头末尾、双向对应行同步、明暗主题、轨道无遮挡、宽窄字体布局和预算回退。 单文件并排及统一视图隐藏补丁 @@ 标题,但保留真正以 @@ 开头的源码、行 ID、源行映射和原始补丁;默认多文件与暂存投影不变。Islands Dark 继承 Darcula 背景,Islands Light 使用 Light 方案,独立差异轨道标记与连接、代码边界共用对应差异背景色。底部保留五个用户实际行高的 padding,模型不增加假行;从任意侧滚到底部验证共同末行对齐与五行留白,字号/行距变化同步更新。 Windows 单文件 Diff 首次打开、左右切文件和同路径新比较均在模型/比较准备完成后定位第一处改动;定位只消费一次,调整词级、空白、字号或主题保持用户当前位置。统一视图的原生 Monaco 增删行、字符与行号栏使用该预览相同的 Islands 背景变量,移除默认字符边框;新增整文件应为深色 #294436 或浅色 #BEE6BE,删除为 #484A4A 或 #D6D6D6,语法前景色不变且其他编辑器主题不被覆盖。 整文件新增采用单流只读代码控件,隐藏不存在的旧侧与连接带,新增背景覆盖行号、代码和底部留白,避免空旧模型的灰色删除行及外圈;新增仅显示新版本、删除仅显示旧版本的一组 hash 与路径。并排两侧及新增单流轨道的增删改标记与对应内容背景共用颜色;原生统一预览关闭 renderIndicators,历史行号旁不显示 +/-。这组新增显示修复按用户要求延后运行验证,提交或 PR 前核对新增文件、普通修改、删除、深浅主题、查找、源跳转与底部留白。 Windows Git Log 单文件和 Commit 文件 Diff 的词级高亮、显示空白符、统一/并排切换均在左侧,最右侧统计当前文件差异,区分无差异/单数/复数;Commit 勾选、部分包含、取消及刷新后 included 数同步变化,全部包含仍显示数量;加载、切换文件及图片/二进制不显示文本差异统计。核对中英文、明暗主题、窄窗口和按钮选中/禁用状态。 Windows 单文件 Diff 上箭头在首处继续进入上一文件最后一处,下箭头在末处进入下一文件首处,集合首尾不循环、加载中禁用;Commit 文件列表随跨文件导航更新选中行并展开祖先、滚动到可见位置,保留编辑器焦点和 include 状态;同文件刷新、历史 Diff、无关仓库和迟到读取不扰动列表。 Windows Git Log 右下角 Commit files 列表同样跟随 Diff 的上一/下一文件导航:选中行移到当前文件,被折叠的父目录自动展开,行滚动到可见位置;预览属于其他提交或范围时不改变选中,点击行不会改变横向滚动。 Windows Git Log 筛选后基于完整已加载历史投影可见祖先:隐藏中间提交以虚线连接,直接父子保持实线,无关分支不误连;合并路径去重并保留永久颜色,未加载父提交经隐藏节点传播,分页补齐后解除缺失标记;清空筛选恢复原图,核对文本、作者、分支名称三种范围、空结果与选择行为。算法和真实表格组件回归通过不代替 WebView2 原生验收,原生验证保持 pending。 Windows 未筛选图与筛选图共用上下半边打印元素:普通浏览的跨行泳道换列必须在共同中点连续连接,合流入边保留各自颜色到提交圆点;核对原始 IDEA 200 条历史输出的列号、方向、终止和颜色身份;截图/原生交互验收仍为 pending,组件输出和 golden 对照不代替 WebView2 验收。 Windows Git Graph 外观:核对明暗主题真实 IDEA RGB、实心节点、直线与筛选虚线、终止箭头及至少六列标题预留;默认 26px 行高,用户字号变化时虚拟行与 SVG 同步扩高;在 100%/125%/150%/200%/300% 缩放及跨屏 DPI 变化下核对奇数物理像素和相邻半边连续性,普通视口调整不重建历史图。箭头能跳到已加载且可见的父/子提交,未加载父节点没有可点击目标;浏览器生产组件验证不代替目标 WebView2 运行验收,原生验证保持 pending。 Windows 历史排序和图导航:在交错分支仓库核对首批/分页/回退及全引用上下文均使用日期顺序,切换分支保持可证明的永久颜色和基础顺序;上下文最多 5000 条,覆盖不完整或哈希重复时保留可见页并回退。核对 upstream/backup 等非 origin 远程引用分类及本地含斜杠分支;背景上下文不阻塞 50 条可见页,分页不取消或重读上下文,切仓库/刷新/卸载取消旧操作且关闭迟到游标。长边开关展开/收起不改变提交、筛选、选择和游标;验证 30/1000 行边界。点击或用 Enter/空格激活上下箭头,选择并滚动到屏幕外端点,挂载后键盘焦点随目标;不误选源行、不误开源行 Diff,保持普通选择/多选/双击/右键和原预览入口。明暗主题及 1x/1.25x/2x 检查箭头尖端与命中范围,真实 WebView2 运行验收保持 pending。 图谱颜色必须跟随 ThemeRegistry 根 data-theme-type 切换;不依赖页面存在 dark class。复用真实 Lithe 明暗主题检查同一已挂载节点与连线的颜色变化、布局不因主题重算,结合主题回归和浏览器真实切换验证。 Windows 分页稳定性:选中顶部提交后向下滚动触发连续分页,确认滚动位置不跳回已选提交,Commit Files 不清空或重读,已选文件及树折叠保持;更新引用、背景上下文和相同 hash 的元数据同样不导航。定位分支头通过独立请求仍能定位已经选中的头提交一次;新选择或切仓库仍读取新文件并丢弃旧结果。组件回归与真实浏览器虚拟列表/IntersectionObserver 验证通过,目标 WebView2 实机验收保持 pending。 Windows Git Log 标题栏不显示固定 Read-only/只读文字,关闭按钮保持靠右,刷新、设置及隐藏操作正常。 Windows Git Log 单个提交右键 New Tag… 输入非空且无空白的标签名,创建指向右键提交完整 hash 的轻量标签并刷新引用和历史;取消不写入,失败保留名称可重试,双击/Enter 不重复创建,多选和写操作期间禁用。仓库或工作区切换关闭旧弹窗,旧请求迟到不得刷新或关闭新弹窗。 Windows 提交区域点击后上下箭头更新可见提交选择和详情,焦点不随虚拟行移除而丢失;筛选、首尾边界、Home/End、Shift 范围选择和 Enter 正常,菜单与输入框保留自己的方向键。 Windows Git Log 鼠标或上下键选择提交后,异步 Diff 自动定位第一处改动但保持提交列表键盘焦点;并排与统一预览均不调用初始 focus。双击/Enter 打开完整 Diff、主动差异跳转及跨文件导航仍进入编辑器。 Windows Git Log 提交右键 New Tag 使用与 Tags 节点同源的 TagIcon(沿用现有标签轮廓图案),核对 16px 图标、菜单对齐和禁用反馈;新补图标的实机视觉确认待完成。 Windows 提交列表菜单键和 Shift+F10 打开当前选中提交的真实菜单;关闭后列表继续上下键导航,菜单操作仍针对所选提交。New Tag 在 tag.gpgSign=true 下生成轻量标签,不运行签名器或消息编辑器,不修改配置。 Windows 提交行高亮和紧凑引用组:在全部引用和其他分支视图核对当前检出分支全父祖先背景,当前分支/HEAD 视图关闭重复高亮;合并提交的说明、作者、日期未选中时弱化、选中后恢复,选中背景优先于分支并区分列表/文件树/窗口焦点。核对明暗主题固定基准色、真实 upstream 合并名称、游离 HEAD、每种最多两个叠加图标、仅标签图标和完整汇总悬停/无障碍提示。宽窄列及字号变化时按至少 22 字符缩略规则布局,只有一个视口尺寸观察器,列拖动不逐行读取宽度,卸载清理;分页不丢滚动/选择,成功后不重读 Commit Files;文件读取失败时 Commit Files 面板显示重试入口,重新选择同一提交或刷新历史也会重试一次,长边导航和新建标签原入口保持。组件与浏览器验证通过,目标 WebView2 实机验收 pending。 Windows Git Log 分支树和提交列表的普通滚轮使用 Community Windows 默认 200ms 和 (0.5,0.505,0.5,1) 曲线;连续同向累积、反向从当前位置重启。核对两处列表的中间帧、Shift 横向滚动、分页保持、顶底边界、键盘定位和滑块拖动取消惯性、关闭工具窗清理帧以及减少动态效果偏好。确定性动画与 DOM 回归通过,真实 WebView2 设备手感验收保持 pending。 Windows graph 标题不再固定预留六列:建议宽度取可见图边数的加权均值与标准差,最多以六列作统一基线,每行按节点与斜边中点扩展,密集行不把其他简单行扩成最大列宽。验证单线/少分支、罕见密集合并、超过六列、筛选、长边开关、引用标签可用宽度及字号/DPI,保持图谱颜色/连线/导航不变。 Windows Git Log 的 Commit Files 文件树沿用 IDEA Windows 默认 200ms 滚轮动画;在文件行、竖向/横向滚动条区域连续滚动、反向及 Shift 横向滚动,确认共用真实视口的动画目标,长文件名保持自然宽度;按下滚动条取消未完成惯性,减少动态效果时直接滚动,切换提交及卸载不残留帧。其它 ScrollArea 默认行为不变。真实 WebView2 交互验收 pending。 Windows Git Log 右键 New Tag 打开弹窗后焦点直接位于标签名输入框,无需点击即可输入,Enter 提交、Esc 关闭。 macOS Git Log 空状态:在非 Git 项目检查明暗主题、窄窗口和键盘操作:13pt 常规文字按 IDEA 左对齐,整组位于内容区上方三分之一处;显示 To track changes to code、GitHub/GitLab 官网链接、Initialize Git repository…、Open Git Settings 和带原始 ContextHelp 图标的 Version control integration(打开 Git 官方文档)。初始化仍先确认,不自动暂存或提交;设置打开既有 Git 设置。未提交仓库、无匹配提交、加载失败及重试保持原动作,所有空状态操作采用文字按钮。 非 Git 项目空状态隐藏 Log/Worktrees/Console 标签、添加按钮、分支树与动作条、筛选/搜索工具栏及提交详情,提示占满工具窗内容区;GitHub、GitLab、初始化、设置及帮助链接悬停仅给当前文字加主题强调色下划线并显示手形,离开后恢复;初始化或切换至真实仓库后恢复原有三栏布局,真实仓库无匹配提交仍保留筛选入口。 空状态字体与间距对照 Community c7f91397:按钮标签显式使用 Inter 13pt Regular;行首破折号按首行基线对齐,21pt 行高加 2pt 间隔,帮助图标间隔 2pt;灰字与链接分别沿 Component.infoForeground 和 Islands Link.activeForeground 核对明暗主题,悬停下划线仍使用当前主题强调色。 macOS 右侧提交详情的分支标签按可见区域懒加载,全部引用仍可滚动查看。重开面板初始化时恢复匹配的提交图和分支树,第一次布局即可读取全部已缓存数据,不依赖出现后的异步任务回填。重开面板复用未变化的分支树,单仓库不重复构建分组副本,多仓库活动根复用已有三类行;引用、仓库、工作树开关、折叠状态改变后拒绝旧树。原生绘制与无障碍日期文本复用缓存,语言改变后刷新。 macOS 大量引用首屏:同提交放置 1,500 个本地分支和 1,500 个远程引用,核对真实 upstream 优先、同名远程自然排序回退、完整提示及每类图标上限;反复切换 Terminal/Git Log,数据版本相同应复用已准备图,改变分支/筛选/图显示选项后不得复用旧图。运行 GitGraphInteractionTests 的 manyReferenceGroups 和 presentationCache;组件耗时仅诊断,整机卡顿以相同数据实测为准。 macOS Git Log 及终端按需创建后由工作台持有实际原生宿主;隐藏再重开及移动停靠位置复用控件。运行 GitLogReferenceResizeTests,验证同一工作台隐藏再重开时,固定原生表面上的提交文档和滚动容器对象身份不变,恢复可见性且尺寸正确,工作台释放后宿主释放;核对语言、主题、搜索、分支操作和终端会话。 Git Log 与终端同时停靠时,从外层横向和纵向分隔条中央及向面板两侧偏移 4pt 处,经窗口发送按下、拖动、松开,确认完整 10pt 命中区仍能提交尺寸、宿主身份保持,面板中央正常交互。;macOS 保留宿主圆角需在明暗主题、侧栏顶部与移动后、项目辉光改变、渐变开关及背景图切换时,与原工作台树四角像素一致(WorkbenchSurfaceBackgroundTests.retainedPaneCornersMatchWorkbenchGradient)。 Windows HEAD 按 Community HeadNodePainter 核对外圈、背景间隔和中心实点:26px 行高、100% 缩放时三层直径为 13/9/5px,125%/150%/200%/300% 按物理像素 FLOOR/ODD 对齐;深浅主题、悬停、当前分支及活动/非活动选择均透出真实行底色,穿过 HEAD 的连线不侵入间隔。普通节点及分叉、筛选、长边展开保持;同边双向终止元素键唯一。目标 WebView2 实机验收保持 pending。 macOS 在明暗主题的窄 Git Log 工具栏中选择长用户名、分支和路径,确认名称与选中值以同一行尾部截断,文字不重叠,清除按钮和重新选择仍可用。 macOS Git Log 接近底部时自动追加较早提交,正常浏览不出现必须点击的加载更多按钮;单一页面最多发起一次边缘请求,追加保留选择和原生视口偏移。无新提交或缺失后续游标时停止并释放游标;失败暂停自动读取,显式刷新重开历史而不重用失败游标。运行 GitGraphInteractionTests.automaticHistoryPaging 与 GitModuleTests.continuousHistoryPagingStopsOnDuplicatePagesAndRefreshesAfterFailure,真实大仓库连续滚动和 Windows WebView2 验收仍 pending。 macOS 深层历史后台刷新:文件状态变化不重开首批,元数据刷新保留已加载窗口和后续页占位,未变结果不重建提交图;外部新提交按可见 hash 与行内偏移保持原生视口,按新行位移补齐旧尾部,保留尾部选择。当前仓库引用只读取一次,多仓库仍独立加载。后台刷新打断自动分页后,即使历史相同也能继续读取新游标;失败仍暂停自动重试。运行 GitModuleTests.backgroundRefreshRetainsDeepHistoryAndSelectionWhileExternalCommitsStillAppear、引用聚合回归及 GitGraphInteractionTests.backgroundHistoryRefreshAnchorsVisibleCommitInsteadOfSelectedCommit/automaticHistoryPaging;真实大仓库长时间滚动仍待用户验收,原生进程 cleanup 超时未独立复现,不将减少重复查询等同于修复所有进程失败。 macOS #1542:显示保留的 Git Log/终端时,横纵分隔条中央及两侧偏移 4pt 仍经窗口事件完成拖动且只提交一次,宿主身份不变;根命中查询深度始终为 1,面板内部连续 100 次查询不产生额外根查询,鼠标移动不回查祖先 SwiftUI。验证禁用、收起(含嵌套分隔条)、恢复、卸载、跨窗口与登记表隔离、实时移动/裁剪、隐藏/透明和弱引用释放;保留宿主内部仍正常命中,外层边缘优先。Diff 两端收起后在普通与保留侧栏下均能经窗口拖回,不触发外层拖动。运行 GitLogReferenceResizeTests、SplitHandleHitTestTests 和 DiffScrollSynchronizationTests;旧实现先出现嵌套命中断言失败,新实现通过。较新 macOS 原生组件回归不替代 macOS 13 Intel 原报告崩溃验收,目标环境验证保持 pending。", "notes": "Windows 两处单文件 Diff 共用工具栏:显示选项移到左侧,右端显示 IDEA 差异统计及 Commit included 数;英文文案、单复数和空白符稳定开关名参考 Community 源码。组件回归覆盖统计及开关行为,原生视觉验收 pending。 Windows 上下差异箭头补齐双向跨文件导航;Commit 列表从活动工作区预览的仓库/路径同步行选择和一次性虚拟滚动,沿用原有加载代次保护。组件回归覆盖双向边界、嵌套仓库、折叠展开、刷新及焦点/include 保持,完整原生验收 pending。 Windows 筛选图沿用 macOS/Community 双向最近可见祖先算法和隐藏边界分组;通过统一打印元素使用保留边身份的 SVG 半边绘制,未筛选和筛选共用同一绘制入口。 Windows 移除未筛选 SVG 的泳道数组推断路径,统一消费 printElements;图宽计入斜向边界中点;外观采用 macOS/Community 完整颜色生成、实心点、直线、方向箭头和字号/DPI 绘制尺寸。 Windows Git Log 显式请求 Core 既有 date 顺序,并独立加载有界仓库上下文,在唯一且完整覆盖当前页时复用永久布局;远程引用身份来自真实引用集合。长边默认紧凑,可展开并保留安全上限,箭头透明按钮复用物理像素几何和既有选择/虚拟滚动。焦点由目标 DOM 挂载消费,拒绝替换提交或筛选的旧请求,不轮询。 Windows 将图谱数据重绘与选择/明确导航区分,分页不再触发选择定位;Inspector 按仓库与实际比较身份读取文件,保留文件树和预览代次保护;只有失败的读取可重试(失败身份与当前选择一致时才重试,避免旧失败让新选择重复加载)。 Windows 按 macOS/Community 补齐当前分支范围、合并文字和活动/非活动选择;引用由独立纯分组模型准备,共享视口字体/宽度度量绘制原始 TagPainter 图标,分组不改变真实引用。 Windows Git Log 两处滚轮显式启用共享动画绑定;每帧只更新视口,保留虚拟列表和既有事件换算,其他滚动区默认行为保持。 Windows 移除固定六列最小标题留白,按 Community PrintElementGeneratorImpl 和 GraphCommitCellUtil 的建议宽度及逐行占用混排图形与说明;真实 IDEA oracle 的 Width 输出也参与回归。macOS 保留已有标题留白,本次只调整 Windows。 Windows 连线改用单一连续 SVG,沿完整边身份拼接相邻半边;HEAD 采用 Community 独立尺寸和外圈加实点,裁掉圆内连线使背景间隔不依赖行状态副本。 连续图层只为虚拟列表及上下各一行生成节点、路径和 HEAD 裁剪,SVG 高度随窗口而非全部历史增长,按全局行偏移定位且边身份稳定;5,000 条历史、四行视口及换列分叉滚动边界有确定性回归,空虚拟范围不绘制。真实 WebView2 滚动性能仍待验收。 Git Log 工具栏按 IDEA 调整:文本筛选框缩短为固定宽度,输入后按 Enter 才搜索(清除按钮立即清空,输入法组词期间的 Enter 不触发);在文本筛选右侧新增 Branch 下拉,可搜索并按 Local/远程分组选择分支(不含 Tags),选中分支后右侧出现 × 一键回到 All;展开的下拉在鼠标位于菜单中部时也能用滚轮上下滚动(WebView2 滚轮粘连到被裁剪子元素的问题,由通用 Dropdown 把滚轮应用到自身滚动容器);所选分支按仓库保存,重新打开项目后恢复,分支已被删除时回退为 All。组件回归覆盖 Enter 搜索、下拉选择、按仓库持久化和分组排序,WebView2 实机验收 pending。 Windows 提交详情双栏 diff 的正文首列与中央 tearline 竖线保留 1px 间距(lineDecorationsWidth: 1),文字不再贴线;行号列与连接带几何不变。 macOS 修复 #1342:Git Log Branch/User/Date/Paths 的名称与选中值共用单段文字布局,保留各自颜色,窄宽度统一尾部截断,避免独立 Text 压缩导致重叠。 macOS Git 主日志新增原生边缘自动加载,保留有界后端分页和可见区域绘制;Windows 已有 IntersectionObserver 自动加载入口,本次未改动。 macOS 后台状态与历史上下文刷新分开,元数据检查原子替换已加载窗口,原生视口按可见提交锚定;复用当前仓库引用结果,避免重复读取。" } diff --git a/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx b/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx index 20c97876b..89b1e8d06 100644 --- a/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx +++ b/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx @@ -1,5 +1,5 @@ import type { CSSProperties } from "react"; -import { useMemo, useRef, useState } from "react"; +import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { ThemedFileIcon } from "@/extensions/icon-themes/components/themed-file-icon"; import { useFileTreePresentation, @@ -165,6 +165,28 @@ export function GitCommitFileTree({ [files], ); const [collapsed, setCollapsed] = useState>(new Set()); + // The selection can move without a click here (the diff preview's next/previous file), so a + // newly selected file must not stay hidden inside a folder the user collapsed earlier. + const [revealedPath, setRevealedPath] = useState(null); + if (selectedPath !== revealedPath) { + setRevealedPath(selectedPath); + const normalized = selectedPath?.replace(/\\/g, "/"); + if (normalized && [...collapsed].some((path) => normalized.startsWith(`${path}/`))) { + setCollapsed(new Set([...collapsed].filter((path) => !normalized.startsWith(`${path}/`)))); + } + } + // Vertical-only "nearest" scrolling: a click on a wide row must not jump the horizontal offset. + useLayoutEffect(() => { + const viewport = viewportRef.current; + const row = viewport?.querySelector('[data-sidebar-tree-row][data-active="true"]'); + if (!viewport || !row) return; + const viewportBounds = viewport.getBoundingClientRect(); + const rowBounds = row.getBoundingClientRect(); + if (rowBounds.top < viewportBounds.top) + viewport.scrollTop -= viewportBounds.top - rowBounds.top; + else if (rowBounds.bottom > viewportBounds.bottom) + viewport.scrollTop += rowBounds.bottom - viewportBounds.bottom; + }, [selectedPath]); const toggle = (path: string) => { setCollapsed((current) => { const next = new Set(current); diff --git a/windows/tauri/src/features/git/components/log/git-commit-inspector.test.tsx b/windows/tauri/src/features/git/components/log/git-commit-inspector.test.tsx index 1b0859c84..b4000ba23 100644 --- a/windows/tauri/src/features/git/components/log/git-commit-inspector.test.tsx +++ b/windows/tauri/src/features/git/components/log/git-commit-inspector.test.tsx @@ -1,11 +1,15 @@ import { afterEach, beforeEach, expect, mock, spyOn, test } from "bun:test"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; +import { createPaneContent } from "@/features/editor/stores/buffer-content-factory"; +import { useBufferStore } from "@/features/editor/stores/buffer.store"; import { LocaleProvider } from "@/i18n/locale-provider"; import { installHappyDom } from "@/test-utils/happy-dom"; import * as commitsApi from "../../api/git-commits-api"; import * as diffApi from "../../api/git-diff-api"; -import type { GitCommit, GitCommitFile } from "../../types/git.types"; +import type { GitCommit, GitCommitFile, GitDiff } from "../../types/git.types"; +import { moveCommitFile } from "../../utils/commit-file-diff-navigation"; +import { COMMIT_FILE_PREVIEW_PATH, createCommitFileDiffPreview } from "../../utils/multi-file-diff"; import { GitCommitInspector } from "./git-commit-inspector"; const commit = (hash: string, parents: string[] = []): GitCommit => ({ @@ -218,3 +222,118 @@ test("a pending read is not restarted by repainting, only failures retry", async await render([{ ...commit("tip"), message: "Repainted" }], "C:/fixture", 1); expect(filesSpy).toHaveBeenCalledTimes(1); }); + +test("the file list selection follows the preview when it moves to another file", async () => { + const diff = (path: string): GitDiff => ({ + file_path: path, + is_new: false, + is_deleted: false, + is_renamed: false, + lines: [], + additions: 1, + deletions: 0, + }); + const showPreview = async ( + data: NonNullable>["diffData"], + ) => + act(async () => + useBufferStore.setState({ + activeBufferId: COMMIT_FILE_PREVIEW_PATH, + buffers: [ + createPaneContent(COMMIT_FILE_PREVIEW_PATH, { + type: "diff", + path: COMMIT_FILE_PREVIEW_PATH, + name: "preview", + content: "", + diffData: data, + }), + ], + }), + ); + const selectedRows = () => + [...container.querySelectorAll('[data-sidebar-tree-row][data-active="true"]')].map( + (row) => row.textContent, + ); + const fixture = (hash: string) => + createCommitFileDiffPreview({ + repoPath: "C:/fixture", + commitHash: hash, + diffs: [diff("a.ts"), diff("b.ts")], + filePath: "a.ts", + label: hash, + })!.diffData; + + await render([commit("tip")]); + await act(async () => + requests[0].resolve([ + { path: "a.ts", status: "M" }, + { path: "b.ts", status: "M" }, + ]), + ); + const first = fixture("tip"); + await showPreview(first); + expect(selectedRows().map((text) => text?.includes("a.ts"))).toEqual([true]); + + // The diff toolbar's next-file action replaces the buffer data without touching the tree. + await showPreview(moveCommitFile(first, 1)!); + expect(selectedRows().map((text) => text?.includes("b.ts"))).toEqual([true]); + + // A preview that belongs to another commit must not move this selection. + await showPreview(fixture("other")); + expect(selectedRows().map((text) => text?.includes("b.ts"))).toEqual([true]); + useBufferStore.setState({ activeBufferId: null, buffers: [] }); +}); + +test("following the preview reopens a folder the user had collapsed", async () => { + const diff = (path: string): GitDiff => ({ + file_path: path, + is_new: false, + is_deleted: false, + is_renamed: false, + lines: [], + additions: 1, + deletions: 0, + }); + const data = createCommitFileDiffPreview({ + repoPath: "C:/fixture", + commitHash: "tip", + diffs: [diff("a/x.ts"), diff("b/y.ts")], + filePath: "a/x.ts", + label: "tip", + })!.diffData; + const show = async (diffData: typeof data) => + act(async () => + useBufferStore.setState({ + activeBufferId: COMMIT_FILE_PREVIEW_PATH, + buffers: [ + createPaneContent(COMMIT_FILE_PREVIEW_PATH, { + type: "diff", + path: COMMIT_FILE_PREVIEW_PATH, + name: "preview", + content: "", + diffData, + }), + ], + }), + ); + const rows = () => [...container.querySelectorAll("[data-sidebar-tree-row]")]; + const rowText = (name: string) => + rows().find((row) => row.textContent?.includes(name)) as HTMLElement | undefined; + + await render([commit("tip")]); + await act(async () => + requests[0].resolve([ + { path: "a/x.ts", status: "M" }, + { path: "b/y.ts", status: "M" }, + ]), + ); + await show(data); + await act(async () => + rowText("b1 file")!.querySelector('[role="treeitem"]')!.click(), + ); + expect(rowText("y.ts")).toBeUndefined(); + + await show(moveCommitFile(data, 1)!); + expect(rowText("y.ts")?.getAttribute("data-active")).toBe("true"); + useBufferStore.setState({ activeBufferId: null, buffers: [] }); +}); diff --git a/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx b/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx index 65908a0d6..966aa0ed7 100644 --- a/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx +++ b/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx @@ -5,15 +5,19 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui/resiz import { useTranslation } from "@/i18n/locale-provider"; import { getCommitFiles } from "../../api/git-commits-api"; import { getRefDiff } from "../../api/git-diff-api"; +import { useBufferStore } from "@/features/editor/stores/buffer.store"; import { useGitLogPreferencesStore } from "../../stores/git-log-preferences.store"; import type { GitCommit, GitCommitFile } from "../../types/git.types"; import { mapGitReadsInBatches } from "../../utils/git-async-batch"; +import { selectedCommitFilePath } from "../../utils/commit-file-diff-navigation"; import { aggregateSelectedCommitFileRows, + commitFilePreviewHash, gitDiffsToCommitFiles, resolveGitCommitSelectionDiff, type GitCommitSelectionDiff, } from "../../utils/git-commit-selection-diff"; +import { COMMIT_FILE_PREVIEW_PATH } from "../../utils/multi-file-diff"; import { GitCommitDetails } from "./git-commit-details"; import { GitCommitFileTree, getFirstCommitFilePath } from "./git-commit-file-tree"; @@ -73,6 +77,30 @@ export function GitCommitInspector({ const [failedSelectionKey, setFailedSelectionKey] = useState(null); const [loadAttempt, setLoadAttempt] = useState(0); const handledPreviewRequestRef = useRef(previewRequest); + // The preview's own previous/next-file navigation changes the buffer, not the tree selection. + // Follow the file it shows, but only when that preview belongs to this very selection. + const previewedPath = useBufferStore((state) => { + if (!repoPath || !selectionDiff) return null; + const buffer = state.buffers.find((candidate) => candidate.path === COMMIT_FILE_PREVIEW_PATH); + const data = buffer?.type === "diff" ? buffer.diffData : undefined; + return data && + "files" in data && + data.commitFilePreview && + data.repoPath === repoPath && + data.commitHash === commitFilePreviewHash(selectionDiff) + ? selectedCommitFilePath(data) + : null; + }); + // Re-evaluated only when the previewed file or the loaded file list changes, so a click that + // has just selected a row is not reverted by the previous preview that is still displayed. + const followPreviewedPath = useEffectEvent(() => { + if (previewedPath && files.some((file) => file.path === previewedPath)) { + setSelectedPath(previewedPath); + } + }); + useEffect(() => { + if (loadedSelectionKey === selectionKey) followPreviewedPath(); + }, [previewedPath, loadedSelectionKey, selectionKey]); // Selecting a commit previews its first file, like the IDEA Git log. Only explicit user // selections bump `previewRequest`, so opening the Log never steals the editor on its own. diff --git a/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts b/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts index 32c2bd75e..b79e9df6d 100644 --- a/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts +++ b/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts @@ -5,6 +5,7 @@ import type { GitDiff } from "../types/git.types"; import { mapGitReadsInBatches } from "../utils/git-async-batch"; import { aggregateSelectedCommitDiffs, + commitFilePreviewHash, type GitCommitSelectionDiff, } from "../utils/git-commit-selection-diff"; import { createCommitFileDiffPreview } from "../utils/multi-file-diff"; @@ -32,14 +33,13 @@ export function useCommitFilePreview( const requestId = requests.current.begin(); try { let diffs: readonly GitDiff[] | null; - let commitHash: string; + const commitHash = commitFilePreviewHash(selection); let label: string; let fileKeys: string[] | undefined; let fileLabels: string[] | undefined; let fileRevisions: DiffRevisionPair[]; if (selection.kind === "commit") { diffs = await getCommitDiff(repoPath, selection.commit.hash); - commitHash = selection.commit.hash; label = selection.commit.shortHash; fileRevisions = (diffs ?? []).map(() => ({ before: selection.commit.parentHashes[0] ?? null, @@ -47,7 +47,6 @@ export function useCommitFilePreview( })); } else if (selection.kind === "range") { diffs = await getRefDiff(repoPath, selection.baseRef, selection.targetRef); - commitHash = `${selection.baseRef ?? "root"}..${selection.targetRef}`; label = `${selection.oldest.shortHash}..${selection.newest.shortHash}`; fileRevisions = (diffs ?? []).map(() => ({ before: selection.baseRef, after: selection.targetRef })); } else { @@ -66,7 +65,6 @@ export function useCommitFilePreview( before: commit.parentHashes[0] ?? null, after: commit.hash, }))); - commitHash = selection.commits[0]?.hash ?? "selection"; label = String(selection.commits.length); } if (!requests.current.isCurrent(requestId) || !diffs) return; diff --git a/windows/tauri/src/features/git/utils/commit-file-diff-navigation.ts b/windows/tauri/src/features/git/utils/commit-file-diff-navigation.ts index 5d1669e05..fb6a4dd7d 100644 --- a/windows/tauri/src/features/git/utils/commit-file-diff-navigation.ts +++ b/windows/tauri/src/features/git/utils/commit-file-diff-navigation.ts @@ -10,6 +10,12 @@ export function selectedCommitFileIndex(data: MultiFileDiff): number { return index >= 0 ? index : data.files.length ? 0 : -1; } +/** Path of the file the preview currently shows, in the form the commit-files tree uses. */ +export function selectedCommitFilePath(data: MultiFileDiff): string | null { + const diff = data.files[selectedCommitFileIndex(data)]; + return diff ? diff.new_path || diff.file_path || diff.old_path || null : null; +} + /** No wrapping: preserve the complete snapshot and change only the visible entry. */ export function moveCommitFile(data: MultiFileDiff, direction: -1 | 1): MultiFileDiff | null { const index = selectedCommitFileIndex(data); diff --git a/windows/tauri/src/features/git/utils/git-commit-selection-diff.ts b/windows/tauri/src/features/git/utils/git-commit-selection-diff.ts index 33c456826..a0a7286f2 100644 --- a/windows/tauri/src/features/git/utils/git-commit-selection-diff.ts +++ b/windows/tauri/src/features/git/utils/git-commit-selection-diff.ts @@ -11,6 +11,13 @@ export type GitCommitSelectionDiff = newest: GitCommit; }; +/** Identity stored in a commit-file preview's `commitHash`, shared by publisher and observers. */ +export function commitFilePreviewHash(selection: GitCommitSelectionDiff): string { + if (selection.kind === "commit") return selection.commit.hash; + if (selection.kind === "range") return `${selection.baseRef ?? "root"}..${selection.targetRef}`; + return selection.commits[0]?.hash ?? "selection"; +} + export interface GitSelectedCommitDiffAggregate { files: GitCommitFile[]; diffs: GitDiff[]; From 5709fb2be418eb85d739fcb96ec993e6640e1a56 Mon Sep 17 00:00:00 2001 From: "frank.wang" Date: Thu, 29 Oct 2026 20:42:32 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(windows):=20=E6=8E=A8=E9=80=81=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E6=8C=89=E9=92=AE=E5=AF=B9=E9=BD=90=20IDEA=20?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E4=B8=8B=E6=8B=89=E8=A2=AB=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E9=81=AE=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 推送按钮组改为 IDEA 默认选项按钮样式:实心主色底、白字,1px 分隔线上下内缩; ButtonGroup 新增 primary 变体,禁用时整组一起变淡 - 下拉箭头悬停和菜单展开时不变色;Button 带 tooltip 会被包一层 span, 分组的直接子元素选择器无法命中,箭头改为自带样式 - DropdownMenu 定位层从 z-50 提到 z-10070,与 context-menu、popover、select 一致; 此前弹窗(z-9999)会盖住强制推送菜单 - DropdownMenuContent 支持透传 anchor,强制推送菜单锚定整个按钮组,宽度与按钮组一致 --- .../git/components/git-push-dialog.tsx | 21 ++++++++++++++----- windows/tauri/src/ui/button-group.tsx | 5 +++++ windows/tauri/src/ui/dropdown-menu.tsx | 6 ++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/windows/tauri/src/features/git/components/git-push-dialog.tsx b/windows/tauri/src/features/git/components/git-push-dialog.tsx index 478d550ab..246948ae4 100644 --- a/windows/tauri/src/features/git/components/git-push-dialog.tsx +++ b/windows/tauri/src/features/git/components/git-push-dialog.tsx @@ -121,6 +121,8 @@ function GitPushDialog({ // that resolves elsewhere means the tracking configuration moved under the choice. const confirmedDestination = useRef<{ remote: string; remoteBranch: string } | null>(null); const previewSequence = useRef(0); + // The push menu anchors to the whole button group so it is exactly as wide as the group. + const pushGroupRef = useRef(null); const loadPreview = useCallback(async () => { const sequence = ++previewSequence.current; @@ -458,9 +460,10 @@ function GitPushDialog({ - +