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/lib/client.js CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
 
20
20
 
21
+
21
22
  window.__ModuleLoader__.load({
22
23
  id: "dsh-github-flow",
23
24
  factory: (require) => {
@@ -1561,6 +1562,11 @@ background: var(--dsw-alias-bg-layer-2, #1c2128) !important;
1561
1562
  }
1562
1563
  .dsh-github-stat-row {
1563
1564
  transition: background-color 0.15s ease;
1565
+ cursor: pointer;
1566
+ }
1567
+ .dsh-github-stat-row:focus-visible {
1568
+ outline: 2px solid var(--dsw-alias-state-accent-primary, #1296db);
1569
+ outline-offset: -2px;
1564
1570
  }
1565
1571
  .dsh-github-stat-row:hover {
1566
1572
  background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04));
@@ -4520,6 +4526,415 @@ function estimateContentLines(src, charsPerLine = 80) {
4520
4526
  return lines;
4521
4527
  }
4522
4528
 
4529
+ // ---- [Bundled: diff-utils.js] ----
4530
+ /**
4531
+ * Unified diff 的切分与路径归一(纯函数:零 React、零 IO)。
4532
+ *
4533
+ * 为什么单独成模块:提交详情要支持「点文件看该文件的 diff」,而一次 `git show` 拿到的是
4534
+ * 整包 patch,必须在前端按 `diff --git` 边界切段。切分规则(边界、重命名路径、二进制判定)
4535
+ * 是纯逻辑,抽出来才能脱离 DOM 单测;放进组件里就只能靠肉眼看界面。
4536
+ *
4537
+ * 为什么不改成「点一次查一次 `git show <sha> -- <path>`」:`git show --stat` 把重命名显示成
4538
+ * `src/{old => new}.ts`,拿它当 pathspec 必然查不到;带空格/引号的路径同样脆。整包 patch 的
4539
+ * `diff --git a/… b/…` 行才是权威来源,按它切分天然免疫这些形态。
4540
+ */
4541
+ /**
4542
+ * 解析 `diff --git a/<old> b/<new>` 头行。
4543
+ *
4544
+ * 路径含空格时这行本身是有歧义的(`a/a b.txt b/a b.txt`),所以按**最后一个** ` b/` 切:
4545
+ * a 侧取前缀,b 侧取剩余全部——b 侧是我们要的路径,尾部截断不会丢字符。
4546
+ */
4547
+ function parseDiffGitLine(line) {
4548
+ if (typeof line !== "string" || line.indexOf("diff --git ") !== 0)
4549
+ return null;
4550
+ var rest = line.slice("diff --git ".length);
4551
+ var sep = rest.lastIndexOf(" b/");
4552
+ if (sep < 0)
4553
+ return null;
4554
+ var aPath = rest.slice(0, sep);
4555
+ var bPath = rest.slice(sep + 1); // 保留 "b/" 前缀,与 git 输出一致
4556
+ if (aPath.indexOf("a/") !== 0)
4557
+ return null;
4558
+ return { aPath: aPath.slice(2), bPath: bPath.slice(2) };
4559
+ }
4560
+ /** 去掉 git 对特殊路径加的引号(core.quotepath=false 后大多数情况不再出现) */
4561
+ function unquote(raw) {
4562
+ var s = String(raw == null ? "" : raw).trim();
4563
+ if (s.length >= 2 && s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') {
4564
+ return s.slice(1, -1).replace(/\\(["\\])/g, "$1");
4565
+ }
4566
+ return s;
4567
+ }
4568
+ /**
4569
+ * 把 `--stat` 里的重命名写法归一成新路径:
4570
+ * - `src/{old => new}.ts` → `src/new.ts`
4571
+ * - `old/name.ts => new/name.ts` → `new/name.ts`
4572
+ * 非重命名原样返回。
4573
+ */
4574
+ function normalizeRenamePath(raw) {
4575
+ var s = unquote(raw);
4576
+ if (s.indexOf("=>") < 0)
4577
+ return s;
4578
+ var brace = s.match(/^(.*)\{([^{}]*)=>\s*([^{}]*)\}(.*)$/);
4579
+ if (brace) {
4580
+ var joined = (brace[1] + brace[3] + brace[4]).replace(/\/{2,}/g, "/");
4581
+ return joined.replace(/^\s+|\s+$/g, "");
4582
+ }
4583
+ var parts = s.split("=>");
4584
+ return parts[parts.length - 1].replace(/^\s+|\s+$/g, "");
4585
+ }
4586
+ /**
4587
+ * 按 `diff --git` 边界把整包 patch 切成逐文件块。
4588
+ * 首个 `diff --git` 之前的内容(如 `--format=` 漏配时的提交头)直接丢弃:
4589
+ * 它们不属于任何文件,混进第一段会污染差异渲染。
4590
+ */
4591
+ function splitUnifiedPatch(patch) {
4592
+ if (typeof patch !== "string" || !patch)
4593
+ return [];
4594
+ var lines = patch.split("\n");
4595
+ var sections = [];
4596
+ var buf = [];
4597
+ var current = null;
4598
+ /**
4599
+ * 是否已进入第一个 hunk。
4600
+ *
4601
+ * 为什么必须有这个状态位:`--- `/`+++ ` 既是文件头,也可能是**正文**行
4602
+ * (删掉一行 `---` 分隔线的补丁行是 `----`,Markdown 文档里很常见)。
4603
+ * 只按行首前缀判断会把正文行当文件头,导致增删计数与配色全错。
4604
+ * git 的格式保证:文件头只出现在第一个 `@@` 之前,`@@` 之后全是正文字节。
4605
+ */
4606
+ var inHunk = false;
4607
+ var flush = function () {
4608
+ if (current && buf.length > 0) {
4609
+ current.text = buf.join("\n");
4610
+ sections.push(current);
4611
+ }
4612
+ buf = [];
4613
+ };
4614
+ for (var i = 0; i < lines.length; i++) {
4615
+ var line = lines[i];
4616
+ if (line.indexOf("diff --git ") === 0) {
4617
+ flush();
4618
+ var parsed = parseDiffGitLine(line);
4619
+ current = {
4620
+ path: parsed ? unquote(parsed.bPath) : "",
4621
+ oldPath: parsed ? unquote(parsed.aPath) : undefined,
4622
+ text: "",
4623
+ isBinary: false,
4624
+ additions: 0,
4625
+ deletions: 0,
4626
+ };
4627
+ buf.push(line);
4628
+ inHunk = false; // 新文件块:文件头区重新开始
4629
+ continue;
4630
+ }
4631
+ if (!current)
4632
+ continue; // 首个 diff --git 之前的头部:丢弃
4633
+ buf.push(line);
4634
+ if (line.indexOf("@@") === 0) {
4635
+ inHunk = true;
4636
+ continue;
4637
+ }
4638
+ if (!inHunk) {
4639
+ // 文件头区:只解析元信息,**绝不计数**——`+++ b/x` 不是新增行
4640
+ if (line.indexOf("rename from ") === 0 && !current.oldPath) {
4641
+ current.oldPath = unquote(line.slice("rename from ".length));
4642
+ }
4643
+ else if (line.indexOf("rename to ") === 0) {
4644
+ current.path = unquote(line.slice("rename to ".length));
4645
+ }
4646
+ else if (line.indexOf("+++ b/") === 0 && !current.path) {
4647
+ current.path = unquote(line.slice(6));
4648
+ }
4649
+ else if (line.indexOf("Binary files ") === 0 || line.indexOf("GIT binary patch") === 0) {
4650
+ current.isBinary = true;
4651
+ }
4652
+ continue;
4653
+ }
4654
+ // hunk 内:行首 + / - 就是真实增删(`` 不计)
4655
+ if (line.charAt(0) === "+")
4656
+ current.additions++;
4657
+ else if (line.charAt(0) === "-")
4658
+ current.deletions++;
4659
+ }
4660
+ flush();
4661
+ return sections;
4662
+ }
4663
+ /**
4664
+ * 在切好的块里找目标文件。
4665
+ * 匹配优先级:新路径 → 旧路径 → 归一化后的重命名写法(`--stat` 的原始形态)。
4666
+ * 找不到返回 null,由调用方给「该文件在本次提交中无文本差异」的兜底文案。
4667
+ */
4668
+ function findPatchSection(sections, targetPath) {
4669
+ if (!Array.isArray(sections) || sections.length === 0)
4670
+ return null;
4671
+ var target = unquote(targetPath);
4672
+ if (!target)
4673
+ return null;
4674
+ var want = normalizeRenamePath(target);
4675
+ for (var i = 0; i < sections.length; i++) {
4676
+ if (sections[i].path === target || sections[i].path === want)
4677
+ return sections[i];
4678
+ }
4679
+ for (var j = 0; j < sections.length; j++) {
4680
+ if (sections[j].oldPath && (sections[j].oldPath === target || sections[j].oldPath === want))
4681
+ return sections[j];
4682
+ }
4683
+ return null;
4684
+ }
4685
+
4686
+ // ---- [Bundled: file-diff-modal.js] ----
4687
+ /**
4688
+ * 文件 Diff 预览弹窗(未提交清单 / 提交详情共用)。
4689
+ *
4690
+ * 为什么合成一个组件:
4691
+ * 1. 未提交清单原本把整个 section 换成「二级视图」(`activeDiffFile ? 二级视图 : 列表`),
4692
+ * 观感像另开一页;改成弹窗后列表留在原位,返回成本从一次重渲染变成一次 Esc;
4693
+ * 2. 提交详情也要「点文件看 diff」,两处只是**数据来源**不同(一个是 `/api/github/file-diff`,
4694
+ * 一个是整包 patch 切出来的一段),外壳/配色/错误态/复制逻辑完全同构,分两份必然漂移。
4695
+ *
4696
+ * 404 降级保留:桌面端未重启时新路由不存在,先回退到常驻的 `/api/github/action`,
4697
+ * 再不行才显式提示「重启桌面端」——这是 file-diff 链路既有的自愈策略。
4698
+ */
4699
+ function FileDiffModal(props) {
4700
+ var h = React.createElement;
4701
+ var f = props.file;
4702
+ var selfFetch = props.diffText === undefined || props.diffText === null;
4703
+ var [fetched, setFetched] = React.useState(null);
4704
+ var [fetching, setFetching] = React.useState(selfFetch);
4705
+ var [fetchError, setFetchError] = React.useState(null);
4706
+ var [copied, setCopied] = React.useState(false);
4707
+ React.useEffect(function () {
4708
+ var onKeyDown = function (e) {
4709
+ if (e.key === "Escape" || e.keyCode === 27)
4710
+ props.onClose();
4711
+ };
4712
+ window.addEventListener("keydown", onKeyDown);
4713
+ return function () { window.removeEventListener("keydown", onKeyDown); };
4714
+ }, [props.onClose]);
4715
+ React.useEffect(function () {
4716
+ if (!selfFetch)
4717
+ return;
4718
+ setFetching(true);
4719
+ setFetchError(null);
4720
+ setFetched(null);
4721
+ var run = async function () {
4722
+ var primaryUrl = "/api/github/file-diff?file=" + encodeURIComponent(f.path) + (props.cwd ? "&cwd=" + encodeURIComponent(props.cwd) : "");
4723
+ var readJson = async function (res) {
4724
+ var text = await res.text().catch(function () { return ""; });
4725
+ var json = null;
4726
+ if (text && (text.trim().charAt(0) === "{" || text.trim().charAt(0) === "[")) {
4727
+ try {
4728
+ json = JSON.parse(text);
4729
+ }
4730
+ catch (e) { }
4731
+ }
4732
+ return { ok: res.ok, status: res.status, text: text, json: json };
4733
+ };
4734
+ try {
4735
+ var p1 = await readJson(await fetch(primaryUrl));
4736
+ if (p1.ok && p1.json && p1.json.ok && p1.json.data) {
4737
+ setFetched(p1.json.data);
4738
+ setFetching(false);
4739
+ return;
4740
+ }
4741
+ // 主路由 404:后端常驻进程还没载入新路由 → 先退到既有 action 通道
4742
+ if (p1.status === 404) {
4743
+ var r2 = await fetch("/api/github/action", {
4744
+ method: "POST",
4745
+ headers: { "Content-Type": "application/json" },
4746
+ body: JSON.stringify({ action: "get_file_diff", file: f.path, cwd: props.cwd || undefined }),
4747
+ }).catch(function () { return null; });
4748
+ if (r2) {
4749
+ var p2 = await readJson(r2);
4750
+ if (p2.ok && p2.json && p2.json.data) {
4751
+ setFetched(p2.json.data);
4752
+ setFetching(false);
4753
+ return;
4754
+ }
4755
+ }
4756
+ setFetching(false);
4757
+ setFetchError("后端对比接口未激活 (404)。请重启 DSH 桌面主程序以载入最新对比引擎;您也可点击上方「在 DSH 中打开」直接预览文件源码。");
4758
+ return;
4759
+ }
4760
+ setFetching(false);
4761
+ setFetchError(p1.json && p1.json.error ? p1.json.error : (p1.text || ("查询差异失败 (HTTP " + p1.status + ")")));
4762
+ }
4763
+ catch (err) {
4764
+ setFetching(false);
4765
+ setFetchError((err && err.message) || "网络请求异常");
4766
+ }
4767
+ };
4768
+ run();
4769
+ }, [f.path, props.cwd, selfFetch]);
4770
+ var diffText = selfFetch ? (fetched && fetched.diff) || "" : String(props.diffText || "");
4771
+ var isBinary = Boolean(props.file.isBinary || (fetched && fetched.isBinary));
4772
+ var additions = props.file.additions !== undefined ? props.file.additions : (fetched ? fetched.additions : undefined);
4773
+ var deletions = props.file.deletions !== undefined ? props.file.deletions : (fetched ? fetched.deletions : undefined);
4774
+ var loading = props.loading === true || (selfFetch && fetching);
4775
+ var error = props.error || (selfFetch ? fetchError : null);
4776
+ var status = String(props.file.status || "");
4777
+ var statusColor = status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
4778
+ : status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
4779
+ : "var(--dsw-alias-state-attention-primary, #9a6700)";
4780
+ var statusText = status === "??" ? "未跟踪新增"
4781
+ : status.indexOf("D") >= 0 ? "已删除"
4782
+ : status.indexOf("R") >= 0 ? "已重命名"
4783
+ : status ? "已修改" : "";
4784
+ var copyDiff = function () {
4785
+ if (!diffText)
4786
+ return;
4787
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
4788
+ navigator.clipboard.writeText(diffText).then(function () {
4789
+ setCopied(true);
4790
+ setTimeout(function () { setCopied(false); }, 2000);
4791
+ }).catch(function () { });
4792
+ }
4793
+ };
4794
+ var renderLines = function () {
4795
+ var lines = diffText.split("\n");
4796
+ /**
4797
+ * hunk 状态位:`--- ` / `+++ ` 既是文件头,也可能是正文(Markdown 的 `---` 分隔线
4798
+ * 被删时补丁行是 `----`)。只按行首前缀判断会把正文染成灰色小字,且与真实增删不符。
4799
+ * git 保证文件头只出现在第一个 `@@` 之前,据此切分才可靠。
4800
+ */
4801
+ var inHunk = false;
4802
+ return lines.map(function (line, idx) {
4803
+ if (line.indexOf("diff --git") === 0)
4804
+ inHunk = false;
4805
+ var isHunk = line.indexOf("@@") === 0;
4806
+ if (isHunk)
4807
+ inHunk = true;
4808
+ var isAdd = inHunk && !isHunk && line.charAt(0) === "+";
4809
+ var isDel = inHunk && !isHunk && line.charAt(0) === "-";
4810
+ var isHeader = !inHunk && !isHunk;
4811
+ var lineStyle = { display: "flex", padding: "0 8px", whiteSpace: "pre", minWidth: "100%", boxSizing: "border-box" };
4812
+ if (isAdd) {
4813
+ lineStyle.backgroundColor = "var(--dsw-alias-state-success-subtle, rgba(46,160,67,0.15))";
4814
+ lineStyle.color = "var(--dsw-alias-state-success-primary, #1a7f37)";
4815
+ }
4816
+ else if (isDel) {
4817
+ lineStyle.backgroundColor = "var(--dsw-alias-state-error-subtle, rgba(248,81,73,0.15))";
4818
+ lineStyle.color = "var(--dsw-alias-state-error-primary, #cf222e)";
4819
+ }
4820
+ else if (isHunk) {
4821
+ lineStyle.backgroundColor = "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.04))";
4822
+ lineStyle.color = "var(--dsw-alias-label-secondary, #656d76)";
4823
+ lineStyle.fontWeight = 600;
4824
+ lineStyle.margin = "2px 0";
4825
+ }
4826
+ else if (isHeader) {
4827
+ lineStyle.color = "var(--dsw-alias-label-muted, #8c959f)";
4828
+ lineStyle.fontSize = "10px";
4829
+ }
4830
+ else {
4831
+ lineStyle.color = "var(--dsw-alias-label-primary, #1f2328)";
4832
+ }
4833
+ return h("div", { key: idx, style: lineStyle }, line || " ");
4834
+ });
4835
+ };
4836
+ return h("div", {
4837
+ className: "dsh-github-modal-overlay",
4838
+ onClick: function (e) {
4839
+ // 叠在提交详情弹窗之上时,遮罩点击必须止住冒泡:
4840
+ // 外层 overlay 的 onClick 是「关闭提交详情」,不拦就会一次点击关掉两层
4841
+ if (e && typeof e.stopPropagation === "function")
4842
+ e.stopPropagation();
4843
+ props.onClose();
4844
+ },
4845
+ style: { zIndex: props.zIndex === undefined ? 100001 : props.zIndex },
4846
+ }, h("div", {
4847
+ className: "dsh-github-modal-dialog",
4848
+ role: "dialog",
4849
+ "aria-label": "文件差异预览",
4850
+ style: { width: "94%", maxWidth: "920px", maxHeight: "88vh", display: "flex", flexDirection: "column", overflow: "hidden" },
4851
+ onClick: function (e) { e.stopPropagation(); },
4852
+ },
4853
+ // 头部:状态 + 路径 + 增删统计 + 操作按钮
4854
+ h("div", { className: "dsh-github-modal-header", style: { flexShrink: 0, gap: "8px" } }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, h("h3", { style: { margin: 0, fontSize: "14px", fontWeight: 600, flexShrink: 0 } }, "文件差异"), status
4855
+ ? h("span", {
4856
+ className: "dsh-github-badge",
4857
+ style: { backgroundColor: statusColor + "22", color: statusColor, fontWeight: 600, fontSize: "11px", flexShrink: 0 },
4858
+ }, status + " " + statusText)
4859
+ : null, additions !== undefined && deletions !== undefined
4860
+ ? h("div", { style: { display: "flex", gap: "4px", fontSize: "11px", fontFamily: "monospace", flexShrink: 0 } }, h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontWeight: 600 } }, "+" + additions), h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600 } }, "-" + deletions))
4861
+ : null, props.subtitle
4862
+ ? h("span", { style: { fontSize: "11px", opacity: 0.7, flexShrink: 0, fontFamily: "monospace" } }, props.subtitle)
4863
+ : null), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, diffText
4864
+ ? h("button", {
4865
+ className: "dsh-github-btn-sm",
4866
+ style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
4867
+ onClick: copyDiff,
4868
+ title: "复制 Unified Diff 原文",
4869
+ }, copied ? "✓ 已复制" : "📋 复制 Diff")
4870
+ : null, props.onOpenInDsh
4871
+ ? h("button", {
4872
+ className: "dsh-github-btn-sm",
4873
+ style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
4874
+ onClick: function () { if (props.onOpenInDsh)
4875
+ props.onOpenInDsh(f.path); },
4876
+ title: "在 DSH 侧边栏打开该文件完整源码",
4877
+ }, "📄 在 DSH 中打开")
4878
+ : null, h("button", {
4879
+ style: { background: "none", border: "none", cursor: "pointer", fontSize: "18px", color: "var(--dsw-alias-label-secondary, #656d76)", padding: "0 4px" },
4880
+ onClick: props.onClose,
4881
+ title: "关闭 (Esc)",
4882
+ }, "✕"))),
4883
+ // 正文
4884
+ h("div", {
4885
+ className: "dsh-github-modal-body",
4886
+ style: { padding: "12px 16px 16px", overflowY: "auto", flex: 1, minHeight: 0, display: "flex", flexDirection: "column", gap: "8px", boxSizing: "border-box" },
4887
+ }, h("div", {
4888
+ style: {
4889
+ fontSize: "11px",
4890
+ fontFamily: "monospace",
4891
+ color: "var(--dsw-alias-label-secondary, #656d76)",
4892
+ wordBreak: "break-all",
4893
+ padding: "3px 6px",
4894
+ backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
4895
+ borderRadius: "4px",
4896
+ flexShrink: 0,
4897
+ },
4898
+ title: f.path,
4899
+ }, f.path), loading
4900
+ ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.65, fontSize: "12px" } }, "⏳ 正在计算 Git 代码比对差异...")
4901
+ : error
4902
+ ? h("div", {
4903
+ style: {
4904
+ padding: "12px 14px",
4905
+ color: "var(--dsw-alias-label-primary, #1f2328)",
4906
+ fontSize: "12px",
4907
+ lineHeight: "1.6",
4908
+ backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
4909
+ borderRadius: "6px",
4910
+ },
4911
+ }, h("div", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600, marginBottom: "6px" } }, "⚠️ " + error))
4912
+ : isBinary
4913
+ ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.8, fontSize: "12px" } }, "📦 该文件为二进制格式(图片或编译产物),无法直接展示文本差异。")
4914
+ : !diffText
4915
+ ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.7, fontSize: "12px" } }, "该文件在本次变更中没有可展示的文本差异。")
4916
+ : h("div", {
4917
+ className: "dsh-github-inline-diff-view",
4918
+ style: { display: "flex", flexDirection: "column", gap: "8px", minHeight: "120px" },
4919
+ }, h("div", {
4920
+ className: "dsh-github-inline-diff-content",
4921
+ style: {
4922
+ margin: 0,
4923
+ padding: "4px 0",
4924
+ fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
4925
+ fontSize: "11px",
4926
+ lineHeight: "1.5",
4927
+ overflowX: "auto",
4928
+ maxHeight: "62vh",
4929
+ borderRadius: "6px",
4930
+ border: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))",
4931
+ backgroundColor: "var(--dsw-alias-bg-canvas, #ffffff)",
4932
+ },
4933
+ }, renderLines())), props.note
4934
+ ? h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", lineHeight: "1.6" } }, "⚠️ " + props.note)
4935
+ : null)));
4936
+ }
4937
+
4523
4938
  // ---- [Bundled: commit-modal.js] ----
