dsh-github-flow 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -132,6 +132,7 @@ summary (≤1.5KB) + facts (structured) + nextActions (concrete next steps) + co
132
132
  * `update_branch`: 🆕 Sync the latest base-branch commits into the PR branch — the self-healing action for "branch is out-of-date" CI gate failures;
133
133
  * `view_images`: 🆕 Download PR discussion screenshots (body + comments; `image_urls` targeting with budget-overflow resume list);
134
134
  * `attach_images` (param): 🆕 Upload local images/attachments to PRs (up to 50 files/batch, symmetric with view_images, requires gh ≥ 2.99).
135
+ * `body_file` (param): 🆕 PR and comment bodies can come straight from a local markdown file (`create` / `edit` / `comment` / `review`), mutually exclusive with `body`; relative paths resolve against the current workspace, the file must be valid UTF-8 (GBK/ANSI fails loudly), and bodies are capped at 65,536 characters — long bodies never enter the model context.
135
136
 
136
137
  ---
137
138
 
@@ -156,6 +157,7 @@ summary (≤1.5KB) + facts (structured) + nextActions (concrete next steps) + co
156
157
  * `status`: 🆕 Retrieve a comprehensive dashboard of issues assigned to the active user;
157
158
  * `view_images`: 🆕 Download screenshot attachments from issue descriptions and comments into local image blocks;
158
159
  * `attach_images` (param): 🆕 Upload local file screenshots alongside issues (up to 50 local files/batch, requires gh ≥ 2.99).
160
+ * `body_file` (param): 🆕 Issue and comment bodies can come straight from a local markdown file, mutually exclusive with `body`; relative paths resolve against the current workspace, the file must be valid UTF-8, and bodies are capped at 65,536 characters.
159
161
 
160
162
  ---
161
163
 
@@ -211,6 +213,7 @@ summary (≤1.5KB) + facts (structured) + nextActions (concrete next steps) + co
211
213
  * `list`: Query release history;
212
214
  * `view`: Inspect notes and asset manifests for a given tag;
213
215
  * `create`: Publish releases (supports `generate_notes`, drafts, prereleases, and asset attachments);
216
+ * `notes_file` (param): 🆕 Release notes can come straight from a local markdown file (replaces `notes`, maps to `--notes-file -`), mutually exclusive with `notes`; relative paths resolve against the current workspace, the file must be valid UTF-8 (release notes do not use the 65,536-character body cap — only the 4MB file-size guard applies);
214
217
  * `upload`: Attach build assets to an existing release (`clobber: true` requires `confirm: true`);
215
218
  * `download`: Download release artifacts with glob pattern filters;
216
219
  * `delete`: Delete releases (`cleanup_tag: true` deletes associated Git tag; requires `confirm: true`).
@@ -445,6 +448,7 @@ Located in the right drawer of every chat workspace, seamlessly bridging your lo
445
448
  * 15 commits displayed by default, with **infinite scroll loading** when scrolling to the bottom (with live `15 / 74` progress counter and click trigger);
446
449
  * Latest Release badge (`🏷️ v0.4.0`) anchored in the top right with hover details and direct release link;
447
450
  * **Commit Detail Modal**: Click any commit row to open a clean modal dialog centered within the right sidebar, featuring smooth vertical scrolling, full 40-char SHA, author email, date, multi-line commit message body, and file diff stat table (`--stat`), pure local 0ms instant display;
451
+ * **File Diff Preview Modal**: Click a file stat row inside the commit detail, or any file in the uncommitted list, to open that file's Unified Diff in a modal (the whole patch is split per file on `diff --git` boundaries, renamed paths normalized, body lines starting with `---`/`+++` are not mistaken for file headers, oversized patches truncated on file boundaries); the modal stacks above the commit detail (Esc closes one layer at a time) and offers copy-source plus "Open in DSH";
448
452
  * **Uncommitted Changes (`📁 Uncommitted (N)`)**: Full-width list of uncommitted files with color-coded status codes (`??` untracked, `M` modified, `D` deleted, `R` renamed) and change counts;
