From ef24b8f387306678835c613391ae91fa64b23187 Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Wed, 9 Sep 2026 12:21:52 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(Log):=20=E6=8F=90=E4=BA=A4=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=94=B1=E6=82=AC=E5=81=9C=E6=B5=AE=E5=B1=82=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8F=B3=E4=BE=A7=E5=B8=B8=E9=A9=BB=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Graph webview 内水平分栏(#main = 图区 + #commit-panel):点击提交行于图右侧打开详情面板,上半区 Changed Files(List/Tree 切换、点击开 Diff),下半区提交信息(作者/时间、HEAD/分支/远程/标签引用分组、完整消息、提交者、统计、SHA、Open on GitHub),替代原 #commit-tip 悬停浮层与底部 Changed Files 区; - 面板可见性 ⟺ 选中态:× / Esc 取消选中即收起,焦点回落图区;选中行随 graphData 消失自动收起,webview 重载/切库按仓库记忆恢复(state v2 零迁移); - host 侧 log/selectCommit 一次并行触达文件 + 详情两路取数(沿用切库竞态守卫);log/commitDetail 回包增 forHash 回显,失败回包非静默(Details unavailable 占位,同 hash 重点击可重试);移除浮层专用 log/showCommitDetail 消息与编辑器区面板遗留死类型; - 移除 i 快捷键(随浮层消亡),Escape 语义改为取消选中;CI 浮层/虚拟滚动/过滤/右键菜单等既有交互零改动; - 文档:新增 docs/features/log-commit-detail-panel.md(布局/数据流 Mermaid + 边缘 Case),移除 log-commit-tooltip.md,同步知识索引/文档中心/CHANGELOG; 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang --- CHANGELOG.md | 13 + docs/.agents/knowledge-map.md | 2 +- docs/README.md | 1 + docs/features/file-list-group-by-directory.md | 2 +- docs/features/log-commit-detail-panel.md | 86 ++++++ docs/features/log-commit-tooltip.md | 41 --- src/adapter/webview/log-webview.ts | 249 ++++++++---------- src/shared/protocol.ts | 19 +- 8 files changed, 223 insertions(+), 190 deletions(-) create mode 100644 docs/features/log-commit-detail-panel.md delete mode 100644 docs/features/log-commit-tooltip.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f3b8d..366cdfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ ## [Unreleased] +### Changed + +- **Log 提交详情由悬停浮层改为右侧常驻面板**:点击 Graph 提交行即在图右侧于 webview 内水平分栏打开详情面板(上半 **Changed Files**——List/Tree 切换、点击打开 Diff;下半**提交信息**——作者/时间、HEAD/分支/远程/标签引用分组、完整消息、提交者(异于作者时)、变更统计、完整 SHA、Open on GitHub),深浅主题自适应。面板可见性 ⟺ 选中态:`×` 或 `Esc` 取消选中即收起,刷新 / webview 重载按仓库记忆恢复选中与面板;`log/selectCommit` 一次并行触达文件 + 详情两路数据(沿用切库竞态守卫,迟到 / 失败回包按 `forHash` 丢弃或显式「Details unavailable」占位,见 [`src/shared/protocol.ts`](./src/shared/protocol.ts))。原 `#commit-tip` 悬停浮层与 `i` 快捷键整体移除,行悬停不再弹层;底部 Changed Files 区随之并入面板上半区。完整叙述见 [Log 提交详情面板](./docs/features/log-commit-detail-panel.md)。 + +### Removed + +- **Graph 悬停浮层专用消息与死类型**:移除 `log/showCommitDetail`(webview → host,随浮层消亡)与编辑器区 Commit 详情面板实验遗留、全仓零引用的 `CommitDetailHostToWebviewMessage` / `CommitDetailWebviewToHostMessage` 协议类型([`src/shared/protocol.ts`](./src/shared/protocol.ts))。 + +### Docs + +- 新增 [Log 提交详情面板](./docs/features/log-commit-detail-panel.md)(布局分栏 / 交互数据流 Mermaid、边缘 Case、`forHash` 守卫语义);移除被替代的 `docs/features/log-commit-tooltip.md`,同步更新 [知识索引](./docs/.agents/knowledge-map.md)、[文档中心](./docs/README.md) 与 [变更文件目录树](./docs/features/file-list-group-by-directory.md) 表述。 + + ## [0.0.16] - 2026-09-01 — 多根工作区仓库切换 · 分支分组树 · Agentic Git 预置 · 面板布局与 Diff 修复 自 v0.0.14 以来的全量积累(承载 0.0.15 版本号预置——该版本号已就位但未单独发布,本版一并发布)。核心新特性:多根工作区多仓库切换、Branches 按 `/` 前缀分组树、Agentic Git 偏好配置与 Claude Code 配置预置(M5 前置铺垫)、Diff 缺失端修复与面板默认布局调整;工程侧升级 pnpm 12 与一批开发依赖。完整用户视角叙述见 [Release Note v0.0.16](./docs/releases/v0.0.16.md)。 diff --git a/docs/.agents/knowledge-map.md b/docs/.agents/knowledge-map.md index 4763135..cd14415 100644 --- a/docs/.agents/knowledge-map.md +++ b/docs/.agents/knowledge-map.md @@ -21,7 +21,7 @@ - [Commit 视图整合](../docs/features/commit-view-consolidation.md) — 移除 CHANGES 视图,其文件操作/changelist 管理/Git 工具栏/未提交角标零回归平移入 Commit 视图。 - [变更文件目录树](../docs/features/file-list-group-by-directory.md) — Commit / Log 文件列表「平铺 ⇄ 按目录分组」切换:host 侧 `buildFileTree` 构树下发、compact 折叠、目录三态。 - [分支前缀分组树](../docs/features/branch-tree-group-by-prefix.md) — Branches 视图 Local/Remote/Tags「平铺 ⇄ 按 `/` 前缀分组」切换:`buildRefTree` 构树、分支感知排序、compact 折叠、隐藏 `origin/HEAD`、按仓库持久化。 -- [Log 提交悬浮详情](../docs/features/log-commit-tooltip.md) — 悬停提交行浮层展示 分支/标签/HEAD/完整消息/作者·提交者/时间/SHA;复用 CI 浮层范式、与其互斥。 +- [Log 提交详情面板](../docs/features/log-commit-detail-panel.md) — 点击提交行于图右侧 webview 内分栏打开常驻详情面板(上半 Changed Files、下半提交信息:引用分组/完整消息/统计/SHA/GitHub);面板可见性 ⟺ 选中态,替代原悬停浮层。 - [Claude Code 配置](../docs/features/claude-code-config.md) — Agentic Git 预置:`hyperGit.claudeCode.executablePath` 设置 + `~/.claude/settings.json` 快捷入口(原生设置 + 命令;纯路径逻辑沉淀 `engine/agent/`)。 - [Agentic Git 偏好配置](../docs/features/agentic-git-preferences.md) — Agentic Git 预置:`hyperGit.agent.*` 四项(baseBranch + commit/createPr/review Preferences;多行文本域 + 内置默认模板,Commit 模板中文规约已英译)。 - [多根工作区仓库切换](../docs/features/multi-root-repo-selection.md) — Graph 工具栏仓库名按钮 + `hyperGit.selectRepository` 切换全局活跃仓库:七视图级联跟随、changelist/收藏/Shelf/最近消息按仓库隔离、三重顺序不变量与安全迁移。 diff --git a/docs/README.md b/docs/README.md index d3cf858..df1aeaf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ ## 功能文档 - [Log 视图 CI 状态](./features/log-ci-status.md) — 按提交显示 GitHub CI 最终状态(绿勾/红叉 + 悬停 Tooltip 明细):认证、限流、懒加载、边界与配置。 +- [Log 提交详情面板](./features/log-commit-detail-panel.md) — 点击提交行于图右侧打开常驻详情面板(上半 Changed Files、下半提交信息);面板可见性 ⟺ 选中态,替代原悬停浮层。 - [Claude Code 配置](./features/claude-code-config.md) — Agentic Git 预置:Claude Code 可执行路径设置 + `~/.claude/settings.json` 快捷入口(原生设置 + 命令;M5 前置铺垫)。 - [Agentic Git 偏好配置](./features/agentic-git-preferences.md) — Agentic Git 预置:Base Branch + Commit / Create PR / Review 三项 agent 指令偏好(多行文本域 + 内置默认模板;M5 前置铺垫)。 diff --git a/docs/features/file-list-group-by-directory.md b/docs/features/file-list-group-by-directory.md index 6aefeaa..fa68f53 100644 --- a/docs/features/file-list-group-by-directory.md +++ b/docs/features/file-list-group-by-directory.md @@ -23,7 +23,7 @@ flowchart LR ## 两视图差异 -| | Commit 视图 | Log 视图(详情面板) | +| | Commit 视图 | Log 视图(右侧详情面板上半区) | |---|---|---| | 数据源 | 活动 Changelist 的 `CommitFileItem[]` | 选中提交 `diff-tree` 的 `LogCommitFileItem[]` | | 建树路径 | 条目 `path`(仓库相对) | `CommitFileChange.path`(干净新路径,重命名归位到新目录) | diff --git a/docs/features/log-commit-detail-panel.md b/docs/features/log-commit-detail-panel.md new file mode 100644 index 0000000..6c47cdb --- /dev/null +++ b/docs/features/log-commit-detail-panel.md @@ -0,0 +1,86 @@ +# Log 提交详情面板(Commit Detail Panel) + +> 点击 Log 视图任一提交行,在**图右侧**于 webview 内水平分栏打开常驻详情面板:上半区 **Changed Files**(List ⇄ Tree 切换、点击打开 Diff),下半区**提交信息**——作者(`name `)+ 时间(绝对 + 相对)、**HEAD / 本地分支 / 远程分支 / 标签**引用分组、完整消息(subject + body)、(与作者不同时的)提交者、变更统计、完整 SHA、Open on GitHub。**面板可见性 ⟺ 选中态**:`×` 或 `Esc` 取消选中即收起,未选中自动隐藏。原悬停浮层(`#commit-tip`)与 `i` 快捷键整体移除。 + +## 布局与定位 + +VS Code 的 panel 视图容器(`hyper-git`)内多视图只能**垂直堆叠**,无法原生并排两个子视图;编辑器区 `WebviewPanel` 方案亦曾实验后回退。故「右侧平级视窗」落地为 **Graph webview 内部左右分栏**(对齐 Git Graph 扩展的 Details 列形态): + +```mermaid +flowchart TB + subgraph body["body(纵向 flex)"] + T[".toolbar(全宽:scope 切换 / 仓库 / CI 登录)"] + subgraph main["#main(横向 flex)"] + VP["#viewport
提交图(flex:1,虚拟滚动)"] + subgraph panel["#commit-panel(flex:0 0 42%,280px–65% 钳制)"] + D["#details(55%,独立滚动)
Changed Files + List/Tree + ×"] + M["#commit-meta(45%,独立滚动)
作者/引用分组/消息/统计/SHA"] + end + end + end + T --- main + VP --- panel + D --- M + style T fill:#6e7681,color:#fff + style VP fill:#1f6feb,color:#fff + style panel fill:#21262d,stroke:#d29922,color:#fff + style D fill:#d29922,color:#fff + style M fill:#238636,color:#fff +``` + +- **可见性不变式**:`#commit-panel.show` ⟺ `selectedHash !== null`。所有路径经三个漏斗收敛——`requestPanelData(hash)`(开面板 + Loading)、`deselectRow()`(收面板 + 清选中 + 焦点回落图区)、`log/graphData` 的恢复/消失分支。 +- **窄视口自适应**:面板开合改变 `#viewport` 宽度,既有 `ResizeObserver` 自动切 `.narrow`(隐 author/date、留 CI 列)。 + +## 交互与数据流 + +```mermaid +flowchart LR + A["点击/方向键选中行"] --> B["selectRow → log/selectCommit"] + B --> C["host 并行取数
sendCommitFiles + showCommitDetail"] + C -->|"log/commitFiles{hash}"| D["renderDetails → #details"] + C -->|"log/commitDetail{forHash, vm}"| E["renderCommitMeta → #commit-meta"] + B --> F["Loading 占位(两区即时反馈)"] + G["× / Esc → deselectRow"] --> H["面板隐藏 + 选中清除 + persist"] + I["log/graphData(刷新/切库/重载)"] -->|"选中消失"| G + I -->|"选中在 + 未装载"| B + style A fill:#1f6feb,color:#fff + style C fill:#8250df,color:#fff + style D fill:#d29922,color:#fff + style E fill:#238636,color:#fff + style G fill:#cf222e,color:#fff +``` + +- **一次选中、两路并行**:host `log/selectCommit` 分支同时发起变更文件与详情取数,均沿用 `rootAtStart` 切库竞态守卫(issue #107)。 +- **过期回包丢弃**:`log/commitFiles` 按 `hash`、`log/commitDetail` 按 **`forHash`** 回显校验——失败回包 `vm=null` 无 hash 可比对,`forHash` 补齐该盲区,杜绝迟到失败在新选中下闪现占位。 +- **失败非静默**:详情取数失败显示 "Details unavailable" 占位;同 hash 重点击放行重试(`metaFailHash` 例外)。 +- **同 hash 重点击防抖**:不重拉防闪烁;方向键导航每次必换 hash,不受影响。 +- **刷新语义**:提交对象不可变——文件不重拉;`meta` 按新行集重渲(新推分支/标签等引用 chips 变化即时反映)。 +- **重载恢复**:选中 hash 本就按仓库持久化(state v2),`graphData` 到达且选中行存在时补拉面板数据,实现跨重载/切库恢复。 +- **XSS 安全**:分支名/标签/作者/正文等一切外部内容均经 `esc()` 转义后注入。 + +## 边缘 Case + +| Case | 行为 | +|---|---| +| 快速连点 A→B→C | 每击即显新 hash Loading;过期回包按 `hash`/`forHash` 丢弃 | +| 取数途中切库 | host 竞态守卫 + `forHash` 丢弃;新库 `graphData` 恢复其记忆选中 | +| 选中期间 git 刷新 | 面板保持,文件不重拉,引用分组按新行集重渲 | +| 根提交 / merge | "No changed files (may be a root or merge commit)" 占位 + 详情正常 | +| CI 图标 | 点击不选行、CI 浮层原样(原互斥代码随浮层一并消失) | +| 非 GitHub 远程 | 无 Open on GitHub 链接(host 不下发 `githubUrl`) | +| 键盘 | 方向键/Home/End 移动选中即更新面板;`Enter` 菜单、右键菜单原样;`Esc` 取消选中 | + +## 实现 + +- 面板与漏斗:[`adapter/webview/log-webview.ts`](../../src/adapter/webview/log-webview.ts)(`#main`/`#commit-panel` 分栏、`selectRow`/`requestPanelData`/`deselectRow`、`renderCommitMeta`、Loading 占位、消息守卫)。 +- 协议:[`shared/protocol.ts`](../../src/shared/protocol.ts)(`log/commitDetail` 增 `forHash`;移除 `log/showCommitDetail` 与编辑器区面板遗留死类型)。 +- 复用:[`engine/tree/file-tree.ts`](../../src/engine/tree/file-tree.ts)(目录树)、[`engine/log/commit-files.ts`](../../src/engine/log/commit-files.ts)(name-status 解析)、[`engine/ci/remote-parser.ts`](../../src/engine/ci/remote-parser.ts)(GitHub URL)、`log/openFile` → `hyperGit.openCommitFileDiff` Diff 链路不变。 + +## 验证 + +`pnpm run compile` + `pnpm run test:unit` 全绿;Extension Development Host(F5):点击行开面板(Loading → 文件计数 + 详情)、行悬停无浮层、CI 浮层鼠标/键盘均可用、文件点击开 Diff、List/Tree 切换、同 hash 重点击无闪烁、`×`/`Esc` 收起且方向键仍可用、快速连点终态正确、选中态下 commit 后面板保持且 chips 更新、切分支选中消失自动收起、切库/重载恢复、根/merge 占位、窄面板自适应。 + +## 展望 + +- 面板宽度拖拽分隔条(现 42% + 280px–65% 钳制,YAGNI 暂缓)。 +- 详情区随 `host` 防抖合并(方向键连续导航时减少单提交 `git` 调用,现为每停 2 次,毫秒级可接受)。 diff --git a/docs/features/log-commit-tooltip.md b/docs/features/log-commit-tooltip.md deleted file mode 100644 index 62a0eeb..0000000 --- a/docs/features/log-commit-tooltip.md +++ /dev/null @@ -1,41 +0,0 @@ -# Log 提交悬浮详情(Commit Tooltip) - -> 鼠标悬停 Log 视图任一提交行,以浮层快速展示该提交的完整信息:所在**本地/远程分支、标签、HEAD**、**完整提交消息**(subject + body)、**作者**(`name `)、(与作者不同时的)**提交者**、**作者/提交时间**(绝对 + 相对)、**完整 SHA**。 - -## 数据流与取数 - -引用标签(分支/标签/HEAD)本就随 `GraphRowVM.chips` 下发;本特性补齐正文与提交者,随首帧一并送达,故悬停**零往返、零延迟**。 - -```mermaid -flowchart LR - A["git log --format=
…%cn %cI %s %b"] --> B["parseLogLines → RawCommit"] - B --> C["GraphRowVM
+authorEmail/committer/body(capBody)"] - C -->|"log/graphData(首帧全量)"| D["webview model.rows"] - D -->|"悬停行 ~400ms"| E["buildCommitTip → #commit-tip"] - style A fill:#1f6feb,color:#fff - style E fill:#d29922,color:#fff -``` - -- **格式扩展**([`engine/log/log-line.ts`](../../src/engine/log/log-line.ts)):`LOG_GRAPH_FORMAT` 增 `%cn`(提交者)、`%cI`(提交时间)、`%b`(正文)。`%b` **置于末字段**:记录以 RS `%x1e` 终止、字段以 NUL `%x00` 分隔,二者不出现于 git 文本,故正文内换行/管道/反斜杠不被误判为边界;解析用 `slice(8).join(NUL)` 稳健重组末字段。 -- **payload 控制**:正文 host 侧 `capBody` 截断至上限(超限省略号、裁尾空白),避免 1000 行图的 `postMessage` 膨胀;浮层内另设滚动区兜底长正文。 - -## 复用 CI 浮层范式 - -沿用既有 CI 状态浮层(`#ci-tip`)的自定义浮层设计,新增 `#commit-tip`: - -- **置于 `#rows` 之外**:虚拟滚动重写行 DOM 时浮层不被销毁。 -- **与 CI 浮层互斥**:悬停 `.ci` 图标只显 CI 浮层(提交浮层隐藏并取消在途显示);悬停行其余区域显提交浮层(显示前先 `hideTip()` 关 CI 浮层)。 -- **延迟与消隐**:悬停 ~400ms 显、离开 ~200ms 隐;滚动、`log/graphData`(刷新/切范围)、`log/ciMeta`、`log/appendData`(整体重绘 `#rows`)均即时消隐并取消在途显示(避免对已脱离节点定位)。 -- **键盘可达**:选中行按 `i` 打开、`Esc` 关闭(`stopPropagation` 避与 `Enter`→提交菜单冲突)。 -- **无双重气泡**:移除提交行 chips 的原生 `title`,引用明细统一由本浮层展示。 -- **XSS 安全**:分支名/标签/作者/正文等一切外部内容均经 `esc()` 转义后注入。 - -## 实现 - -- 数据:[`engine/log/log-line.ts`](../../src/engine/log/log-line.ts)(格式 + `RawCommit` + 解析;[`tests/unit/log-line.test.ts`](../../tests/unit/log-line.test.ts)/[`log-query.test.ts`](../../tests/unit/log-query.test.ts) 更新含多行/空正文/提交者异于作者用例)。 -- 协议:[`shared/protocol.ts`](../../src/shared/protocol.ts)(`GraphRowVM` 增 `authorEmail/committerName/committerDate/body`)。 -- 浮层:[`adapter/webview/log-webview.ts`](../../src/adapter/webview/log-webview.ts)(`#commit-tip` + `buildCommitTip`/`positionCommitTip`/`scheduleShowCommit`/`hideCommitTip`、行 hover 处理、`fmtAbs`/`fmtRel`)。 - -## 验证 - -`pnpm run test:unit` 全绿;Extension Development Host(F5):悬停提交行 ~400ms 出浮层含各引用组 + 消息(多行保留、超长滚动 + 截断)+ 作者/提交者/时间/完整 SHA;悬停 CI 图标只出 CI 浮层(互斥);chips 无原生气泡;滚动/切换范围/刷新自动消隐;`i` 开 `Esc` 关且不误触菜单;合并 / 无正文 / detached HEAD / 多引用均正常。 diff --git a/src/adapter/webview/log-webview.ts b/src/adapter/webview/log-webview.ts index aa85ae1..7bbf3f1 100644 --- a/src/adapter/webview/log-webview.ts +++ b/src/adapter/webview/log-webview.ts @@ -171,7 +171,9 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter void this.loadMore(msg.payload.cursor); break; case 'log/selectCommit': + // 选中即开右侧详情面板:变更文件 + 提交详情两路并行取数(webview 按 selectedHash 丢弃过期回包)。 void this.sendCommitFiles(msg.payload.hash); + void this.showCommitDetail(msg.payload.hash); break; case 'log/openFile': void vscode.commands.executeCommand( @@ -203,9 +205,6 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter case 'log/ciSignIn': void this.handleCiSignIn(); break; - case 'log/showCommitDetail': - void this.showCommitDetail(msg.payload.hash); - break; case 'log/selectRepo': // 复用 postMessage → 原生交互 → executeCommand 通路(同 handleCommitMenu 形态)。 void vscode.commands.executeCommand('hyperGit.selectRepository'); @@ -213,10 +212,10 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter } } - /** 组装提交详情 VM(基础字段 + 预格式化时间 + 变更统计 + GitHub URL),下发给 webview 浮层渲染。 */ + /** 组装提交详情 VM(基础字段 + 预格式化时间 + 变更统计 + GitHub URL),下发给 webview 右侧详情面板(#commit-meta)渲染。 */ private async showCommitDetail(hash: string): Promise { if (!this.service.repo) { - this.post({ type: 'log/commitDetail', payload: { vm: null } }); + this.post({ type: 'log/commitDetail', payload: { forHash: hash, vm: null } }); return; } // 切库竞态守卫(issue #107):hash 属旧仓库语境,迟到响应不作数(可能取到同名歧义提交)。 @@ -226,12 +225,12 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter const fmt = '%H%x00%s%x00%b%x00%an%x00%ae%x00%aI%x00%cn%x00%cI%x00%P'; const raw = await this.service.execGit(['show', '-s', `--format=${fmt}`, hash]); if (this.service.repoRoot !== rootAtStart) { - this.post({ type: 'log/commitDetail', payload: { vm: null } }); + this.post({ type: 'log/commitDetail', payload: { forHash: hash, vm: null } }); return; } const f = raw.split('\0'); if (f.length < 9 || !f[0]) { - this.post({ type: 'log/commitDetail', payload: { vm: null } }); + this.post({ type: 'log/commitDetail', payload: { forHash: hash, vm: null } }); return; } const [fullHash, subject, body, authorName, authorEmail, authorDate, committerName, committerDate, parentsRaw] = f; @@ -239,7 +238,7 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter await this.service.execGit(['diff-tree', '--no-commit-id', '--shortstat', '-r', '--root', hash]), ); if (this.service.repoRoot !== rootAtStart) { - this.post({ type: 'log/commitDetail', payload: { vm: null } }); + this.post({ type: 'log/commitDetail', payload: { forHash: hash, vm: null } }); return; } const remote = this.ciService.getGitHubRemote(); @@ -262,9 +261,9 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter stat, githubUrl: remote ? commitWebUrl(remote, fullHash) : undefined, }; - this.post({ type: 'log/commitDetail', payload: { vm } }); + this.post({ type: 'log/commitDetail', payload: { forHash: hash, vm } }); } catch { - this.post({ type: 'log/commitDetail', payload: { vm: null } }); + this.post({ type: 'log/commitDetail', payload: { forHash: hash, vm: null } }); } } @@ -516,7 +515,7 @@ body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscod button.repo { background: transparent; color: var(--vscode-foreground); border: none; padding: 1px 6px; border-radius: 3px; cursor: pointer; } button.repo.switchable:hover { opacity: 1; background: var(--vscode-list-hoverBackground); } button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; } -#viewport { flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; outline: none; } +#viewport { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; position: relative; outline: none; } #spacer { position: relative; } #rows { position: absolute; left: 0; right: 0; } .row { display: flex; align-items: center; height: var(--hg-row); padding-right: 8px; cursor: pointer; white-space: nowrap; } @@ -539,8 +538,13 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin .author { flex: 0 0 auto; font-size: 11px; opacity: 0.7; max-width: 110px; overflow: hidden; text-overflow: ellipsis; padding-left: 8px; } .date { flex: 0 0 auto; font-size: 11px; opacity: 0.55; padding-left: 8px; } #viewport.narrow .author, #viewport.narrow .date { display: none; } -#details { flex: 0 0 auto; max-height: 38%; overflow-y: auto; border-top: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.25)); display: none; } -#details.show { display: block; } +/* ── 图 + 右侧详情面板水平分栏(panel 容器无法并排子视图 → webview 内自分栏)── */ +#main { flex: 1 1 auto; display: flex; min-height: 0; } +#commit-panel { display: none; flex: 0 0 42%; min-width: 280px; max-width: 65%; flex-direction: column; overflow: hidden; border-left: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.25)); } +#commit-panel.show { display: flex; } +#details { flex: 1 1 55%; min-height: 0; overflow-y: auto; border-bottom: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.15)); } +#commit-meta { flex: 1 1 45%; min-height: 0; overflow-y: auto; } +.panel-loading { padding: 10px 12px; font-size: 12px; color: var(--vscode-descriptionForeground); } #details .dh { position: sticky; top: 0; display: flex; align-items: center; gap: 6px; background: var(--vscode-sideBar-background); padding: 4px 8px; font-size: 11px; color: var(--vscode-descriptionForeground); border-bottom: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.15)); } #details .dh #details-title { flex: 1 1 auto; } .dh-close { flex: 0 0 auto; background: transparent; border: none; color: var(--vscode-descriptionForeground); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 4px; border-radius: var(--hg-radius-control); } @@ -588,38 +592,36 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin #ci-tip .g-failure { color: var(--vscode-testing-iconFailed, var(--vscode-errorForeground, #f85149)); } #ci-tip .g-pending { color: var(--vscode-testing-iconQueued, var(--vscode-editorWarning-foreground, #d29922)); } #ci-tip .g-skipped, #ci-tip .g-unknown { color: var(--vscode-descriptionForeground, #8b949e); } -/* ── 提交详情悬浮卡(cursor-anchored;editorHoverWidget 语义令牌,与 CI 浮层同款视觉语言)── */ -#commit-tip { position: fixed; z-index: 50; display: none; max-width: 480px; min-width: 300px; max-height: 80vh; overflow: hidden; background: var(--vscode-editorHoverWidget-background, var(--vscode-editorWidget-background)); color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground)); border: 1px solid var(--vscode-editorHoverWidget-border, var(--vscode-editorWidget-border, rgba(128,128,128,.3))); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,.4); font-size: 12px; } -#commit-tip.show { display: flex; flex-direction: column; } -#commit-tip .ct-scroll { overflow-y: auto; max-height: 80vh; padding: 12px 14px; } -#commit-tip .ct-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } -#commit-tip .ct-avatar { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%; background: var(--vscode-badge-background, rgba(128,128,128,.25)); color: var(--vscode-badge-foreground, var(--vscode-foreground)); display: inline-flex; align-items: center; justify-content: center; } -#commit-tip .ct-avatar svg { width: 16px; height: 16px; opacity: 0.85; } -#commit-tip .ct-who { display: flex; flex-direction: column; min-width: 0; } -#commit-tip .ct-author { font-weight: 600; font-size: 13px; } -#commit-tip .ct-time { font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 1px; } -#commit-tip .ct-msg { margin-bottom: 10px; } -#commit-tip .ct-subj { font-size: 13px; font-weight: 600; line-height: 1.4; word-break: break-word; } -#commit-tip .ct-body { margin-top: 6px; white-space: pre-wrap; word-break: break-word; font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; line-height: 1.5; opacity: 0.9; } -#commit-tip .ct-refs-wrap { margin-bottom: 10px; display: flex; flex-direction: column; gap: 5px; } -#commit-tip .ct-sec { display: flex; gap: 8px; align-items: baseline; font-size: 12px; } -#commit-tip .ct-sec .ct-k { flex: 0 0 66px; color: var(--vscode-descriptionForeground); font-size: 10px; text-transform: uppercase; letter-spacing: .3px; } -#commit-tip .ct-sec .ct-v { flex: 1 1 auto; min-width: 0; word-break: break-word; } -#commit-tip .ct-refs { display: flex; flex-wrap: wrap; gap: 4px; } -/* 浮层内引用胶囊完整显示(覆盖行内 .chip 的 max-width/省略号截断):换行不截断,空间由浮层承载。 */ -#commit-tip .chip { max-width: none; } -#commit-tip .chip .chip-nm { overflow: visible; text-overflow: clip; white-space: normal; word-break: break-all; } -#commit-tip .ct-dim { color: var(--vscode-descriptionForeground); } -#commit-tip .ct-stat { display: flex; gap: 12px; padding: 8px 0; border-top: 1px solid var(--vscode-editorHoverWidget-border, rgba(128,128,128,.2)); border-bottom: 1px solid var(--vscode-editorHoverWidget-border, rgba(128,128,128,.2)); font-size: 12px; font-variant-numeric: tabular-nums; } -#commit-tip .ct-stat .files { color: var(--vscode-descriptionForeground); } -#commit-tip .ct-stat .ins { color: var(--vscode-gitDecoration-addedResourceForeground, #3fb950); } -#commit-tip .ct-stat .del { color: var(--vscode-gitDecoration-deletedResourceForeground, #f14c4c); } -#commit-tip .ct-foot { display: flex; align-items: center; gap: 14px; margin-top: 10px; flex-wrap: wrap; } -#commit-tip .ct-sha { font-family: var(--vscode-editor-font-family, monospace); font-size: 11px; color: var(--vscode-descriptionForeground); word-break: break-all; } -#commit-tip .ct-gh { color: var(--vscode-textLink-foreground); cursor: pointer; font-size: 12px; display: inline-flex; align-items: center; gap: 4px; } -#commit-tip .ct-gh:hover { text-decoration: underline; } -#commit-tip .ct-gh:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; border-radius: 2px; } -#commit-tip .ct-gh svg { width: 13px; height: 13px; } +/* ── 提交详情面板下半区(#commit-meta,复用 .ct-* 视觉语言;editorHoverWidget 语义令牌与 CI 浮层同源)── */ +#commit-meta .ct-scroll { padding: 12px 14px; } +#commit-meta .ct-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } +#commit-meta .ct-avatar { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%; background: var(--vscode-badge-background, rgba(128,128,128,.25)); color: var(--vscode-badge-foreground, var(--vscode-foreground)); display: inline-flex; align-items: center; justify-content: center; } +#commit-meta .ct-avatar svg { width: 16px; height: 16px; opacity: 0.85; } +#commit-meta .ct-who { display: flex; flex-direction: column; min-width: 0; } +#commit-meta .ct-author { font-weight: 600; font-size: 13px; } +#commit-meta .ct-time { font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 1px; } +#commit-meta .ct-msg { margin-bottom: 10px; } +#commit-meta .ct-subj { font-size: 13px; font-weight: 600; line-height: 1.4; word-break: break-word; } +#commit-meta .ct-body { margin-top: 6px; white-space: pre-wrap; word-break: break-word; font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; line-height: 1.5; opacity: 0.9; } +#commit-meta .ct-refs-wrap { margin-bottom: 10px; display: flex; flex-direction: column; gap: 5px; } +#commit-meta .ct-sec { display: flex; gap: 8px; align-items: baseline; font-size: 12px; } +#commit-meta .ct-sec .ct-k { flex: 0 0 66px; color: var(--vscode-descriptionForeground); font-size: 10px; text-transform: uppercase; letter-spacing: .3px; } +#commit-meta .ct-sec .ct-v { flex: 1 1 auto; min-width: 0; word-break: break-word; } +#commit-meta .ct-refs { display: flex; flex-wrap: wrap; gap: 4px; } +/* 面板内引用胶囊完整显示(覆盖行内 .chip 的 max-width/省略号截断):换行不截断,空间由面板承载。 */ +#commit-meta .chip { max-width: none; } +#commit-meta .chip .chip-nm { overflow: visible; text-overflow: clip; white-space: normal; word-break: break-all; } +#commit-meta .ct-dim { color: var(--vscode-descriptionForeground); } +#commit-meta .ct-stat { display: flex; gap: 12px; padding: 8px 0; border-top: 1px solid var(--vscode-editorHoverWidget-border, rgba(128,128,128,.2)); border-bottom: 1px solid var(--vscode-editorHoverWidget-border, rgba(128,128,128,.2)); font-size: 12px; font-variant-numeric: tabular-nums; } +#commit-meta .ct-stat .files { color: var(--vscode-descriptionForeground); } +#commit-meta .ct-stat .ins { color: var(--vscode-gitDecoration-addedResourceForeground, #3fb950); } +#commit-meta .ct-stat .del { color: var(--vscode-gitDecoration-deletedResourceForeground, #f14c4c); } +#commit-meta .ct-foot { display: flex; align-items: center; gap: 14px; margin-top: 10px; flex-wrap: wrap; } +#commit-meta .ct-sha { font-family: var(--vscode-editor-font-family, monospace); font-size: 11px; color: var(--vscode-descriptionForeground); word-break: break-all; } +#commit-meta .ct-gh { color: var(--vscode-textLink-foreground); cursor: pointer; font-size: 12px; display: inline-flex; align-items: center; gap: 4px; } +#commit-meta .ct-gh:hover { text-decoration: underline; } +#commit-meta .ct-gh:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; border-radius: 2px; } +#commit-meta .ct-gh svg { width: 13px; height: 13px; } /* ── 变更文件目录树(详情面板 Group By Directory 形态)── */ #details .dh .seg { flex: 0 0 auto; } #details .tree-dir { display: flex; align-items: center; gap: 6px; padding: 2px 10px; font-size: 12px; cursor: pointer; user-select: none; } @@ -638,15 +640,19 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin -
-
-
No Commits
No commits match the current scope or filter.
- -
Loading…
+
+
+
+
No Commits
No commits match the current scope or filter.
+ +
Loading…
+
+
-
- From fde65391a26b6b7a01a45537514f1499ee51dcc5 Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Wed, 9 Sep 2026 20:10:20 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(Log):=20=E8=A7=86=E5=9B=BE=E6=8E=A7?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E7=A7=BB=E6=A0=87=E9=A2=98=E6=A0=8F=E3=80=81?= =?UTF-8?q?=E4=B8=89=E5=8C=BA=E5=88=86=E5=89=B2=E7=BA=BF=E5=8F=AF=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E3=80=81=E5=86=8D=E6=AC=A1=E7=82=B9=E5=87=BB=E5=8F=96?= =?UTF-8?q?=E5=8F=8D=E9=9D=A2=E6=9D=BF;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工具栏整体上移 VS Code 原生标题栏(省一整行竖直空间):scope(All/Current/Checkpoints)改 $(layers) 图标下拉子菜单(toggled 勾选、默认 All、置于 Refresh Graph 左侧);List ⇄ Tree 改 $(list-tree)/$(list-flat) 互斥图标(同 Branches 分组切换范式,文案适配);多仓库「切换仓库」$(repo) 与 CI「登录 GitHub」$(sign-in) 按钮条件显隐;仓库路径常驻 WebviewView.description 副标题(标题同行右侧); - scope / dmode 事实源移交 host:workspaceState 按仓库持久化(hyperGit.log.scope: / hyperGit.log.dmode:,同 branchesGrouping 范式)+ context key 驱动标题栏勾选/显隐;dmode 经 LogGraphState 与 log/detailMode 定向下发(免整图重拉);原 webview state 旧偏好一次性重置为默认(选中/目录折叠不受影响),移除 log/setScope、log/selectRepo、log/ciSignIn 消息; - 三区两根 gutter 可拖拽:图 ⇄ 面板(横向面板 ≥200px 且图区 ≥280px、纵向 18%–75%)与 Changed Files ⇄ Commit 信息(15%–85%),pointer capture 实时调比例、松手按仓库持久化(panelPct/detailPct),role=separator 键盘可达(方向键 ±2%、Home/End 归边界),面板收起时主 gutter 隐藏; - 再次点击已选中的提交行 = 反向操作收起面板:300ms 内双击第二击豁免(保留「双击即打开」语义、无开合抖动),方向键/Home/End 边界防抖早退不误触,取数失败态(metaFailHash)例外放行重试; - 验证:check-types / lint / 403 单测 / esbuild 全绿;离屏夹具(真实 CSS + webview 脚本)复验——工具栏 DOM 零残留、真实鼠标拖拽主 gutter 150px 面板 462→614px 且 panelPct=0.5582 入库、meta gutter 275→357px 且 detailPct=0.714、极限钳制 200px/280px、键盘 ±2%、堆叠态双向拖拽、再次点击取反与双击豁免、log/detailMode 与 graphData.dmode 驱动 Tree 渲染; - 文档:log-commit-detail-panel.md 重写布局/交互/边缘 Case/实测数据,同步 file-list-group-by-directory、log-ci-status、multi-root-repo-selection、知识索引与 CHANGELOG; 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang --- CHANGELOG.md | 4 + docs/.agents/knowledge-map.md | 4 +- docs/features/file-list-group-by-directory.md | 5 +- docs/features/log-ci-status.md | 4 +- docs/features/log-commit-detail-panel.md | 61 ++-- docs/features/multi-root-repo-selection.md | 4 +- package.json | 79 +++++ src/adapter/webview/log-webview.ts | 313 +++++++++++------- src/extension.ts | 8 +- src/shared/protocol.ts | 15 +- 10 files changed, 339 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34771da..40bc1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ### Changed +- **Graph 视图工具栏整体上移 VS Code 标题栏(省一整行竖直空间)**:scope(All / Current / Checkpoints)改为 `$(layers)` 图标下拉子菜单(`toggled` 勾选、默认 All,置于 Refresh Graph 图标左侧);仓库路径常驻 `WebviewView.description` 副标题(标题「Graph」同行右侧);多仓库态「切换仓库」`$(repo)` 图标按钮与 CI「登录 GitHub」`$(sign-in)` 图标按钮(授权后自动隐藏)条件显隐;Changed Files 的 List ⇄ Tree 切换改为 `$(list-tree)`/`$(list-flat)` 互斥图标(交互同 Branches 分组切换,文案适配为 "Group Changed Files by Directory" / "Show Changed Files as Flat List")。scope 与 List/Tree 偏好随之移交 host `workspaceState` 按仓库持久化(`hyperGit.log.scope:` / `hyperGit.log.dmode:`)——**原 webview state 中的旧偏好一次性重置为默认**(All / flat;选中与目录折叠不受影响)。 +- **提交详情三区分割线可拖拽**:图 ⇄ 面板(横向面板 ≥200px 且图区 ≥280px、纵向 18%–75%)与面板内 Changed Files ⇄ Commit 信息(15%–85%)两根 gutter,实时调比例、按仓库记忆、键盘可达(方向键 ±2%、Home/End 归边界)。 +- **再次点击已选中的提交行 = 反向操作**:收起该提交的 Changed Files 与 Commit 详情面板(300ms 内双击第二击豁免,保留「双击即打开」语义;方向键/Home/End 边界防抖不误触)。 + - **Log 提交详情由悬停浮层改为右侧常驻面板**:点击 Graph 提交行即在图右侧于 webview 内水平分栏打开详情面板(上半 **Changed Files**——List/Tree 切换、点击打开 Diff;下半**提交信息**——作者/时间、HEAD/分支/远程/标签引用分组、完整消息、提交者(异于作者时)、变更统计、完整 SHA、Open on GitHub),深浅主题自适应。面板可见性 ⟺ 选中态:`×` 或 `Esc` 取消选中即收起,刷新 / webview 重载按仓库记忆恢复选中与面板;`log/selectCommit` 一次并行触达文件 + 详情两路数据(沿用切库竞态守卫,迟到 / 失败回包按 `forHash` 丢弃或显式「Details unavailable」占位,见 [`src/shared/protocol.ts`](./src/shared/protocol.ts))。原 `#commit-tip` 悬停浮层与 `i` 快捷键整体移除,行悬停不再弹层;底部 Changed Files 区随之并入面板上半区。窄视图(`#main` < 560px)自动退化为上下堆叠(面板 45%、图区保 55%),避免面板 280px 下限把提交图挤成零宽;面板收起守卫同时校验选中态与可见性,切到无记忆选中的仓库不残留上一仓库的面板内容。完整叙述见 [Log 提交详情面板](./docs/features/log-commit-detail-panel.md)。 ### Removed diff --git a/docs/.agents/knowledge-map.md b/docs/.agents/knowledge-map.md index cd14415..868e95f 100644 --- a/docs/.agents/knowledge-map.md +++ b/docs/.agents/knowledge-map.md @@ -21,10 +21,10 @@ - [Commit 视图整合](../docs/features/commit-view-consolidation.md) — 移除 CHANGES 视图,其文件操作/changelist 管理/Git 工具栏/未提交角标零回归平移入 Commit 视图。 - [变更文件目录树](../docs/features/file-list-group-by-directory.md) — Commit / Log 文件列表「平铺 ⇄ 按目录分组」切换:host 侧 `buildFileTree` 构树下发、compact 折叠、目录三态。 - [分支前缀分组树](../docs/features/branch-tree-group-by-prefix.md) — Branches 视图 Local/Remote/Tags「平铺 ⇄ 按 `/` 前缀分组」切换:`buildRefTree` 构树、分支感知排序、compact 折叠、隐藏 `origin/HEAD`、按仓库持久化。 -- [Log 提交详情面板](../docs/features/log-commit-detail-panel.md) — 点击提交行于图右侧 webview 内分栏打开常驻详情面板(上半 Changed Files、下半提交信息:引用分组/完整消息/统计/SHA/GitHub);面板可见性 ⟺ 选中态,替代原悬停浮层。 +- [Log 提交详情面板](../docs/features/log-commit-detail-panel.md) — 点击提交行于图右侧 webview 内分栏打开常驻详情面板(上半 Changed Files、下半提交信息:引用分组/完整消息/统计/SHA/GitHub);面板可见性 ⟺ 选中态,再次点击取反;三区两根 gutter 可拖拽调比例,替代原悬停浮层。 - [Claude Code 配置](../docs/features/claude-code-config.md) — Agentic Git 预置:`hyperGit.claudeCode.executablePath` 设置 + `~/.claude/settings.json` 快捷入口(原生设置 + 命令;纯路径逻辑沉淀 `engine/agent/`)。 - [Agentic Git 偏好配置](../docs/features/agentic-git-preferences.md) — Agentic Git 预置:`hyperGit.agent.*` 四项(baseBranch + commit/createPr/review Preferences;多行文本域 + 内置默认模板,Commit 模板中文规约已英译)。 -- [多根工作区仓库切换](../docs/features/multi-root-repo-selection.md) — Graph 工具栏仓库名按钮 + `hyperGit.selectRepository` 切换全局活跃仓库:七视图级联跟随、changelist/收藏/Shelf/最近消息按仓库隔离、三重顺序不变量与安全迁移。 +- [多根工作区仓库切换](../docs/features/multi-root-repo-selection.md) — Graph 标题栏切换仓库图标按钮 + `hyperGit.selectRepository` 切换全局活跃仓库:七视图级联跟随、changelist/收藏/Shelf/最近消息按仓库隔离、三重顺序不变量与安全迁移。 - [实施状态总览](../docs/milestones/implementation-status.md) — M0-M5 交付记录 + API 限制 + M5 AI 设计 + 验证/发布(**实施看板**)。 - [工程实施方案](../docs/architecture/engineering-plan.md) — 路径 B 架构 + M0-M5 里程碑(**开发蓝图**)。 - [Git 功能矩阵](../docs/requirements/idea-feature-matrix.md) — 56 功能点 / 8 组(**验收基线**,参考 IDEA 等成熟实现)。 diff --git a/docs/features/file-list-group-by-directory.md b/docs/features/file-list-group-by-directory.md index fa68f53..1f3793b 100644 --- a/docs/features/file-list-group-by-directory.md +++ b/docs/features/file-list-group-by-directory.md @@ -29,13 +29,14 @@ flowchart LR | 建树路径 | 条目 `path`(仓库相对) | `CommitFileChange.path`(干净新路径,重命名归位到新目录) | | 复选框 | 有:叶子勾选 + **目录三态**级联;Select All 基于全量文件 | 无(只读浏览) | | 叶子单击 | 打开 Diff(`commit/openFile`) | 打开单文件 Diff(`log/openFile`,`data-path` 保持展示串不变) | -| 偏好持久化 | `setState({mode, collapsed, checked})` | `setState({dmode, dcollapsed, ...})` | +| 偏好持久化 | `setState({mode, collapsed, checked})` | host `workspaceState`(`hyperGit.log.dmode:`)+ webview `dcollapsed` 按仓库记忆 | +| 切换控件 | webview 内 List/Tree 段控 | Graph 标题栏 `$(list-tree)`/`$(list-flat)` 互斥图标(同 Branches 分组切换范式) | ## 实现 - 引擎:[`engine/tree/file-tree.ts`](../../src/engine/tree/file-tree.ts)(+ [`tests/unit/file-tree.test.ts`](../../tests/unit/file-tree.test.ts),15 用例)。 - 协议:[`shared/protocol.ts`](../../src/shared/protocol.ts)(`FileTreeNode`;`CommitViewState.tree`;`log/commitFiles` payload `tree`)。 -- 渲染:[`commit-webview.ts`](../../src/adapter/webview/commit-webview.ts)(List/Tree 段控、`renderFlat`/`renderTree`、`makeLeafRow`、目录三态 `updateDirStates`)、[`log-webview.ts`](../../src/adapter/webview/log-webview.ts)(`renderDetails` 分派、`renderDetailNode`)。 +- 渲染:[`commit-webview.ts`](../../src/adapter/webview/commit-webview.ts)(List/Tree 段控、`renderFlat`/`renderTree`、`makeLeafRow`、目录三态 `updateDirStates`)、[`log-webview.ts`](../../src/adapter/webview/log-webview.ts)(`renderDetails` 分派、`renderDetailNode`;Log 侧切换控件为 Graph 标题栏 `hyperGit.log.detailTree/detailFlat` 命令图标,模式经 `log/detailMode` 定向下发免整图重拉)。 ## 验证 diff --git a/docs/features/log-ci-status.md b/docs/features/log-ci-status.md index f976421..4675912 100644 --- a/docs/features/log-ci-status.md +++ b/docs/features/log-ci-status.md @@ -34,8 +34,8 @@ flowchart LR - 复用 **VS Code 内置 GitHub 认证**(`vscode.authentication`),凭证由编辑器托管,**绝不经过 chat / 日志 / webview**。 - 范围 `repo`:覆盖私有仓库的 Checks(Actions)+ Commit Status 读取(`repo:status` 不覆盖 Checks API)。 -- **静默优先**:加载时 `getSession({createIfNone:false})` 仅复用已有会话,**绝不自动弹窗**;仅当用户点击工具栏「登录 GitHub」 - 按钮时才以 `{createIfNone:true}` 触发原生授权 UI。未登录 → 显示登录提示、不渲染图标、不发请求。 +- **静默优先**:加载时 `getSession({createIfNone:false})` 仅复用已有会话,**绝不自动弹窗**;仅当用户点击 Graph 标题栏「登录 GitHub」 + 图标按钮(`$(sign-in)`,`when: hyperGit.log.ciNeedsSignIn`,授权完成自动隐藏)时才以 `{createIfNone:true}` 触发原生授权 UI。未登录 → 仅显该按钮、不渲染图标、不发请求。 - **反 SSRF**:Tooltip 的跳转链接(`detailsUrl` / `targetUrl`,属「观察内容」)由 host 校验 `https` 且主机 ∈ {仓库主机、`*.github.com`} 后才 `openExternal`。 ## 限流与性能 diff --git a/docs/features/log-commit-detail-panel.md b/docs/features/log-commit-detail-panel.md index 5aedeb9..4155735 100644 --- a/docs/features/log-commit-detail-panel.md +++ b/docs/features/log-commit-detail-panel.md @@ -1,33 +1,47 @@ # Log 提交详情面板(Commit Detail Panel) -> 点击 Log 视图任一提交行,在**图右侧**于 webview 内水平分栏打开常驻详情面板:上半区 **Changed Files**(List ⇄ Tree 切换、点击打开 Diff),下半区**提交信息**——作者(`name `)+ 时间(绝对 + 相对)、**HEAD / 本地分支 / 远程分支 / 标签**引用分组、完整消息(subject + body)、(与作者不同时的)提交者、变更统计、完整 SHA、Open on GitHub。**面板可见性 ⟺ 选中态**:`×` 或 `Esc` 取消选中即收起,未选中自动隐藏。原悬停浮层(`#commit-tip`)与 `i` 快捷键整体移除。 +> 点击 Log 视图任一提交行,在**图右侧**于 webview 内水平分栏打开常驻详情面板:上半区 **Changed Files**(List ⇄ Tree 切换、点击打开 Diff),下半区**提交信息**——作者(`name `)+ 时间(绝对 + 相对)、**HEAD / 本地分支 / 远程分支 / 标签**引用分组、完整消息(subject + body)、(与作者不同时的)提交者、变更统计、完整 SHA、Open on GitHub。**面板可见性 ⟺ 选中态**:`×` 或 `Esc` 取消选中即收起,**再次点击已选中的提交行同样取反收起**(300ms 内的双击第二击豁免)。**三区两根 gutter 可拖拽**:图 ⇄ 面板、上半区 ⇄ 下半区的宽高比例随拖实时调整、按仓库记忆。原悬停浮层(`#commit-tip`)与 `i` 快捷键整体移除;原 webview 工具栏(scope 段控 / 仓库名 / CI 登录)整体上移 VS Code 标题栏,仓库路径改由 `WebviewView.description` 呈现,省一整行竖直空间。 ## 布局与定位 -VS Code 的 panel 视图容器(`hyper-git`)内多视图只能**垂直堆叠**,无法原生并排两个子视图;编辑器区 `WebviewPanel` 方案亦曾实验后回退。故「右侧平级视窗」落地为 **Graph webview 内部左右分栏**(对齐 Git Graph 扩展的 Details 列形态): +VS Code 的 panel 视图容器(`hyper-git`)内多视图只能**垂直堆叠**,无法原生并排两个子视图;编辑器区 `WebviewPanel` 方案亦曾实验后回退。故「右侧平级视窗」落地为 **Graph webview 内部左右分栏**(对齐 Git Graph 扩展的 Details 列形态)。视图级控件全部上移 VS Code 原生标题栏(`view/title` 贡献点): + +| 控件 | 形态 | 事实源 / 显隐 | +|---|---|---| +| Scope(All / Current / Checkpoints) | `$(layers)` 图标下拉子菜单,`toggled` 勾选当前项,默认 All | host `workspaceState`(`hyperGit.log.scope:`)+ `hyperGit.log.scope` context key | +| Changed Files List ⇄ Tree | `$(list-tree)` / `$(list-flat)` 互斥图标(同 Branches 分组切换范式) | host `workspaceState`(`hyperGit.log.dmode:`)+ `hyperGit.log.tree` | +| 切换仓库(多仓库态) | `$(repo)` 图标按钮 | `hyperGit.log.multiRepo` | +| 登录 GitHub(CI) | `$(sign-in)` 图标按钮,授权完成自动隐藏 | `hyperGit.log.ciNeedsSignIn` | +| 仓库路径 | `WebviewView.description` 副标题(标题「Graph」右侧常驻) | `pushState` 时随 `repoRoot` 更新 | ```mermaid flowchart TB - subgraph body["body(纵向 flex)"] - T[".toolbar(全宽:scope 切换 / 仓库 / CI 登录)"] - subgraph main["#main(≥560px:横向 flex / <560px:.stacked 纵向)"] - VP["#viewport
提交图(flex:1,虚拟滚动)"] - subgraph panel["#commit-panel
横向 flex:0 0 42%(min 280px)
纵向 flex:0 0 45%(图区保 55%)"] - D["#details(55%,独立滚动)
Changed Files + List/Tree + ×"] - M["#commit-meta(45%,独立滚动)
作者/引用分组/消息/统计/SHA"] - end + subgraph shell["VS Code 标题栏(view/title)"] + H["Graph + description(仓库路径)
… [layers Scope▾] [list-tree/flat] [refresh] [sign-in*] [repo*] …"] + end + subgraph main["#main(≥560px 横向 flex / <560px .stacked 纵向)"] + VP["#viewport
提交图(flex:1,虚拟滚动)"] + G1["#gutter-main
拖拽 ⇄ 面板宽/高(18%–75%,
横向面板≥200px 且图≥280px)"] + subgraph panel["#commit-panel(flex-basis 拖拽钳制)"] + D["#details(独立滚动)
Changed Files + ×"] + G2["#gutter-meta
拖拽 ⇄ 上/下半区高(15%–85%)"] + M["#commit-meta(独立滚动)
作者/引用分组/消息/统计/SHA"] end end - T --- main - VP --- panel - D --- M - style T fill:#6e7681,color:#fff + H --- main + VP --- G1 --- panel + D --- G2 --- M + style H fill:#6e7681,color:#fff style VP fill:#1f6feb,color:#fff + style G1 fill:#8250df,color:#fff + style G2 fill:#8250df,color:#fff style panel fill:#21262d,stroke:#d29922,color:#fff style D fill:#d29922,color:#fff style M fill:#238636,color:#fff ``` +gutter 交互:`pointerdown` 捕获指针 → `pointermove` 实时更新 flex-basis → `pointerup` 持久化(`panelPct`/`detailPct` 随选中/目录折叠一起按仓库入 webview state);`role="separator"` + `tabindex` 键盘可达(方向键 ±2%、Home/End 归边界),`.dragging`/`:hover` 高亮分隔线。 + - **可见性不变式**:`#commit-panel.show` ⟺ `selectedHash !== null`。所有路径经三个漏斗收敛——`requestPanelData(hash)`(开面板 + Loading)、`deselectRow()`(收面板 + 清选中 + 焦点回落图区)、`log/graphData` 的恢复/消失分支。`deselectRow()` 的幂等守卫**同时校验选中态与面板可见性**:`graphData` 解析出「无选中」(如切到无记忆选中的仓库)时 `selectedHash` 本已为 `null`,仅按选中态早退会把上一仓库的面板连内容一起留在原地。 - **窄视口降级**:面板 `min-width: 280px` 不可收缩,横向并排会把 `#viewport` 挤至零宽(`flex: 1` + `min-width: 0`)。故 `#main` 宽度 < 560px 时切 `.stacked` 退化为上下堆叠(面板 45%,图区保 55%)——观察 `#main` 而非 `#viewport`,因后者宽度随面板开合变化会形成「开面板→变窄→堆叠→变宽」反馈环。面板开合引起的 `#viewport` 宽度变化仍由既有 `ResizeObserver` 切 `.narrow`(隐 author/date、留 CI 列)。 @@ -53,10 +67,11 @@ flowchart LR - **一次选中、两路并行**:host `log/selectCommit` 分支同时发起变更文件与详情取数,均沿用 `rootAtStart` 切库竞态守卫(issue #107)。 - **过期回包丢弃**:`log/commitFiles` 按 `hash`、`log/commitDetail` 按 **`forHash`** 回显校验——失败回包 `vm=null` 无 hash 可比对,`forHash` 补齐该盲区,杜绝迟到失败在新选中下闪现占位。 - **失败非静默**:详情取数失败显示 "Details unavailable" 占位;同 hash 重点击放行重试(`metaFailHash` 例外)。 -- **同 hash 重点击防抖**:不重拉防闪烁;方向键导航每次必换 hash,不受影响。 +- **同 hash 重点击取反**:点击已选中的提交行 = 反向操作收起面板(方向键/Home/End 边界仍走防抖早退,不误触);距选中 <300ms 的第二击视为双击成分、豁免取反(双击=「打开」语义保留,无开合抖动);取数失败态(`metaFailHash`)例外放行重试。 - **刷新语义**:提交对象不可变——文件不重拉;`meta` 按新行集重渲(新推分支/标签等引用 chips 变化即时反映)。 - **重载恢复**:选中 hash 本就按仓库持久化(state v2),`graphData` 到达且选中行存在时补拉面板数据,实现跨重载/切库恢复。 - **XSS 安全**:分支名/标签/作者/正文等一切外部内容均经 `esc()` 转义后注入。 +- **标题栏切换通路**:scope 切换 → host `setScope`(memento + context key + 防抖重拉);List/Tree 切换 → host `setDetailMode`(`log/detailMode` 定向下发,免整图重拉);两者均为 host 侧事实源,随 `graphData`(`scope`/`dmode` 字段)在重载/切库后恢复。 ## 边缘 Case @@ -68,19 +83,23 @@ flowchart LR | 根提交 / merge | "No changed files (may be a root or merge commit)" 占位 + 详情正常 | | CI 图标 | 点击不选行、CI 浮层原样(原互斥代码随浮层一并消失) | | 非 GitHub 远程 | 无 Open on GitHub 链接(host 不下发 `githubUrl`) | -| 键盘 | 方向键/Home/End 移动选中即更新面板;`Enter` 菜单、右键菜单原样;`Esc` 取消选中 | +| 键盘 | 方向键/Home/End 移动选中即更新面板;`Enter` 菜单、右键菜单原样;`Esc` 取消选中;gutter 聚焦后方向键 ±2%、Home/End 归边界 | +| 再次点击已选中行 | 反向操作收起面板;双击(<300ms 第二击)豁免;取数失败态重点击 = 重试 | | 切到无记忆选中的仓库 | 面板收起并清空(不残留上一仓库的文件列表 / 详情,杜绝旧 hash 误发 `log/openFile`) | | 视图窄于 560px | `.stacked` 上下堆叠,图区恒占满宽、不被面板压缩;`≥560px` 恢复左右分栏 | +| gutter 极限拖拽 | 横向钳制:面板 ≥200px 且图区 ≥280px;纵向钳制 18%–75% / 上半区 15%–85%,比例按仓库记忆 | +| 切库后拖拽比例 | `loadPersistedFor` 重装载该仓库记忆的 `panelPct`/`detailPct`(选中/目录折叠同批) | ## 实现 -- 面板与漏斗:[`adapter/webview/log-webview.ts`](../../src/adapter/webview/log-webview.ts)(`#main`/`#commit-panel` 分栏、`selectRow`/`requestPanelData`/`deselectRow`、`renderCommitMeta`、Loading 占位、消息守卫)。 -- 协议:[`shared/protocol.ts`](../../src/shared/protocol.ts)(`log/commitDetail` 增 `forHash`;移除 `log/showCommitDetail` 与编辑器区面板遗留死类型)。 +- 面板与漏斗:[`adapter/webview/log-webview.ts`](../../src/adapter/webview/log-webview.ts)(`#main`/`#commit-panel` 分栏、`selectRow`/`requestPanelData`/`deselectRow`、`renderCommitMeta`、Loading 占位、消息守卫、gutter 拖拽/键盘、再次点击取反与 300ms 双击守卫)。 +- 标题栏控件:[`package.json`](../../package.json)(`view/title` 贡献:`hyperGit.log.scopeMenu` 子菜单 + `toggled` 勾选、`detailTree/detailFlat` 互斥图标、`selectRepository`/`ci.signIn` 条件按钮)+ host `setScope`/`setDetailMode`(memento + context key);命令注册见 [`extension.ts`](../../src/extension.ts)。 +- 协议:[`shared/protocol.ts`](../../src/shared/protocol.ts)(`log/commitDetail` 增 `forHash`、`LogGraphState` 增 `dmode`、新增 `log/detailMode`;移除 `log/showCommitDetail`、`log/setScope`、`log/selectRepo`、`log/ciSignIn` 与编辑器区面板遗留死类型)。 - 复用:[`engine/tree/file-tree.ts`](../../src/engine/tree/file-tree.ts)(目录树)、[`engine/log/commit-files.ts`](../../src/engine/log/commit-files.ts)(name-status 解析)、[`engine/ci/remote-parser.ts`](../../src/engine/ci/remote-parser.ts)(GitHub URL)、`log/openFile` → `hyperGit.openCommitFileDiff` Diff 链路不变。 ## 验证 -`pnpm run compile` + `pnpm run test:unit` 全绿;Extension Development Host(F5):点击行开面板(Loading → 文件计数 + 详情)、行悬停无浮层、CI 浮层鼠标/键盘均可用、文件点击开 Diff、List/Tree 切换、同 hash 重点击无闪烁、`×`/`Esc` 收起且方向键仍可用、快速连点终态正确、选中态下 commit 后面板保持且 chips 更新、切分支选中消失自动收起、切库/重载恢复、根/merge 占位、窄面板自适应。 +`pnpm run check-types` + `pnpm run lint` + `pnpm run test:unit`(403 用例)全绿;Extension Development Host(F5):标题栏 Scope 下拉勾选切换、List/Tree 图标切换(免整图重拉)、仓库切换/CI 登录按钮条件显隐、description 路径常驻;点击行开面板(Loading → 文件计数 + 详情)、**再次点击取反收起、双击不抖动**、`×`/`Esc` 收起且方向键仍可用、gutter 鼠标拖拽/键盘微调、快速连点终态正确、选中态下 commit 后面板保持且 chips 更新、切分支选中消失自动收起、切库/重载恢复(含拖拽比例)、根/merge 占位、窄视图堆叠自适应。 分栏与状态机另经**离屏夹具实测**(抽取本文件真实 CSS + webview 脚本、桩掉 `acquireVsCodeApi` 后在浏览器中量测): @@ -93,9 +112,9 @@ flowchart LR | 320px | 40px | `.stacked`,图区 320px | | 280px | **0px** | `.stacked`,图区 280px | -状态机六步序列(repo1 选中 → 切 repo2 无记忆选中 → 迟到 `vm=null` 回包 → 切回 repo1 → `×` 取消 → 重复取消)全部命中预期:面板可见性与 `selectedHash` 始终同步,重复 `deselectRow()` 零额外 `postMessage`,横向无溢出(`scrollWidth - clientWidth === 0`)。 +状态机六步序列(repo1 选中 → 切 repo2 无记忆选中 → 迟到 `vm=null` 回包 → 切回 repo1 → `×` 取消 → 重复取消)全部命中预期:面板可见性与 `selectedHash` 始终同步,重复 `deselectRow()` 零额外 `postMessage`,横向无溢出(`scrollWidth - clientWidth === 0`)。本轮标题栏迁移 + gutter + 取反交互复验:工具栏 DOM 零残留;真实鼠标拖拽主 gutter 150px → 面板 462→614px、图区 634→482px、`panelPct=0.5582` 入库;meta gutter 拖拽 80px → 上半区 275→357px、`detailPct=0.714`;极限拖拽钳制面板 200px / 图区 280px;键盘 ±2% 生效;堆叠态(420px)拖拽双向生效且图区恒满宽;`log/detailMode` 与 `graphData.dmode` 均正确驱动 Tree 渲染;webview 持久化键收敛为 `selectedHash/dcollapsed/panelPct/detailPct`。 ## 展望 -- 面板宽度拖拽分隔条(现横向 42% + 280px 下限、纵向 45%,560px 处自动降级,YAGNI 暂缓)。 +- 分栏比例持久化迁移至 host `workspaceState`(现随 webview state 按仓库记忆,与 scope/dmode 的宿主对齐;低优先)。 - 详情区随 `host` 防抖合并(方向键连续导航时减少单提交 `git` 调用,现为每停 2 次,毫秒级可接受)。 diff --git a/docs/features/multi-root-repo-selection.md b/docs/features/multi-root-repo-selection.md index 24e8636..c38533a 100644 --- a/docs/features/multi-root-repo-selection.md +++ b/docs/features/multi-root-repo-selection.md @@ -1,6 +1,6 @@ # 多根工作区仓库切换(Multi-root Repository Selection) -> 多根工作区(multi-root)含多个 Git 仓库时,**Graph 工具栏仓库名升级为可切换按钮**(对标 Git Graph 的 repo selector),Command Palette 亦可直达。切换的是**全局活跃仓库**:Graph / Branches / Commit(changelist)/ Stash / Shelf / Worktrees / 未提交角标**全部视图跟随**,changelist 分配、分支收藏、分支分组偏好、Shelf 存储、最近提交消息**按仓库隔离记忆**,重开窗口回到上次操作的仓库。 +> 多根工作区(multi-root)含多个 Git 仓库时,**Graph 标题栏出现「切换仓库」图标按钮**(`$(repo)`,对标 Git Graph 的 repo selector;仓库路径常驻 `WebviewView.description` 副标题),Command Palette 亦可直达。切换的是**全局活跃仓库**:Graph / Branches / Commit(changelist)/ Stash / Shelf / Worktrees / 未提交角标**全部视图跟随**,changelist 分配、分支收藏、分支分组偏好、Shelf 存储、最近提交消息**按仓库隔离记忆**,重开窗口回到上次操作的仓库。 ## 交互形态 @@ -17,7 +17,7 @@ flowchart LR style E fill:#8957e5,color:#fff ``` -- **入口一(主)**:Graph 工具栏右侧仓库名按钮——多仓库时显示 `basename ▾`、hover 高亮,单仓库退化为纯文本(现状观感,多数用户零感知); +- **入口一(主)**:Graph 标题栏右侧「切换仓库」图标按钮(`when: hyperGit.log.multiRepo` 显隐;原 webview 工具栏仓库名按钮上移,省一行竖直空间); - **入口二**:Command Palette → `Hyper Git: Select Repository…`(单仓库时提示 no-op); - **持久化**:活跃仓库记于 `workspaceState`(`hyperGit.activeRepoRoot`,per-workspace 天然隔离),重开窗口恢复。 diff --git a/package.json b/package.json index a92adef..334a1a7 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,33 @@ "category": "Hyper Git", "icon": "$(refresh)" }, + { + "command": "hyperGit.log.scopeAll", + "title": "All Commits", + "category": "Hyper Git" + }, + { + "command": "hyperGit.log.scopeCurrent", + "title": "Current Branch", + "category": "Hyper Git" + }, + { + "command": "hyperGit.log.scopeCheckpointer", + "title": "Checkpoints", + "category": "Hyper Git" + }, + { + "command": "hyperGit.log.detailTree", + "title": "Group Changed Files by Directory", + "category": "Hyper Git", + "icon": "$(list-tree)" + }, + { + "command": "hyperGit.log.detailFlat", + "title": "Show Changed Files as Flat List", + "category": "Hyper Git", + "icon": "$(list-flat)" + }, { "command": "hyperGit.selectRepository", "title": "Select Repository…", @@ -697,7 +724,34 @@ "icon": "$(clear-all)" } ], + "submenus": [ + { + "id": "hyperGit.log.scopeMenu", + "label": "Scope", + "icon": "$(layers)" + } + ], "menus": { + "hyperGit.log.scopeMenu": [ + { + "command": "hyperGit.log.scopeAll", + "when": "view == hyperGit.log", + "group": "navigation@1", + "toggled": "hyperGit.log.scope == 'all'" + }, + { + "command": "hyperGit.log.scopeCurrent", + "when": "view == hyperGit.log", + "group": "navigation@2", + "toggled": "hyperGit.log.scope == 'current'" + }, + { + "command": "hyperGit.log.scopeCheckpointer", + "when": "view == hyperGit.log", + "group": "navigation@3", + "toggled": "hyperGit.log.scope == 'checkpointer'" + } + ], "view/title": [ { "command": "hyperGit.refresh", @@ -744,11 +798,36 @@ "when": "view == hyperGit.commit", "group": "1_sync@6" }, + { + "submenu": "hyperGit.log.scopeMenu", + "when": "view == hyperGit.log", + "group": "navigation@0.5" + }, + { + "command": "hyperGit.log.detailTree", + "when": "view == hyperGit.log && !hyperGit.log.tree", + "group": "navigation@0.75" + }, + { + "command": "hyperGit.log.detailFlat", + "when": "view == hyperGit.log && hyperGit.log.tree", + "group": "navigation@0.75" + }, { "command": "hyperGit.refreshLog", "when": "view == hyperGit.log", "group": "navigation@1" }, + { + "command": "hyperGit.ci.signIn", + "when": "view == hyperGit.log && hyperGit.log.ciNeedsSignIn", + "group": "navigation@1.2" + }, + { + "command": "hyperGit.selectRepository", + "when": "view == hyperGit.log && hyperGit.log.multiRepo", + "group": "navigation@1.5" + }, { "command": "hyperGit.logFilter", "when": "view == hyperGit.log", diff --git a/src/adapter/webview/log-webview.ts b/src/adapter/webview/log-webview.ts index b5a6ba2..da94014 100644 --- a/src/adapter/webview/log-webview.ts +++ b/src/adapter/webview/log-webview.ts @@ -96,12 +96,28 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter private view?: vscode.WebviewView; private filter: LogFilter = {}; private scope: LogScope = 'all'; - /** scope 按仓库记忆(内存级,issue #107):切回仓库恢复其视图范围,host 侧即事实源随 graphData 下发。 */ - private readonly scopeByRepo = new Map(); + /** 变更文件展示模式(List/Tree,标题栏图标切换):host 为事实源,随 graphData / log/detailMode 下发。 */ + private detailMode: 'flat' | 'tree' = 'flat'; private refreshTimer: ReturnType | undefined; private readonly disposables: vscode.Disposable[] = []; - constructor(private readonly service: GitRepositoryService, private readonly ciService: GitHubCiService) { + /** scope / dmode 按仓库持久化 key(issue #107 同 branchesGrouping 范式:workspaceState 换 key 重绑)。 */ + private static scopeKey(root: string): string { + return `hyperGit.log.scope:${root}`; + } + private static dmodeKey(root: string): string { + return `hyperGit.log.dmode:${root}`; + } + /** context key 同步(标题栏按钮显隐 / 勾选态由 when 子句驱动)。 */ + private static setCtx(key: string, value: string | boolean): void { + void vscode.commands.executeCommand('setContext', key, value); + } + + constructor( + private readonly service: GitRepositoryService, + private readonly ciService: GitHubCiService, + private readonly workspaceState: vscode.Memento, + ) { // 兜底实时刷新:git 状态变化(commit/checkout 等)防抖重拉首页。 let t: ReturnType | undefined; this.disposables.push( @@ -110,16 +126,52 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter t = setTimeout(() => this.refresh(), 400); }), ); + // 激活即恢复当前仓库的 scope/dmode 记忆并同步 context key(标题栏控件先于视图解析渲染)。 + this.loadRepoScopedPrefs(); // 活跃仓库切换(issue #107):host 级过滤条件是旧仓库语境的产物,跨仓库无意义 → 清空; - // scope 恢复该仓库的记忆值(无记忆回退 'all');图数据重拉由上方 onDidChange 订阅驱动。 + // scope/dmode 恢复该仓库的记忆值(无记忆回退 'all'/'flat');图数据重拉由上方 onDidChange 订阅驱动。 this.disposables.push( - service.onDidChangeRepository((e) => { + service.onDidChangeRepository(() => { this.filter = {}; - this.scope = (e.root ? this.scopeByRepo.get(e.root) : undefined) ?? 'all'; + this.loadRepoScopedPrefs(); }), ); } + /** 装载当前仓库的 scope / dmode 偏好(memento → 内存 + context key)。 */ + private loadRepoScopedPrefs(): void { + const root = this.service.repoRoot; + this.scope = (root ? this.workspaceState.get(LogWebviewProvider.scopeKey(root)) : undefined) ?? 'all'; + this.detailMode = (root ? this.workspaceState.get<'flat' | 'tree'>(LogWebviewProvider.dmodeKey(root)) : undefined) ?? 'flat'; + LogWebviewProvider.setCtx('hyperGit.log.scope', this.scope); + LogWebviewProvider.setCtx('hyperGit.log.tree', this.detailMode === 'tree'); + } + + /** 标题栏 Scope 子菜单选中项(hyperGit.log.scopeAll/Current/Checkpointer 命令入口)。 */ + setScope(scope: LogScope): void { + const root = this.service.repoRoot; + if (root) { + void this.workspaceState.update(LogWebviewProvider.scopeKey(root), scope); + } + this.scope = scope; + LogWebviewProvider.setCtx('hyperGit.log.scope', scope); + this.refresh(); + } + + /** 标题栏 List/Tree 互斥图标切换(同 Branches 平铺/分组范式):免整图重拉,定向下发渲染模式。 */ + setDetailMode(mode: 'flat' | 'tree'): void { + if (this.detailMode === mode) { + return; + } + const root = this.service.repoRoot; + if (root) { + void this.workspaceState.update(LogWebviewProvider.dmodeKey(root), mode); + } + this.detailMode = mode; + LogWebviewProvider.setCtx('hyperGit.log.tree', mode === 'tree'); + this.post({ type: 'log/detailMode', payload: { mode } }); + } + setFilter(filter: LogFilter): void { this.filter = filter; this.refresh(); @@ -184,13 +236,6 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter msg.payload.oldPath, ); break; - case 'log/setScope': - this.scope = msg.payload.scope; - if (this.service.repoRoot) { - this.scopeByRepo.set(this.service.repoRoot, msg.payload.scope); - } - void this.pushState(); - break; case 'log/commitAction': if (msg.payload.op === 'menu') { void this.handleCommitMenu(msg.payload.hash); @@ -202,13 +247,6 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter case 'log/openExternal': void this.ciService.openExternal(msg.payload.url); break; - case 'log/ciSignIn': - void this.handleCiSignIn(); - break; - case 'log/selectRepo': - // 复用 postMessage → 原生交互 → executeCommand 通路(同 handleCommitMenu 形态)。 - void vscode.commands.executeCommand('hyperGit.selectRepository'); - break; } } @@ -288,9 +326,14 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter maxLanes: page.maxLanes, hasMore: page.hasMore, scope: this.scope, + dmode: this.detailMode, repoRoot: this.service.repoRoot ?? '', multiRepo: this.service.listRepositories().length > 1, }; + // 标题栏副标题 = 仓库路径(原 webview 工具栏文本上移,省一行竖直空间); + // 多仓库态同步 context key 以显隐「切换仓库」图标按钮。 + this.view.description = this.service.repoRoot ?? undefined; + LogWebviewProvider.setCtx('hyperGit.log.multiRepo', !!state.multiRepo); this.post({ type: 'log/graphData', payload: state }); // CI 元信息异步随附(不阻塞建图):远程为 GitHub 才启用,未授权则提示登录。 void this.pushCiMeta(); @@ -308,6 +351,8 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter } catch { meta = { available: false, needsSignIn: false }; } + // 登录入口迁至标题栏图标按钮(when: hyperGit.log.ciNeedsSignIn),授权完成即自动隐藏。 + LogWebviewProvider.setCtx('hyperGit.log.ciNeedsSignIn', meta.needsSignIn); if (this.view) { this.post({ type: 'log/ciMeta', payload: meta }); } @@ -329,12 +374,6 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter this.post({ type: 'log/ciData', payload: { map: rec } }); } - /** 用户点击「登录 GitHub 查看 CI」:走原生授权,完成后刷新 CI 元信息。 */ - private async handleCiSignIn(): Promise { - await this.ciService.signIn(); - await this.pushCiMeta(); - } - private async loadMore(cursor: number): Promise { const page = await this.fetchPage(cursor); if (!page || page.rows.length === 0) { @@ -505,16 +544,6 @@ ${getBaseStyles()} :root { --hg-row: 24px; --hg-lane: 14px; } * { box-sizing: border-box; } body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); color: var(--vscode-foreground); background: var(--vscode-sideBar-background); display: flex; flex-direction: column; height: 100vh; overflow: hidden; } -.toolbar { display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-bottom: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.25)); } -.seg { display: inline-flex; border: 1px solid var(--vscode-input-border, transparent); border-radius: 4px; overflow: hidden; } -.seg button { background: transparent; color: var(--vscode-foreground); border: none; padding: 2px 9px; font-size: 11px; cursor: pointer; opacity: 0.65; transition: background-color .12s ease, opacity .12s ease; } -.seg button:hover { background: var(--vscode-toolbar-hoverBackground, var(--vscode-list-hoverBackground)); opacity: 0.9; } -.seg button.active { background: var(--vscode-inputOption-activeBackground, var(--vscode-button-background)); color: var(--vscode-inputOption-activeForeground, var(--vscode-button-foreground)); opacity: 1; } -.repo { margin-left: auto; font-size: 10px; opacity: 0.55; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* 多仓库态:仓库名升级为可点击切换按钮(Git Graph 形态);单仓库保持纯文本观感。 */ -button.repo { background: transparent; color: var(--vscode-foreground); border: none; padding: 1px 6px; border-radius: 3px; cursor: pointer; } -button.repo.switchable:hover { opacity: 1; background: var(--vscode-list-hoverBackground); } -button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; } #viewport { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; position: relative; outline: none; } #spacer { position: relative; } #rows { position: absolute; left: 0; right: 0; } @@ -538,15 +567,26 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin .author { flex: 0 0 auto; font-size: 11px; opacity: 0.7; max-width: 110px; overflow: hidden; text-overflow: ellipsis; padding-left: 8px; } .date { flex: 0 0 auto; font-size: 11px; opacity: 0.55; padding-left: 8px; } #viewport.narrow .author, #viewport.narrow .date { display: none; } -/* ── 图 + 右侧详情面板水平分栏(panel 容器无法并排子视图 → webview 内自分栏)── */ +/* ── 图 + 右侧详情面板水平分栏(panel 容器无法并排子视图 → webview 内自分栏)── + 工具栏整体上移 VS Code 标题栏(scope 子菜单 / List-Tree 图标 / 仓库切换 / CI 登录), + 仓库路径由 WebviewView.description 呈现 → 省一整行竖直空间。 */ #main { flex: 1 1 auto; display: flex; min-height: 0; } -#commit-panel { display: none; flex: 0 0 42%; min-width: 280px; max-width: 65%; flex-direction: column; overflow: hidden; border-left: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.25)); } +#commit-panel { display: none; flex: 0 0 42%; min-width: 200px; flex-direction: column; overflow: hidden; } #commit-panel.show { display: flex; } -/* 分栏降级:#main 窄于 560px 时上下堆叠——面板 280px 硬底不可收缩,横向并排会把图区挤成零宽。 */ +/* 可拖拽分割线(gutter):图 ⇄ 面板(横向 col-resize / 堆叠态 row-resize)与面板内 上半区 ⇄ 下半区。 + 宽高比例经拖拽实时更新(flex-basis 内联),松手持久化(按仓库记忆)。 */ +.gutter { flex: 0 0 4px; background: transparent; touch-action: none; } +#gutter-main { cursor: col-resize; } +#main:not(.panel-open) > #gutter-main { display: none; } +#gutter-meta { cursor: row-resize; } +.gutter:hover, .gutter.dragging { background: var(--vscode-focusBorder, rgba(128,128,128,.4)); } +.gutter:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; } +/* 分栏降级:#main 窄于 560px 时上下堆叠——面板 200px 硬底不可收缩,横向并排会把图区挤成零宽。 */ #main.stacked { flex-direction: column; } -#main.stacked #commit-panel { flex: 0 0 45%; min-width: 0; max-width: none; border-left: none; border-top: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.25)); } -#details { flex: 1 1 55%; min-height: 0; overflow-y: auto; border-bottom: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.15)); } -#commit-meta { flex: 1 1 45%; min-height: 0; overflow-y: auto; } +#main.stacked #commit-panel { min-width: 0; } +#main.stacked #gutter-main { cursor: row-resize; } +#details { flex: 0 0 55%; min-height: 0; overflow-y: auto; } +#commit-meta { flex: 1 1 auto; min-height: 0; overflow-y: auto; } .panel-loading { padding: 10px 12px; font-size: 12px; color: var(--vscode-descriptionForeground); } #details .dh { position: sticky; top: 0; display: flex; align-items: center; gap: 6px; background: var(--vscode-sideBar-background); padding: 4px 8px; font-size: 11px; color: var(--vscode-descriptionForeground); border-bottom: 1px solid var(--vscode-editorWidget-border, rgba(128,128,128,.15)); } #details .dh #details-title { flex: 1 1 auto; } @@ -575,8 +615,6 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin @keyframes ci-rot { to { transform: rotate(360deg); } } .ci-spin { transform-origin: 50% 50%; animation: ci-rot 1s linear infinite; } @media (prefers-reduced-motion: reduce) { .ci-spin { animation: none; } } -.ci-signin { display: none; background: transparent; border: 1px solid var(--vscode-button-border, var(--vscode-input-border, transparent)); color: var(--vscode-textLink-foreground); font-size: 10px; padding: 1px 6px; border-radius: 3px; cursor: pointer; opacity: 0.85; } -.ci-signin:hover { opacity: 1; background: var(--vscode-list-hoverBackground); } /* ── CI Tooltip(自定义浮层,置于 #rows 之外,虚拟滚动重写不销毁)── */ #ci-tip { position: fixed; z-index: 50; display: none; max-width: 360px; min-width: 220px; max-height: 320px; overflow: hidden; background: var(--vscode-editorHoverWidget-background, var(--vscode-editorWidget-background)); color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground)); border: 1px solid var(--vscode-editorHoverWidget-border, var(--vscode-editorWidget-border, rgba(128,128,128,.3))); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,.35); font-size: 12px; } #ci-tip.show { display: flex; flex-direction: column; } @@ -626,7 +664,6 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin #commit-meta .ct-gh:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; border-radius: 2px; } #commit-meta .ct-gh svg { width: 13px; height: 13px; } /* ── 变更文件目录树(详情面板 Group By Directory 形态)── */ -#details .dh .seg { flex: 0 0 auto; } #details .tree-dir { display: flex; align-items: center; gap: 6px; padding: 2px 10px; font-size: 12px; cursor: pointer; user-select: none; } #details .tree-dir:hover { background: var(--vscode-list-hoverBackground); } #details .tree-dir .tree-twist { flex: 0 0 12px; text-align: center; font-size: 10px; opacity: 0.8; } @@ -634,15 +671,6 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin -
- - - - - - - -
@@ -650,8 +678,10 @@ button.repo:focus-visible { outline: 1px solid var(--vscode-focusBorder); outlin
Loading…
+
@@ -676,33 +706,35 @@ const PALETTE = (function () { }); })(); const ROW_H = 24, LANE_W = 14, NODE_R = 4, GUTTER = 10, OVERSCAN = 8, LOAD_THRESHOLD = 40; -/** scope 白名单兜底:仅接受三态,否则回退默认 'all'(兼容未来废弃的持久化值)。 */ -function normalizeScope(v) { return v === 'all' || v === 'current' || v === 'checkpointer' ? v : 'all'; } -// ── 视图状态按仓库分区(v2,issue #107):scope/选中/dmode/dcollapsed 记忆跟随仓库, -// 切换仓库换装载互不串扰;无 v2 时从旧平铺结构一次性升级(旧值归首个见到的仓库,不丢偏好)。── +// ── 视图状态按仓库分区(v2,issue #107):选中/目录折叠/分栏比例记忆跟随仓库,切换仓库换装载互不串扰; +// scope 与 List/Tree 模式已上移标题栏(host workspaceState 为事实源,随 graphData 下发),不再入 webview state; +// 无 v2 时从旧平铺结构一次性升级(旧值归首个见到的仓库,不丢偏好)。── const persistedRaw = vscode.getState() || {}; let persistedRepo = ''; let persistedByRepo = {}; if (persistedRaw.v === 2 && persistedRaw.byRepo) { persistedByRepo = persistedRaw.byRepo; } else if (persistedRaw.selectedHash || persistedRaw.scope || persistedRaw.dmode || persistedRaw.dcollapsed) { - persistedByRepo = { '': { selectedHash: persistedRaw.selectedHash, scope: persistedRaw.scope, dmode: persistedRaw.dmode, dcollapsed: persistedRaw.dcollapsed } }; + persistedByRepo = { '': { selectedHash: persistedRaw.selectedHash, dcollapsed: persistedRaw.dcollapsed } }; } let selectedHash = null; -let scope = 'all'; -let detailsMode = 'flat'; +let detailsMode = 'flat'; // host 为事实源(graphData.dmode / log/detailMode 下发),webview 仅渲染。 let dcollapsed = new Set(); -/** 装载某仓库的分区状态(graphData 到达时调用;host 下发的 scope 为该仓库事实源)。 */ -function loadPersistedFor(repoRoot, hostScope) { +// 分栏比例(拖拽 gutter 调整,按仓库记忆):panelPct = 面板占 #main 宽(堆叠态为高)比例;detailPct = 上半区占面板高比例。 +let panelPct = 0.42, detailPct = 0.55; +function clamp01(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } +/** 装载某仓库的分区状态(graphData 到达时调用)。 */ +function loadPersistedFor(repoRoot) { persistedRepo = repoRoot; const s = persistedByRepo[repoRoot] || persistedByRepo[''] || {}; selectedHash = s.selectedHash || null; - scope = hostScope || normalizeScope(s.scope); - detailsMode = s.dmode === 'tree' ? 'tree' : 'flat'; dcollapsed = new Set(s.dcollapsed || []); + panelPct = clamp01(typeof s.panelPct === 'number' ? s.panelPct : 0.42, 0.18, 0.75); + detailPct = clamp01(typeof s.detailPct === 'number' ? s.detailPct : 0.55, 0.15, 0.85); + applyPanelSizes(); } function persist() { - persistedByRepo[persistedRepo] = { selectedHash: selectedHash, scope: scope, dmode: detailsMode, dcollapsed: Array.from(dcollapsed) }; + persistedByRepo[persistedRepo] = { selectedHash: selectedHash, dcollapsed: Array.from(dcollapsed), panelPct: panelPct, detailPct: detailPct }; vscode.setState({ v: 2, byRepo: persistedByRepo }); } let model = { rows: [], maxLanes: 0, hasMore: false, repoRoot: '', multiRepo: false }; @@ -718,20 +750,19 @@ let ciReqTimer = null; let ciPendingRefreshTimer = null; let ciRefreshing = false; const ciTipEl = document.getElementById('ci-tip'); -const ciSignInEl = document.getElementById('ci-signin'); let tipHash = null, tipShowT = null, tipHideT = null, overIcon = false, overTip = false; const viewport = document.getElementById('viewport'); const spacer = document.getElementById('spacer'); const rowsEl = document.getElementById('rows'); -const repoEl = document.getElementById('repo'); const emptyEl = document.getElementById('empty'); const spinnerEl = document.getElementById('spinner'); +const detailsEl = document.getElementById('details'); const detailsList = document.getElementById('details-list'); const detailsTitleEl = document.getElementById('details-title'); const detailsCloseEl = document.getElementById('details-close'); -const dmodeFlatEl = document.getElementById('dmode-flat'); -const dmodeTreeEl = document.getElementById('dmode-tree'); const mainEl = document.getElementById('main'); +const gutterMainEl = document.getElementById('gutter-main'); +const gutterMetaEl = document.getElementById('gutter-meta'); const commitPanelEl = document.getElementById('commit-panel'); const commitMetaEl = document.getElementById('commit-meta'); let curDetailHash = null, curDetailFiles = [], curDetailTree = []; @@ -741,8 +772,6 @@ const errorMsgEl = document.getElementById('error-msg'); const retryBtnEl = document.getElementById('retry-btn'); function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } -// 仓库根路径 → basename(多仓库态按钮文案;尾分隔符安全)。 -function repoBasename(p) { const parts = String(p).split(/[\\\\/]/).filter(Boolean); return parts.pop() || String(p); } // 引用胶囊图标(内联 SVG,仿 codicon git-branch / cloud / tag;fill=currentColor 继承 chip 前景色)。 // 项目未引入 codicon 字体(localResourceRoots=[]、CSP 无 font-src),故图标一律内联,与 ciGlyph 一致。 const ICO_BRANCH = ''; @@ -872,12 +901,6 @@ function render() { spacer.style.height = (total * ROW_H) + 'px'; emptyEl.style.display = total === 0 ? 'block' : 'none'; errorEl.style.display = 'none'; - document.getElementById('scope-all').classList.toggle('active', scope === 'all'); - document.getElementById('scope-current').classList.toggle('active', scope === 'current'); - document.getElementById('scope-checkpointer').classList.toggle('active', scope === 'checkpointer'); - document.getElementById('scope-all').setAttribute('aria-pressed', String(scope === 'all')); - document.getElementById('scope-current').setAttribute('aria-pressed', String(scope === 'current')); - document.getElementById('scope-checkpointer').setAttribute('aria-pressed', String(scope === 'checkpointer')); if (model.hasMore && !fetching && l >= total - LOAD_THRESHOLD) { fetching = true; spinnerEl.style.display = 'block'; vscode.postMessage({ type: 'log/loadMore', payload: { cursor: total } }); @@ -1051,11 +1074,11 @@ function hideTip() { tipHash = null; } function openCiUrl(url) { if (url) vscode.postMessage({ type: 'log/openExternal', payload: { url: url } }); } -function renderCiMeta() { ciSignInEl.style.display = ciMeta.needsSignIn ? 'inline-block' : 'none'; } function selectRow(hash) { if (hash === selectedHash && metaFailHash !== hash) return; // 同 hash 重点击不重拉防闪烁;失败态例外放行重试。 selectedHash = hash; + selectedAt = Date.now(); persist(); renderedFirst = -1; scheduleRender(); requestPanelData(hash); @@ -1065,6 +1088,7 @@ function requestPanelData(hash) { curMetaVm = null; metaFailHash = null; curDetailHash = null; curDetailFiles = []; curDetailTree = []; // 清旧选中残留,Loading 期间 List/Tree 切换不渲染上一提交。 commitPanelEl.classList.add('show'); + mainEl.classList.add('panel-open'); // 主 gutter(图 ⇄ 面板分割线)随面板显隐。 detailsTitleEl.textContent = 'Changed Files · ' + hash.slice(0, 7); // 计数待回包补齐。 detailsList.innerHTML = '
Loading…
'; commitMetaEl.innerHTML = '
Loading…
'; @@ -1078,6 +1102,7 @@ function deselectRow() { persist(); renderedFirst = -1; scheduleRender(); commitPanelEl.classList.remove('show'); + mainEl.classList.remove('panel-open'); if (commitPanelEl.contains(document.activeElement)) viewport.focus(); commitMetaEl.innerHTML = ''; detailsList.innerHTML = ''; detailsTitleEl.textContent = ''; curDetailHash = null; curDetailFiles = []; curDetailTree = []; @@ -1099,10 +1124,17 @@ function moveSel(delta) { } } +let selectedAt = 0; // 选中时刻:双击守卫——dblclick 的第二击落在 300ms 内不视为「再次点击取反」。 rowsEl.addEventListener('click', function (e) { if (e.target.closest('.ci')) return; // 点击 CI 图标不选中提交行 const r = e.target.closest('.row'); if (!r) return; - selectRow(r.getAttribute('data-hash')); + const h = r.getAttribute('data-hash'); + // 再次点击已选中的提交 = 反向操作:收起 ChangeFiles / Commit 详情面板(取数失败态例外,放行重试)。 + if (h === selectedHash && metaFailHash !== h) { + if (Date.now() - selectedAt > 300) deselectRow(); + return; + } + selectRow(h); }); rowsEl.addEventListener('mouseover', function (e) { const icon = e.target.closest && e.target.closest('.ci'); @@ -1140,26 +1172,11 @@ ciTipEl.addEventListener('keydown', function (e) { if (e.key === 'Escape') { hideTip(); } else if (e.key === 'Enter') { const t = e.target.closest('[data-url]'); if (t) openCiUrl(t.getAttribute('data-url')); } }); -ciSignInEl.addEventListener('click', function () { vscode.postMessage({ type: 'log/ciSignIn' }); }); -rowsEl.addEventListener('dblclick', function (e) { - // 双击 = 打开提交(选中 + 展开详情),与 VS Code「双击即打开」一致;提交操作菜单保留在右键与 Enter。 - const r = e.target.closest('.row'); if (!r) return; - selectRow(r.getAttribute('data-hash')); -}); rowsEl.addEventListener('contextmenu', function (e) { const r = e.target.closest('.row'); if (!r) return; e.preventDefault(); vscode.postMessage({ type: 'log/commitAction', payload: { op: 'menu', hash: r.getAttribute('data-hash') } }); }); -function setScope(next) { if (scope !== next) { scope = next; persist(); vscode.postMessage({ type: 'log/setScope', payload: { scope: next } }); } } -document.getElementById('scope-all').addEventListener('click', function () { setScope('all'); }); -document.getElementById('scope-current').addEventListener('click', function () { setScope('current'); }); -document.getElementById('scope-checkpointer').addEventListener('click', function () { setScope('checkpointer'); }); -// 仓库名按钮(多仓库态):点击弹出原生仓库选择(host 复用 hyperGit.selectRepository)。 -repoEl.addEventListener('click', function () { - if (!model.multiRepo) { return; } - vscode.postMessage({ type: 'log/selectRepo' }); -}); detailsCloseEl.addEventListener('click', function () { deselectRow(); }); retryBtnEl.addEventListener('click', function () { errorEl.style.display = 'none'; spinnerEl.style.display = 'block'; vscode.postMessage({ type: 'log/retry' }); }); viewport.addEventListener('scroll', scheduleRender, { passive: true }); @@ -1202,20 +1219,10 @@ function toggleDetailCollapse(p) { persist(); renderDetails(curDetailHash, curDetailFiles, curDetailTree); } -function updateDetailModeButtons() { - dmodeFlatEl.classList.toggle('active', detailsMode === 'flat'); - dmodeTreeEl.classList.toggle('active', detailsMode === 'tree'); - dmodeFlatEl.setAttribute('aria-pressed', String(detailsMode === 'flat')); - dmodeTreeEl.setAttribute('aria-pressed', String(detailsMode === 'tree')); -} -function setDetailMode(m) { if (detailsMode === m) return; detailsMode = m; persist(); updateDetailModeButtons(); if (curDetailHash) renderDetails(curDetailHash, curDetailFiles, curDetailTree); } -dmodeFlatEl.addEventListener('click', function () { setDetailMode('flat'); }); -dmodeTreeEl.addEventListener('click', function () { setDetailMode('tree'); }); /** 渲染面板上半区(Changed Files);可见性由选中态经 requestPanelData/deselectRow 驱动,此处不再触碰 .show。 */ function renderDetails(hash, files, tree) { curDetailHash = hash; curDetailFiles = files || []; curDetailTree = tree || []; - updateDetailModeButtons(); detailsTitleEl.textContent = 'Changed Files (' + curDetailFiles.length + ') · ' + hash.slice(0, 7); if (curDetailFiles.length === 0) { detailsList.innerHTML = '
No changed files (may be a root or merge commit)
'; return; } pruneDetailCollapsed(curDetailTree); @@ -1300,18 +1307,8 @@ window.addEventListener('message', function (e) { const m = e.data; if (m.type === 'log/graphData') { model = { rows: m.payload.rows, maxLanes: m.payload.maxLanes, hasMore: m.payload.hasMore, repoRoot: m.payload.repoRoot, multiRepo: !!m.payload.multiRepo }; - loadPersistedFor(m.payload.repoRoot, m.payload.scope); - // 仓库名按钮:多仓库态显示 basename + ▾(Git Graph 形态),单仓库保持完整路径纯文本观感。 - if (model.multiRepo) { - repoEl.textContent = repoBasename(m.payload.repoRoot) + ' ▾'; - repoEl.title = m.payload.repoRoot + ' — Switch repository'; - repoEl.setAttribute('aria-label', 'Switch repository: ' + m.payload.repoRoot); - repoEl.classList.add('switchable'); - } else { - repoEl.textContent = m.payload.repoRoot; repoEl.title = m.payload.repoRoot; - repoEl.removeAttribute('aria-label'); - repoEl.classList.remove('switchable'); - } + loadPersistedFor(m.payload.repoRoot); + detailsMode = m.payload.dmode === 'tree' ? 'tree' : 'flat'; // host 为事实源(标题栏 List/Tree 图标切换)。 // 保留 ciByHash 稳定缓存(CI 状态以不可变 hash 为键):图重置只清请求去重集合, // 已缓存的提交重绘时立即可见图标,避免「清缓存→重拉→整行重建」的闪烁。 ciRequested.clear(); ciPending.clear(); @@ -1332,6 +1329,9 @@ window.addEventListener('message', function (e) { } else if (m.type === 'log/commitFiles') { if (m.payload.hash !== selectedHash) return; // 过期回包(快速换选/切库)丢弃。 renderDetails(m.payload.hash, m.payload.files, m.payload.tree); + } else if (m.type === 'log/detailMode') { + detailsMode = m.payload.mode === 'tree' ? 'tree' : 'flat'; + if (curDetailHash) renderDetails(curDetailHash, curDetailFiles, curDetailTree); } else if (m.type === 'log/busy') { spinnerEl.style.display = m.payload.busy ? 'block' : 'none'; } else if (m.type === 'log/error') { @@ -1340,7 +1340,6 @@ window.addEventListener('message', function (e) { errorEl.style.display = 'block'; } else if (m.type === 'log/ciMeta') { ciMeta = { available: !!m.payload.available, needsSignIn: !!m.payload.needsSignIn, error: m.payload.error || '' }; - renderCiMeta(); if (ciMeta.available) ensurePendingRefresh(); else stopPendingRefresh(); renderedFirst = -1; // 强制重绘可见行(CI 槽位/登录提示出现或消失) scheduleRender(); @@ -1373,10 +1372,84 @@ function updateWidthClass() { new ResizeObserver(updateWidthClass).observe(viewport); updateWidthClass(); // 分栏降级观察器:观察 #main(宽度不随面板开合变化)而非 #viewport,杜绝「开面板→变窄→堆叠→变宽」反馈环。 -function updateLayoutClass() { mainEl.classList.toggle('stacked', mainEl.clientWidth < 560); } +function updateLayoutClass() { + const stacked = mainEl.clientWidth < 560; + mainEl.classList.toggle('stacked', stacked); + gutterMainEl.setAttribute('aria-orientation', stacked ? 'horizontal' : 'vertical'); +} new ResizeObserver(updateLayoutClass).observe(mainEl); updateLayoutClass(); +// ── 分栏比例应用与拖拽(Commits 图 / ChangeFiles / Commit 详情 三区两根 gutter)── +function applyPanelSizes() { + commitPanelEl.style.flexBasis = (panelPct * 100).toFixed(2) + '%'; + detailsEl.style.flexBasis = (detailPct * 100).toFixed(2) + '%'; +} +/** 横向模式下面板比例钳制:面板 ≥ 200px 且图区 ≥ 280px(分栏降级阈值之外的人为下限)。 */ +function clampPanelPctRow(p) { + const w = Math.max(mainEl.clientWidth, 1); + return clamp01(p, 200 / w, Math.max(200 / w, (w - 280) / w)); +} +function adjustPanelPct(delta) { + // 键盘微调无 pointer 坐标:以面板实际几何占 #main 的比例反推当前值,再叠加增量。 + const stacked = mainEl.classList.contains('stacked'); + const mr = mainEl.getBoundingClientRect(), pr = commitPanelEl.getBoundingClientRect(); + const cur = stacked ? pr.height / mr.height : pr.width / mr.width; + panelPct = stacked ? clamp01(cur + delta, 0.18, 0.75) : clampPanelPctRow(cur + delta); + applyPanelSizes(); persist(); +} +function adjustDetailPct(delta) { + const pr = commitPanelEl.getBoundingClientRect(); + const cur = clamp01((gutterMetaEl.getBoundingClientRect().top - pr.top) / Math.max(pr.height, 1), 0.15, 0.85); + detailPct = clamp01(cur + delta, 0.15, 0.85); + applyPanelSizes(); persist(); +} +/** gutter 通用拖拽:pointer capture + 实时改比例 + 松手持久化;dragging 态高亮分隔线。 */ +function draggableGutter(el, onMove) { + el.addEventListener('pointerdown', function (e) { + if (e.button !== 0) return; + e.preventDefault(); + try { el.setPointerCapture(e.pointerId); } catch { /* 指针已失效(合成事件/竞态):退化为无捕获拖拽 */ } + el.classList.add('dragging'); + const move = function (ev) { onMove(ev); applyPanelSizes(); }; + const done = function (ev) { + try { el.releasePointerCapture(ev.pointerId); } catch { /* 未持有捕获(指针已释放) */ } + el.classList.remove('dragging'); + el.removeEventListener('pointermove', move); + el.removeEventListener('pointerup', done); + el.removeEventListener('pointercancel', done); + applyPanelSizes(); persist(); + }; + el.addEventListener('pointermove', move); + el.addEventListener('pointerup', done); + el.addEventListener('pointercancel', done); + }); +} +draggableGutter(gutterMainEl, function (ev) { + const r = mainEl.getBoundingClientRect(); + if (mainEl.classList.contains('stacked')) panelPct = clamp01((r.bottom - ev.clientY) / r.height, 0.18, 0.75); + else panelPct = clampPanelPctRow((r.right - ev.clientX) / r.width); +}); +draggableGutter(gutterMetaEl, function (ev) { + const r = commitPanelEl.getBoundingClientRect(); + detailPct = clamp01((ev.clientY - r.top) / Math.max(r.height, 1), 0.15, 0.85); +}); +// 键盘可达:方向键 ±2%(横向 Left/Right 调面板、纵向 Up/Down;meta gutter 固定 Up/Down)。 +gutterMainEl.addEventListener('keydown', function (e) { + const stacked = mainEl.classList.contains('stacked'); + const dec = stacked ? 'ArrowUp' : 'ArrowLeft', inc = stacked ? 'ArrowDown' : 'ArrowRight'; + if (e.key === dec) { e.preventDefault(); adjustPanelPct(-0.02); } + else if (e.key === inc) { e.preventDefault(); adjustPanelPct(0.02); } + else if (e.key === 'Home') { e.preventDefault(); panelPct = stacked ? 0.18 : clampPanelPctRow(0.18); applyPanelSizes(); persist(); } + else if (e.key === 'End') { e.preventDefault(); panelPct = 0.75; applyPanelSizes(); persist(); } +}); +gutterMetaEl.addEventListener('keydown', function (e) { + if (e.key === 'ArrowUp') { e.preventDefault(); adjustDetailPct(-0.02); } + else if (e.key === 'ArrowDown') { e.preventDefault(); adjustDetailPct(0.02); } + else if (e.key === 'Home') { e.preventDefault(); detailPct = 0.15; applyPanelSizes(); persist(); } + else if (e.key === 'End') { e.preventDefault(); detailPct = 0.85; applyPanelSizes(); persist(); } +}); + vscode.postMessage({ type: 'log/requestState' }); diff --git a/src/extension.ts b/src/extension.ts index dd9629c..e1a8174 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -88,7 +88,7 @@ export async function activate( conflict: new NullConflictResolver(), }); const commitView = new CommitWebviewProvider(service, registry, commit); - const logTree = new LogWebviewProvider(service, ciService); + const logTree = new LogWebviewProvider(service, ciService, context.workspaceState); const branchesTree = new BranchesTreeProvider( service, favorites, @@ -185,6 +185,12 @@ export async function activate( vscode.commands.registerCommand('hyperGit.commit', focusCommitView), vscode.commands.registerCommand('hyperGit.commitAndPush', focusCommitView), vscode.commands.registerCommand('hyperGit.ci.signIn', () => ciService.signIn()), + // Graph 标题栏视图级控件(原 webview 工具栏上移):Scope 下拉子菜单 + Changed Files List/Tree 互斥图标。 + vscode.commands.registerCommand('hyperGit.log.scopeAll', () => logTree.setScope('all')), + vscode.commands.registerCommand('hyperGit.log.scopeCurrent', () => logTree.setScope('current')), + vscode.commands.registerCommand('hyperGit.log.scopeCheckpointer', () => logTree.setScope('checkpointer')), + vscode.commands.registerCommand('hyperGit.log.detailTree', () => logTree.setDetailMode('tree')), + vscode.commands.registerCommand('hyperGit.log.detailFlat', () => logTree.setDetailMode('flat')), vscode.commands.registerCommand('hyperGit.showConsole', () => showGitConsole()), vscode.commands.registerCommand('hyperGit.startRebase', () => RebaseWebview.open(service)), vscode.languages.registerCodeLensProvider({ scheme: 'file' }, inlineLens), diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 478840e..882c7e4 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -177,8 +177,10 @@ export interface LogGraphState { readonly maxLanes: number; readonly hasMore: boolean; readonly scope: LogScope; + /** 变更文件展示模式(host 为事实源,标题栏 List/Tree 图标切换;webview 据此渲染)。 */ + readonly dmode: 'flat' | 'tree'; readonly repoRoot: string; - /** 工作区含多个 git 仓库时为 true:仓库名按钮呈现可切换态(缺省视为 false,向后兼容)。 */ + /** 工作区含多个 git 仓库时为 true:标题栏「切换仓库」图标按钮显隐(缺省视为 false,向后兼容)。 */ readonly multiRepo?: boolean; } @@ -199,7 +201,7 @@ export type LogCommitOp = export interface CiMetaVM { /** 远程为 GitHub 且功能启用时为 true;否则整列隐藏、不发起任何请求。 */ readonly available: boolean; - /** 远程是 GitHub 但尚未授权:webview 显示「登录 GitHub 查看 CI」提示。 */ + /** 远程是 GitHub 但尚未授权:标题栏显示「登录 GitHub」图标按钮(授权完成自动隐藏)。 */ readonly needsSignIn: boolean; /** 软错误(限流/网络)摘要,供 webview 给出一次性提示;为空表示正常。 */ readonly error?: string; @@ -229,7 +231,8 @@ export type LogHostToWebviewMessage = /** forHash:请求时的选中 hash。失败回包 vm=null 无 hash 可比对,webview 据此丢弃过期响应。 */ readonly type: 'log/commitDetail'; readonly payload: { readonly forHash: string; readonly vm: CommitDetailVM | null }; - }; + } + | { readonly type: 'log/detailMode'; readonly payload: { readonly mode: 'flat' | 'tree' } }; /** Webview → Host(Log Graph)。 */ export type LogWebviewToHostMessage = @@ -238,10 +241,6 @@ export type LogWebviewToHostMessage = | { readonly type: 'log/loadMore'; readonly payload: { readonly cursor: number } } | { readonly type: 'log/selectCommit'; readonly payload: { readonly hash: string } } | { readonly type: 'log/commitAction'; readonly payload: { readonly op: LogCommitOp; readonly hash: string } } - | { readonly type: 'log/setScope'; readonly payload: { readonly scope: LogScope } } | { readonly type: 'log/openFile'; readonly payload: { readonly hash: string; readonly path: string; readonly status: string; readonly oldPath?: string } } | { readonly type: 'log/requestCi'; readonly payload: { readonly hashes: readonly string[] } } - | { readonly type: 'log/ciSignIn' } - | { readonly type: 'log/openExternal'; readonly payload: { readonly url: string } } - /** 工具栏仓库名按钮 → 打开原生仓库选择(host 复用 hyperGit.selectRepository 命令,issue #107)。 */ - | { readonly type: 'log/selectRepo' }; + | { readonly type: 'log/openExternal'; readonly payload: { readonly url: string } };