4524
4939
  /**
4525
4940
  * 提交详情模态弹窗组件(与设置弹窗同构)
@@ -4531,14 +4946,76 @@ function CommitDetailModal(props) {
4531
4946
  var h = React.createElement;
4532
4947
  var [fetchedDetail, setFetchedDetail] = React.useState(null);
4533
4948
  var [fetchingDetail, setFetchingDetail] = React.useState(false);
4949
+ /**
4950
+ * 点某个文件后的 diff 预览状态。
4951
+ *
4952
+ * 为什么补丁要懒加载而不是随弹窗一起取:多数时候用户只想看提交说明,
4953
+ * 大提交的整包 patch 可以到几百 KB;只在真的点开某个文件时才拉一次,且整个
4954
+ * 弹窗生命周期内复用同一份 patch(点第二个文件零延迟)。
4955
+ */
4956
+ var [patchState, setPatchState] = React.useState(null);
4957
+ var [patchLoading, setPatchLoading] = React.useState(false);
4958
+ var [patchError, setPatchError] = React.useState(null);
4959
+ var [selectedFile, setSelectedFile] = React.useState(null);
4960
+ var loadPatch = function () {
4961
+ if (patchState || patchLoading)
4962
+ return;
4963
+ setPatchLoading(true);
4964
+ setPatchError(null);
4965
+ var url = "/api/github/commit-diff?sha=" + encodeURIComponent(c.sha) + (props.cwd ? "&cwd=" + encodeURIComponent(props.cwd) : "");
4966
+ fetch(url)
4967
+ .then(function (r) {
4968
+ if (r.status === 404) {
4969
+ setPatchLoading(false);
4970
+ setPatchError("后端未提供提交补丁接口 (404)。请重启 DSH 桌面主程序以载入最新对比引擎。");
4971
+ return null;
4972
+ }
4973
+ return r.json().catch(function () { return null; });
4974
+ })
4975
+ .then(function (j) {
4976
+ if (!j)
4977
+ return;
4978
+ setPatchLoading(false);
4979
+ if (j.ok && j.data && typeof j.data.patch === "string") {
4980
+ setPatchState({ patch: j.data.patch, truncated: Boolean(j.data.truncated) });
4981
+ }
4982
+ else {
4983
+ setPatchError((j && j.error) || "获取提交补丁失败");
4984
+ }
4985
+ })
4986
+ .catch(function (err) {
4987
+ setPatchLoading(false);
4988
+ setPatchError("网络请求异常:" + ((err && err.message) || "未知错误"));
4989
+ });
4990
+ };
4991
+ var openFileDiff = function (f) {
4992
+ setSelectedFile(f);
4993
+ loadPatch();
4994
+ };
4534
4995
  React.useEffect(function () {
4535
4996
  var onKeyDown = function (e) {
4536
- if (e.key === "Escape" || e.keyCode === 27)
4997
+ if (e.key === "Escape" || e.keyCode === 27) {
4998
+ // 逐层关闭:先收内层 diff 预览,再按一次才关整个提交详情
4999
+ if (selectedFile) {
5000
+ setSelectedFile(null);
5001
+ return;
5002
+ }
4537
5003
  props.onClose();
5004
+ }
4538
5005
  };
4539
5006
  window.addEventListener("keydown", onKeyDown);
4540
5007
  return function () { window.removeEventListener("keydown", onKeyDown); };
4541
- }, [props.onClose]);
5008
+ }, [props.onClose, selectedFile]);
5009
+ /**
5010
+ * 换提交时清空上一份补丁与选中文件。
5011
+ * 当前调用方在关闭时卸载组件(状态自然复位),但组件一旦被复用(不 key、不卸载),
5012
+ * 残留的 patch 会把 A 提交的差异套到 B 提交的文件路径上——这类错配极难排查,先兜住。
5013
+ */
5014
+ React.useEffect(function () {
5015
+ setPatchState(null);
5016
+ setPatchError(null);
5017
+ setSelectedFile(null);
5018
+ }, [c.sha]);
4542
5019
  React.useEffect(function () {
4543
5020
  setFetchedDetail(null);
4544
5021
  if (c.body || c.stat)
@@ -4562,6 +5039,13 @@ function CommitDetailModal(props) {
4562
5039
  var parsedStat = React.useMemo(() => {
4563
5040
  return effectiveStat && typeof parseGitStatText === "function" ? parseGitStatText(effectiveStat) : null;
4564
5041
  }, [effectiveStat]);
5042
+ /**
5043
+ * 切段结果必须缓存:大提交的整包 patch 可达 400k 字符,而本组件在弹窗生命周期里
5044
+ * 会因 selectedFile / patchError 等状态多次重渲染——每次重渲染都重新切一遍是纯浪费。
5045
+ */
5046
+ var patchSections = React.useMemo(function () {
5047
+ return patchState ? splitUnifiedPatch(patchState.patch) : [];
5048
+ }, [patchState]);
4565
5049
  return h("div", {
4566
5050
  className: "dsh-github-modal-overlay",
4567
5051
  onClick: props.onClose,
@@ -4665,6 +5149,18 @@ function CommitDetailModal(props) {
4665
5149
  return h("div", {
4666
5150
  key: idx,
4667
5151
  className: "dsh-github-stat-row",
5152
+ // 可点击 + 可键盘操作:文件行是入口而不是纯展示,缺 aria/键盘会让
5153
+ // 只用 Tab 的人根本发现不了「能点开看 diff」
5154
+ role: "button",
5155
+ tabIndex: 0,
5156
+ "aria-label": "查看 " + f.path + " 的差异",
5157
+ onClick: function () { openFileDiff(f); },
5158
+ onKeyDown: function (e) {
5159
+ if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 32) {
5160
+ e.preventDefault();
5161
+ openFileDiff(f);
5162
+ }
5163
+ },
4668
5164
  style: {
4669
5165
  display: "flex",
4670
5166
  alignItems: "center",
@@ -4674,8 +5170,10 @@ function CommitDetailModal(props) {
4674
5170
  lineHeight: "1.4",
4675
5171
  borderBottom: idx < parsedStat.files.length - 1 ? "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.06))" : "none",
4676
5172
  gap: "8px",
5173
+ cursor: "pointer",
5174
+ userSelect: "none",
4677
5175
  },
4678
- title: f.path + (f.isBinary ? " (二进制文件)" : ` (+${f.additions}, -${f.deletions})`),
5176
+ title: f.path + (f.isBinary ? " (二进制文件)" : ` (+${f.additions}, -${f.deletions})`) + "\n点击查看该文件的代码改动",
4679
5177
  }, h("div", {
4680
5178
  style: {
4681
5179
  fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
@@ -4716,6 +5214,7 @@ function CommitDetailModal(props) {
4716
5214
  style: { display: "flex", gap: "1px", marginLeft: "2px", alignItems: "center" },
4717
5215
  }, ...diffBlocks)
4718
5216
  : null,
5217
+ h("span", { key: "chev", style: { opacity: 0.4, marginLeft: "2px", fontSize: "12px" }, "aria-hidden": "true" }, "›"),
4719
5218
  ]));
4720
5219
  }))
4721
5220
  : h("pre", {
@@ -4738,7 +5237,25 @@ function CommitDetailModal(props) {
4738
5237
  ? (fetchingDetail
4739
5238
  ? h("div", { style: { fontSize: "12px", opacity: 0.7, padding: "8px 0" } }, "⏳ 正在获取详细变更说明…")
4740
5239
  : h("div", { style: { fontSize: "11px", opacity: 0.6, fontStyle: "italic", padding: "6px 0" } }, "(该提交未包含多行正文;重启 DSH 桌面端可同步最新格式并直接呈现变更统计)"))
4741
- : null)));
5240
+ : null)),
5241
+ // 内层 diff 预览:作为 overlay 的兄弟节点而非 dialog 的子节点,
5242
+ // 这样它是独立的一层遮罩(zIndex 更高),点遮罩只关它自己
5243
+ selectedFile
5244
+ ? h(FileDiffModal, {
5245
+ file: selectedFile,
5246
+ cwd: props.cwd,
5247
+ // 传字符串而不是 null:null 会被组件当成「未提供正文 → 自己去拉未提交 diff」
5248
+ diffText: (findPatchSection(patchSections, normalizeRenamePath(selectedFile.path)) || { text: "" }).text,
5249
+ subtitle: String(c.fullSha || c.sha || "").slice(0, 7),
5250
+ loading: patchLoading,
5251
+ error: patchError,
5252
+ note: patchState && patchState.truncated
5253
+ ? "该提交差异过大,预览只覆盖补丁被截断前的文件;其余文件请用命令行查看。"
5254
+ : null,
5255
+ zIndex: 100001,
5256
+ onClose: function () { setSelectedFile(null); },
5257
+ })
5258
+ : null);
4742
5259
  }