449
453
  * **Pull Requests (`🔀 PR (N)`)**: Shows **all** PRs (merged/closed included, default "All"), with status pills `All / 🟢 Open / 🟣 Merged / ⚪ Closed` showing per-state counts; 3-state CI rollup (`✅ CI Passed` / `❌ CI Failed` / `⏳ CI Pending`), merge conflict detection, review decisions (`✔ Approved` / `✋ Changes Requested` / `👀 Review Required`), Draft pills, and humanized timestamps; **click a card to open the in-app PR review workbench**, while `GitHub ↗` still jumps to the browser;
450
454
  * **Workspace Issues (`📋 Issues (N)`)**: Shows **all** issues (closed included, default "All"), with status pills `All / 🟢 Open / ⚪ Closed`; **click a card to open the in-app Issue viewer** (Markdown body / comment timeline / labels & milestone, Esc to close), while `GitHub ↗` still jumps to the browser;
package/README.md CHANGED
@@ -132,6 +132,7 @@ summary(结论,≤1.5KB) + facts(结构化事实) + nextActions(可
132
132
  * `update_branch`:🆕 将基准分支最新提交同步进 PR 分支——「分支落后需更新」CI 门禁失败的自愈动作;
133
133
  * `view_images`:🆕 下载 PR 讨论截图(正文+评论,支持 `image_urls` 精准提取与触顶续传清单);
134
134
  * `attach_images`(参数):🆕 上传本地图片/附件至 PR(单批上限 50 个本地文件,与 `view_images` 形成对称闭环,需 gh ≥ 2.99)。
135
+ * `body_file`(参数):🆕 PR 正文/评论正文可直接来自本地 md 文件(`create` / `edit` / `comment` / `review` 均适用),与 `body` 二选一;相对路径按当前工作区解析,文件须为合法 UTF-8(GBK/ANSI 会明确报错),正文上限 65,536 字符——长正文不必读进上下文。
135
136
 
136
137
  ---
137
138
 
@@ -156,6 +157,7 @@ summary(结论,≤1.5KB) + facts(结构化事实) + nextActions(可
156
157
  * `status`:🆕 检索指派给当前登录用户的待办议题全景面板;
157
158
  * `view_images`:🆕 下载 Issue 报障截图(正文+评论全量识别并转为本地图片块,Agent 直接"看"截图);落地目录 `~/.dsh/data/dsh-github-flow/images/`(内容寻址去重),**超 15MB 时按 mtime 自动 LRU 淘汰至 10MB 低水位**,并保护当前批次已写入的文件;测试可用 `DSH_IMAGE_DIR` 重定向该目录;
158
159
  * `attach_images`(参数):🆕 支持本地文件/截图直接作为附件随 Issue 发布(单批上限 50 个文件,需 gh ≥ 2.99)。
160
+ * `body_file`(参数):🆕 Issue 正文/评论正文可直接来自本地 md 文件,与 `body` 二选一;相对路径按当前工作区解析,文件须为合法 UTF-8,正文上限 65,536 字符,长正文不必读进上下文。
159
161
 
160
162
  ---
161
163
 
@@ -211,6 +213,7 @@ summary(结论,≤1.5KB) + facts(结构化事实) + nextActions(可
211
213
  * `list`:查看历史 Release 版本清单;
212
214
  * `view`:查看指定 Tag 的发布说明与附属构建资产清单;
213
215
  * `create`:创建新版本 Release(支持 `generate_notes` 自动从 commit 生成说明,支持 `draft` / `prerelease` / 预置资产上传);
216
+ * `notes_file`(参数):🆕 发版说明可直接来自本地 md 文件(替代 `notes`,映射 `--notes-file -`),与 `notes` 二选一;相对路径按当前工作区解析,文件须为合法 UTF-8(发版说明不套用正文的 65,536 字符上限,只有 4MB 文件字节护栏),长说明不必读进上下文;
214
217
  * `upload`:向已有 Release 追加上传本地资产(支持 `clobber: true` 覆盖上传,需 `confirm: true`);
215
218
  * `download`:下载 Release 产物文件(支持 `pattern` glob 模式过滤);
216
219
  * `delete`:删除 Release(支持 `cleanup_tag: true` 连同 Git Tag 一并清理,需 `confirm: true`)。
@@ -447,6 +450,7 @@ summary(结论,≤1.5KB) + facts(结构化事实) + nextActions(可
447
450
  * 默认展示 15 条,支持**向下滑动触底自动分页加载**(伴随 `15 / 74` 实时进度提示与点击后备);
448
451
  * 右上角锚定最新发布 Release 徽章(`🏷️ v0.4.0`),悬停查看发布时间与预发布标示,点击直达;
449
452
  * **提交详情模态弹窗**:点击任意提交记录行,在右侧栏内局部居中弹出,内置上下滑动区域,完整展示 40 位 SHA、提交者姓名邮箱、日期、多行详细正文说明与文件变更统计(`--stat`),纯本地 0ms 秒开,支持按 `Esc` 或点击遮罩随时关闭;
453
+ * **文件 Diff 弹窗预览**:点击提交详情里的文件变更统计行,或未提交清单中的任意文件,均可弹出该文件的 Unified Diff(整包 patch 由前端按 `diff --git` 按文件切分、重命名路径自动归一、正文里的 `---`/`+++` 不会误判成文件头、超长补丁按文件边界截断),弹窗可叠加在提交详情之上(Esc 逐层关闭),支持复制原文与「在 DSH 中打开」源码;
450
454
  * **未提交清单 (`📁 未提交 (N)`)**:展示工作区未提交的文件清单,状态码色彩区分(`??` 新增、`M` 修改、`D` 删除、`R` 重命名)与变更总数;
451
455
  * **拉取请求 (`🔀 PR (N)`)**:展示**全部** PR(含已合并/已关闭,默认「全部」),顶部状态胶囊 `全部 / 🟢 Open / 🟣 Merged / ⚪ Closed` 一键筛选并显示各档计数;带三态 CI 门禁(`✅ CI 通过` / `❌ CI 失败` / `⏳ CI 运行中`)、冲突检测、审核判定(`✔ 已批准` / `✋ 需修改` / `👀 待审查`)、Draft 草稿标示与人性化相对时间;**点击卡片在线唤起 PR 审阅工作台(描述 / 评论 / 文件 Diff)**,右侧 `GitHub ↗` 保留浏览器直达;
452
456
  * **仓库议题 (`📋 Issues (N)`)**:展示**全部** Issue(含已关闭,默认「全部」),顶部状态胶囊 `全部 / 🟢 Open / ⚪ Closed` 一键筛选并显示各档计数;支持指派者、标签与多维状态药丸;**点击卡片在线查看 Issue 详情(Markdown 正文 / 评论时间线 / 标签与里程碑,Esc 关闭)**,右侧 `GitHub ↗` 保留浏览器直达;
@@ -10,8 +10,8 @@
10
10
 
11
11
  | # | 文件 | 位置 | 当前值 | 说明 |
12
12
  |---|---|---|---|---|
13
- | 1 | [package.json](../package.json) | 第 3 行 `"version": "0.7.0"` | `0.7.0` | npm 发布版本、CI 取版来源 |
14
- | 2 | [src/version.ts](../src/version.ts) | 第 9 行 `CURRENT_PLUGIN_VERSION = '0.7.0'` | `'0.7.0'` | 运行时版本比较基准(假横幅的教训来源) |
13
+ | 1 | [package.json](../package.json) | 第 3 行 `"version": "0.7.1"` | `0.7.1` | npm 发布版本、CI 取版来源 |
14
+ | 2 | [src/version.ts](../src/version.ts) | 第 9 行 `CURRENT_PLUGIN_VERSION = '0.7.1'` | `'0.7.1'` | 运行时版本比较基准(假横幅的教训来源) |
15
15
 
16
16
  **两处必须同值**。`pnpm run build` 会硬校验([scripts/build.mjs](../scripts/build.mjs) 的
17
17
  `assertVersionConsistency`),不一致直接构建失败——但不要依赖拦截,发版前手动核对。
@@ -41,14 +41,14 @@
41
41
  | # | 机制 | 说明 |
42
42
  |---|---|---|
43
43
  | 9 | CI 取版(.github/workflows/npm-publish.yml:74) | `VERSION=$(node -p "require('./package.json').version")`——从 package.json 读,无硬编码 |
44
- | 10 | Git tag | `git tag v0.7.0` 手动打,值必须 = package.json 版本(tag 与 package.json 不一致时:CI 发布的 npm 版本以 package.json 为准,GitHub Release 标题以 tag 为准,出现两者错位) |
44
+ | 10 | Git tag | `git tag v0.7.1` 手动打,值必须 = package.json 版本(tag 与 package.json 不一致时:CI 发布的 npm 版本以 package.json 为准,GitHub Release 标题以 tag 为准,出现两者错位) |
45
45
 
46
46
  ## 五、发版完整流程(含版本修改)
47
47
 
48
48
  ```powershell
49
49
  # 1. 改版本(两处同值)
50
- # package.json → "version": "0.7.0"
51
- # src/version.ts → CURRENT_PLUGIN_VERSION = '0.7.0'
50
+ # package.json → "version": "0.7.1"
51
+ # src/version.ts → CURRENT_PLUGIN_VERSION = '0.7.1'
52
52
 
53
53
  # 2. 文档已用占位符(npm-publish-guide 示例、README 安装命令),发版无需改文档版本号
54
54
 
@@ -57,12 +57,12 @@ pnpm run build
57
57
  pnpm test # 全量测试全绿
58
58
 
59
59
  # 4. 本机 e2e(必须先重启 DSH 桌面进程——Node 模块缓存,软链重建不注入运行中进程)
60
- # 重启后验证:GET /api/github/check-update 的 currentVersion = 0.7.0
60
+ # 重启后验证:GET /api/github/check-update 的 currentVersion = 0.7.1
61
61
 
62
62
  # 5. 提交 + 打 tag + 推送(tag 值 = package.json 版本)
63
63
  git add -A
64
- git commit -m "chore(release): v0.7.0"
65
- git tag v0.7.0
64
+ git commit -m "chore(release): v0.7.1"
65
+ git tag v0.7.1
66
66
  git push origin master --tags
67
67
  # CI(npm-publish.yml)自动:构建→发布 npm→创建 GitHub Release
68
68
 
@@ -130,6 +130,13 @@
130
130
  - **低版本安全提示而非硬拒绝**:gh <2.102 时下载仍可执行,但返回值携带 `securityNote` 升级指引;需要自定义落盘位置的场景显式引导经 `github_api` 手动拉取;
131
131
  - **下载后零本地符号化操作**:插件对落盘产物仅做递归枚举(相对路径 + 大小清单),不执行链接、移动或执行类操作。
132
132
 
133
+ ### 2.3.3 本地文件入参 (Local File Inputs)
134
+ - **与 §2.3.2 方向相反,但不放松那条防线**:§2.3.2 管的是**产出写盘**——落盘路径由插件派生、绝不接受调用方给的路径(GHSA-39wj-f2f4-978v 防线);本节管的是**输入读盘**——路径由调用方显式给出,插件只读。两条控制流不共享任何代码路径:读进来的字节只进正文,不落盘、不回写、不参与任何目录派生,因此不会把「路径参数」重新引进下载/写盘通道;
135
+ - **统一入口与统一校验**:`src/tools/body-input.ts` 的 `resolveBodySource` 是唯一实现(纯函数、零 gh 依赖),issue / pr / discussion / release / task / api / agent-task 只传各自的 flag 与字段名,杜绝「某条通道忘了校验」。相对路径按当前工作区解析、绝对路径原样使用;文件必须存在且是普通文件、不超过 4MB、严格 UTF-8 解码(GBK/ANSI 存的中文 md 直接报错而不是产出乱码)、含 NUL 字节视为二进制拒绝、BOM 剥除;
136
+ - **长度口径分层**:GitHub 正文(Issue / PR / 评论 / 讨论)统一 65,536 字符上限,超限显式拒绝并提示拆分;**发版说明(`notes_file`)与 `github_api` 的 `input_file` 不套用这条口径**——前者在 GitHub 侧的上限宽得多,后者是请求体不是正文,两者都只保留 4MB 字节护栏,真正超限交由 GitHub 报错(错误信息带字段名,可定位);
137
+ - **二选一而非优先级**:`body` 与 `body_file`(`notes` 与 `notes_file`、`prompt` 与 `prompt_file`)同时出现即报错,不做静默覆盖——模型拿到一次明确失败,远好过「不知道哪一份真正生效」;空文件等同于没有正文。内联旧路径的 argv 与引入本能力前**逐字节一致**,新通道只在真的传了 `*_file` 时才改变 argv;
138
+ - **为什么不让 gh 自己读文件**:`gh pr review` 没有行级参数,行级评注的唯一通道是 `POST /pulls/{n}/reviews`(REST),那条链路根本没有 `--body-file` 一类 flag,只能插件侧读文件后写进 JSON 请求体。若 CLI 走 `--body-file -`、REST 各写一份,必然漂移成「带 comments 时 body_file 静默失效」;因此统一为**插件读一次** → CLI 通道 `[fileFlag, '-']` + stdin、REST/GraphQL 通道直接用文本。
139
+
133
140
  ---
134
141
 
135
142
  ## 3. 工程解耦与单文件瘦身规范 (Modularization Specs)
@@ -141,7 +148,8 @@
141
148
  - [src/executor/git-parser.ts](../src/executor/git-parser.ts):纯文本解析器(`status -sb`、`log`、`branch -a`),无 I/O、无副作用;
142
149
  - [src/executor/version-utils.ts](../src/executor/version-utils.ts):语义化版本数值分段比对纯算法;
143
150
  - [src/executor/process-utils.ts](../src/executor/process-utils.ts):`spawnWithStdin` 进程管道流控与 UTF-8 字符边界截断算法;
144
- - [src/executor/platform-guide.ts](../src/executor/platform-guide.ts):跨平台 CLI 安装引导与升级命令文本生成。
151
+ - [src/executor/platform-guide.ts](../src/executor/platform-guide.ts):跨平台 CLI 安装引导与升级命令文本生成;
152
+ - [src/tools/body-input.ts](../src/tools/body-input.ts):正文入参解析纯函数(工具层共享,非执行引擎模块)——`resolveBodySource` 与 `GITHUB_BODY_MAX_CHARS` / `BODY_FILE_MAX_BYTES`,供 issue / pr / discussion / release / task / api / agent-task 复用,零 gh 依赖、无副作用,纪律见 §2.3.3。
145
153
 
146
154
  ### 3.2 渲染层领域化细分 (`src/renderers/`)
147
155
  - [src/renderers/markdown.ts](../src/renderers/markdown.ts):门面出口,统一聚合 Re-export 全部领域子模块;
@@ -156,7 +164,7 @@
156
164
  ### 3.4 前端客户端解耦与自包含 Bundle 机制 (`src/ui/` + `src/client.ts`)
157
165
  - **源码物理剥离**:
158
166
  - [src/client.ts](../src/client.ts)(保留右侧栏协同层与插件入口):保留右侧栏核心协同 `renderGithubRepoView` 与 `GithubRightbarBody` 及插件入口;新增 UI 优先下沉到 `src/ui/`,不在入口文件里堆叠;
159
- - **19 个独立子模块**下沉至 [src/ui/](../src/ui/) 目录:样式(最大的单模块)、全景大面板、PR 审阅弹窗、Issue 查看弹窗、Run 详情弹窗、设置弹窗、使用指南,其余卡片类组件体量更小;
167
+ - **21 个独立子模块**下沉至 [src/ui/](../src/ui/) 目录:样式(最大的单模块)、全景大面板、PR 审阅弹窗、Issue 查看弹窗、Run 详情弹窗、设置弹窗、使用指南;文件 Diff 能力拆成两个模块——`diff-utils.ts`(整包 Unified patch 的切分/重命名归一纯函数,零 React 依赖、可独立单测)与 `file-diff-modal.ts`(提交详情与未提交清单共用的 Diff 弹窗);
160
168
  > 这里刻意不写绝对行数。行数会随每次重构变化,而本文档没人会跟着改——历史上同一个 `client.ts` 在本文件里同时存在「~1,260 行」与「约 2,400 行」两个互斥数字,`executor.ts` 也写着早已失效的「~820 行」。需要精确数字请直接看文件。
161
169
  - **命名空间防护**:
162
170
  - 严禁在与 `package.json` 的 `exports["./client"]` 同级创建名为 `client/` 的目录,统一命名为 `src/ui/` 与编译产物 `lib/ui/`,彻底杜绝模块解析器产生“同名文件 vs 目录”的歧义报错;
@@ -201,7 +209,7 @@
201
209
 
202
210
  ## 4. 自动化测试体系 (Test Suite)
203
211
 
204
- 项目配备了 **47 个独立阶段的全量自动化回归测试套件(`test/run-all.mjs`)**,合计 **2000+ 项断言**(精确条数以 `node test/run-all.mjs` 末尾的汇总输出为准,此处只给量级):
212
+ 项目配备了 **49 个独立阶段的全量自动化回归测试套件(`test/run-all.mjs`)**,合计 **2000+ 项断言**(精确条数以 `node test/run-all.mjs` 末尾的汇总输出为准,此处只给量级):
205
213
  - 阶段数与 `test/run-all.mjs` 的 `phases` 数组由 `test/phase35-doc.test.mjs` 对账,改一处漏一处会直接红;
206
214
  - `phase1`:Issue / Run / PR / Repo / Help 命令装配与确认门禁;
207
215
  - `phase2`:Native Markdown 渲染与 Token 压缩率实测(>60% 降幅);
@@ -247,5 +255,7 @@
247
255
  - `phase44`:**仓库规则与严重度契约测试**(s7:`extractSection` 的标题边界——代码围栏内的 `##` 不算标题、一级标题同样终止截取;`normalizeSeverity` 只补不删、幂等、`[P0]` 永不降级;`readReviewRules` 无 `AGENTS.md` 时返回 `null` 且 `pr view` 输出**不含** `reviewRules` 键)。
248
256
  - `phase45`:**CI 失败日志锚点萃取测试**(s7:pytest `FAILED` / node `AssertionError` / Go `panic:` / tsc `error TSxxxx` 四类锚点命中;相邻锚点合并成一块;单块行数 ≤ `2*radius+1`;`maxBlocks` 与 `droppedBlocks` 显式计数;无锚点走尾部 200 行回退;`log_failed` 已把输出预算放开到 2MB 字符/8MB 字节——否则截断发生在萃取之前)。
249
257
  - `phase46`:**任务级原语契约测试**(s8:`classifyCommits` 的 `feat!:` 必须归 breaking(否则版本少跳一级);`suggestVersion` 的 auto 推导(breaking→major / feat→minor / 其余→patch)与无 tag 时从 0.1.0 起步;`selectFilesByBudget` 超预算只给元信息不硬塞;`finalizeEnvelope` 的 `cost.chars` 由信封体积计算而非手写;`github_task` 三个原语的端到端行为(review_pr 读模式零写回、写模式必须 confirm 且 commit_id 钉在 head SHA、diagnose_ci 只挑失败 job 并在日志截断时 `totalUnknown=true`、prepare_release **绝不自动发版**);档位层面 core=3 个工具、只读模式剪掉 review_pr 保留另两个原语)。
250
- - `phase47`:**Actions Run 在线详情契约测试**(`getRunDetail` 的 `run_id` 只认纯数字——它是 `gh run view` 的位置参数,`--force`/`--web` 必须在触达子进程前拒绝且 argv 零产出;repo 白名单额外拒 `-` 开头与 `..`;`--json` 字段集含 `jobs` 且预算 2MB/8MB;缺 `steps` 归一化为空数组、`failedJobs`/`failedSteps` 只在服务端算一次;`truncated` 或解析缺失显式失败并给浏览器直达;`Unknown JSON field` 走 BASE 字段集重试且只重试一次;`getRunLogExcerpts` 用 `-j <jobId> --log-failed` **定向**拉取、最多 3 个 job、绝不使用 `--log` 整包、单 job 失败进 `errors[]` 而整体仍 `ok:true`;`run-log.ts` 的纯文本摘要(锚点块 / 尾部 200 行回退 / 两道截断声明 / 不产生 Markdown 围栏);`/api/github/run-detail` 与 `/api/github/run-logs` 的 200/400/405/500 与 `job_id` 逗号/重复参数两种写法;前端 `onSelectRun` 追加在形参末尾、Actions 卡片标题行拆分 + 外跳 `stopPropagation` + 禁内联 `display:flex`;弹窗 Hook 先于 early return、日志必须走 `<pre>` 且不接 markdown-lite;bundle 内联顺序 `issue-modal → run-modal` 与 20 个子模块计数)。
258
+ - `phase47`:**Actions Run 在线详情契约测试**(`getRunDetail` 的 `run_id` 只认纯数字——它是 `gh run view` 的位置参数,`--force`/`--web` 必须在触达子进程前拒绝且 argv 零产出;repo 白名单额外拒 `-` 开头与 `..`;`--json` 字段集含 `jobs` 且预算 2MB/8MB;缺 `steps` 归一化为空数组、`failedJobs`/`failedSteps` 只在服务端算一次;`truncated` 或解析缺失显式失败并给浏览器直达;`Unknown JSON field` 走 BASE 字段集重试且只重试一次;`getRunLogExcerpts` 用 `-j <jobId> --log-failed` **定向**拉取、最多 3 个 job、绝不使用 `--log` 整包、单 job 失败进 `errors[]` 而整体仍 `ok:true`;`run-log.ts` 的纯文本摘要(锚点块 / 尾部 200 行回退 / 两道截断声明 / 不产生 Markdown 围栏);`/api/github/run-detail` 与 `/api/github/run-logs` 的 200/400/405/500 与 `job_id` 逗号/重复参数两种写法;前端 `onSelectRun` 追加在形参末尾、Actions 卡片标题行拆分 + 外跳 `stopPropagation` + 禁内联 `display:flex`;弹窗 Hook 先于 early return、日志必须走 `<pre>` 且不接 markdown-lite;bundle 内联顺序 `issue-modal → run-modal` 与当时的子模块计数)。
251
259
  - `phase41`:**侧栏入口图标契约测试**(issue #9 回归网:宿主 `renderSlot("sidebar.panellist", { size: wide ? 16 : 18, active })` 注入的 `size` 必须被透传——从 `lib/client.js` 抽出 `GithubSidebarIcon` 函数体、在沙箱里真调,断言 16/18 两档尺寸、根节点必须是裸 `svg`(有 wrapper 就会把图标格撑宽、标签右移 12px)、`fill` 用 `currentColor` 继承行颜色、缺省/非数字 `size` 兜底 16、`active` 不改变输出结构)。
260
+ - `phase48`:**本地文件入参契约测试**(`resolveBodySource` 的语义表逐行对账:文件来源走 `[fileFlag, '-']` + stdin、内联来源的 argv 与改造前**逐字节一致**、`body` 与 `body_file` 同时给出直接报错而不是静默选一个、空文件等同无正文;相对路径按工作区解析、严格 UTF-8 拒绝 GBK/ANSI 与 NUL 二进制、BOM 剥除、4MB 字节与 65,536 字符双上限(`input_file` 与 `notes_file` 关闭字符校验);七个工具的 flag 接线——`--body-file -` / `--notes-file -` / `--input -` / `--from-file -`,`review` 写回走 REST 请求体而非 flag)。
261
+ - `phase49`:**提交详情 / 未提交清单的文件 Diff 弹窗契约测试**(`diff-utils.ts` 四个纯函数:`parseDiffGitLine` 对含空格路径按**最后一个** `" b/"` 切出 b 侧(截错就点开是空的)、`normalizeRenamePath` 把 `src/{old => new}.ts` 与 `old => new` 两种 `--stat` 写法归一成新路径、`splitUnifiedPatch` 按 `diff --git` 边界切段并**丢弃首个 `diff --git` 之前的提交头**(否则第一段会渲染成幽灵差异)、`Binary files` 判二进制、`rename from`/`rename to` 定新旧路径、**按「首个 `@@` 之前才算文件头」的状态机解析**(正文里以 `---`/`+++` 开头的行是 Markdown 分隔线,按行首前缀判会把它们当文件头,导致增删计数为 0 且染错颜色)、`findPatchSection` 支持新路径 / 旧路径 / `{old => new}` 原文三态命中;`executor.getCommitDiff` 在临时 git 仓库真跑——两文件提交切出两段且 `truncated=false`、`git mv` 提交的 patch 含 `rename from`/`rename to`、非法 sha 在触达子进程前拒绝;整包 patch 超过 400k 字符时在**最后一个 `diff --git` 边界**截断(绝不切半个 hunk,只有单文件巨型 diff 找不到边界才硬切)并置 `truncated`;`/api/github/commit-diff` 路由与 `get_commit_diff` action 双向可用;`lib/client.js` 内联 `FileDiffModal`/`splitUnifiedPatch`、`commit-modal.ts` 的文件变更统计行绑 `onClick` 弹窗)。
@@ -314,6 +314,20 @@ export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
314
314
  sendJson(res, 200, { ok: r.ok, data: r, error: r.error });
315
315
  break;
316
316
  }
317
+ case 'commit_diff':
318
+ case 'get_commit_diff': {
319
+ // 与 get_file_diff 同构:只读查询走 POST 是为了让 Agent/脚本复用同一个
320
+ // action 通道,外层形状保持一致(ok/data/error),调用方不必区分两套契约
321
+ const commitSha = typeof payload.sha === 'string' ? payload.sha.trim() : '';
322
+ if (!commitSha) {
323
+ sendJson(res, 400, { ok: false, error: '缺少必需的 sha 参数' });
324
+ return;
325
+ }
326
+ const resolvedCwd = executor.resolveWorkspaceCwd(cwd);
327
+ const r = await executor.getCommitDiff(resolvedCwd, commitSha);
328
+ sendJson(res, 200, { ok: r.ok, data: r, error: r.error });
329
+ break;
330
+ }
317
331
  case 'uncommitted_files':
318
332
  case 'get_uncommitted_files': {
319
333
  const limit = Math.min(Math.max(1, Number(payload.limit) || 15), 100);
@@ -663,7 +677,43 @@ export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
663
677
  }
664
678
  },
665
679
  });
666
- // 10. 本地未提交文件列表轻量流式分页查询(供右侧栏滑动加载调用)
680
+ // 10. 单次提交的完整补丁(提交详情弹窗点某个文件后,前端按 `diff --git` 切段预览)
681
+ // 与 file-diff 的分工:那条读「工作区未提交改动」,这条读「历史某次提交」,
682
+ // 两者都只读、都走同一套 cwd 解析 + hasGitDir 前置门禁。
683
+ bindRoute({
684
+ kind: 'exact',
685
+ path: '/api/github/commit-diff',
686
+ handler: async (req, res) => {
687
+ if (req.method !== 'GET') {
688
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
689
+ return;
690
+ }
691
+ try {
692
+ const url = new URL(req.url || '', 'http://127.0.0.1');
693
+ const cwd = url.searchParams.get('cwd') || undefined;
694
+ const sha = url.searchParams.get('sha');
695
+ if (!sha || !sha.trim()) {
696
+ sendJson(res, 400, { ok: false, error: '缺少必需的 sha 参数' });
697
+ return;
698
+ }
699
+ const resolvedCwd = executor.resolveWorkspaceCwd(cwd);
700
+ if (!hasGitDir(resolvedCwd)) {
701
+ sendJson(res, 400, { ok: false, error: '当前工作区不是 Git 仓库' });
702
+ return;
703
+ }
704
+ const diffResult = await executor.getCommitDiff(resolvedCwd, sha.trim());
705
+ if (!diffResult.ok) {
706
+ sendJson(res, 400, { ok: false, error: diffResult.error || '获取提交补丁失败' });
707
+ return;
708
+ }
709
+ sendJson(res, 200, { ok: true, data: diffResult });
710
+ }
711
+ catch (err) {
712
+ sendJson(res, 500, { ok: false, error: err?.message || '获取提交补丁失败' });
713
+ }
714
+ },
715
+ });
716
+ // 11. 本地未提交文件列表轻量流式分页查询(供右侧栏滑动加载调用)
667
717
  bindRoute({
668
718
  kind: 'exact',
669
719
  path: '/api/github/uncommitted-files',