4743
5260
 
4744
5261
  // ---- [Bundled: pr-modal.js] ----
@@ -6670,8 +7187,8 @@ function GithubHelpModal(props) {
6670
7187
  title: "2. 会话右侧栏面板 (Sidebar Panel) —— 深度协同当前工作区 · 5 个 Tab",
6671
7188
  items: [
6672
7189
  "打开入口:位于每个会话工作区的右侧抽屉栏,深度锁定当前会话的物理 Git 仓库;",
6673
- "仓库身份与切换:顶部展示当前关联仓库、Fork 血缘行与权限徽章(只读/可写);若配置了多个 remote,可通过 RepoSelectorDropdown 下拉一键切换基准仓库;", "Git Tab:三段式提交时间线(待推送 / 待拉取 / 最近),默认 15 条、下滑触底自动加载;点任意提交行弹出完整 Commit 详情;",
6674
- "未提交 Tab:实时监听本地未提交文件与状态码(?? 新增 / M 修改 / D 删除 / R 重命名),点击文件可就地下钻查看该文件的 Unified Diff;",
7190
+ "仓库身份与切换:顶部展示当前关联仓库、Fork 血缘行与权限徽章(只读/可写);若配置了多个 remote,可通过 RepoSelectorDropdown 下拉一键切换基准仓库;", "Git Tab:三段式提交时间线(待推送 / 待拉取 / 最近),默认 15 条、下滑触底自动加载;点任意提交行弹出完整 Commit 详情;提交详情里点「📁 文件变更统计」的任意文件行,可再叠一层该文件的 Unified Diff 预览(Esc 先关 Diff、再关详情);",
7191
+ "未提交 Tab:实时监听本地未提交文件与状态码(?? 新增 / M 修改 / D 删除 / R 重命名),点击文件弹出该文件的 Unified Diff 弹窗(清单保留原位,可复制原文或在 DSH 中打开源码);",
6675
7192
  "PR Tab:展示全部 PR 并按状态过滤;带三态 CI 门禁、冲突检测、审核判定与 Draft 标示;点击卡片在线唤起 PR 审阅工作台(描述 / 评论 / 文件 Diff 双栏比对);",
6676
7193
  "Issues Tab:展示全部 Issue 并按状态过滤;点击卡片在线查看详情(Markdown 正文 / 评论时间线 / 标签色块 / 里程碑 / 关联 PR / 隐藏评论标注);",
6677
7194
  "Actions Tab:最近工作流运行记录与触发方式;点击卡片在线查看本次运行的 job / step 树(每步结论与耗时,失败步骤高亮,默认展开第一个失败 job),失败时可按需提取日志锚点片段(一次最多 3 个失败 job、服务端批次预算 90 秒);",
@@ -6679,9 +7196,11 @@ function GithubHelpModal(props) {
6679
7196
  ],
6680
7197
  },
6681
7198
  {
6682
- title: "3. 三个详情弹窗的通用操作习惯",
7199
+ title: "3. 详情弹窗的通用操作习惯",
6683
7200
  items: [
6684
7201
  "PR / Issue / Actions 三个弹窗都支持:按 Esc 或点击遮罩关闭、右上角「GitHub ↗」直达官方页面;",
7202
+ "文件 Diff 弹窗可以叠在提交详情之上:Esc 或点遮罩先关 Diff,再按一次才关提交详情;弹窗内可「📋 复制 Diff」原文或「📄 在 DSH 中打开」查看文件源码;",
7203
+ "Diff 弹窗对正文里的 `---` / `+++` 不会误判成文件头(按 hunk 解析),重命名文件按新路径展示、二进制文件给明确提示;",
6685
7204
  "列表卡片上的「GitHub ↗」按钮是独立的浏览器外跳入口,点它不会触发弹窗(已做事件隔离);",
6686
7205
  "PR 弹窗的文件区可拖拽分隔条调宽、双击或点 ◀ 折叠;布局偏好会记在本地,下次打开保持原样;",
6687
7206
  "长评论默认折叠(显示「⌄ 展开全部」),PR/Issue 正文默认展开——避免一屏被一段超长文字占满;",
@@ -6844,233 +7363,6 @@ function GithubHelpModal(props) {
6844
7363
  btn.setAttribute("aria-selected", active ? "true" : "false");
6845
7364
  });
6846
7365
  }
6847
- /**
6848
- * 未提交文件原生二级 Git Diff 下钻视图组件(右侧栏内就地渲染,零模态遮罩)
6849
- */
6850
- function UncommittedDiffView(props) {
6851
- var f = props.file;
6852
- var h = React.createElement;
6853
- var [diffData, setDiffData] = React.useState(null);
6854
- var [loading, setLoading] = React.useState(true);
6855
- var [error, setError] = React.useState(null);
6856
- var [copied, setCopied] = React.useState(false);
6857
- React.useEffect(function () {
6858
- var onKeyDown = function (e) {
6859
- if (e.key === "Escape" || e.keyCode === 27) {
6860
- props.onBack();
6861
- }
6862
- };
6863
- window.addEventListener("keydown", onKeyDown);
6864
- return function () { window.removeEventListener("keydown", onKeyDown); };
6865
- }, [props.onBack]);
6866
- React.useEffect(function () {
6867
- setLoading(true);
6868
- setError(null);
6869
- setDiffData(null);
6870
- var fetchDiff = async function () {
6871
- var primaryUrl = "/api/github/file-diff?file=" + encodeURIComponent(f.path) + (props.cwd ? "&cwd=" + encodeURIComponent(props.cwd) : "");
6872
- var tryParseResponse = async function (res) {
6873
- var text = await res.text().catch(function () { return ""; });
6874
- var json = null;
6875
- if (text && (text.trim().startsWith("{") || text.trim().startsWith("["))) {
6876
- try {
6877
- json = JSON.parse(text);
6878
- }
6879
- catch (e) { }
6880
- }
6881
- return { ok: res.ok, status: res.status, text: text, json: json };
6882
- };
6883
- try {
6884
- var r1 = await fetch(primaryUrl);
6885
- var p1 = await tryParseResponse(r1);
6886
- if (p1.ok && p1.json && p1.json.ok && p1.json.data) {
6887
- setDiffData(p1.json.data);
6888
- setLoading(false);
6889
- return;
6890
- }
6891
- // 若主路由遇到 404 (后端常驻进程未重启),自动降级请求常驻的 /api/github/action 端点
6892
- if (p1.status === 404) {
6893
- var r2 = await fetch("/api/github/action", {
6894
- method: "POST",
6895
- headers: { "Content-Type": "application/json" },
6896
- body: JSON.stringify({
6897
- action: "get_file_diff",
6898
- file: f.path,
6899
- cwd: props.cwd || undefined,
6900
- }),
6901
- }).catch(function () { return null; });
6902
- if (r2) {
6903
- var p2 = await tryParseResponse(r2);
6904
- if (p2.ok && p2.json && p2.json.data) {
6905
- setDiffData(p2.json.data);
6906
- setLoading(false);
6907
- return;
6908
- }
6909
- }
6910
- setLoading(false);
6911
- setError("后端对比接口未激活 (404)。请重启 DSH 桌面主程序以载入最新对比引擎;您也可点击上方「在 DSH 中打开」直接预览文件源码。");
6912
- return;
6913
- }
6914
- setLoading(false);
6915
- setError(p1.json?.error || p1.text || ("查询差异失败 (HTTP " + p1.status + ")"));
6916
- }
6917
- catch (err) {
6918
- setLoading(false);
6919
- setError(err?.message || "网络请求异常");
6920
- }
6921
- };
6922
- fetchDiff();
6923
- }, [f.path, props.cwd]);
6924
- var copyDiff = function () {
6925
- if (!diffData?.diff)
6926
- return;
6927
- if (navigator?.clipboard?.writeText) {
6928
- navigator.clipboard.writeText(diffData.diff).then(function () {
6929
- setCopied(true);
6930
- setTimeout(function () { setCopied(false); }, 2000);
6931
- }).catch(function () { });
6932
- }
6933
- };
6934
- var statusColor = f.status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
6935
- : f.status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
6936
- : "var(--dsw-alias-state-attention-primary, #9a6700)";
6937
- var statusText = f.status === "??" ? "未跟踪新增"
6938
- : f.status.indexOf("D") >= 0 ? "已删除"
6939
- : f.status.indexOf("R") >= 0 ? "已重命名"
6940
- : "已修改";
6941
- return h("div", {
6942
- className: "dsh-github-inline-diff-view",
6943
- style: {
6944
- display: "flex",
6945
- flexDirection: "column",
6946
- gap: "8px",
6947
- minHeight: "260px",
6948
- padding: "4px 0",
6949
- },
6950
- },
6951
- // 顶部操作与状态栏
6952
- h("div", {
6953
- style: {
6954
- display: "flex",
6955
- alignItems: "center",
6956
- justifyContent: "space-between",
6957
- flexWrap: "wrap",
6958
- gap: "6px",
6959
- paddingBottom: "8px",
6960
- borderBottom: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.1))",
6961
- },
6962
- }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, h("button", {
6963
- className: "dsh-github-btn-sm",
6964
- style: { padding: "3px 8px", cursor: "pointer", fontWeight: 600, fontSize: "11px" },
6965
- onClick: props.onBack,
6966
- title: "返回未提交文件清单 (Esc)",
6967
- }, "← 返回列表"), h("span", {
6968
- className: "dsh-github-badge",
6969
- style: {
6970
- backgroundColor: statusColor + "22",
6971
- color: statusColor,
6972
- fontWeight: 600,
6973
- fontSize: "11px",
6974
- },
6975
- }, `${f.status} ${statusText}`), diffData && diffData.additions !== undefined
6976
- ? h("div", { style: { display: "flex", gap: "4px", fontSize: "11px", fontFamily: "monospace" } }, h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontWeight: 600 } }, `+${diffData.additions}`), h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600 } }, `-${diffData.deletions}`))
6977
- : null), h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, diffData?.diff
6978
- ? h("button", {
6979
- className: "dsh-github-btn-sm",
6980
- style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
6981
- onClick: copyDiff,
6982
- title: "复制 Unified Diff 原文",
6983
- }, copied ? "✓ 已复制" : "📋 复制 Diff")
6984
- : null, props.onOpenInDsh
6985
- ? h("button", {
6986
- className: "dsh-github-btn-sm",
6987
- style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
6988
- onClick: function () {
6989
- if (props.onOpenInDsh)
6990
- props.onOpenInDsh(f.path);
6991
- },
6992
- title: "在 DSH 侧边栏打开查看该文件完整源码",
6993
- }, "📄 在 DSH 中打开")
6994
- : null)),
6995
- // 文件路径栏
6996
- h("div", {
6997
- style: {
6998
- fontSize: "11px",
6999
- fontFamily: "monospace",
7000
- color: "var(--dsw-alias-label-secondary, #656d76)",
7001
- wordBreak: "break-all",
7002
- padding: "3px 6px",
7003
- backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
7004
- borderRadius: "4px",
7005
- },
7006
- title: f.path,
7007
- }, f.path),
7008
- // Diff 主体内容
7009
- loading
7010
- ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.65, fontSize: "12px" } }, "⏳ 正在计算 Git 代码比对差异...")
7011
- : error
7012
- ? h("div", {
7013
- style: {
7014
- padding: "16px",
7015
- color: "var(--dsw-alias-label-primary, #1f2328)",
7016
- fontSize: "12px",
7017
- lineHeight: "1.6",
7018
- backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
7019
- borderRadius: "6px",
7020
- },
7021
- }, h("div", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600, marginBottom: "6px" } }, `⚠️ ${error}`), h("div", { style: { fontSize: "11px", opacity: 0.75 } }, "💡 提示:重启 DSH 桌面主程序即可激活本地比对路由;在此期间您也可点击上方「📄 在 DSH 中打开」直接预览文件源码。"))
7022
- : diffData?.isBinary
7023
- ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.8, fontSize: "12px" } }, "📦 该文件为二进制格式(图片或编译产物),无法直接展示文本差异。")
7024
- : h("div", {
7025
- className: "dsh-github-inline-diff-content",
7026
- style: {
7027
- margin: 0,
7028
- padding: "4px 0",
7029
- fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
7030
- fontSize: "11px",
7031
- lineHeight: "1.5",
7032
- overflowX: "auto",
7033
- maxHeight: "68vh",
7034
- borderRadius: "6px",
7035
- border: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))",
7036
- backgroundColor: "var(--dsw-alias-bg-canvas, #ffffff)",
7037
- },
7038
- }, (diffData?.diff || "").split("\n").map(function (line, idx) {
7039
- var isAdd = line.startsWith("+") && !line.startsWith("+++");
7040
- var isDel = line.startsWith("-") && !line.startsWith("---");
7041
- var isHunk = line.startsWith("@@");
7042
- var isHeader = line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++");
7043
- var lineStyle = {
7044
- display: "flex",
7045
- padding: "0 8px",
7046
- whiteSpace: "pre",
7047
- minWidth: "100%",
7048
- boxSizing: "border-box",
7049
- };
7050
- if (isAdd) {
7051
- lineStyle.backgroundColor = "var(--dsw-alias-state-success-subtle, rgba(46,160,67,0.15))";
7052
- lineStyle.color = "var(--dsw-alias-state-success-primary, #1a7f37)";
7053
- }
7054
- else if (isDel) {
7055
- lineStyle.backgroundColor = "var(--dsw-alias-state-error-subtle, rgba(248,81,73,0.15))";
7056
- lineStyle.color = "var(--dsw-alias-state-error-primary, #cf222e)";
7057
- }
7058
- else if (isHunk) {
7059
- lineStyle.backgroundColor = "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.04))";
7060
- lineStyle.color = "var(--dsw-alias-label-secondary, #656d76)";
7061
- lineStyle.fontWeight = 600;
7062
- lineStyle.margin = "2px 0";
7063
- }
7064
- else if (isHeader) {
7065
- lineStyle.color = "var(--dsw-alias-label-muted, #8c959f)";
7066
- lineStyle.fontSize = "10px";
7067
- }
7068
- else {
7069
- lineStyle.color = "var(--dsw-alias-label-primary, #1f2328)";
7070
- }
7071
- return h("div", { key: idx, style: lineStyle }, line || " ");
7072
- })));
7073
- }
7074
7366
  /** 列表分页步长:与服务端 parseListLimit 的默认页大小保持一致(15) */
7075
7367
  var OVERVIEW_PAGE_SIZE = 15;
7076
7368
  function renderGithubRepoView(h, data, loading, error, onRefresh, isRightbar = false, updateInfo = null, syncStatus = "", lastSyncedAt = 0, onOpenSettings = null, cwd = null, onSelectCommit = null, gitLogPagination = null, onShowToast = null, onOpenHelp = null, onOpenFile = null, dirtyPagination = null, activeDiffFile = null, onSelectDiffFile = null, onCloseDiff = null, onOpenInDsh = null, headerCardsVisible = true, onToggleHeaderCards = null, onSelectPr = null,
@@ -7472,69 +7764,71 @@ function GithubHelpModal(props) {
7472
7764
  var dirtyHeaderTitle = gitSt && totalDirtyCount > displayedDirty.length
7473
7765
  ? `📁 未提交清单 (${displayedDirty.length} / ${totalDirtyCount})`
7474
7766
  : `📁 未提交清单 (${gitSt ? gitSt.dirtyCount : 0})`;
7475
- var dirtySectionEl = activeDiffFile
7476
- ? h(UncommittedDiffView, {
7767
+ // 清单常驻 + 弹窗叠加:原先 activeDiffFile 会把整个 section 换成二级视图,
7768
+ // 观感等同「另开一页」;改成 overlay 后关闭弹窗即回原位,列表滚动位置也不丢。
7769
+ var dirtySectionEl = h("div", { className: "dsh-github-dirty-host" }, h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, dirtyHeaderTitle)), !gitSt || gitSt.dirtyCount === 0
7770
+ ? h("div", { className: "dsh-github-empty" }, "工作区干净,没有未提交的变更 ✓")
7771
+ : h("div", { style: { fontSize: "12px", lineHeight: "1.9" } }, displayedDirty.map(function (f, i) {
7772
+ var statusColor = f.status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
7773
+ : f.status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
7774
+ : f.status.indexOf("M") >= 0 || f.status.indexOf("R") >= 0 ? "var(--dsw-alias-state-attention-primary, #9a6700)"
7775
+ : "inherit";
7776
+ var statusText = f.status === "??" ? "未跟踪新增"
7777
+ : f.status.indexOf("D") >= 0 ? "已删除"
7778
+ : f.status.indexOf("R") >= 0 ? "已重命名"
7779
+ : "已修改";
7780
+ return h("div", {
7781
+ key: f.path || i,
7782
+ className: "dsh-github-dirty-item",
7783
+ title: `${f.path} (${statusText} [${f.status}])\n点击查看代码改动对比 (Unified Diff)`,
7784
+ onClick: function () {
7785
+ if (typeof onSelectDiffFile === "function") {
7786
+ onSelectDiffFile(f);
7787
+ }
7788
+ },
7789
+ }, h("span", {
7790
+ style: {
7791
+ minWidth: "24px",
7792
+ textAlign: "center",
7793
+ opacity: 0.85,
7794
+ fontSize: "11px",
7795
+ fontFamily: "monospace",
7796
+ color: statusColor,
7797
+ fontWeight: 600,
7798
+ },
7799
+ }, f.status === "??" ? "??" : f.status), h("span", { style: { wordBreak: "break-all", flex: 1, fontSize: "12px" } }, f.path));
7800
+ }), hasMoreDirtyToLoad
7801
+ ? h("div", { style: { marginTop: "10px", textAlign: "center" } }, dirtyPagination?.loadingMore
7802
+ ? h("div", { style: { fontSize: "11px", opacity: 0.75, padding: "6px 0" } }, "⏳ 正在加载更多未提交变更…")
7803
+ : h("div", {
7804
+ style: {
7805
+ fontSize: "11px",
7806
+ opacity: 0.7,
7807
+ padding: "6px 0",
7808
+ display: "flex",
7809
+ alignItems: "center",
7810
+ justifyContent: "center",
7811
+ gap: "6px",
7812
+ cursor: "pointer",
7813
+ borderRadius: "4px",
7814
+ },
7815
+ title: "向下滑动滚轮或点击加载更多",
7816
+ onClick: function () {
7817
+ if (dirtyPagination && typeof dirtyPagination.onLoadMore === "function") {
7818
+ dirtyPagination.onLoadMore();
7819
+ }
7820
+ },
7821
+ }, h("span", null, "⬇️ 向下滑动自动加载"), h("span", { style: { fontFamily: "monospace" } }, `(${displayedDirty.length} / ${totalDirtyCount})`)))
7822
+ : totalDirtyCount > 15
7823
+ ? h("div", { style: { textAlign: "center", fontSize: "11px", opacity: 0.5, padding: "6px 0", marginTop: "8px" } }, `✓ 已加载全部 ${totalDirtyCount} 个未提交变更`)
7824
+ : null)), activeDiffFile
7825
+ ? h(FileDiffModal, {
7477
7826
  file: activeDiffFile,
7478
- onBack: onCloseDiff || (() => { }),
7479
7827
  cwd: cwd,
7828
+ onClose: onCloseDiff || (() => { }),
7480
7829
  onOpenInDsh: onOpenInDsh,
7481
7830
  })
7482
- : h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, dirtyHeaderTitle)), !gitSt || gitSt.dirtyCount === 0
7483
- ? h("div", { className: "dsh-github-empty" }, "工作区干净,没有未提交的变更 ✓")
7484
- : h("div", { style: { fontSize: "12px", lineHeight: "1.9" } }, displayedDirty.map(function (f, i) {
7485
- var statusColor = f.status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
7486
- : f.status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
7487
- : f.status.indexOf("M") >= 0 || f.status.indexOf("R") >= 0 ? "var(--dsw-alias-state-attention-primary, #9a6700)"
7488
- : "inherit";
7489
- var statusText = f.status === "??" ? "未跟踪新增"
7490
- : f.status.indexOf("D") >= 0 ? "已删除"
7491
- : f.status.indexOf("R") >= 0 ? "已重命名"
7492
- : "已修改";
7493
- return h("div", {
7494
- key: f.path || i,
7495
- className: "dsh-github-dirty-item",
7496
- title: `${f.path} (${statusText} [${f.status}])\n点击查看代码改动对比 (Unified Diff)`,
7497
- onClick: function () {
7498
- if (typeof onSelectDiffFile === "function") {
7499
- onSelectDiffFile(f);
7500
- }
7501
- },
7502
- }, h("span", {
7503
- style: {
7504
- minWidth: "24px",
7505
- textAlign: "center",
7506
- opacity: 0.85,
7507
- fontSize: "11px",
7508
- fontFamily: "monospace",
7509
- color: statusColor,
7510
- fontWeight: 600,
7511
- },
7512
- }, f.status === "??" ? "??" : f.status), h("span", { style: { wordBreak: "break-all", flex: 1, fontSize: "12px" } }, f.path));
7513
- }), hasMoreDirtyToLoad
7514
- ? h("div", { style: { marginTop: "10px", textAlign: "center" } }, dirtyPagination?.loadingMore
7515
- ? h("div", { style: { fontSize: "11px", opacity: 0.75, padding: "6px 0" } }, "⏳ 正在加载更多未提交变更…")
7516
- : h("div", {
7517
- style: {
7518
- fontSize: "11px",
7519
- opacity: 0.7,
7520
- padding: "6px 0",
7521
- display: "flex",
7522
- alignItems: "center",
7523
- justifyContent: "center",
7524
- gap: "6px",
7525
- cursor: "pointer",
7526
- borderRadius: "4px",
7527
- },
7528
- title: "向下滑动滚轮或点击加载更多",
7529
- onClick: function () {
7530
- if (dirtyPagination && typeof dirtyPagination.onLoadMore === "function") {
7531
- dirtyPagination.onLoadMore();
7532
- }
7533
- },
7534
- }, h("span", null, "⬇️ 向下滑动自动加载"), h("span", { style: { fontFamily: "monospace" } }, `(${displayedDirty.length} / ${totalDirtyCount})`)))
7535
- : totalDirtyCount > 15
7536
- ? h("div", { style: { textAlign: "center", fontSize: "11px", opacity: 0.5, padding: "6px 0", marginTop: "8px" } }, `✓ 已加载全部 ${totalDirtyCount} 个未提交变更`)
7537
- : null));
7831
+ : null);
7538
7832
  // 快捷操作栏 (仅在检测到关联仓库时展示,未绑定仓库时不展示虚假按钮)
7539
7833
  var actionsBarEl = repo ? h("div", { className: "dsh-github-quick-actions" }, h("button", {
7540
7834
  className: "dsh-github-action-btn",