dsh-github-flow 0.6.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.
Files changed (95) hide show
  1. package/README.en.md +102 -15
  2. package/README.md +105 -15
  3. package/docs/UPGRADE-NOTES.md +12 -11
  4. package/docs/architecture.md +67 -14
  5. package/lib/api/route-helpers.d.ts +19 -0
  6. package/lib/api/route-helpers.js +54 -0
  7. package/lib/api/routes-actions.js +126 -2
  8. package/lib/api/routes-lifecycle.js +8 -1
  9. package/lib/api/routes-overview.d.ts +26 -0
  10. package/lib/api/routes-overview.js +276 -34
  11. package/lib/api/routes-proxy.js +8 -1
  12. package/lib/api/routes.d.ts +11 -1
  13. package/lib/api/routes.js +15 -2
  14. package/lib/cache.d.ts +0 -21
  15. package/lib/cache.js +0 -37
  16. package/lib/client.d.ts +2 -1
  17. package/lib/client.js +2108 -658
  18. package/lib/destructive.d.ts +59 -0
  19. package/lib/destructive.js +285 -0
  20. package/lib/executor/args-guard.d.ts +17 -0
  21. package/lib/executor/args-guard.js +23 -0
  22. package/lib/executor/failure-anchors.d.ts +52 -0
  23. package/lib/executor/failure-anchors.js +116 -0
  24. package/lib/executor/pr-review.d.ts +95 -0
  25. package/lib/executor/pr-review.js +175 -0
  26. package/lib/executor/run-log.d.ts +36 -0
  27. package/lib/executor/run-log.js +78 -0
  28. package/lib/executor.d.ts +204 -3
  29. package/lib/executor.js +670 -28
  30. package/lib/github-service.d.ts +1 -1
  31. package/lib/main.d.ts +92 -18
  32. package/lib/main.js +186 -95
  33. package/lib/renderers/common.d.ts +0 -3
  34. package/lib/renderers/common.js +8 -44
  35. package/lib/renderers/markdown.d.ts +1 -0
  36. package/lib/renderers/markdown.js +1 -0
  37. package/lib/renderers/pr.js +26 -1
  38. package/lib/renderers/task.d.ts +9 -0
  39. package/lib/renderers/task.js +120 -0
  40. package/lib/review-rules.d.ts +60 -0
  41. package/lib/review-rules.js +204 -0
  42. package/lib/shared/state-badges.d.ts +42 -0
  43. package/lib/shared/state-badges.js +102 -0
  44. package/lib/task-primitives.d.ts +109 -0
  45. package/lib/task-primitives.js +194 -0
  46. package/lib/tool-tiers.d.ts +57 -0
  47. package/lib/tool-tiers.js +168 -0
  48. package/lib/tools/agent-task.d.ts +4 -0
  49. package/lib/tools/agent-task.js +22 -9
  50. package/lib/tools/api.d.ts +4 -0
  51. package/lib/tools/api.js +48 -12
  52. package/lib/tools/body-input.d.ts +58 -0
  53. package/lib/tools/body-input.js +123 -0
  54. package/lib/tools/cache.js +42 -40
  55. package/lib/tools/codespace.js +4 -12
  56. package/lib/tools/discussion.d.ts +4 -0
  57. package/lib/tools/discussion.js +33 -17
  58. package/lib/tools/gist.d.ts +1 -39
  59. package/lib/tools/gist.js +4 -14
  60. package/lib/tools/help.js +65 -13
  61. package/lib/tools/issue.d.ts +4 -0
  62. package/lib/tools/issue.js +37 -38
  63. package/lib/tools/label.js +4 -15
  64. package/lib/tools/pr.d.ts +51 -0
  65. package/lib/tools/pr.js +204 -46
  66. package/lib/tools/project.js +4 -13
  67. package/lib/tools/release.d.ts +4 -0
  68. package/lib/tools/release.js +26 -26
  69. package/lib/tools/repo.js +4 -4
  70. package/lib/tools/ruleset.js +9 -6
  71. package/lib/tools/run.js +76 -24
  72. package/lib/tools/secret.js +11 -17
  73. package/lib/tools/skill.js +18 -22
  74. package/lib/tools/status.js +4 -4
  75. package/lib/tools/task.d.ts +223 -0
  76. package/lib/tools/task.js +464 -0
  77. package/lib/types.d.ts +183 -1
  78. package/lib/ui/commit-modal.js +108 -4
  79. package/lib/ui/diff-utils.d.ts +51 -0
  80. package/lib/ui/diff-utils.js +155 -0
  81. package/lib/ui/file-diff-modal.d.ts +33 -0
  82. package/lib/ui/file-diff-modal.js +250 -0
  83. package/lib/ui/help-modal.d.ts +1 -1
  84. package/lib/ui/help-modal.js +24 -12
  85. package/lib/ui/issue-modal.js +7 -3
  86. package/lib/ui/pr-modal.js +50 -11
  87. package/lib/ui/run-modal.d.ts +26 -0
  88. package/lib/ui/run-modal.js +486 -0
  89. package/lib/ui/styles.d.ts +1 -1
  90. package/lib/ui/styles.js +376 -44
  91. package/lib/ui/sync-settings-modal.js +1 -1
  92. package/lib/ui/workspace-panel.js +78 -23
  93. package/lib/version.d.ts +1 -1
  94. package/lib/version.js +1 -1
  95. package/package.json +9 -2
package/lib/client.js CHANGED
@@ -1,3 +1,24 @@
1
+ // 状态→徽章映射与服务端 CLI 输出共用同一份实现(构建期被内联进本 bundle 的作用域)
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
1
22
  window.__ModuleLoader__.load({
2
23
  id: "dsh-github-flow",
3
24
  factory: (require) => {
@@ -6,54 +27,116 @@ window.__ModuleLoader__.load({
6
27
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
28
  var React = require("react");
8
29
 
30
+ // ---- [Bundled: state-badges.js] ----
31
+ /**
32
+ * 状态 → 徽章的唯一映射(服务端 Markdown 与客户端 UI 共用)。
33
+ *
34
+ * 为什么必须共用:同一个状态(OPEN / MERGED / CI failure …)在 CLI 输出与右侧栏
35
+ * 面板上必须是同一套颜色与同一套文案。此前这条映射在 9 个文件里各写了一遍
36
+ * (服务端 5 个 `format*Badge`、客户端 6 处内联三元式),新增一个状态要改 12 处,
37
+ * 漏一处就出现「同一个 PR 在两处显示不同颜色」。
38
+ *
39
+ * 约束(构建期强依赖):本文件必须**零依赖**。scripts/build.mjs 会把它的 import
40
+ * 行剥掉、整体内联进客户端 bundle;一旦这里 import 了别的东西,那些符号在 bundle
41
+ * 作用域里就是 undefined,表现是整个驾驶舱白屏。
42
+ *
43
+ * `tone` 直接就是 CSS class 后缀(`dsh-badge-${tone}`),客户端不再需要映射表。
44
+ */
45
+ /** 服务端 Markdown 渲染用:`🟢 OPEN` / `OPEN` / `-` */
46
+ function badgeText(badge) {
47
+ if (!badge)
48
+ return '-';
49
+ return badge.icon ? `${badge.icon} ${badge.label}` : badge.label;
50
+ }
51
+ /** PR / Issue 状态(空 state 优先于 draft,与旧实现判定顺序一致) */
52
+ function prStateBadge(state, isDraft) {
53
+ const s = String(state || '').toUpperCase();
54
+ // 旧实现把 `if (!state) return '-'` 放在最前、draft 判定在其后。
55
+ // 顺序反了会让 formatStateBadge(undefined, true) 从 '-' 变成 '⚪ DRAFT'——那是行为变化。
56
+ if (!s)
57
+ return null;
58
+ if (isDraft)
59
+ return { icon: '⚪', label: 'DRAFT', tone: 'yellow' };
60
+ if (s === 'OPEN')
61
+ return { icon: '🟢', label: 'OPEN', tone: 'green' };
62
+ if (s === 'MERGED')
63
+ return { icon: '🟣', label: 'MERGED', tone: 'purple' };
64
+ if (s === 'CLOSED')
65
+ return { icon: '🔴', label: 'CLOSED', tone: 'red' };
66
+ return genericStateBadge(s);
67
+ }
68
+ /** Issue 状态(没有 MERGED 这一档) */
69
+ function issueStateBadge(state) {
70
+ const s = String(state || '').toUpperCase();
71
+ if (s === 'OPEN')
72
+ return { icon: '🟢', label: 'OPEN', tone: 'green' };
73
+ if (s === 'CLOSED')
74
+ return { icon: '🔴', label: 'CLOSED', tone: 'red' };
75
+ return genericStateBadge(s);
76
+ }
77
+ /**
78
+ * CI 状态。
79
+ * 第二参数按调用域语义不同:run 域传 conclusion(success/failure/…),
80
+ * pr checks 域传 bucket(pass/fail/pending/…)——取值族不同但映射到同一组徽章;
81
+ * 第一参数兜底(in_progress/queued 等进行中态)。
82
+ */
83
+ function ciBadge(status, conclusion) {
84
+ const conc = String(conclusion || '').toLowerCase();
85
+ const st = String(status || '').toLowerCase();
86
+ if (conc === 'success' || conc === 'pass')
87
+ return { icon: '✅', label: conc, tone: 'green' };
88
+ if (conc === 'failure' || conc === 'fail')
89
+ return { icon: '❌', label: conc, tone: 'red' };
90
+ if (conc === 'cancelled' || conc === 'cancel')
91
+ return { icon: '🚫', label: conc, tone: 'red' };
92
+ if (conc === 'skipped' || conc === 'skipping')
93
+ return { icon: '⏭️', label: conc, tone: 'neutral' };
94
+ if (conc === 'pending')
95
+ return { icon: '⏳', label: 'pending', tone: 'yellow' };
96
+ if (st === 'in_progress')
97
+ return { icon: '⏳', label: 'running', tone: 'yellow' };
98
+ if (st === 'queued')
99
+ return { icon: '⏱️', label: 'queued', tone: 'yellow' };
100
+ const fallback = conc || st;
101
+ if (!fallback)
102
+ return null;
103
+ return { icon: '', label: fallback, tone: 'neutral' };
104
+ }
105
+ /** 合并冲突状态 */
106
+ function conflictBadge(status) {
107
+ const s = String(status || '').toUpperCase();
108
+ if (s === 'CLEAN')
109
+ return { icon: '✅', label: 'CLEAN', tone: 'green' };
110
+ if (s === 'DIRTY' || s === 'CONFLICT')
111
+ return { icon: '⚠️', label: 'CONFLICT', tone: 'yellow' };
112
+ if (s === 'BLOCKED')
113
+ return { icon: '⛔', label: 'BLOCKED', tone: 'red' };
114
+ if (!s)
115
+ return null;
116
+ return { icon: '', label: s, tone: 'neutral' };
117
+ }
118
+ /** Release 状态(Draft > Prerelease > Stable) */
119
+ function releaseBadge(isDraft, isPrerelease) {
120
+ if (isDraft)
121
+ return { icon: '⚪', label: 'DRAFT', tone: 'yellow' };
122
+ if (isPrerelease)
123
+ return { icon: '🍼', label: 'PRE', tone: 'yellow' };
124
+ return { icon: '✅', label: 'STABLE', tone: 'green' };
125
+ }
126
+ /** 兜底:任意状态字符串转徽章(未知状态给 neutral,不猜颜色) */
127
+ function genericStateBadge(state) {
128
+ const s = String(state || '').toUpperCase();
129
+ if (!s)
130
+ return null;
131
+ return { icon: '', label: s, tone: 'neutral' };
132
+ }
133
+
9
134
  // ---- [Bundled: styles.js] ----
10
135
  /**
11
136
  * DSH GitHub Flow - 主题自适应样式系统 (Light / Dark 动态联动)
12
137
  * 遵循 DSH 原生 DSW (DeepSeek Web) 设计语言规范
13
138
  */
14
139
  const GITHUB_PANEL_CSS = `
15
- .dsh-github-icon {
16
- display: flex;
17
- align-items: center;
18
- justify-content: center;
19
- width: 100%;
20
- height: 100%;
21
- cursor: pointer;
22
- color: var(--dsw-alias-label-secondary, #656d76);
23
- transition: color 0.15s ease;
24
- }
25
- .dsh-github-icon:hover {
26
- color: var(--dsw-alias-label-primary, #1f2328);
27
- }
28
- .dsh-github-icon-inner {
29
- display: flex;
30
- align-items: center;
31
- justify-content: center;
32
- width: 28px;
33
- height: 28px;
34
- border-radius: 7px;
35
- background: transparent;
36
- color: inherit;
37
- border: 1px solid transparent;
38
- transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
39
- }
40
- .dsh-github-icon-inner.active {
41
- background: var(--dsw-alias-bg-module-platform, rgba(9, 105, 218, 0.08));
42
- color: var(--dsw-alias-link, #0969da);
43
- border-color: var(--dsw-alias-border-l2, rgba(9, 105, 218, 0.2));
44
- }
45
- body[data-ds-dark-theme] .dsh-github-icon {
46
- color: var(--dsw-alias-label-secondary, #9ca3af);
47
- }
48
- body[data-ds-dark-theme] .dsh-github-icon:hover {
49
- color: var(--dsw-alias-label-primary, #ffffff);
50
- }
51
- body[data-ds-dark-theme] .dsh-github-icon-inner.active {
52
- background: linear-gradient(135deg, rgba(46, 160, 67, 0.3) 0%, rgba(31, 111, 235, 0.45) 100%);
53
- color: #ffffff;
54
- box-shadow: 0 0 12px rgba(31, 111, 235, 0.4);
55
- border-color: rgba(88, 166, 255, 0.45);
56
- }
57
140
  .dsh-octocat-badge {
58
141
  display: flex;
59
142
  align-items: center;
@@ -1479,6 +1562,11 @@ background: var(--dsw-alias-bg-layer-2, #1c2128) !important;
1479
1562
  }
1480
1563
  .dsh-github-stat-row {
1481
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;
1482
1570
  }
1483
1571
  .dsh-github-stat-row:hover {
1484
1572
  background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04));
@@ -2291,175 +2379,492 @@ font-size: 12px;
2291
2379
  opacity: 0.6;
2292
2380
  flex: 0 0 auto;
2293
2381
  }
2294
- `;
2295
- function injectStyles() {
2296
- var styleId = "dsh-plugin-github-styles";
2297
- var existing = document.getElementById(styleId);
2298
- if (existing) {
2299
- existing.textContent = GITHUB_PANEL_CSS;
2300
- return () => { };
2301
- }
2302
- var style = document.createElement("style");
2303
- style.id = styleId;
2304
- style.textContent = GITHUB_PANEL_CSS;
2305
- document.head.appendChild(style);
2306
- return () => style.remove();
2382
+ .dsh-github-run-modal-dialog {
2383
+ width: 92% !important;
2384
+ max-width: 960px !important;
2385
+ height: 84vh !important;
2386
+ max-height: 84vh !important;
2387
+ display: flex !important;
2388
+ flex-direction: column !important;
2389
+ overflow: hidden !important;
2390
+ padding: 0 !important;
2307
2391
  }
2308
-
2309
- // ---- [Bundled: utils.js] ----
2310
- /**
2311
- * 面板公共工具函数(纯函数,可被单元测试直接覆盖)
2312
- */
2313
- function formatRelativeTime(iso) {
2314
- if (!iso || typeof iso !== "string")
2315
- return "";
2316
- var t = Date.parse(iso);
2317
- if (isNaN(t))
2318
- return "";
2319
- var diff = Date.now() - t;
2320
- if (diff < 0)
2321
- return "刚刚";
2322
- var MIN = 60 * 1000, HOUR = 60 * MIN, DAY = 24 * HOUR;
2323
- if (diff < MIN)
2324
- return "刚刚";
2325
- if (diff < HOUR)
2326
- return Math.floor(diff / MIN) + "分钟前";
2327
- if (diff < DAY)
2328
- return Math.floor(diff / HOUR) + "小时前";
2329
- if (diff < 7 * DAY)
2330
- return Math.floor(diff / DAY) + "天前";
2331
- var d = new Date(t);
2332
- return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
2392
+ .dsh-github-run-modal-header {
2393
+ padding: 12px 16px !important;
2394
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
2395
+ background: var(--dsw-alias-bg-layer-1, #f6f8fa);
2396
+ flex-shrink: 0;
2333
2397
  }
2334
- function rollupCiStatus(checks) {
2335
- if (!Array.isArray(checks) || checks.length === 0)
2336
- return null;
2337
- var hasFail = false, hasPending = false;
2338
- for (var i = 0; i < checks.length; i++) {
2339
- var c = checks[i] || {};
2340
- var status = String(c.status || "").toLowerCase();
2341
- var conclusion = String(c.conclusion || "").toLowerCase();
2342
- if (conclusion === "failure" || conclusion === "timed_out" || conclusion === "cancelled")
2343
- hasFail = true;
2344
- else if (conclusion === "success" || conclusion === "skipped" || conclusion === "neutral") { }
2345
- else if (status === "in_progress" || status === "queued" || conclusion === "pending" || (!conclusion && status))
2346
- hasPending = true;
2347
- }
2348
- if (hasFail)
2349
- return "fail";
2350
- if (hasPending)
2351
- return "pending";
2352
- return "pass";
2398
+ body[data-ds-dark-theme] .dsh-github-run-modal-header {
2399
+ border-bottom-color: var(--dsw-alias-border-l1, #30363d);
2400
+ background: var(--dsw-alias-bg-layer-1, #16161a);
2353
2401
  }
2354
- function reviewDecisionBadge(decision) {
2355
- var d = String(decision || "").toUpperCase();
2356
- if (d === "APPROVED")
2357
- return { text: "✔ 已批准", kind: "green" };
2358
- if (d === "CHANGES_REQUESTED")
2359
- return { text: "✋ 需修改", kind: "red" };
2360
- if (d === "REVIEW_REQUIRED")
2361
- return { text: "👀 待审查", kind: "yellow" };
2362
- return null;
2402
+ .dsh-github-run-body {
2403
+ padding: 14px 16px 20px;
2404
+ overflow-y: auto;
2405
+ flex: 1;
2406
+ min-height: 0;
2407
+ display: flex;
2408
+ flex-direction: column;
2409
+ gap: 10px;
2410
+ overscroll-behavior: contain;
2363
2411
  }
2364
- function eventLabel(event) {
2365
- var e = String(event || "");
2366
- if (e === "push")
2367
- return "⇡ push";
2368
- if (e === "schedule")
2369
- return "⏰ 定时";
2370
- if (e === "workflow_dispatch")
2371
- return "▶ 手动";
2372
- if (e === "pull_request")
2373
- return "🔀 PR";
2374
- return e || "";
2412
+ .dsh-github-run-meta {
2413
+ display: flex;
2414
+ flex-wrap: wrap;
2415
+ align-items: center;
2416
+ gap: 6px;
2417
+ padding: 8px 16px;
2418
+ font-size: 11px;
2419
+ line-height: 18px;
2420
+ color: var(--dsw-alias-label-secondary, #57606a);
2421
+ background: var(--dsw-alias-bg-module-platform, #f6f8fa);
2422
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
2423
+ flex: 0 0 auto;
2375
2424
  }
2376
- /**
2377
- * 确定性失败的特征(客户端侧兜底判定)。
2378
- *
2379
- * 为什么这里还要再写一份:后端 GhExecutor 已统一分类并通过 errorKind 分流,
2380
- * 但浏览器端拿不到 errorKind,只能看到拼接后的错误文本。这类错误重试一万次
2381
- * 也不会变好(仓库就是关着 Issues),必须阻止阶梯退避把 7 秒白白烧掉。
2382
- * 与服务端共用同一组特征串,改动时两边同步。
2383
- */
2384
- var DETERMINISTIC_HINT_RE = /repository has disabled|Could not resolve to a Repository|Branch not found|HTTP 403|Not Found \(HTTP 404\)|rate limit/i;
2385
- /**
2386
- * 带有阶梯退避重试(Backoff Retry)的通用异步拉取函数
2387
- *
2388
- * 核心设计理由与约束:
2389
- * 1. 应对代理节点不稳定、TLS 握手卡顿、短时网络抖动(半秒至两秒微断流);
2390
- * 2. 阶梯退避时序:第 1 次失败等 1s (1000ms),第 2 次等 2s (2000ms),第 3 次等 4s (4000ms),附加 0~200ms Jitter;
2391
- * 3. 智能区分软硬错误:401/403/404/400 确定性错误坚决不重试,仅在网络超时、不可达、或带有 networkError 时重试;
2392
- * 4. 严格响应外部 AbortSignal,组件卸载或用户手动打断时立即退出等待,绝无内存泄漏。
2393
- */
2394
- async function fetchWithRetry(url, options = {}, config = {}) {
2395
- var maxRetries = config.maxRetries !== undefined ? config.maxRetries : 3;
2396
- var baseDelayMs = config.baseDelayMs !== undefined ? config.baseDelayMs : 1000;
2397
- var signal = config.signal || options.signal;
2398
- var attempt = 0;
2399
- while (true) {
2400
- attempt++;
2401
- var currentRes = null;
2402
- var currentJson = null;
2403
- var failureReason = "";
2404
- var shouldRetry = false;
2405
- try {
2406
- if (signal && signal.aborted) {
2407
- throw new Error("Aborted");
2408
- }
2409
- currentRes = await fetch(url, { ...options, signal });
2410
- // 硬错误守卫:客户端确定性错误(如未授权、权限拒绝、不存在、参数错误)坚决不重试
2411
- if (currentRes.status === 401 || currentRes.status === 403 || currentRes.status === 404 || currentRes.status === 400) {
2412
- currentJson = await currentRes.json().catch(() => ({}));
2413
- return { res: currentRes, json: currentJson };
2414
- }
2415
- if (!currentRes.ok) {
2416
- failureReason = "HTTP " + currentRes.status + " " + currentRes.statusText;
2417
- shouldRetry = true;
2418
- }
2419
- else {
2420
- currentJson = await currentRes.json().catch(() => null);
2421
- // 业务层软错误检测(后端返回了 200,但标记了 networkError 或 partialFailure)
2422
- var isNetworkError = Boolean(currentJson && (currentJson.partialFailure ||
2423
- currentJson.data?.partialFailure ||
2424
- currentJson.data?.networkError ||
2425
- currentJson.data?.auth?.networkError));
2426
- if (isNetworkError) {
2427
- failureReason = currentJson.error || currentJson.data?.networkErrorReason || "云端接口网络抖动";
2428
- // 后端已把「确定性失败」(仓库关了 Issues / 仓库不存在 / 无权限)从
2429
- // networkError 里剥离,只留真正的传输层故障。但历史版本或第三方实现
2430
- // 仍可能把确定性文本塞进同一条链路,这里再兜一道:命中即放弃重试。
2431
- shouldRetry = !DETERMINISTIC_HINT_RE.test(String(failureReason));
2432
- }
2433
- else {
2434
- // 成功获取完整且无网络错误的数据
2435
- return { res: currentRes, json: currentJson };
2436
- }
2437
- }
2438
- }
2439
- catch (err) {
2440
- if ((signal && signal.aborted) || err.name === "AbortError") {
2441
- throw err;
2442
- }
2443
- failureReason = err.message || "网络连接中断或超时";
2444
- shouldRetry = true;
2445
- }
2446
- // 判断是否已耗尽重试次数
2447
- if (!shouldRetry || attempt > maxRetries) {
2448
- if (currentRes) {
2449
- return { res: currentRes, json: currentJson || {} };
2450
- }
2451
- throw new Error(failureReason || "网络请求失败");
2452
- }
2453
- // 计算阶梯退避等待时间:baseDelayMs * 2^(attempt - 1) + Jitter 自适应微扰
2454
- var exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1);
2455
- var maxJitter = Math.min(200, Math.floor(exponentialDelay * 0.2));
2456
- var jitter = Math.floor(Math.random() * (maxJitter + 1));
2457
- var totalDelay = exponentialDelay + jitter;
2458
- if (config.onRetry) {
2459
- config.onRetry(attempt, totalDelay, failureReason);
2460
- }
2461
- // 可中断的睡眠等待
2462
- await new Promise((resolve, reject) => {
2425
+ .dsh-github-run-meta > span:not(:first-child)::before {
2426
+ content: "·";
2427
+ margin-right: 6px;
2428
+ opacity: 0.5;
2429
+ }
2430
+ body[data-ds-dark-theme] .dsh-github-run-meta {
2431
+ color: var(--dsw-alias-label-secondary, #8b949e);
2432
+ background: var(--dsw-alias-bg-layer-1, #16161a);
2433
+ border-bottom-color: var(--dsw-alias-border-l1, #30363d);
2434
+ }
2435
+ .dsh-github-run-mono {
2436
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
2437
+ opacity: 0.9;
2438
+ }
2439
+ .dsh-github-run-degraded {
2440
+ color: var(--dsw-alias-state-warning-primary, #9a6700);
2441
+ }
2442
+ body[data-ds-dark-theme] .dsh-github-run-degraded {
2443
+ color: #d29922;
2444
+ }
2445
+ .dsh-github-run-job {
2446
+ border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
2447
+ border-radius: 6px;
2448
+ background: var(--dsw-alias-bg-base, #ffffff);
2449
+ overflow: hidden;
2450
+ flex: 0 0 auto;
2451
+ }
2452
+ body[data-ds-dark-theme] .dsh-github-run-job {
2453
+ border-color: var(--dsw-alias-border-l1, #30363d);
2454
+ background: var(--dsw-alias-bg-layer-2, #1e1e22);
2455
+ }
2456
+ .dsh-github-run-job-header {
2457
+ display: flex;
2458
+ align-items: center;
2459
+ gap: 8px;
2460
+ width: 100%;
2461
+ padding: 7px 10px;
2462
+ border: none;
2463
+ background: transparent;
2464
+ cursor: pointer;
2465
+ text-align: left;
2466
+ font-family: inherit;
2467
+ font-size: 12px;
2468
+ line-height: 18px;
2469
+ color: var(--dsw-alias-label-primary, #1f2328);
2470
+ border-radius: 6px;
2471
+ transition: background 0.15s ease;
2472
+ }
2473
+ .dsh-github-run-job-header:hover {
2474
+ background: var(--dsw-alias-interactive-bg-hover, #eaeef2);
2475
+ }
2476
+ body[data-ds-dark-theme] .dsh-github-run-job-header {
2477
+ color: var(--dsw-alias-label-primary, #f0f6fc);
2478
+ }
2479
+ body[data-ds-dark-theme] .dsh-github-run-job-header:hover {
2480
+ background: rgba(56, 139, 253, 0.12);
2481
+ }
2482
+ .dsh-github-run-caret {
2483
+ flex-shrink: 0;
2484
+ width: 10px;
2485
+ font-size: 10px;
2486
+ opacity: 0.65;
2487
+ }
2488
+ .dsh-github-run-job-name {
2489
+ flex: 1 1 auto;
2490
+ min-width: 0;
2491
+ font-weight: 500;
2492
+ overflow: hidden;
2493
+ text-overflow: ellipsis;
2494
+ white-space: nowrap;
2495
+ }
2496
+ .dsh-github-run-failed-count {
2497
+ flex-shrink: 0;
2498
+ font-size: 11px;
2499
+ opacity: 0.6;
2500
+ white-space: nowrap;
2501
+ }
2502
+ .dsh-github-run-failed-count.is-hit {
2503
+ opacity: 1;
2504
+ font-weight: 600;
2505
+ color: var(--dsw-alias-state-error-primary, #cf222e);
2506
+ }
2507
+ body[data-ds-dark-theme] .dsh-github-run-failed-count.is-hit {
2508
+ color: #f85149;
2509
+ }
2510
+ .dsh-github-run-steps {
2511
+ border-top: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
2512
+ display: flex;
2513
+ flex-direction: column;
2514
+ }
2515
+ body[data-ds-dark-theme] .dsh-github-run-steps {
2516
+ border-top-color: var(--dsw-alias-border-l1, #30363d);
2517
+ }
2518
+ .dsh-github-run-step {
2519
+ display: grid;
2520
+ grid-template-columns: 32px minmax(0, 1fr) auto 62px;
2521
+ align-items: center;
2522
+ gap: 8px;
2523
+ padding: 5px 10px 5px 18px;
2524
+ font-size: 11px;
2525
+ line-height: 17px;
2526
+ color: var(--dsw-alias-label-primary, #1f2328);
2527
+ border-bottom: 1px solid var(--dsw-alias-border-l2, #eaeef2);
2528
+ }
2529
+ .dsh-github-run-step:last-child {
2530
+ border-bottom: none;
2531
+ }
2532
+ .dsh-github-run-step > span:first-child {
2533
+ font-variant-numeric: tabular-nums;
2534
+ opacity: 0.55;
2535
+ }
2536
+ .dsh-github-run-step > span:nth-child(2) {
2537
+ overflow: hidden;
2538
+ text-overflow: ellipsis;
2539
+ white-space: nowrap;
2540
+ }
2541
+ .dsh-github-run-step > span:nth-child(3) {
2542
+ justify-self: start;
2543
+ }
2544
+ body[data-ds-dark-theme] .dsh-github-run-step {
2545
+ color: var(--dsw-alias-label-primary, #f0f6fc);
2546
+ border-bottom-color: var(--dsw-alias-border-l1, #30363d);
2547
+ }
2548
+ .dsh-github-run-step-failed {
2549
+ background: rgba(207, 34, 46, 0.07);
2550
+ box-shadow: inset 3px 0 0 rgba(207, 34, 46, 0.55);
2551
+ }
2552
+ body[data-ds-dark-theme] .dsh-github-run-step-failed {
2553
+ background: rgba(248, 81, 73, 0.12);
2554
+ box-shadow: inset 3px 0 0 rgba(248, 81, 73, 0.7);
2555
+ }
2556
+ .dsh-github-run-duration {
2557
+ font-size: 11px;
2558
+ font-variant-numeric: tabular-nums;
2559
+ white-space: nowrap;
2560
+ text-align: right;
2561
+ justify-self: end;
2562
+ opacity: 0.65;
2563
+ }
2564
+ .dsh-github-run-log-btn {
2565
+ align-self: flex-start;
2566
+ flex-shrink: 0;
2567
+ font-family: inherit;
2568
+ font-size: 11px;
2569
+ line-height: 18px;
2570
+ padding: 5px 12px;
2571
+ border-radius: 6px;
2572
+ border: 1px solid var(--dsw-alias-border-l2, #d0d7de);
2573
+ background: var(--dsw-alias-bg-module-platform, #f6f8fa);
2574
+ color: var(--dsw-alias-label-primary, #1f2328);
2575
+ cursor: pointer;
2576
+ transition: border-color 0.15s ease, color 0.15s ease;
2577
+ }
2578
+ .dsh-github-run-log-btn:hover:not(:disabled) {
2579
+ color: var(--dsw-alias-link, #0969da);
2580
+ border-color: var(--dsw-alias-link, #0969da);
2581
+ }
2582
+ .dsh-github-run-log-btn:disabled {
2583
+ cursor: progress;
2584
+ opacity: 0.6;
2585
+ }
2586
+ body[data-ds-dark-theme] .dsh-github-run-log-btn {
2587
+ border-color: var(--dsw-alias-border-l1, #30363d);
2588
+ background: var(--dsw-alias-bg-layer-2, #1e1e22);
2589
+ color: var(--dsw-alias-label-primary, #f0f6fc);
2590
+ }
2591
+ body[data-ds-dark-theme] .dsh-github-run-log-btn:hover:not(:disabled) {
2592
+ color: #58a6ff;
2593
+ border-color: #58a6ff;
2594
+ }
2595
+ .dsh-github-run-log-section {
2596
+ display: flex;
2597
+ flex-direction: column;
2598
+ gap: 8px;
2599
+ flex: 0 0 auto;
2600
+ }
2601
+ .dsh-github-run-log-block {
2602
+ display: flex;
2603
+ flex-direction: column;
2604
+ gap: 6px;
2605
+ }
2606
+ .dsh-github-run-log-title {
2607
+ font-size: 12px;
2608
+ font-weight: 600;
2609
+ color: var(--dsw-alias-label-primary, #1f2328);
2610
+ overflow: hidden;
2611
+ text-overflow: ellipsis;
2612
+ white-space: nowrap;
2613
+ }
2614
+ body[data-ds-dark-theme] .dsh-github-run-log-title {
2615
+ color: var(--dsw-alias-label-primary, #f0f6fc);
2616
+ }
2617
+ .dsh-github-run-log-truncated {
2618
+ font-size: 11px;
2619
+ color: var(--dsw-alias-state-warning-primary, #9a6700);
2620
+ }
2621
+ body[data-ds-dark-theme] .dsh-github-run-log-truncated {
2622
+ color: #d29922;
2623
+ }
2624
+ .dsh-github-run-log-hint {
2625
+ font-size: 11px;
2626
+ line-height: 1.6;
2627
+ opacity: 0.7;
2628
+ }
2629
+ .dsh-github-run-log {
2630
+ margin: 0;
2631
+ padding: 8px 10px;
2632
+ max-height: 320px;
2633
+ overflow: auto;
2634
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
2635
+ font-size: 11px;
2636
+ line-height: 1.5;
2637
+ white-space: pre-wrap;
2638
+ word-break: break-word;
2639
+ border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
2640
+ border-radius: 6px;
2641
+ background: var(--dsw-alias-bg-layer-1, #f6f8fa);
2642
+ color: var(--dsw-alias-label-primary, #1f2328);
2643
+ overscroll-behavior: contain;
2644
+ }
2645
+ body[data-ds-dark-theme] .dsh-github-run-log {
2646
+ border-color: var(--dsw-alias-border-l1, #30363d);
2647
+ background: var(--dsw-alias-bg-layer-1, #16161a);
2648
+ color: var(--dsw-alias-label-primary, #f0f6fc);
2649
+ }
2650
+ .dsh-github-run-error {
2651
+ display: flex;
2652
+ flex-direction: column;
2653
+ gap: 6px;
2654
+ padding: 10px 12px;
2655
+ border-radius: 6px;
2656
+ border: 1px solid rgba(248, 81, 73, 0.35);
2657
+ background: rgba(248, 81, 73, 0.08);
2658
+ color: var(--dsw-alias-state-error-primary, #cf222e);
2659
+ font-size: 12px;
2660
+ flex: 0 0 auto;
2661
+ }
2662
+ body[data-ds-dark-theme] .dsh-github-run-error {
2663
+ border-color: rgba(248, 81, 73, 0.45);
2664
+ background: rgba(248, 81, 73, 0.12);
2665
+ color: #f85149;
2666
+ }
2667
+ .dsh-github-run-error-title {
2668
+ font-weight: 600;
2669
+ }
2670
+ .dsh-github-run-error-text {
2671
+ max-height: 180px;
2672
+ overflow: auto;
2673
+ opacity: 0.9;
2674
+ line-height: 1.6;
2675
+ word-break: break-word;
2676
+ white-space: pre-wrap;
2677
+ overscroll-behavior: contain;
2678
+ }
2679
+ .dsh-github-run-empty {
2680
+ padding: 18px 0;
2681
+ text-align: center;
2682
+ font-size: 12px;
2683
+ opacity: 0.6;
2684
+ flex: 0 0 auto;
2685
+ }
2686
+ body[data-ds-dark-theme] .dsh-github-run-empty {
2687
+ color: var(--dsw-alias-label-secondary, #8b949e);
2688
+ }
2689
+ .dsh-github-run-modal-dialog .dsh-badge-neutral {
2690
+ background: rgba(110, 118, 129, 0.14);
2691
+ border: 1px solid rgba(110, 118, 129, 0.28);
2692
+ color: var(--dsw-alias-label-secondary, #57606a);
2693
+ }
2694
+ body[data-ds-dark-theme] .dsh-github-run-modal-dialog .dsh-badge-neutral {
2695
+ background: rgba(139, 148, 158, 0.18);
2696
+ border-color: rgba(139, 148, 158, 0.35);
2697
+ color: var(--dsw-alias-label-secondary, #8b949e);
2698
+ }
2699
+ `;
2700
+ function injectStyles() {
2701
+ var styleId = "dsh-plugin-github-styles";
2702
+ var existing = document.getElementById(styleId);
2703
+ if (existing) {
2704
+ existing.textContent = GITHUB_PANEL_CSS;
2705
+ return () => { };
2706
+ }
2707
+ var style = document.createElement("style");
2708
+ style.id = styleId;
2709
+ style.textContent = GITHUB_PANEL_CSS;
2710
+ document.head.appendChild(style);
2711
+ return () => style.remove();
2712
+ }
2713
+
2714
+ // ---- [Bundled: utils.js] ----
2715
+ /**
2716
+ * 面板公共工具函数(纯函数,可被单元测试直接覆盖)
2717
+ */
2718
+ function formatRelativeTime(iso) {
2719
+ if (!iso || typeof iso !== "string")
2720
+ return "";
2721
+ var t = Date.parse(iso);
2722
+ if (isNaN(t))
2723
+ return "";
2724
+ var diff = Date.now() - t;
2725
+ if (diff < 0)
2726
+ return "刚刚";
2727
+ var MIN = 60 * 1000, HOUR = 60 * MIN, DAY = 24 * HOUR;
2728
+ if (diff < MIN)
2729
+ return "刚刚";
2730
+ if (diff < HOUR)
2731
+ return Math.floor(diff / MIN) + "分钟前";
2732
+ if (diff < DAY)
2733
+ return Math.floor(diff / HOUR) + "小时前";
2734
+ if (diff < 7 * DAY)
2735
+ return Math.floor(diff / DAY) + "天前";
2736
+ var d = new Date(t);
2737
+ return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
2738
+ }
2739
+ function rollupCiStatus(checks) {
2740
+ if (!Array.isArray(checks) || checks.length === 0)
2741
+ return null;
2742
+ var hasFail = false, hasPending = false;
2743
+ for (var i = 0; i < checks.length; i++) {
2744
+ var c = checks[i] || {};
2745
+ var status = String(c.status || "").toLowerCase();
2746
+ var conclusion = String(c.conclusion || "").toLowerCase();
2747
+ if (conclusion === "failure" || conclusion === "timed_out" || conclusion === "cancelled")
2748
+ hasFail = true;
2749
+ else if (conclusion === "success" || conclusion === "skipped" || conclusion === "neutral") { }
2750
+ else if (status === "in_progress" || status === "queued" || conclusion === "pending" || (!conclusion && status))
2751
+ hasPending = true;
2752
+ }
2753
+ if (hasFail)
2754
+ return "fail";
2755
+ if (hasPending)
2756
+ return "pending";
2757
+ return "pass";
2758
+ }
2759
+ function reviewDecisionBadge(decision) {
2760
+ var d = String(decision || "").toUpperCase();
2761
+ if (d === "APPROVED")
2762
+ return { text: "✔ 已批准", kind: "green" };
2763
+ if (d === "CHANGES_REQUESTED")
2764
+ return { text: "✋ 需修改", kind: "red" };
2765
+ if (d === "REVIEW_REQUIRED")
2766
+ return { text: "👀 待审查", kind: "yellow" };
2767
+ return null;
2768
+ }
2769
+ function eventLabel(event) {
2770
+ var e = String(event || "");
2771
+ if (e === "push")
2772
+ return "⇡ push";
2773
+ if (e === "schedule")
2774
+ return "⏰ 定时";
2775
+ if (e === "workflow_dispatch")
2776
+ return "▶ 手动";
2777
+ if (e === "pull_request")
2778
+ return "🔀 PR";
2779
+ return e || "";
2780
+ }
2781
+ /**
2782
+ * 确定性失败的特征(客户端侧兜底判定)。
2783
+ *
2784
+ * 为什么这里还要再写一份:后端 GhExecutor 已统一分类并通过 errorKind 分流,
2785
+ * 但浏览器端拿不到 errorKind,只能看到拼接后的错误文本。这类错误重试一万次
2786
+ * 也不会变好(仓库就是关着 Issues),必须阻止阶梯退避把 7 秒白白烧掉。
2787
+ * 与服务端共用同一组特征串,改动时两边同步。
2788
+ */
2789
+ var DETERMINISTIC_HINT_RE = /repository has disabled|Could not resolve to a Repository|Branch not found|HTTP 403|Not Found \(HTTP 404\)|rate limit/i;
2790
+ /**
2791
+ * 带有阶梯退避重试(Backoff Retry)的通用异步拉取函数
2792
+ *
2793
+ * 核心设计理由与约束:
2794
+ * 1. 应对代理节点不稳定、TLS 握手卡顿、短时网络抖动(半秒至两秒微断流);
2795
+ * 2. 阶梯退避时序:第 1 次失败等 1s (1000ms),第 2 次等 2s (2000ms),第 3 次等 4s (4000ms),附加 0~200ms Jitter;
2796
+ * 3. 智能区分软硬错误:401/403/404/400 确定性错误坚决不重试,仅在网络超时、不可达、或带有 networkError 时重试;
2797
+ * 4. 严格响应外部 AbortSignal,组件卸载或用户手动打断时立即退出等待,绝无内存泄漏。
2798
+ */
2799
+ async function fetchWithRetry(url, options = {}, config = {}) {
2800
+ var maxRetries = config.maxRetries !== undefined ? config.maxRetries : 3;
2801
+ var baseDelayMs = config.baseDelayMs !== undefined ? config.baseDelayMs : 1000;
2802
+ var signal = config.signal || options.signal;
2803
+ var attempt = 0;
2804
+ while (true) {
2805
+ attempt++;
2806
+ var currentRes = null;
2807
+ var currentJson = null;
2808
+ var failureReason = "";
2809
+ var shouldRetry = false;
2810
+ try {
2811
+ if (signal && signal.aborted) {
2812
+ throw new Error("Aborted");
2813
+ }
2814
+ currentRes = await fetch(url, { ...options, signal });
2815
+ // 硬错误守卫:客户端确定性错误(如未授权、权限拒绝、不存在、参数错误)坚决不重试
2816
+ if (currentRes.status === 401 || currentRes.status === 403 || currentRes.status === 404 || currentRes.status === 400) {
2817
+ currentJson = await currentRes.json().catch(() => ({}));
2818
+ return { res: currentRes, json: currentJson };
2819
+ }
2820
+ if (!currentRes.ok) {
2821
+ failureReason = "HTTP " + currentRes.status + " " + currentRes.statusText;
2822
+ shouldRetry = true;
2823
+ }
2824
+ else {
2825
+ currentJson = await currentRes.json().catch(() => null);
2826
+ // 业务层软错误检测(后端返回了 200,但标记了 networkError 或 partialFailure)
2827
+ var isNetworkError = Boolean(currentJson && (currentJson.partialFailure ||
2828
+ currentJson.data?.partialFailure ||
2829
+ currentJson.data?.networkError ||
2830
+ currentJson.data?.auth?.networkError));
2831
+ if (isNetworkError) {
2832
+ failureReason = currentJson.error || currentJson.data?.networkErrorReason || "云端接口网络抖动";
2833
+ // 后端已把「确定性失败」(仓库关了 Issues / 仓库不存在 / 无权限)从
2834
+ // networkError 里剥离,只留真正的传输层故障。但历史版本或第三方实现
2835
+ // 仍可能把确定性文本塞进同一条链路,这里再兜一道:命中即放弃重试。
2836
+ shouldRetry = !DETERMINISTIC_HINT_RE.test(String(failureReason));
2837
+ }
2838
+ else {
2839
+ // 成功获取完整且无网络错误的数据
2840
+ return { res: currentRes, json: currentJson };
2841
+ }
2842
+ }
2843
+ }
2844
+ catch (err) {
2845
+ if ((signal && signal.aborted) || err.name === "AbortError") {
2846
+ throw err;
2847
+ }
2848
+ failureReason = err.message || "网络连接中断或超时";
2849
+ shouldRetry = true;
2850
+ }
2851
+ // 判断是否已耗尽重试次数
2852
+ if (!shouldRetry || attempt > maxRetries) {
2853
+ if (currentRes) {
2854
+ return { res: currentRes, json: currentJson || {} };
2855
+ }
2856
+ throw new Error(failureReason || "网络请求失败");
2857
+ }
2858
+ // 计算阶梯退避等待时间:baseDelayMs * 2^(attempt - 1) + Jitter 自适应微扰
2859
+ var exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1);
2860
+ var maxJitter = Math.min(200, Math.floor(exponentialDelay * 0.2));
2861
+ var jitter = Math.floor(Math.random() * (maxJitter + 1));
2862
+ var totalDelay = exponentialDelay + jitter;
2863
+ if (config.onRetry) {
2864
+ config.onRetry(attempt, totalDelay, failureReason);
2865
+ }
2866
+ // 可中断的睡眠等待
2867
+ await new Promise((resolve, reject) => {
2463
2868
  var timer = setTimeout(resolve, totalDelay);
2464
2869
  if (signal) {
2465
2870
  signal.addEventListener("abort", () => {
@@ -4121,6 +4526,415 @@ function estimateContentLines(src, charsPerLine = 80) {
4121
4526
  return lines;
4122
4527
  }
4123
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
+
4124
4938
  // ---- [Bundled: commit-modal.js] ----
4125
4939
  /**
4126
4940
  * 提交详情模态弹窗组件(与设置弹窗同构)
@@ -4132,14 +4946,76 @@ function CommitDetailModal(props) {
4132
4946
  var h = React.createElement;
4133
4947
  var [fetchedDetail, setFetchedDetail] = React.useState(null);
4134
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
+ };
4135
4995
  React.useEffect(function () {
4136
4996
  var onKeyDown = function (e) {
4137
- 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
+ }
4138
5003
  props.onClose();
5004
+ }
4139
5005
  };
4140
5006
  window.addEventListener("keydown", onKeyDown);
4141
5007
  return function () { window.removeEventListener("keydown", onKeyDown); };
4142
- }, [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]);
4143
5019
  React.useEffect(function () {
4144
5020
  setFetchedDetail(null);
4145
5021
  if (c.body || c.stat)
@@ -4163,6 +5039,13 @@ function CommitDetailModal(props) {
4163
5039
  var parsedStat = React.useMemo(() => {
4164
5040
  return effectiveStat && typeof parseGitStatText === "function" ? parseGitStatText(effectiveStat) : null;
4165
5041
  }, [effectiveStat]);
5042
+ /**
5043
+ * 切段结果必须缓存:大提交的整包 patch 可达 400k 字符,而本组件在弹窗生命周期里
5044
+ * 会因 selectedFile / patchError 等状态多次重渲染——每次重渲染都重新切一遍是纯浪费。
5045
+ */
5046
+ var patchSections = React.useMemo(function () {
5047
+ return patchState ? splitUnifiedPatch(patchState.patch) : [];
5048
+ }, [patchState]);
4166
5049
  return h("div", {
4167
5050
  className: "dsh-github-modal-overlay",
4168
5051
  onClick: props.onClose,
@@ -4266,6 +5149,18 @@ function CommitDetailModal(props) {
4266
5149
  return h("div", {
4267
5150
  key: idx,
4268
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
+ },
4269
5164
  style: {
4270
5165
  display: "flex",
4271
5166
  alignItems: "center",
@@ -4275,8 +5170,10 @@ function CommitDetailModal(props) {
4275
5170
  lineHeight: "1.4",
4276
5171
  borderBottom: idx < parsedStat.files.length - 1 ? "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.06))" : "none",
4277
5172
  gap: "8px",
5173
+ cursor: "pointer",
5174
+ userSelect: "none",
4278
5175
  },
4279
- title: f.path + (f.isBinary ? " (二进制文件)" : ` (+${f.additions}, -${f.deletions})`),
5176
+ title: f.path + (f.isBinary ? " (二进制文件)" : ` (+${f.additions}, -${f.deletions})`) + "\n点击查看该文件的代码改动",
4280
5177
  }, h("div", {
4281
5178
  style: {
4282
5179
  fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
@@ -4317,6 +5214,7 @@ function CommitDetailModal(props) {
4317
5214
  style: { display: "flex", gap: "1px", marginLeft: "2px", alignItems: "center" },
4318
5215
  }, ...diffBlocks)
4319
5216
  : null,
5217
+ h("span", { key: "chev", style: { opacity: 0.4, marginLeft: "2px", fontSize: "12px" }, "aria-hidden": "true" }, "›"),
4320
5218
  ]));
4321
5219
  }))
4322
5220
  : h("pre", {
@@ -4339,7 +5237,25 @@ function CommitDetailModal(props) {
4339
5237
  ? (fetchingDetail
4340
5238
  ? h("div", { style: { fontSize: "12px", opacity: 0.7, padding: "8px 0" } }, "⏳ 正在获取详细变更说明…")
4341
5239
  : h("div", { style: { fontSize: "11px", opacity: 0.6, fontStyle: "italic", padding: "6px 0" } }, "(该提交未包含多行正文;重启 DSH 桌面端可同步最新格式并直接呈现变更统计)"))
4342
- : 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);
4343
5259
  }
4344
5260
 
4345
5261
  // ---- [Bundled: pr-modal.js] ----
@@ -4681,18 +5597,36 @@ function PrDetailModal(props) {
4681
5597
  },
4682
5598
  }, data ? `#${data.number} ${data.title}` : `PR #${prNumber}`), data
4683
5599
  ? h("span", {
5600
+ // 色调来自 shared/state-badges(与右侧栏 / CLI 输出同一份映射);
5601
+ // 文案保持原样(Draft 用大驼峰、其余直接展示 GitHub 原始 state)。
5602
+ // 未知状态沿用旧实现的绿色兜底(旧代码的 else 分支就是 green)。
4684
5603
  className: "dsh-github-badge " +
4685
- (data.isDraft
4686
- ? "dsh-badge-yellow"
4687
- : data.state === "MERGED"
4688
- ? "dsh-badge-purple"
4689
- : data.state === "CLOSED"
4690
- ? "dsh-badge-red"
4691
- : "dsh-badge-green"),
5604
+ (function () {
5605
+ var b = prStateBadge(data.state, data.isDraft);
5606
+ return "dsh-badge-" + (b && b.tone !== "neutral" ? b.tone : "green");
5607
+ })(),
4692
5608
  style: { fontSize: "10px", padding: "1px 6px" },
4693
5609
  }, data.isDraft ? "Draft" : data.state)
4694
5610
  : null), data
4695
- ? h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" } }, h("span", null, `👤 @${data.author.login}`), data.createdAt ? h("span", null, `· 提单于 ${formatRelativeTime(data.createdAt)}`) : null, h("span", { style: { fontFamily: "monospace", opacity: 0.85 } }, `(${data.headRefName} → ${data.baseRefName})`))
5611
+ ? h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" } }, h("span", null, `👤 @${data.author.login}`), data.createdAt ? h("span", null, `· 提单于 ${formatRelativeTime(data.createdAt)}`) : null, h("span", { style: { fontFamily: "monospace", opacity: 0.85 } }, `(${data.headRefName} → ${data.baseRefName})`),
5612
+ /*
5613
+ * 审查锚点:分支名会移动,SHA 不会。
5614
+ * 点开这个弹窗时先看这一枚 chip,就能确认「我看到的是不是最新一次推送」;
5615
+ * title 里放完整 SHA,方便复制去精确引用。老服务端没有该字段时不渲染。
5616
+ */
5617
+ typeof data.headRefOid === "string" && data.headRefOid.length >= 7
5618
+ ? h("span", {
5619
+ title: `headRefOid: ${data.headRefOid}`,
5620
+ style: {
5621
+ fontFamily: "monospace",
5622
+ fontSize: "10px",
5623
+ padding: "1px 6px",
5624
+ borderRadius: "10px",
5625
+ border: "1px solid var(--dsw-alias-border-l2, rgba(31,35,40,0.15))",
5626
+ opacity: 0.9,
5627
+ },
5628
+ }, `🔖 ${data.headRefOid.slice(0, 7)}`)
5629
+ : null)
4696
5630
  : null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, data?.url
4697
5631
  ? h("button", {
4698
5632
  className: "dsh-github-btn-sm",
@@ -4735,7 +5669,10 @@ function PrDetailModal(props) {
4735
5669
  }, `💬 描述与讨论 (${data.comments ? data.comments.length : 0})`), h("button", {
4736
5670
  className: `dsh-github-pr-tab-btn ${activeTab === "files" ? "active" : ""}`,
4737
5671
  onClick: () => setActiveTab("files"),
4738
- }, `📁 变动文件 (${data.files ? data.files.length : 0} 文件 · +${data.additions || 0} / -${data.deletions || 0})`)),
5672
+ },
5673
+ // 截断时显示 N/M:M 来自 changedFiles(权威总数),N 是 gh 实际返回的条数。
5674
+ // 只显示 N 会让「被截断」看起来像「就这么多」——这是 cli/cli#12522 的静默截断。
5675
+ `📁 变动文件 (${data.files ? data.files.length : 0}${data.filesTruncated && data.changedFiles ? '/' + data.changedFiles : ''} 文件 · +${data.additions || 0} / -${data.deletions || 0})${data.filesTruncated ? ' ⚠️' : ''}`)),
4739
5676
  // Tab 1: 描述与讨论
4740
5677
  activeTab === "conversation"
4741
5678
  ? h("div", { className: "dsh-github-modal-body dsh-github-pr-conversation-pane" },
@@ -4806,7 +5743,9 @@ function PrDetailModal(props) {
4806
5743
  onClick: toggleFilePane,
4807
5744
  style: { padding: "2px 8px", fontSize: "10px", flexShrink: 0 },
4808
5745
  title: "展开左侧文件列表",
4809
- }, `📁 文件列表 (${(data.files && data.files.length) || 0})`)
5746
+ },
5747
+ // 折叠态同样要透出截断事实(N/M),否则展开前的第一眼就被骗了
5748
+ `📁 文件列表 (${(data.files && data.files.length) || 0}${data.filesTruncated && data.changedFiles ? '/' + data.changedFiles : ''})`)
4810
5749
  : null, h("span", { style: { fontFamily: "monospace", fontWeight: 600, fontSize: "11px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, selectedFilePath), data.fileDiffs && data.fileDiffs[selectedFilePath]
4811
5750
  ? h("button", {
4812
5751
  className: "dsh-github-btn-sm",
@@ -4814,7 +5753,9 @@ function PrDetailModal(props) {
4814
5753
  style: { padding: "2px 8px", fontSize: "10px" },
4815
5754
  }, copied ? "✓ 已复制" : "📋 复制文件 Diff")
4816
5755
  : null), h("div", { style: { flex: 1, overflowY: "auto", minHeight: 0 } },
4817
- // 截断警告必须显式呈现:否则用户会把「拿不到 diff」误读成「代码没改动」
5756
+ // 两种「看不到 diff」的原因必须分开说,否则用户不知道该做什么:
5757
+ // ① diffTruncated:整段 diff 被输出阈值裁掉(去 GitHub 看完整内容)
5758
+ // ② diffIncomplete:这个文件在清单里但没取到切片(换一个文件/重试)
4818
5759
  data.diffTruncated
4819
5760
  ? h("div", {
4820
5761
  style: {
@@ -4828,6 +5769,19 @@ function PrDetailModal(props) {
4828
5769
  color: "var(--dsw-alias-state-attention-primary, #9a6700)",
4829
5770
  },
4830
5771
  }, "⚠️ 该 PR 的 diff 体量过大已触达输出上限,部分文件可能无法展示差异。可点击右上角「↗ 浏览器打开」查看完整内容。")
5772
+ : null, data.diffIncomplete
5773
+ ? h("div", {
5774
+ style: {
5775
+ margin: "8px",
5776
+ padding: "8px 12px",
5777
+ borderRadius: "6px",
5778
+ fontSize: "11px",
5779
+ lineHeight: 1.5,
5780
+ background: "rgba(210, 153, 34, 0.12)",
5781
+ border: "1px solid rgba(210, 153, 34, 0.35)",
5782
+ color: "var(--dsw-alias-state-attention-primary, #9a6700)",
5783
+ },
5784
+ }, "⚠️ 有文件未能取到 diff 正文(二进制、重命名或超出切片范围)。这些文件显示为空不代表没有改动,请到 GitHub 核对。")
4831
5785
  : null, data.fileDiffs && data.fileDiffs[selectedFilePath]
4832
5786
  ? renderDiffContent(data.fileDiffs[selectedFilePath])
4833
5787
  : renderDiffContent(""),
@@ -5064,11 +6018,14 @@ function IssueDetailModal(props) {
5064
6018
  var stateBadge = function (d) {
5065
6019
  if (!d)
5066
6020
  return null;
5067
- var isOpen = String(d.state).toUpperCase() === "OPEN";
6021
+ // 状态→色调来自 shared/state-badges(与右侧栏 / CLI 输出同一份映射)
6022
+ var b = issueStateBadge(d.state);
6023
+ if (!b)
6024
+ return null;
5068
6025
  return h("span", {
5069
- className: "dsh-github-badge " + (isOpen ? "dsh-badge-green" : "dsh-badge-red"),
6026
+ className: "dsh-github-badge dsh-badge-" + b.tone,
5070
6027
  style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 },
5071
- }, isOpen ? "OPEN" : "CLOSED");
6028
+ }, b.label);
5072
6029
  };
5073
6030
  /**
5074
6031
  * 元数据条:只渲染真实存在的字段。
@@ -5167,26 +6124,509 @@ function IssueDetailModal(props) {
5167
6124
  }, h("span", null, "👤 @" + data.author.login), data.createdAt ? h("span", null, "· 提单于 " + formatRelativeTime(data.createdAt)) : null, data.closedAt ? h("span", null, "· 关闭于 " + formatRelativeTime(data.closedAt)) : null, repoNameWithOwner
5168
6125
  ? h("span", { style: { fontFamily: "monospace", opacity: 0.85 } }, repoNameWithOwner)
5169
6126
  : null)
5170
- : null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, data && data.url
5171
- ? h("button", {
5172
- type: "button",
5173
- className: "dsh-github-btn-sm",
5174
- style: { padding: "4px 8px", fontSize: "11px" },
5175
- onClick: function () { copyLink(data.url); },
5176
- title: "复制该 Issue 网页链接",
5177
- }, copied ? "✓ 已复制" : "📋 复制链接")
5178
- : null, data && data.url
6127
+ : null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, data && data.url
6128
+ ? h("button", {
6129
+ type: "button",
6130
+ className: "dsh-github-btn-sm",
6131
+ style: { padding: "4px 8px", fontSize: "11px" },
6132
+ onClick: function () { copyLink(data.url); },
6133
+ title: "复制该 Issue 网页链接",
6134
+ }, copied ? "✓ 已复制" : "📋 复制链接")
6135
+ : null, data && data.url
6136
+ ? h("a", {
6137
+ href: data.url,
6138
+ target: "_blank",
6139
+ rel: "noopener noreferrer",
6140
+ className: "dsh-github-btn-sm",
6141
+ style: { padding: "4px 8px", fontSize: "11px", textDecoration: "none", color: "inherit" },
6142
+ title: "在系统默认浏览器中打开该 Issue",
6143
+ }, "↗ 浏览器打开")
6144
+ : null, h("button", {
6145
+ type: "button",
6146
+ "aria-label": "关闭 (Esc)",
6147
+ style: {
6148
+ background: "none",
6149
+ border: "none",
6150
+ cursor: "pointer",
6151
+ fontSize: "18px",
6152
+ color: "var(--dsw-alias-label-secondary, #656d76)",
6153
+ padding: "0 6px",
6154
+ },
6155
+ onClick: onClose,
6156
+ title: "关闭 (Esc)",
6157
+ }, "✕"))),
6158
+ // 主体
6159
+ loading
6160
+ ? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 Issue 详情与讨论…")
6161
+ : error
6162
+ ? h("div", {
6163
+ style: {
6164
+ padding: "24px",
6165
+ margin: "16px",
6166
+ borderRadius: "8px",
6167
+ background: "rgba(248,81,73,0.08)",
6168
+ border: "1px solid rgba(248,81,73,0.3)",
6169
+ },
6170
+ }, h("div", { style: { fontWeight: 600, color: "#f85149", marginBottom: "6px" } }, "⚠️ 读取 Issue 详情失败"), h("div", { style: { fontSize: "12px", opacity: 0.85, lineHeight: 1.6 } }, error), repoNameWithOwner
6171
+ ? h("a", {
6172
+ href: "https://github.com/" + repoNameWithOwner + "/issues/" + issueNumber,
6173
+ target: "_blank",
6174
+ rel: "noopener noreferrer",
6175
+ className: "dsh-github-btn-sm",
6176
+ style: { display: "inline-block", marginTop: "12px", padding: "4px 10px", fontSize: "11px", textDecoration: "none" },
6177
+ }, "↗ 在浏览器中查看")
6178
+ : null)
6179
+ : data
6180
+ ? h("div", { className: "dsh-github-issue-conversation-pane" }, metaChips(data), bodyCard(data), h("div", { className: "dsh-github-issue-timeline-title" }, "💬 讨论与评论时间线 (" + (data.comments ? data.comments.length : 0) + ")"), data.comments && data.comments.length > 0
6181
+ ? data.comments.map(function (c) {
6182
+ return h(IssueCommentCard, { key: c.id || c.createdAt, comment: c });
6183
+ })
6184
+ : h("div", { className: "dsh-github-issue-empty-timeline" }, "💬 暂无讨论与评论——这是一个还没有人回复的议题。"))
6185
+ : null));
6186
+ }
6187
+
6188
+ // ---- [Bundled: run-modal.js] ----
6189
+ /**
6190
+ * 日志请求的客户端超时。
6191
+ *
6192
+ * 服务端总预算 90s,这里放 100s:让服务端的「超预算」显式错误先到;
6193
+ * 而连接卡在代理/DNS 层永远不返回时,客户端的 AbortController 是最后一道闸——
6194
+ * 否则按钮会永远停在「正在下载日志…」,用户只能刷新页面。
6195
+ */
6196
+ var LOG_FETCH_TIMEOUT_MS = 100_000;
6197
+ /**
6198
+ * 结论 → 中文文案。
6199
+ *
6200
+ * ciBadge 只统一了 tone 与英文 label(服务端 Markdown 与右侧栏共用同一份映射),
6201
+ * 中文必须在本组件单独补一层;改 ciBadge 会连带改掉 CLI 与服务端输出。
6202
+ */
6203
+ var RUN_CONCLUSION_LABEL = {
6204
+ success: "成功",
6205
+ failure: "失败",
6206
+ cancelled: "已取消",
6207
+ skipped: "已跳过",
6208
+ };
6209
+ /**
6210
+ * 未命中中文表时保留 GitHub 原始英文(running / queued / timed_out 等):
6211
+ * 这些取值没有稳定的中文对应,硬翻反而让用户对不上 GitHub 页面。
6212
+ */
6213
+ function zhConclusion(conclusion, fallback) {
6214
+ var zh = RUN_CONCLUSION_LABEL[String(conclusion || "").toLowerCase()];
6215
+ return zh || fallback;
6216
+ }
6217
+ function pad2(n) {
6218
+ return n < 10 ? "0" + n : String(n);
6219
+ }
6220
+ /**
6221
+ * Actions run / job / step 的耗时格式化(导出以便单测直接覆盖)。
6222
+ *
6223
+ * 为什么不用 formatRelativeTime:那个函数答的是「发生在多久前」,这里是「跑了多久」;
6224
+ * 而且 run → job → step 三层共用同一套精度规则(<60s → `12s`、<60m → `3m20s`、
6225
+ * 其余 `1h05m`),分钟/秒补零才能让三层的列宽稳定对齐。
6226
+ * 任一端缺失或不可解析一律返回空串:排队中/已跳过的 job 没有 startedAt/completedAt,
6227
+ * 硬算只会渲染出「0s」这种假精确值,比不显示更误导。
6228
+ */
6229
+ function formatRunDuration(startISO, endISO) {
6230
+ var startT = typeof startISO === "string" ? Date.parse(startISO) : NaN;
6231
+ var endT = typeof endISO === "string" ? Date.parse(endISO) : NaN;
6232
+ // endT < startT 视为非法区间(时钟漂移 / 字段错配),同样不显示
6233
+ if (isNaN(startT) || isNaN(endT) || endT < startT)
6234
+ return "";
6235
+ var totalSec = Math.round((endT - startT) / 1000);
6236
+ if (totalSec < 60)
6237
+ return totalSec + "s";
6238
+ var totalMin = Math.floor(totalSec / 60);
6239
+ if (totalMin < 60)
6240
+ return totalMin + "m" + pad2(totalSec % 60) + "s";
6241
+ return Math.floor(totalMin / 60) + "h" + pad2(totalMin % 60) + "m";
6242
+ }
6243
+ /**
6244
+ * 初始展开哪一个 job:优先第一个失败的(排障入口就在那里),全绿时展开第一个。
6245
+ * 必须在 setData 之后用真实数据计算——写死 {} 会让「默认展开」永远不生效。
6246
+ */
6247
+ function computeInitialExpanded(d) {
6248
+ var init = {};
6249
+ var jobs = d && Array.isArray(d.jobs) ? d.jobs : [];
6250
+ if (jobs.length === 0)
6251
+ return init;
6252
+ var target = jobs[0];
6253
+ for (var i = 0; i < jobs.length; i++) {
6254
+ var job = jobs[i];
6255
+ if (String(job && job.conclusion ? job.conclusion : "").toLowerCase() === "failure") {
6256
+ target = job;
6257
+ break;
6258
+ }
6259
+ }
6260
+ if (target && target.databaseId !== undefined && target.databaseId !== null) {
6261
+ init[target.databaseId] = true;
6262
+ }
6263
+ return init;
6264
+ }
6265
+ /**
6266
+ * Actions Run 详情弹窗(只读:run 元信息 + job/step 树 + 失败日志片段)。
6267
+ *
6268
+ * 单列纵向:run 的信息天然是「job 列表 + 一条日志流」的阅读顺序,没有需要并排
6269
+ * 对照的第二个信息源(PR 才有 diff 双栏)。
6270
+ * 日志刻意渲染成 <pre> 原文而不是 markdown-lite:原始日志里的 `#`、`|`、`---`
6271
+ * 是 shell 注释、表格与 diff 分隔符,交给 Markdown 解析会把堆栈与 diff 撕碎。
6272
+ */
6273
+ function RunDetailModal(props) {
6274
+ var runId = props.runId;
6275
+ var repoNameWithOwner = props.repoNameWithOwner || "";
6276
+ var cwd = props.cwd;
6277
+ var onClose = props.onClose;
6278
+ var showToast = props.showToast || (function () { });
6279
+ // Hooks 必须先于任何 early return 无条件执行(见 issue-modal 的同类注释):
6280
+ // runId 由空变有值时两次渲染的 Hook 数量必须一致,否则 React 直接抛错。
6281
+ // 因此 useState 与两个 useEffect 全部集中在这里,`if (!runId) return null`
6282
+ // 只能出现在它们之后。
6283
+ var [data, setData] = React.useState(null);
6284
+ var [loading, setLoading] = React.useState(true);
6285
+ var [error, setError] = React.useState(null);
6286
+ var [expanded, setExpanded] = React.useState({});
6287
+ var [logState, setLogState] = React.useState({ status: "idle" });
6288
+ React.useEffect(function () {
6289
+ var onKeyDown = function (e) {
6290
+ if (e.key === "Escape" || e.keyCode === 27)
6291
+ onClose();
6292
+ };
6293
+ window.addEventListener("keydown", onKeyDown);
6294
+ // 必须成对移除:弹窗在面板里会被反复开关,漏掉会累积监听器(Esc 一次关多个)
6295
+ return function () { window.removeEventListener("keydown", onKeyDown); };
6296
+ }, [onClose]);
6297
+ React.useEffect(function () {
6298
+ if (!runId) {
6299
+ setLoading(false);
6300
+ return;
6301
+ }
6302
+ setLoading(true);
6303
+ setError(null);
6304
+ // 换了 run 就是全新上下文:展开态与日志结果都不能沿用上一个 run 的
6305
+ setExpanded({});
6306
+ setLogState({ status: "idle" });
6307
+ var url = "/api/github/run-detail?run_id=" +
6308
+ encodeURIComponent(String(runId)) +
6309
+ (repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
6310
+ (cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
6311
+ fetch(url)
6312
+ .then(async function (res) {
6313
+ if (!res.ok) {
6314
+ // 非 2xx 时 body 里可能带一段很长的 gh 原始报错,优先原样透出给用户
6315
+ var errText = await res.text().catch(function () { return ""; });
6316
+ var j = null;
6317
+ try {
6318
+ j = JSON.parse(errText);
6319
+ }
6320
+ catch (e) { }
6321
+ throw new Error((j && j.error) || errText || ("服务响应异常 (" + res.status + ")"));
6322
+ }
6323
+ return res.json();
6324
+ })
6325
+ .then(function (json) {
6326
+ setLoading(false);
6327
+ if (json && json.ok && json.data) {
6328
+ setData(json.data);
6329
+ // 默认展开必须基于刚拿到的数据算,不能预设 {}
6330
+ setExpanded(computeInitialExpanded(json.data));
6331
+ }
6332
+ else {
6333
+ setError((json && json.error) || "获取 Actions Run 详情失败");
6334
+ }
6335
+ })
6336
+ .catch(function (err) {
6337
+ setLoading(false);
6338
+ setError((err && err.message) || "网络请求异常");
6339
+ });
6340
+ }, [runId, repoNameWithOwner, cwd]);
6341
+ if (!runId)
6342
+ return null;
6343
+ var h = React.createElement;
6344
+ var toggleJob = function (jobId) {
6345
+ setExpanded(function (prev) {
6346
+ // 必须新建对象:原地改 prev 引用不变,React 判定状态未变,不会重渲染
6347
+ var next = {};
6348
+ for (var k in prev) {
6349
+ if (Object.prototype.hasOwnProperty.call(prev, k))
6350
+ next[k] = prev[k];
6351
+ }
6352
+ next[jobId] = !next[jobId];
6353
+ return next;
6354
+ });
6355
+ };
6356
+ /** 结论徽章:tone 来自 shared/state-badges,文案走中文表 */
6357
+ var badgeEl = function (conclusion, status) {
6358
+ var b = ciBadge(status, conclusion);
6359
+ if (!b)
6360
+ return null;
6361
+ return h("span", {
6362
+ className: "dsh-github-badge dsh-badge-" + b.tone,
6363
+ style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 },
6364
+ }, zhConclusion(conclusion, b.label));
6365
+ };
6366
+ var renderMeta = function (d) {
6367
+ var parts = [];
6368
+ var ev = eventLabel(d.event);
6369
+ if (ev)
6370
+ parts.push(h("span", { key: "ev" }, ev));
6371
+ if (d.headBranch) {
6372
+ parts.push(h("span", { key: "br", className: "dsh-github-run-mono", title: "分支 " + d.headBranch }, d.headBranch));
6373
+ }
6374
+ if (d.headSha) {
6375
+ // 短 SHA 只留 7 位(GitHub 约定长度),完整值放 title,方便核对而不是复制错
6376
+ parts.push(h("span", { key: "sha", className: "dsh-github-run-mono", title: "完整 SHA: " + d.headSha }, String(d.headSha).slice(0, 7)));
6377
+ }
6378
+ if (d.createdAt)
6379
+ parts.push(h("span", { key: "created" }, formatRelativeTime(d.createdAt)));
6380
+ // run 没有 completedAt 字段,用 updatedAt 作终点:运行结束后它就是完成时间
6381
+ var dur = formatRunDuration(d.startedAt, d.updatedAt);
6382
+ if (dur)
6383
+ parts.push(h("span", { key: "dur" }, "耗时 " + dur));
6384
+ if (d.fieldsDegraded) {
6385
+ parts.push(h("span", { key: "degraded", className: "dsh-github-run-degraded", title: "部分字段未取到(job 列表或结论缺失),服务端已降级返回" }, "⚠️ 部分字段降级"));
6386
+ }
6387
+ if (parts.length === 0)
6388
+ return null;
6389
+ return h("div", { className: "dsh-github-run-meta" }, ...parts);
6390
+ };
6391
+ var renderSteps = function (job) {
6392
+ var steps = job && Array.isArray(job.steps) ? job.steps : [];
6393
+ if (steps.length === 0) {
6394
+ return h("div", { className: "dsh-github-run-steps" }, h("div", { className: "dsh-github-run-empty" }, "该 job 没有可读步骤(可能仍在排队)"));
6395
+ }
6396
+ return h("div", { className: "dsh-github-run-steps" }, steps.map(function (s, i) {
6397
+ var failed = String((s && s.conclusion) || "").toLowerCase() === "failure";
6398
+ return h("div", {
6399
+ key: "step-" + (s && s.number !== undefined ? s.number : i),
6400
+ className: "dsh-github-run-step" + (failed ? " dsh-github-run-step-failed" : ""),
6401
+ }, h("span", null, String(s && s.number !== undefined ? s.number : i + 1)), h("span", { title: (s && s.name) || "" }, (s && s.name) || "未命名步骤"),
6402
+ // 徽章外层固定包一个 span:徽章为空时若不占位,grid 第 3 列消失,
6403
+ // 后面的耗时列会左移到徽章列,四列对不齐
6404
+ h("span", null, badgeEl(s && s.conclusion, s && s.status)), h("span", { className: "dsh-github-run-duration" }, formatRunDuration(s && s.startedAt, s && s.completedAt)));
6405
+ }));
6406
+ };
6407
+ var renderJob = function (job, idx) {
6408
+ var steps = job && Array.isArray(job.steps) ? job.steps : [];
6409
+ var failedSteps = 0;
6410
+ steps.forEach(function (s) {
6411
+ if (String((s && s.conclusion) || "").toLowerCase() === "failure")
6412
+ failedSteps++;
6413
+ });
6414
+ var jobId = job && job.databaseId;
6415
+ var isOpen = Boolean(expanded[jobId]);
6416
+ var jobDur = formatRunDuration(job && job.startedAt, job && job.completedAt);
6417
+ return h("div", { key: "job-" + (jobId !== undefined && jobId !== null ? jobId : idx), className: "dsh-github-run-job" }, h("button", {
6418
+ type: "button",
6419
+ className: "dsh-github-run-job-header",
6420
+ "aria-expanded": isOpen ? "true" : "false",
6421
+ onClick: function () { toggleJob(jobId); },
6422
+ title: isOpen ? "收起该 job 的步骤" : "展开该 job 的步骤",
6423
+ }, h("span", { className: "dsh-github-run-caret", "aria-hidden": "true" }, isOpen ? "▾" : "▸"), h("span", { className: "dsh-github-run-job-name", title: (job && job.name) || "" }, (job && job.name) || "未命名 job"), badgeEl(job && job.conclusion, job && job.status), jobDur ? h("span", { className: "dsh-github-run-duration" }, jobDur) : null, steps.length > 0
6424
+ ? h("span", { className: "dsh-github-run-failed-count" + (failedSteps > 0 ? " is-hit" : "") }, failedSteps + "/" + steps.length + " 步失败")
6425
+ : null), isOpen ? renderSteps(job) : null);
6426
+ };
6427
+ /**
6428
+ * 选出该拉日志的 job。
6429
+ *
6430
+ * 为什么不能只看 `job.conclusion === 'failure'`:按钮的出现条件是「有失败 step
6431
+ * 或失败 job」,而 `timed_out` / `cancelled` 的 job 完全可能带着失败 step
6432
+ * (`gh run view --help` 对 `--log-failed` 的定义是 "any failed steps")。
6433
+ * 两处口径不一致,用户就会看到「按钮在、点下去说没有失败的 job」。
6434
+ */
6435
+ var pickLogJobIds = function (jobs) {
6436
+ var ids = [];
6437
+ for (var i = 0; i < jobs.length; i++) {
6438
+ var job = jobs[i];
6439
+ var jobId = job && job.databaseId;
6440
+ // databaseId 缺失或为 0 的 job 无法定向拉取(服务端 requirement 是正整数)
6441
+ if (typeof jobId !== "number" || jobId <= 0)
6442
+ continue;
6443
+ var jobFailed = String((job && job.conclusion) || "").toLowerCase() === "failure";
6444
+ var stepFailed = Array.isArray(job && job.steps) &&
6445
+ job.steps.some(function (s) {
6446
+ return String((s && s.conclusion) || "").toLowerCase() === "failure";
6447
+ });
6448
+ if (jobFailed || stepFailed)
6449
+ ids.push(jobId);
6450
+ }
6451
+ return ids;
6452
+ };
6453
+ var loadLogs = function () {
6454
+ if (!data || logState.status === "loading")
6455
+ return;
6456
+ var jobs = Array.isArray(data.jobs) ? data.jobs : [];
6457
+ var allIds = pickLogJobIds(jobs);
6458
+ if (allIds.length === 0) {
6459
+ showToast("⚠️ 没有失败的 job 可提取日志");
6460
+ return;
6461
+ }
6462
+ // 服务端上限 3 个(超标直接 400)。这里先按上限请求,并在界面上明示「只取了前 3 个」,
6463
+ // 不让人以为这次把全部失败 job 都拉完了。
6464
+ var jobIds = allIds.slice(0, 3);
6465
+ setLogState({ status: "loading" });
6466
+ showToast("⏳ 正在下载失败日志(最长 90 秒)");
6467
+ var url = "/api/github/run-logs?run_id=" +
6468
+ encodeURIComponent(String(runId)) +
6469
+ // job_id 是逗号分隔的数字列表:数字与逗号都是 URL 安全字符,不再编码,
6470
+ // 免得某些服务端实现按未解码的原始串切分
6471
+ "&job_id=" + jobIds.join(",") +
6472
+ (repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
6473
+ (cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
6474
+ /*
6475
+ * 客户端超时不是可选项:服务端总预算 90s,但连接可能停在 DNS/代理层永远不返回。
6476
+ * 不设闸门的话按钮会永远停在「正在下载日志…」,用户只能刷新页面。
6477
+ * 100s 比服务端预算略长,让服务端的显式错误优先到达。
6478
+ */
6479
+ var controller = typeof AbortController === "function" ? new AbortController() : null;
6480
+ // 闭包里不能再依赖可空收窄(TS 对捕获变量不做 narrow),在 if 内绑一个非空引用
6481
+ var timer = null;
6482
+ if (controller) {
6483
+ var ctrl = controller;
6484
+ timer = setTimeout(function () { ctrl.abort(); }, LOG_FETCH_TIMEOUT_MS);
6485
+ }
6486
+ var clearTimer = function () {
6487
+ if (timer) {
6488
+ clearTimeout(timer);
6489
+ timer = null;
6490
+ }
6491
+ };
6492
+ fetch(url, controller ? { signal: controller.signal } : undefined)
6493
+ .then(async function (res) {
6494
+ if (!res.ok) {
6495
+ var errText = await res.text().catch(function () { return ""; });
6496
+ var j = null;
6497
+ try {
6498
+ j = JSON.parse(errText);
6499
+ }
6500
+ catch (e) { }
6501
+ throw new Error((j && j.error) || errText || ("日志接口响应异常 (" + res.status + ")"));
6502
+ }
6503
+ return res.json();
6504
+ })
6505
+ .then(function (json) {
6506
+ clearTimer();
6507
+ if (json && json.ok && json.data) {
6508
+ setLogState({ status: "ok", data: json.data });
6509
+ }
6510
+ else {
6511
+ setLogState({ status: "error", error: (json && json.error) || "获取运行日志失败" });
6512
+ }
6513
+ })
6514
+ .catch(function (err) {
6515
+ clearTimer();
6516
+ var aborted = err && (err.name === "AbortError" || err.code === 20);
6517
+ setLogState({
6518
+ status: "error",
6519
+ error: aborted
6520
+ ? "日志拉取超时(100 秒)已中止。GitHub 日志接口在代理下常常长时间无响应,请稍后重试或直接点右上角「GitHub ↗」查看。"
6521
+ : (err && err.message) || "网络请求异常(日志链路较慢,可直接重试)",
6522
+ });
6523
+ });
6524
+ };
6525
+ var renderLogSection = function () {
6526
+ if (!data)
6527
+ return null;
6528
+ // failedSteps 只覆盖「step 级失败」;job 级失败但零失败 step 是真实形态
6529
+ // (setup 阶段失败、被 cancellation 打断),此时日志按钮同样必须出现。
6530
+ var hasFailed = Number(data.failedSteps) > 0 || Number(data.failedJobs) > 0;
6531
+ if (!hasFailed && logState.status === "idle")
6532
+ return null;
6533
+ var loadingLog = logState.status === "loading";
6534
+ var children = [];
6535
+ if (hasFailed) {
6536
+ children.push(h("button", {
6537
+ key: "log-btn",
6538
+ type: "button",
6539
+ className: "dsh-github-run-log-btn",
6540
+ onClick: loadLogs,
6541
+ disabled: loadingLog,
6542
+ title: "从 GitHub 拉取失败 job 的原始日志片段(较慢,最长 90 秒)",
6543
+ }, loadingLog ? "正在下载日志…" : logState.status === "ok" ? "📄 重新提取失败日志" : "📄 提取失败日志"));
6544
+ // 服务端一次最多 3 个 job:超出的必须说出来,否则用户以为全拉完了
6545
+ var failedJobTotal = pickLogJobIds(Array.isArray(data.jobs) ? data.jobs : []).length;
6546
+ if (failedJobTotal > 3) {
6547
+ children.push(h("div", { key: "log-cap", className: "dsh-github-run-log-hint" }, "本次仅提取前 3 个失败 job 的日志(共 " + failedJobTotal + " 个),其余请到 GitHub 查看。"));
6548
+ }
6549
+ }
6550
+ if (loadingLog) {
6551
+ children.push(h("div", { key: "log-hint", className: "dsh-github-run-log-hint" }, "⏳ 正在拉取失败日志——GitHub 日志接口较慢(最长 90 秒),请不要关闭弹窗。"));
6552
+ }
6553
+ if (logState.status === "error") {
6554
+ children.push(h("div", { key: "log-error", className: "dsh-github-run-error" }, h("div", { className: "dsh-github-run-error-title" }, "⚠️ 提取失败日志失败"), h("div", { className: "dsh-github-run-error-text" }, logState.error || "未知错误"),
6555
+ // 只有显式点击才重试:慢链路不能自动重试(见 RunLogState 注释)
6556
+ h("button", { key: "retry", type: "button", className: "dsh-github-run-log-btn", onClick: loadLogs }, "🔁 重试")));
6557
+ }
6558
+ if (logState.status === "ok" && logState.data) {
6559
+ var logData = logState.data;
6560
+ var logJobs = Array.isArray(logData.jobs) ? logData.jobs : [];
6561
+ var logErrors = Array.isArray(logData.errors) ? logData.errors : [];
6562
+ if (logJobs.length === 0 && logErrors.length === 0) {
6563
+ children.push(h("div", { key: "log-none", className: "dsh-github-run-empty" }, "GitHub 未返回可读日志(可能日志已过期或权限不足)"));
6564
+ }
6565
+ logJobs.forEach(function (block, i) {
6566
+ children.push(h("div", { key: "log-block-" + i, className: "dsh-github-run-log-block" }, h("div", { className: "dsh-github-run-log-title" }, "📄 " + (block.jobName || ("job " + block.jobId))), block.truncated
6567
+ ? h("div", { className: "dsh-github-run-log-truncated" }, "⚠️ 原始日志超出上限,仅显示已获取部分")
6568
+ : null,
6569
+ // <pre> 原文直出:日志里的 #、|、--- 不能进 markdown-lite
6570
+ h("pre", { className: "dsh-github-run-log" }, block.excerpt || "(该 job 无日志输出)")));
6571
+ });
6572
+ logErrors.forEach(function (e, i) {
6573
+ children.push(h("div", { key: "log-err-" + i, className: "dsh-github-run-error" }, h("div", { className: "dsh-github-run-error-title" }, "⚠️ " + (e.jobName || "job") + " 日志获取失败"), h("div", { className: "dsh-github-run-error-text" }, e.error || "未知错误")));
6574
+ });
6575
+ if (logData.fetchedAt) {
6576
+ children.push(h("div", { key: "log-at", className: "dsh-github-run-log-hint" }, "日志获取于 " + formatRelativeTime(logData.fetchedAt)));
6577
+ }
6578
+ // 名称降级必须可见(服务端 details 降级时 jobName 会退化成 job #<id>)
6579
+ if (logData.namesUnavailable) {
6580
+ children.push(h("div", { key: "log-names", className: "dsh-github-run-log-hint" }, "⚠️ job 名称未取到(详情字段降级),以 job #编号 显示。"));
6581
+ }
6582
+ }
6583
+ return h("div", { key: "logs", className: "dsh-github-run-log-section" }, ...children);
6584
+ };
6585
+ var headerTitle = data
6586
+ ? data.workflowName || data.name || "Actions Run"
6587
+ : "Actions Run";
6588
+ var ghHref = (data && data.url) || (repoNameWithOwner ? "https://github.com/" + repoNameWithOwner + "/actions/runs/" + runId : "");
6589
+ var header = h("div", { className: "dsh-github-modal-header dsh-github-run-modal-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, h("span", { style: { fontSize: "16px", lineHeight: 1, flexShrink: 0 }, "aria-hidden": "true" }, "⚡"), h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 } }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("h3", {
6590
+ style: {
6591
+ margin: 0,
6592
+ fontSize: "14px",
6593
+ fontWeight: 600,
6594
+ overflow: "hidden",
6595
+ textOverflow: "ellipsis",
6596
+ whiteSpace: "nowrap",
6597
+ maxWidth: "520px",
6598
+ },
6599
+ title: headerTitle + " #" + runId,
6600
+ }, headerTitle + " #" + runId), badgeEl(data ? data.conclusion : undefined, data ? data.status : undefined), data && Number(data.attempt) > 1
6601
+ ? h("span", { className: "dsh-github-badge dsh-badge-yellow", style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 } }, "第 " + data.attempt + " 次尝试")
6602
+ : null), data && (data.displayTitle || repoNameWithOwner)
6603
+ ? h("div", {
6604
+ style: {
6605
+ fontSize: "11px",
6606
+ color: "var(--dsw-alias-label-secondary, #656d76)",
6607
+ display: "flex",
6608
+ gap: "6px",
6609
+ alignItems: "center",
6610
+ flexWrap: "wrap",
6611
+ minWidth: 0,
6612
+ },
6613
+ }, data.displayTitle
6614
+ ? h("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, title: data.displayTitle }, data.displayTitle)
6615
+ : null, repoNameWithOwner
6616
+ ? h("span", { style: { fontFamily: "monospace", opacity: 0.85, flexShrink: 0 } }, repoNameWithOwner)
6617
+ : null)
6618
+ : null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, ghHref
5179
6619
  ? h("a", {
5180
- href: data.url,
6620
+ href: ghHref,
5181
6621
  target: "_blank",
5182
6622
  rel: "noopener noreferrer",
5183
- className: "dsh-github-btn-sm",
5184
- style: { padding: "4px 8px", fontSize: "11px", textDecoration: "none", color: "inherit" },
5185
- title: "在系统默认浏览器中打开该 Issue",
5186
- }, "↗ 浏览器打开")
6623
+ className: "dsh-github-pr-gh-btn",
6624
+ title: "在 GitHub 中打开该次运行",
6625
+ }, "GitHub ↗")
5187
6626
  : null, h("button", {
5188
6627
  type: "button",
5189
- "aria-label": "关闭 (Esc)",
6628
+ "aria-label": "关闭",
6629
+ title: "关闭 (Esc)",
5190
6630
  style: {
5191
6631
  background: "none",
5192
6632
  border: "none",
@@ -5196,36 +6636,40 @@ function IssueDetailModal(props) {
5196
6636
  padding: "0 6px",
5197
6637
  },
5198
6638
  onClick: onClose,
5199
- title: "关闭 (Esc)",
5200
- }, "✕"))),
5201
- // 主体
5202
- loading
5203
- ? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 Issue 详情与讨论…")
6639
+ }, "✕")));
6640
+ var jobs = data && Array.isArray(data.jobs) ? data.jobs : [];
6641
+ var bodyChildren = [];
6642
+ if (data) {
6643
+ if (jobs.length === 0) {
6644
+ bodyChildren.push(h("div", { key: "empty", className: "dsh-github-run-empty" }, "暂无可读 job(可能仍在排队或已跳过)"));
6645
+ }
6646
+ else {
6647
+ jobs.forEach(function (job, i) {
6648
+ bodyChildren.push(renderJob(job, i));
6649
+ });
6650
+ }
6651
+ var logSection = renderLogSection();
6652
+ if (logSection)
6653
+ bodyChildren.push(logSection);
6654
+ }
6655
+ var bodyNode = loading
6656
+ ? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 Actions Run 详情…")
5204
6657
  : error
5205
- ? h("div", {
5206
- style: {
5207
- padding: "24px",
5208
- margin: "16px",
5209
- borderRadius: "8px",
5210
- background: "rgba(248,81,73,0.08)",
5211
- border: "1px solid rgba(248,81,73,0.3)",
5212
- },
5213
- }, h("div", { style: { fontWeight: 600, color: "#f85149", marginBottom: "6px" } }, "⚠️ 读取 Issue 详情失败"), h("div", { style: { fontSize: "12px", opacity: 0.85, lineHeight: 1.6 } }, error), repoNameWithOwner
5214
- ? h("a", {
5215
- href: "https://github.com/" + repoNameWithOwner + "/issues/" + issueNumber,
5216
- target: "_blank",
5217
- rel: "noopener noreferrer",
5218
- className: "dsh-github-btn-sm",
5219
- style: { display: "inline-block", marginTop: "12px", padding: "4px 10px", fontSize: "11px", textDecoration: "none" },
5220
- }, "↗ 在浏览器中查看")
5221
- : null)
5222
- : data
5223
- ? h("div", { className: "dsh-github-issue-conversation-pane" }, metaChips(data), bodyCard(data), h("div", { className: "dsh-github-issue-timeline-title" }, "💬 讨论与评论时间线 (" + (data.comments ? data.comments.length : 0) + ")"), data.comments && data.comments.length > 0
5224
- ? data.comments.map(function (c) {
5225
- return h(IssueCommentCard, { key: c.id || c.createdAt, comment: c });
5226
- })
5227
- : h("div", { className: "dsh-github-issue-empty-timeline" }, "💬 暂无讨论与评论——这是一个还没有人回复的议题。"))
5228
- : null));
6658
+ ? h("div", { className: "dsh-github-run-error" }, h("div", { className: "dsh-github-run-error-title" }, "⚠️ 读取 Actions Run 详情失败"),
6659
+ // 服务端会把 gh 的原始报错透传上来,可能很长,交给 .dsh-github-run-error-text 滚动
6660
+ h("div", { className: "dsh-github-run-error-text" }, error),
6661
+ // 不给「重试」按钮:详情 effect 只依赖 [runId, repoNameWithOwner, cwd],
6662
+ // 三者不变时重建同一份 effect 不会重新发起 fetch,按钮只会空转。
6663
+ // 需要重试的用户可直接用头部的 GitHub 外链核对。
6664
+ h("div", { className: "dsh-github-run-log-hint" }, "可关闭本弹窗后重新打开,或点击右上角「GitHub ↗」直接查看。"))
6665
+ : bodyChildren;
6666
+ return h("div", { className: "dsh-github-modal-overlay", onClick: onClose, style: { zIndex: 100000 } }, h("div", {
6667
+ className: "dsh-github-modal-dialog dsh-github-run-modal-dialog",
6668
+ role: "dialog",
6669
+ "aria-modal": "true",
6670
+ "aria-label": "Actions Run #" + runId + " 详情",
6671
+ onClick: function (e) { e.stopPropagation(); },
6672
+ }, header, data ? renderMeta(data) : null, h("div", { className: "dsh-github-run-body" }, bodyNode)));
5229
6673
  }
5230
6674
 
5231
6675
  // ---- [Bundled: sync-settings-modal.js] ----
@@ -5496,7 +6940,7 @@ function GlobalSyncSettingsModal(props) {
5496
6940
  ? "var(--dsw-alias-state-error-primary, #cf222e)"
5497
6941
  : "var(--dsw-alias-label-secondary, #656d76)",
5498
6942
  },
5499
- }, updateMsg.text) : null)) : activeSection === "about" ? h("div", null, h("div", { style: { display: "flex", alignItems: "center", gap: "10px", marginBottom: "14px" } }, renderOctocatBadge(40), h("div", null, h("div", { style: { fontSize: "15px", fontWeight: 600 } }, "DSH GitHub Flow"), h("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary, #656d76)", marginTop: "2px" } }, "当前版本: v" + (aboutVersion || "…")))), h("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary, #656d76)", marginBottom: "14px", lineHeight: "1.6" } }, "让 DeepSeek Harness Agent 拥有操作 GitHub 的原生手脚。原生复用宿主机 gh CLI 安全凭据,深度打通本地 Git 工作区与云端协作,提供 18 大领域 Agent 工具、人类 Slash 指令、Web 全景驾驶舱与一键跨平台升级。"), h("div", { style: { display: "flex", flexDirection: "column", gap: "8px", marginBottom: "14px" } }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px" } }, h("span", { style: { minWidth: "64px", color: "var(--dsw-alias-label-secondary, #656d76)" } }, "仓库地址"), h("a", { href: "https://github.com/ItQianChen/dsh-github-flow", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-link, #0969da)" } }, "github.com/ItQianChen/dsh-github-flow")), h("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px" } }, h("span", { style: { minWidth: "64px", color: "var(--dsw-alias-label-secondary, #656d76)" } }, "npm 包"), h("a", { href: "https://www.npmjs.com/package/dsh-github-flow", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-link, #0969da)" } }, "npmjs.com/package/dsh-github-flow")), h("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px" } }, h("span", { style: { minWidth: "64px", color: "var(--dsw-alias-label-secondary, #656d76)" } }, "开源协议"), h("a", { href: "https://github.com/ItQianChen/dsh-github-flow/blob/master/LICENSE", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-link, #0969da)" } }, "MIT License"))), h("div", {
6943
+ }, updateMsg.text) : null)) : activeSection === "about" ? h("div", null, h("div", { style: { display: "flex", alignItems: "center", gap: "10px", marginBottom: "14px" } }, renderOctocatBadge(40), h("div", null, h("div", { style: { fontSize: "15px", fontWeight: 600 } }, "DSH GitHub Flow"), h("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary, #656d76)", marginTop: "2px" } }, "当前版本: v" + (aboutVersion || "…")))), h("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary, #656d76)", marginBottom: "14px", lineHeight: "1.6" } }, "让 DeepSeek Harness Agent 拥有操作 GitHub 的原生手脚。原生复用宿主机 gh CLI 安全凭据,深度打通本地 Git 工作区与云端协作,提供 19 大领域 Agent 工具、人类 Slash 指令、Web 全景驾驶舱与一键跨平台升级。"), h("div", { style: { display: "flex", flexDirection: "column", gap: "8px", marginBottom: "14px" } }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px" } }, h("span", { style: { minWidth: "64px", color: "var(--dsw-alias-label-secondary, #656d76)" } }, "仓库地址"), h("a", { href: "https://github.com/ItQianChen/dsh-github-flow", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-link, #0969da)" } }, "github.com/ItQianChen/dsh-github-flow")), h("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px" } }, h("span", { style: { minWidth: "64px", color: "var(--dsw-alias-label-secondary, #656d76)" } }, "npm 包"), h("a", { href: "https://www.npmjs.com/package/dsh-github-flow", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-link, #0969da)" } }, "npmjs.com/package/dsh-github-flow")), h("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px" } }, h("span", { style: { minWidth: "64px", color: "var(--dsw-alias-label-secondary, #656d76)" } }, "开源协议"), h("a", { href: "https://github.com/ItQianChen/dsh-github-flow/blob/master/LICENSE", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-link, #0969da)" } }, "MIT License"))), h("div", {
5500
6944
  className: "dsh-github-about-star-box",
5501
6945
  style: {
5502
6946
  display: "flex",
@@ -5593,7 +7037,7 @@ function GlobalSyncSettingsModal(props) {
5593
7037
  *
5594
7038
  * 【内容与代码同步的纪律】
5595
7039
  * 本文件里出现的工具数量、action 数量、Tab 名称、按钮文案,全部以当前代码为准:
5596
- * 工具矩阵 = src/main.ts 的 18 处 ctx.tools.register;
7040
+ * 工具矩阵 = src/main.ts 的 19 处 ctx.tools.register;
5597
7041
  * Tab 布局 = src/client.ts 的 TAB_DEFS 与 src/ui/workspace-panel.ts 的四个标签;
5598
7042
  * 若后续增删工具或 Tab,必须同步改这里,否则用户会照着过时说明去找不存在的入口。
5599
7043
  */
@@ -5632,7 +7076,7 @@ function GithubHelpModal(props) {
5632
7076
  { id: "quickstart", label: "🚀 3分钟极速上手" },
5633
7077
  { id: "prompts", label: "🤖 Agent 提示词实战" },
5634
7078
  { id: "panels", label: "🖥️ 两大界面操作图解" },
5635
- { id: "tools", label: "🧰 18 大工具速查" },
7079
+ { id: "tools", label: "🧰 19 大工具速查" },
5636
7080
  { id: "faq", label: "🔑 常见自愈排错" },
5637
7081
  ];
5638
7082
  // 2. 渲染各 Tab 独立视图
@@ -5661,7 +7105,7 @@ function GithubHelpModal(props) {
5661
7105
  return h("div", { key: i, className: "dsh-github-help-card", style: { display: "flex", gap: "14px", alignItems: "flex-start" } }, h("div", { className: "dsh-github-help-step-badge" }, s.step), h("div", { style: { flex: 1 } }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "4px" } }, s.title), h("div", { className: "dsh-github-help-desc", style: { marginBottom: "6px" } }, s.desc), h("div", { style: { fontSize: "12px", lineHeight: "1.6", whiteSpace: "pre-line", color: "inherit" } }, s.actionText)));
5662
7106
  }),
5663
7107
  // 能力总览:让用户三秒钟知道这插件到底能干什么,而不是去翻 README
5664
- h("div", { className: "dsh-github-help-card", style: { marginTop: "4px" } }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "8px" } }, "📦 能力总览:18 大工具 · 103 个业务动作 · 2 套可视化工作台"), h("div", { style: { fontSize: "12px", lineHeight: "1.8", color: "var(--dsw-alias-label-secondary, #8b949e)" } }, "Agent 侧可直接调用 18 个 github_* 工具(PR 审查合并、Issue 任务流转、Actions 排障、免 clone 读文件、发版交付、Secrets 管理、Skills 生态、标签与规则集、构建缓存、Gist、跨仓待办大盘、Copilot 智能体、Discussions、Projects v2、Codespaces、运行时遥测与元指南)。", h("br"), "人类侧在每个会话右侧栏拥有 5 个 Tab(Git / 未提交 / PR / Issues / Actions),在最左侧 Octocat 入口拥有 4 个标签的全景驾驶舱。")));
7108
+ h("div", { className: "dsh-github-help-card", style: { marginTop: "4px" } }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "8px" } }, "📦 能力总览:19 大工具 · 106 个业务动作 · 2 套可视化工作台"), h("div", { style: { fontSize: "12px", lineHeight: "1.8", color: "var(--dsw-alias-label-secondary, #8b949e)" } }, "Agent 侧可直接调用 19 个 github_* 工具(PR 审查合并、Issue 任务流转、Actions 排障、免 clone 读文件、发版交付、Secrets 管理、Skills 生态、标签与规则集、构建缓存、Gist、跨仓待办大盘、Copilot 智能体、Discussions、Projects v2、Codespaces、运行时遥测与元指南)。", h("br"), "人类侧在每个会话右侧栏拥有 5 个 Tab(Git / 未提交 / PR / Issues / Actions),在最左侧 Octocat 入口拥有 4 个标签的全景驾驶舱。PR / Issue / Actions 的卡片点击都能在线查看详情——Actions 看到的是 job / step 树与失败步骤,失败时还能按需拉一段日志,不用切浏览器。")));
5665
7109
  }
5666
7110
  if (activeTab === "prompts") {
5667
7111
  var promptCards = [
@@ -5675,7 +7119,7 @@ function GithubHelpModal(props) {
5675
7119
  title: "🚨 场景二:CI / Actions 门禁报错秒级排查与自动修复",
5676
7120
  desc: "构建挂了不用在网页反复翻几十页日志,直接吩咐 Agent 定位报错步骤并就地改代码。",
5677
7121
  prompt: "检查当前仓库最近一次 CI 失败的原因,精确定位报错步骤并帮我尝试修复代码",
5678
- aiAction: "Agent 自动调用失败日志探测工具,秒级提取具体挂掉的构建步骤输出,精准定位语法/单测异常并就地修改本地代码。",
7122
+ aiAction: "Agent 会调 github_task(action:'diagnose_ci') 或 github_run(action:'log_failed'):先定位失败 job/step,再按框架锚点萃取日志(pytest / node / Go / tsc / npm),最后汇总疑似相关文件。日志被截断时它会明确告诉你「总数未知」,不要当成「就这些失败」。",
5679
7123
  },
5680
7124
  {
5681
7125
  title: "📌 场景三:早会与每日任务待办盘点",
@@ -5743,21 +7187,25 @@ function GithubHelpModal(props) {
5743
7187
  title: "2. 会话右侧栏面板 (Sidebar Panel) —— 深度协同当前工作区 · 5 个 Tab",
5744
7188
  items: [
5745
7189
  "打开入口:位于每个会话工作区的右侧抽屉栏,深度锁定当前会话的物理 Git 仓库;",
5746
- "仓库身份与切换:顶部展示当前关联仓库、Fork 血缘行与权限徽章(只读/可写);若配置了多个 remote,可通过 RepoSelectorDropdown 下拉一键切换基准仓库;", "Git Tab:三段式提交时间线(待推送 / 待拉取 / 最近),默认 15 条、下滑触底自动加载;点任意提交行弹出完整 Commit 详情;",
5747
- "未提交 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 中打开源码);",
5748
7192
  "PR Tab:展示全部 PR 并按状态过滤;带三态 CI 门禁、冲突检测、审核判定与 Draft 标示;点击卡片在线唤起 PR 审阅工作台(描述 / 评论 / 文件 Diff 双栏比对);",
5749
7193
  "Issues Tab:展示全部 Issue 并按状态过滤;点击卡片在线查看详情(Markdown 正文 / 评论时间线 / 标签色块 / 里程碑 / 关联 PR / 隐藏评论标注);",
5750
- "Actions Tab:最近工作流运行记录与触发方式,失败时给出可直接交给 Agent 的诊断指引;",
7194
+ "Actions Tab:最近工作流运行记录与触发方式;点击卡片在线查看本次运行的 job / step 树(每步结论与耗时,失败步骤高亮,默认展开第一个失败 job),失败时可按需提取日志锚点片段(一次最多 3 个失败 job、服务端批次预算 90 秒);",
5751
7195
  "动静解耦架构:修改本地代码只定向查询本地 .git 核心元数据(耗时仅 5ms),绝对不发起远端网络请求,杜绝网络风暴。",
5752
7196
  ],
5753
7197
  },
5754
7198
  {
5755
- title: "3. 两个弹窗的通用操作习惯",
7199
+ title: "3. 详情弹窗的通用操作习惯",
5756
7200
  items: [
5757
- "PR / Issue 弹窗都支持:按 Esc 或点击遮罩关闭、右上角「📋 复制链接」、「↗ 浏览器打开」直达 GitHub;",
7201
+ "PR / Issue / Actions 三个弹窗都支持:按 Esc 或点击遮罩关闭、右上角「GitHub ↗」直达官方页面;",
7202
+ "文件 Diff 弹窗可以叠在提交详情之上:Esc 或点遮罩先关 Diff,再按一次才关提交详情;弹窗内可「📋 复制 Diff」原文或「📄 在 DSH 中打开」查看文件源码;",
7203
+ "Diff 弹窗对正文里的 `---` / `+++` 不会误判成文件头(按 hunk 解析),重命名文件按新路径展示、二进制文件给明确提示;",
5758
7204
  "列表卡片上的「GitHub ↗」按钮是独立的浏览器外跳入口,点它不会触发弹窗(已做事件隔离);",
5759
7205
  "PR 弹窗的文件区可拖拽分隔条调宽、双击或点 ◀ 折叠;布局偏好会记在本地,下次打开保持原样;",
5760
- "长评论默认折叠(显示「⌄ 展开全部」),PR/Issue 正文默认展开——避免一屏被一段超长文字占满。",
7206
+ "长评论默认折叠(显示「⌄ 展开全部」),PR/Issue 正文默认展开——避免一屏被一段超长文字占满;",
7207
+ "Actions 弹窗按 job 折叠/展开步骤,默认展开第一个失败 job;「📄 提取失败日志」只在存在失败时出现,点一次拉一次(慢链路不做自动重试,失败可手动重试);",
7208
+ "日志片段是按失败锚点萃取过的——看到的是「失败块 + 上下文」或尾部 200 行,不是完整日志;要全文请点「GitHub ↗」。",
5761
7209
  ],
5762
7210
  },
5763
7211
  ].map(function (sec, idx) {
@@ -5771,7 +7219,7 @@ function GithubHelpModal(props) {
5771
7219
  items: [
5772
7220
  { name: "github_pr", desc: "PR 全生命周期:列表/详情/diff/checks/create/merge/review/ready/update_branch/revert/preview/checkout,含行级评论与附件上传" },
5773
7221
  { name: "github_issue", desc: "Issue 任务流转:list/view/create/comment/edit/close/reopen/develop/transfer/pin/lock/delete/status,含截图查看 view_images" },
5774
- { name: "github_run", desc: "Actions 排障:list/view/log_failed(失败步骤日志秒级提取)/rerun/cancel/trigger/download 与 workflow 启停" },
7222
+ { name: "github_run", desc: "Actions 排障:list、view(run + job/step 树)、log_failed(失败步骤日志秒级提取)/rerun/cancel/trigger/download 与 workflow 启停" },
5775
7223
  { name: "github_repo", desc: "仓库元数据、免 clone 读文件(read_file/read_dir)、跨仓检索(code/issues/prs/commits)、Fork 同步" },
5776
7224
  ],
5777
7225
  },
@@ -5805,7 +7253,7 @@ function GithubHelpModal(props) {
5805
7253
  ],
5806
7254
  },
5807
7255
  ];
5808
- return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "14px" } }, "18 个 Agent 工具、合计 103 个业务动作(不含元指南的 21 个查询主题与 api 逃生门的 4 个 HTTP method)。你不必记住工具名——直接用自然语言描述目标,Agent 会自己挑工具。下表用于你了解能力边界,以及排查「为什么 Agent 说做不到」:"), toolGroups.map(function (g, gi) {
7256
+ return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "14px" } }, "19 个 Agent 工具、合计 106 个业务动作(不含元指南的 22 个查询主题与 api 逃生门的 4 个 HTTP method)。你不必记住工具名——直接用自然语言描述目标,Agent 会自己挑工具。下表用于你了解能力边界,以及排查「为什么 Agent 说做不到」:"), toolGroups.map(function (g, gi) {
5809
7257
  return h("div", { key: gi, className: "dsh-github-help-card" }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "8px" } }, g.group), g.items.map(function (t, ti) {
5810
7258
  return h("div", { key: ti, style: { display: "flex", gap: "10px", alignItems: "flex-start", padding: "5px 0", borderTop: ti === 0 ? "none" : "1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.06))" } }, h("code", {
5811
7259
  style: {
@@ -5845,6 +7293,14 @@ function GithubHelpModal(props) {
5845
7293
  q: "Q6: 为什么改了代码,插件界面要重启 DSH 才生效?",
5846
7294
  a: "如果你是在开发这个插件本身:后端 HTTP 路由是在插件 `apply()` 时一次性注册到宿主的 WebServer 上的,`pnpm run build` 只更新磁盘产物,不会给已运行的进程热注册新路由。改完代码需重启 DSH 才能挂上新端点。\n若只是普通使用,请忽略此条——正常升级走一键升级即可。",
5847
7295
  },
7296
+ {
7297
+ q: "Q7: Actions 弹窗里点「提取失败日志」很久没反应,或报超时?",
7298
+ a: "GitHub 的日志接口是「先打包再下载」,实测单次 4–40 秒,走代理时还可能被直接重置——这是上游链路特性,不是插件卡死。当前实现有三道闸:\n- 服务端单 job 上限 60 秒、整批总预算 90 秒;超预算的 job 会明确列出「未拉取,可单独重试」,不会静默丢掉;\n- 客户端 100 秒自动中止,避免按钮永远停在「正在下载日志…」;\n- 失败后弹窗内给「🔁 重试」,不做自动重试(重试会把等待时间叠加)。\n急的话点右上角「GitHub ↗」看完整日志,或让 Agent 调 `github_task(action:'diagnose_ci', run_id:'…')` 直接拿结论包。",
7299
+ },
7300
+ {
7301
+ q: "Q8: 为什么 Actions 弹窗只能看 3 个失败 job 的日志?",
7302
+ a: "一次点击串行拉 3 个 job 已经是 60–90 秒量级;矩阵构建下失败 job 可能有十几个,全拉会把一次点击变成几分钟。当前策略:只取前 3 个失败 job,并在按钮下方明示「共 N 个,其余请到 GitHub 查看」。需要逐个深挖时,让 Agent 用 `github_run(action:'log_failed', run_id:'…')` 或 `github_task(action:'diagnose_ci')` 分批处理更合适。",
7303
+ },
5848
7304
  ];
5849
7305
  return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "14px" } }, "常见环境、权限与网络问题的排查与自愈指引:"), faqs.map(function (f, idx) {
5850
7306
  return h("div", { key: idx, className: "dsh-github-help-card" }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "6px", fontSize: "13px" } }, f.q), h("div", { className: "dsh-github-help-desc", style: { lineHeight: "1.6", whiteSpace: "pre-line" } }, f.a));
@@ -5907,233 +7363,8 @@ function GithubHelpModal(props) {
5907
7363
  btn.setAttribute("aria-selected", active ? "true" : "false");
5908
7364
  });
5909
7365
  }
5910
- /**
5911
- * 未提交文件原生二级 Git Diff 下钻视图组件(右侧栏内就地渲染,零模态遮罩)
5912
- */
5913
- function UncommittedDiffView(props) {
5914
- var f = props.file;
5915
- var h = React.createElement;
5916
- var [diffData, setDiffData] = React.useState(null);
5917
- var [loading, setLoading] = React.useState(true);
5918
- var [error, setError] = React.useState(null);
5919
- var [copied, setCopied] = React.useState(false);
5920
- React.useEffect(function () {
5921
- var onKeyDown = function (e) {
5922
- if (e.key === "Escape" || e.keyCode === 27) {
5923
- props.onBack();
5924
- }
5925
- };
5926
- window.addEventListener("keydown", onKeyDown);
5927
- return function () { window.removeEventListener("keydown", onKeyDown); };
5928
- }, [props.onBack]);
5929
- React.useEffect(function () {
5930
- setLoading(true);
5931
- setError(null);
5932
- setDiffData(null);
5933
- var fetchDiff = async function () {
5934
- var primaryUrl = "/api/github/file-diff?file=" + encodeURIComponent(f.path) + (props.cwd ? "&cwd=" + encodeURIComponent(props.cwd) : "");
5935
- var tryParseResponse = async function (res) {
5936
- var text = await res.text().catch(function () { return ""; });
5937
- var json = null;
5938
- if (text && (text.trim().startsWith("{") || text.trim().startsWith("["))) {
5939
- try {
5940
- json = JSON.parse(text);
5941
- }
5942
- catch (e) { }
5943
- }
5944
- return { ok: res.ok, status: res.status, text: text, json: json };
5945
- };
5946
- try {
5947
- var r1 = await fetch(primaryUrl);
5948
- var p1 = await tryParseResponse(r1);
5949
- if (p1.ok && p1.json && p1.json.ok && p1.json.data) {
5950
- setDiffData(p1.json.data);
5951
- setLoading(false);
5952
- return;
5953
- }
5954
- // 若主路由遇到 404 (后端常驻进程未重启),自动降级请求常驻的 /api/github/action 端点
5955
- if (p1.status === 404) {
5956
- var r2 = await fetch("/api/github/action", {
5957
- method: "POST",
5958
- headers: { "Content-Type": "application/json" },
5959
- body: JSON.stringify({
5960
- action: "get_file_diff",
5961
- file: f.path,
5962
- cwd: props.cwd || undefined,
5963
- }),
5964
- }).catch(function () { return null; });
5965
- if (r2) {
5966
- var p2 = await tryParseResponse(r2);
5967
- if (p2.ok && p2.json && p2.json.data) {
5968
- setDiffData(p2.json.data);
5969
- setLoading(false);
5970
- return;
5971
- }
5972
- }
5973
- setLoading(false);
5974
- setError("后端对比接口未激活 (404)。请重启 DSH 桌面主程序以载入最新对比引擎;您也可点击上方「在 DSH 中打开」直接预览文件源码。");
5975
- return;
5976
- }
5977
- setLoading(false);
5978
- setError(p1.json?.error || p1.text || ("查询差异失败 (HTTP " + p1.status + ")"));
5979
- }
5980
- catch (err) {
5981
- setLoading(false);
5982
- setError(err?.message || "网络请求异常");
5983
- }
5984
- };
5985
- fetchDiff();
5986
- }, [f.path, props.cwd]);
5987
- var copyDiff = function () {
5988
- if (!diffData?.diff)
5989
- return;
5990
- if (navigator?.clipboard?.writeText) {
5991
- navigator.clipboard.writeText(diffData.diff).then(function () {
5992
- setCopied(true);
5993
- setTimeout(function () { setCopied(false); }, 2000);
5994
- }).catch(function () { });
5995
- }
5996
- };
5997
- var statusColor = f.status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
5998
- : f.status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
5999
- : "var(--dsw-alias-state-attention-primary, #9a6700)";
6000
- var statusText = f.status === "??" ? "未跟踪新增"
6001
- : f.status.indexOf("D") >= 0 ? "已删除"
6002
- : f.status.indexOf("R") >= 0 ? "已重命名"
6003
- : "已修改";
6004
- return h("div", {
6005
- className: "dsh-github-inline-diff-view",
6006
- style: {
6007
- display: "flex",
6008
- flexDirection: "column",
6009
- gap: "8px",
6010
- minHeight: "260px",
6011
- padding: "4px 0",
6012
- },
6013
- },
6014
- // 顶部操作与状态栏
6015
- h("div", {
6016
- style: {
6017
- display: "flex",
6018
- alignItems: "center",
6019
- justifyContent: "space-between",
6020
- flexWrap: "wrap",
6021
- gap: "6px",
6022
- paddingBottom: "8px",
6023
- borderBottom: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.1))",
6024
- },
6025
- }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, h("button", {
6026
- className: "dsh-github-btn-sm",
6027
- style: { padding: "3px 8px", cursor: "pointer", fontWeight: 600, fontSize: "11px" },
6028
- onClick: props.onBack,
6029
- title: "返回未提交文件清单 (Esc)",
6030
- }, "← 返回列表"), h("span", {
6031
- className: "dsh-github-badge",
6032
- style: {
6033
- backgroundColor: statusColor + "22",
6034
- color: statusColor,
6035
- fontWeight: 600,
6036
- fontSize: "11px",
6037
- },
6038
- }, `${f.status} ${statusText}`), diffData && diffData.additions !== undefined
6039
- ? 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}`))
6040
- : null), h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, diffData?.diff
6041
- ? h("button", {
6042
- className: "dsh-github-btn-sm",
6043
- style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
6044
- onClick: copyDiff,
6045
- title: "复制 Unified Diff 原文",
6046
- }, copied ? "✓ 已复制" : "📋 复制 Diff")
6047
- : null, props.onOpenInDsh
6048
- ? h("button", {
6049
- className: "dsh-github-btn-sm",
6050
- style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
6051
- onClick: function () {
6052
- if (props.onOpenInDsh)
6053
- props.onOpenInDsh(f.path);
6054
- },
6055
- title: "在 DSH 侧边栏打开查看该文件完整源码",
6056
- }, "📄 在 DSH 中打开")
6057
- : null)),
6058
- // 文件路径栏
6059
- h("div", {
6060
- style: {
6061
- fontSize: "11px",
6062
- fontFamily: "monospace",
6063
- color: "var(--dsw-alias-label-secondary, #656d76)",
6064
- wordBreak: "break-all",
6065
- padding: "3px 6px",
6066
- backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
6067
- borderRadius: "4px",
6068
- },
6069
- title: f.path,
6070
- }, f.path),
6071
- // Diff 主体内容
6072
- loading
6073
- ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.65, fontSize: "12px" } }, "⏳ 正在计算 Git 代码比对差异...")
6074
- : error
6075
- ? h("div", {
6076
- style: {
6077
- padding: "16px",
6078
- color: "var(--dsw-alias-label-primary, #1f2328)",
6079
- fontSize: "12px",
6080
- lineHeight: "1.6",
6081
- backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
6082
- borderRadius: "6px",
6083
- },
6084
- }, 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 中打开」直接预览文件源码。"))
6085
- : diffData?.isBinary
6086
- ? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.8, fontSize: "12px" } }, "📦 该文件为二进制格式(图片或编译产物),无法直接展示文本差异。")
6087
- : h("div", {
6088
- className: "dsh-github-inline-diff-content",
6089
- style: {
6090
- margin: 0,
6091
- padding: "4px 0",
6092
- fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
6093
- fontSize: "11px",
6094
- lineHeight: "1.5",
6095
- overflowX: "auto",
6096
- maxHeight: "68vh",
6097
- borderRadius: "6px",
6098
- border: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))",
6099
- backgroundColor: "var(--dsw-alias-bg-canvas, #ffffff)",
6100
- },
6101
- }, (diffData?.diff || "").split("\n").map(function (line, idx) {
6102
- var isAdd = line.startsWith("+") && !line.startsWith("+++");
6103
- var isDel = line.startsWith("-") && !line.startsWith("---");
6104
- var isHunk = line.startsWith("@@");
6105
- var isHeader = line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++");
6106
- var lineStyle = {
6107
- display: "flex",
6108
- padding: "0 8px",
6109
- whiteSpace: "pre",
6110
- minWidth: "100%",
6111
- boxSizing: "border-box",
6112
- };
6113
- if (isAdd) {
6114
- lineStyle.backgroundColor = "var(--dsw-alias-state-success-subtle, rgba(46,160,67,0.15))";
6115
- lineStyle.color = "var(--dsw-alias-state-success-primary, #1a7f37)";
6116
- }
6117
- else if (isDel) {
6118
- lineStyle.backgroundColor = "var(--dsw-alias-state-error-subtle, rgba(248,81,73,0.15))";
6119
- lineStyle.color = "var(--dsw-alias-state-error-primary, #cf222e)";
6120
- }
6121
- else if (isHunk) {
6122
- lineStyle.backgroundColor = "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.04))";
6123
- lineStyle.color = "var(--dsw-alias-label-secondary, #656d76)";
6124
- lineStyle.fontWeight = 600;
6125
- lineStyle.margin = "2px 0";
6126
- }
6127
- else if (isHeader) {
6128
- lineStyle.color = "var(--dsw-alias-label-muted, #8c959f)";
6129
- lineStyle.fontSize = "10px";
6130
- }
6131
- else {
6132
- lineStyle.color = "var(--dsw-alias-label-primary, #1f2328)";
6133
- }
6134
- return h("div", { key: idx, style: lineStyle }, line || " ");
6135
- })));
6136
- }
7366
+ /** 列表分页步长:与服务端 parseListLimit 的默认页大小保持一致(15) */
7367
+ var OVERVIEW_PAGE_SIZE = 15;
6137
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,
6138
7369
  // 必须追加在形参末尾:该函数有 17 个位置参数,插在中间会静默错位
6139
7370
  onSelectIssue = null,
@@ -6143,12 +7374,80 @@ function GithubHelpModal(props) {
6143
7374
  * 普通函数,内部调用 useState 会被 React 当成新的 Hook 序列直接抛错。
6144
7375
  * 状态必须住在 GithubRightbarBody 组件体内,这里只接收值与 setter。
6145
7376
  */
6146
- prStateFilter = "all", issueStateFilter = "all", onChangePrFilter = null, onChangeIssueFilter = null) {
7377
+ prStateFilter = "all", issueStateFilter = "all", onChangePrFilter = null, onChangeIssueFilter = null,
7378
+ /**
7379
+ * PR / Issue / Actions 三个列表的「加载更多」回调(按 tab 独立触发)。
7380
+ */
7381
+ onLoadMoreLists = null,
7382
+ /**
7383
+ * 各 Tab 的「正在加载更多」标记。
7384
+ *
7385
+ * 为什么需要它:请求往返几百毫秒到数秒,页脚不变化的话用户会以为点击没生效而连点,
7386
+ * 触发重复请求(服务端虽然有 loadingMoreListsRef 兜底,但界面必须有反馈)。
7387
+ */
7388
+ listLoading = null,
7389
+ /**
7390
+ * Actions 卡片点击 → Run 详情弹窗。
7391
+ * 同样必须追加在形参末尾(理由见上一条:位置参数错位不会报错,只会静默串值)。
7392
+ */
7393
+ onSelectRun = null) {
6147
7394
  var auth = data ? data.auth : null;
6148
7395
  var repo = data ? data.repo : null;
6149
7396
  var prs = (data && data.pullRequests) || [];
6150
7397
  var issues = (data && data.issues) || [];
6151
7398
  var runs = (data && data.runs) || [];
7399
+ /**
7400
+ * 列表「加载更多」页脚:与「未提交变更 / Git 提交记录」两个已有分页同视觉同交互。
7401
+ * 触发条件是服务端下发的 `*Truncated`(= 本次拉满,可能还有更早记录),不是本地条数比较——
7402
+ * 本地比不出「还有没有」。
7403
+ */
7404
+ /**
7405
+ * 列表「加载更多」页脚:与「未提交变更 / Git 提交记录」两个已有分页同视觉同交互。
7406
+ * 触发条件是服务端下发的 `*Truncated`(= 本次拉满,可能还有更早记录),不是本地条数比较——
7407
+ * 本地比不出「还有没有」。
7408
+ *
7409
+ * `loading` 为真时切换成「正在加载中」并禁用点击:请求往返可能几百毫秒到数秒,
7410
+ * 不反馈的话用户会以为点击没生效而连点,触发重复请求。
7411
+ */
7412
+ var renderListMore = function (kind, hasMore, count, loading) {
7413
+ if (!hasMore || typeof onLoadMoreLists !== "function")
7414
+ return null;
7415
+ return h("div", {
7416
+ key: "load-more-" + kind,
7417
+ style: {
7418
+ fontSize: "11px",
7419
+ opacity: loading ? 1 : 0.8,
7420
+ padding: "8px 0",
7421
+ textAlign: "center",
7422
+ cursor: loading ? "default" : "pointer",
7423
+ },
7424
+ onClick: function () { if (!loading)
7425
+ onLoadMoreLists(kind); },
7426
+ title: loading ? "正在加载更多…" : "点击加载更多(滚动到底部也会自动加载)",
7427
+ "aria-busy": loading ? "true" : "false",
7428
+ }, loading
7429
+ ? "⏳ 正在加载中…"
7430
+ : "⬇️ 下滑或点击加载更多(已显示 " + count + " 条)");
7431
+ };
7432
+ /**
7433
+ * 列表「到底了」终态提示。
7434
+ *
7435
+ * 只在「服务端说不能再加载了,但仓库里确实还有更早记录」时出现——
7436
+ * 触发条件是 `parseListLimit` 的 100 条硬上限被顶满(或搜索索引与列表口径不一致)。
7437
+ * 此时若什么都不说,用户看到「已加载 100 / 共 323」却没有入口,会以为界面坏了;
7438
+ * 说清楚「更早记录请到 GitHub 查看」才是诚实的收尾。
7439
+ */
7440
+ var renderListEnd = function (kind, hasMore, count, total) {
7441
+ if (hasMore)
7442
+ return null;
7443
+ if (typeof total !== "number" || count >= total)
7444
+ return null;
7445
+ return h("div", {
7446
+ key: "list-end-" + kind,
7447
+ style: { fontSize: "11px", opacity: 0.6, padding: "8px 0", textAlign: "center" },
7448
+ title: "单次最多加载 100 条,更早的记录请在 GitHub 查看",
7449
+ }, "已加载 " + count + " / 共 " + total + " 条,更早记录请到 GitHub 查看");
7450
+ };
6152
7451
  var effectiveUpdateInfo = updateInfo || data?.updateInfo;
6153
7452
  var upgradeBannerEl = effectiveUpdateInfo && effectiveUpdateInfo.hasUpdate
6154
7453
  ? h(UpgradeNotificationCard, { updateInfo: effectiveUpdateInfo, isRightbar })
@@ -6465,69 +7764,71 @@ function GithubHelpModal(props) {
6465
7764
  var dirtyHeaderTitle = gitSt && totalDirtyCount > displayedDirty.length
6466
7765
  ? `📁 未提交清单 (${displayedDirty.length} / ${totalDirtyCount})`
6467
7766
  : `📁 未提交清单 (${gitSt ? gitSt.dirtyCount : 0})`;
6468
- var dirtySectionEl = activeDiffFile
6469
- ? 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, {
6470
7826
  file: activeDiffFile,
6471
- onBack: onCloseDiff || (() => { }),
6472
7827
  cwd: cwd,
7828
+ onClose: onCloseDiff || (() => { }),
6473
7829
  onOpenInDsh: onOpenInDsh,
6474
7830
  })
6475
- : h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, dirtyHeaderTitle)), !gitSt || gitSt.dirtyCount === 0
6476
- ? h("div", { className: "dsh-github-empty" }, "工作区干净,没有未提交的变更 ✓")
6477
- : h("div", { style: { fontSize: "12px", lineHeight: "1.9" } }, displayedDirty.map(function (f, i) {
6478
- var statusColor = f.status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
6479
- : f.status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
6480
- : f.status.indexOf("M") >= 0 || f.status.indexOf("R") >= 0 ? "var(--dsw-alias-state-attention-primary, #9a6700)"
6481
- : "inherit";
6482
- var statusText = f.status === "??" ? "未跟踪新增"
6483
- : f.status.indexOf("D") >= 0 ? "已删除"
6484
- : f.status.indexOf("R") >= 0 ? "已重命名"
6485
- : "已修改";
6486
- return h("div", {
6487
- key: f.path || i,
6488
- className: "dsh-github-dirty-item",
6489
- title: `${f.path} (${statusText} [${f.status}])\n点击查看代码改动对比 (Unified Diff)`,
6490
- onClick: function () {
6491
- if (typeof onSelectDiffFile === "function") {
6492
- onSelectDiffFile(f);
6493
- }
6494
- },
6495
- }, h("span", {
6496
- style: {
6497
- minWidth: "24px",
6498
- textAlign: "center",
6499
- opacity: 0.85,
6500
- fontSize: "11px",
6501
- fontFamily: "monospace",
6502
- color: statusColor,
6503
- fontWeight: 600,
6504
- },
6505
- }, f.status === "??" ? "??" : f.status), h("span", { style: { wordBreak: "break-all", flex: 1, fontSize: "12px" } }, f.path));
6506
- }), hasMoreDirtyToLoad
6507
- ? h("div", { style: { marginTop: "10px", textAlign: "center" } }, dirtyPagination?.loadingMore
6508
- ? h("div", { style: { fontSize: "11px", opacity: 0.75, padding: "6px 0" } }, "⏳ 正在加载更多未提交变更…")
6509
- : h("div", {
6510
- style: {
6511
- fontSize: "11px",
6512
- opacity: 0.7,
6513
- padding: "6px 0",
6514
- display: "flex",
6515
- alignItems: "center",
6516
- justifyContent: "center",
6517
- gap: "6px",
6518
- cursor: "pointer",
6519
- borderRadius: "4px",
6520
- },
6521
- title: "向下滑动滚轮或点击加载更多",
6522
- onClick: function () {
6523
- if (dirtyPagination && typeof dirtyPagination.onLoadMore === "function") {
6524
- dirtyPagination.onLoadMore();
6525
- }
6526
- },
6527
- }, h("span", null, "⬇️ 向下滑动自动加载"), h("span", { style: { fontFamily: "monospace" } }, `(${displayedDirty.length} / ${totalDirtyCount})`)))
6528
- : totalDirtyCount > 15
6529
- ? h("div", { style: { textAlign: "center", fontSize: "11px", opacity: 0.5, padding: "6px 0", marginTop: "8px" } }, `✓ 已加载全部 ${totalDirtyCount} 个未提交变更`)
6530
- : null));
7831
+ : null);
6531
7832
  // 快捷操作栏 (仅在检测到关联仓库时展示,未绑定仓库时不展示虚假按钮)
6532
7833
  var actionsBarEl = repo ? h("div", { className: "dsh-github-quick-actions" }, h("button", {
6533
7834
  className: "dsh-github-action-btn",
@@ -6591,27 +7892,25 @@ function GithubHelpModal(props) {
6591
7892
  // 空态文案要给人看,不能把内部档位键(merged)直接抛给用户
6592
7893
  var PR_STATE_LABEL = { all: "全部", open: "Open", merged: "Merged", closed: "Closed" };
6593
7894
  var prFilterLabel = PR_STATE_LABEL[prStateFilter] || "全部";
6594
- var prPillsEl = h("div", { className: "dsh-github-pills" }, h("button", { className: `dsh-github-pill-btn ${prStateFilter === "all" ? "active" : ""}`, "aria-pressed": prStateFilter === "all" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("all") }, `全部 (${prs.length})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === "open" ? "active" : ""}`, "aria-pressed": prStateFilter === "open" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("open") }, `🟢 Open (${prByState.open})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === "merged" ? "active" : ""}`, "aria-pressed": prStateFilter === "merged" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("merged") }, `🟣 Merged (${prByState.merged})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === "closed" ? "active" : ""}`, "aria-pressed": prStateFilter === "closed" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("closed") }, `⚪ Closed (${prByState.closed})`));
7895
+ /*
7896
+ * 计数口径:**优先用仓库真实总数**(服务端 GraphQL `search.issueCount` 下发),
7897
+ * 只有拿不到总数时才回落到「已加载条数」。
7898
+ *
7899
+ * 为什么不能直接用 prs.length:列表按页拉,prs.length 是「已加载」,
7900
+ * 用户每下滑一次它就从 15 → 30 → 45,看起来像仓库总数在变——这是 bug。
7901
+ */
7902
+ var prTotalCount = typeof data?.prTotal === "number" ? data.prTotal : prs.length;
7903
+ // Actions 总数来自 REST actions/runs.total_count;拿不到才回落已加载条数
7904
+ var runTotalCount = typeof data?.runTotal === "number" ? data.runTotal : runs.length;
7905
+ var prOpenCount = typeof data?.prOpenTotal === "number" ? data.prOpenTotal : prByState.open;
7906
+ var prMergedCount = typeof data?.prMergedTotal === "number" ? data.prMergedTotal : prByState.merged;
7907
+ var prClosedCount = typeof data?.prClosedTotal === "number" ? data.prClosedTotal : prByState.closed;
7908
+ var prPillsEl = h("div", { className: "dsh-github-pills" }, h("button", { className: `dsh-github-pill-btn ${prStateFilter === "all" ? "active" : ""}`, "aria-pressed": prStateFilter === "all" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("all") }, `全部 (${prTotalCount})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === "open" ? "active" : ""}`, "aria-pressed": prStateFilter === "open" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("open") }, `🟢 Open (${prOpenCount})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === "merged" ? "active" : ""}`, "aria-pressed": prStateFilter === "merged" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("merged") }, `🟣 Merged (${prMergedCount})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === "closed" ? "active" : ""}`, "aria-pressed": prStateFilter === "closed" ? "true" : "false", onClick: () => onChangePrFilter && onChangePrFilter("closed") }, `⚪ Closed (${prClosedCount})`));
6595
7909
  // 空态必须区分「仓库确实没有」与「当前筛选下没有」——否则用户会把筛空当成数据丢失
6596
7910
  var prEmptyEl = prs.length === 0
6597
7911
  ? h("div", { className: "dsh-github-empty" }, "该仓库暂无 Pull Request 记录")
6598
7912
  : h("div", { className: "dsh-github-empty" }, `当前筛选「${prFilterLabel}」下无结果,共 ${prs.length} 条记录(切换到「全部」查看)`);
6599
- var prSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `🔀 Pull Requests (${filteredPrs.length}/${prs.length})`)), prPillsEl,
6600
- // 触顶提示:让用户知道列表被截断了,而不是以为历史 PR 凭空消失
6601
- data && data.prsTruncated
6602
- ? h("div", {
6603
- style: {
6604
- margin: "0 0 10px 0",
6605
- padding: "6px 10px",
6606
- borderRadius: "6px",
6607
- fontSize: "11px",
6608
- lineHeight: 1.5,
6609
- background: "rgba(210, 153, 34, 0.12)",
6610
- border: "1px solid rgba(210, 153, 34, 0.35)",
6611
- color: "var(--dsw-alias-state-attention-primary, #9a6700)",
6612
- },
6613
- }, `⚠️ 仅显示最近 ${prs.length} 条 PR(已触及检索上限)。更早的记录请到 GitHub 查看。`)
6614
- : null, filteredPrs.length === 0
7913
+ var prSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `🔀 Pull Requests(${prs.length} / 共 ${prTotalCount})`)), prPillsEl, filteredPrs.length === 0
6615
7914
  ? prEmptyEl
6616
7915
  : h("div", { className: "dsh-github-list" }, filteredPrs.map((pr) => {
6617
7916
  var ci = rollupCiStatus(pr.statusCheckRollup);
@@ -6644,25 +7943,28 @@ function GithubHelpModal(props) {
6644
7943
  onClick: (e) => e.stopPropagation(),
6645
7944
  title: "在浏览器打开该 PR(不进入在线审阅)",
6646
7945
  }, "GitHub ↗")), h("div", { className: "dsh-github-item-meta" }, h("span", null, `${pr.headRefName} → ${pr.baseRefName}`),
6647
- // 状态徽章:与主面板保持一致,否则同一 PR 在两处显示不同信息
7946
+ // 状态徽章:与主面板保持一致,否则同一 PR 在两处显示不同信息。
7947
+ // 状态→色调的映射来自 src/shared/state-badges.ts(服务端 CLI 输出用同一份),
7948
+ // tone 直接就是 CSS 后缀,这里不再自己判 state。
6648
7949
  (() => {
6649
- var s = String(pr.state || "").toUpperCase();
6650
- if (s === "MERGED")
6651
- return h("span", { className: "dsh-github-badge dsh-badge-purple" }, "MERGED");
6652
- if (s === "CLOSED")
6653
- return h("span", { className: "dsh-github-badge dsh-badge-red" }, "CLOSED");
6654
- if (s === "OPEN")
6655
- return h("span", { className: "dsh-github-badge dsh-badge-green" }, "OPEN");
6656
- return null;
7950
+ var b = prStateBadge(pr.state);
7951
+ // neutral = 未知状态:旧实现对这些值不渲染徽章,保持一致
7952
+ if (!b || b.tone === "neutral")
7953
+ return null;
7954
+ return h("span", { className: "dsh-github-badge dsh-badge-" + b.tone }, b.label);
6657
7955
  })(), pr.mergeable === "CONFLICTING"
6658
7956
  ? h("span", { className: "dsh-github-badge dsh-badge-red" }, "代码冲突")
6659
- : h("span", { className: "dsh-github-badge dsh-badge-green" }, "可合并"), ci === "fail"
6660
- ? h("span", { className: "dsh-github-badge dsh-badge-red" }, "❌ CI 失败")
6661
- : ci === "pending"
6662
- ? h("span", { className: "dsh-github-badge dsh-badge-yellow" }, "⏳ CI 运行中")
6663
- : ci === "pass"
6664
- ? h("span", { className: "dsh-github-badge dsh-badge-green" }, "✅ CI 通过")
6665
- : null, rv
7957
+ : h("span", { className: "dsh-github-badge dsh-badge-green" }, "可合并"), (() => {
7958
+ // 命中判定保持旧白名单(只有 fail/pending/pass 渲染),
7959
+ // 色调与文案则复用共享映射,避免同一状态两处颜色不一致
7960
+ if (ci !== "fail" && ci !== "pending" && ci !== "pass")
7961
+ return null;
7962
+ var cb = ciBadge(undefined, ci);
7963
+ if (!cb)
7964
+ return null;
7965
+ var text = ci === "fail" ? "❌ CI 失败" : ci === "pending" ? "⏳ CI 运行中" : "✅ CI 通过";
7966
+ return h("span", { className: "dsh-github-badge dsh-badge-" + cb.tone }, text);
7967
+ })(), rv
6666
7968
  ? h("span", { className: `dsh-github-badge dsh-badge-${rv.kind}` }, rv.text)
6667
7969
  : null, relTime ? h("span", null, relTime) : null));
6668
7970
  })));
@@ -6680,30 +7982,21 @@ function GithubHelpModal(props) {
6680
7982
  });
6681
7983
  var ISSUE_STATE_LABEL = { all: "全部", open: "Open", closed: "Closed" };
6682
7984
  var issueFilterLabel = ISSUE_STATE_LABEL[issueStateFilter] || "全部";
6683
- var issuePillsEl = h("div", { className: "dsh-github-pills" }, h("button", { className: `dsh-github-pill-btn ${issueStateFilter === "all" ? "active" : ""}`, "aria-pressed": issueStateFilter === "all" ? "true" : "false", onClick: () => onChangeIssueFilter && onChangeIssueFilter("all") }, `全部 (${issues.length})`), h("button", { className: `dsh-github-pill-btn ${issueStateFilter === "open" ? "active" : ""}`, "aria-pressed": issueStateFilter === "open" ? "true" : "false", onClick: () => onChangeIssueFilter && onChangeIssueFilter("open") }, `🟢 Open (${issueByState.open})`), h("button", { className: `dsh-github-pill-btn ${issueStateFilter === "closed" ? "active" : ""}`, "aria-pressed": issueStateFilter === "closed" ? "true" : "false", onClick: () => onChangeIssueFilter && onChangeIssueFilter("closed") }, `⚪ Closed (${issueByState.closed})`));
7985
+ // Issue 计数同 PR 口径:优先仓库真实总数,拿不到才回落已加载条数
7986
+ var issueTotalCount = typeof data?.issueTotal === "number" ? data.issueTotal : issues.length;
7987
+ var issueOpenCount = typeof data?.issueOpenTotal === "number" ? data.issueOpenTotal : issueByState.open;
7988
+ var issueClosedCount = typeof data?.issueClosedTotal === "number" ? data.issueClosedTotal : issueByState.closed;
7989
+ var issuePillsEl = h("div", { className: "dsh-github-pills" }, h("button", { className: `dsh-github-pill-btn ${issueStateFilter === "all" ? "active" : ""}`, "aria-pressed": issueStateFilter === "all" ? "true" : "false", onClick: () => onChangeIssueFilter && onChangeIssueFilter("all") }, `全部 (${issueTotalCount})`), h("button", { className: `dsh-github-pill-btn ${issueStateFilter === "open" ? "active" : ""}`, "aria-pressed": issueStateFilter === "open" ? "true" : "false", onClick: () => onChangeIssueFilter && onChangeIssueFilter("open") }, `🟢 Open (${issueOpenCount})`), h("button", { className: `dsh-github-pill-btn ${issueStateFilter === "closed" ? "active" : ""}`, "aria-pressed": issueStateFilter === "closed" ? "true" : "false", onClick: () => onChangeIssueFilter && onChangeIssueFilter("closed") }, `⚪ Closed (${issueClosedCount})`));
6684
7990
  var issueEmptyEl = issues.length === 0
6685
7991
  ? h("div", { className: "dsh-github-empty" }, "该仓库暂无 Issue 记录")
6686
7992
  : h("div", { className: "dsh-github-empty" }, `当前筛选「${issueFilterLabel}」下无结果,共 ${issues.length} 条记录(切换到「全部」查看)`);
6687
- var issueSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `📋 仓库 Issues (${filteredIssues.length}/${issues.length})`), repo?.url
7993
+ var issueSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `📋 仓库 Issues(${issues.length} / 共 ${issueTotalCount})`), repo?.url
6688
7994
  ? h("a", {
6689
7995
  href: `${repo.url}/issues`,
6690
7996
  target: "_blank",
6691
7997
  style: { fontSize: "11px", color: "var(--dsw-alias-link, #0969da)", textDecoration: "none" },
6692
7998
  }, "全部 ↗")
6693
- : null), issuePillsEl, data && data.issuesTruncated
6694
- ? h("div", {
6695
- style: {
6696
- margin: "0 0 10px 0",
6697
- padding: "6px 10px",
6698
- borderRadius: "6px",
6699
- fontSize: "11px",
6700
- lineHeight: 1.5,
6701
- background: "rgba(210, 153, 34, 0.12)",
6702
- border: "1px solid rgba(210, 153, 34, 0.35)",
6703
- color: "var(--dsw-alias-state-attention-primary, #9a6700)",
6704
- },
6705
- }, `⚠️ 仅显示最近 ${issues.length} 条 Issue(已触及检索上限)。更早的记录请到 GitHub 查看。`)
6706
- : null, filteredIssues.length === 0
7999
+ : null), issuePillsEl, filteredIssues.length === 0
6707
8000
  ? issueEmptyEl
6708
8001
  : h("div", { className: "dsh-github-list" }, filteredIssues.map((issue) => {
6709
8002
  var cats = issue.categories || [];
@@ -6753,14 +8046,34 @@ function GithubHelpModal(props) {
6753
8046
  ? issue.labels.map((lbl) => h("span", { key: lbl.name, className: "dsh-github-badge dsh-badge-yellow" }, lbl.name))
6754
8047
  : null));
6755
8048
  })));
6756
- var runSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `⚡ 最近 Actions 流水线 (${runs.length})`)), runs.length === 0
8049
+ var runSectionEl = h("div", { className: "dsh-github-section" }, h("div", { className: "dsh-github-section-header" }, h("h3", null, `⚡ 最近 Actions 流水线(${runs.length} / 共 ${runTotalCount})`)), runs.length === 0
6757
8050
  ? h("div", { className: "dsh-github-empty" }, "暂无工作流记录")
6758
- : h("div", { className: "dsh-github-list" }, runs.map((r) => h("div", { key: r.databaseId, className: "dsh-github-item" }, h("a", {
8051
+ : h("div", { className: "dsh-github-list" }, runs.map((r) => h("div", {
8052
+ key: r.databaseId,
8053
+ className: "dsh-github-item",
8054
+ // 只加 cursor:内联 display:flex 会顶掉 .dsh-github-item 的 column 布局
8055
+ style: onSelectRun ? { cursor: "pointer" } : undefined,
8056
+ title: onSelectRun ? "点击在线查看该运行(job / step / 失败日志)" : undefined,
8057
+ onClick: onSelectRun
8058
+ ? function () {
8059
+ onSelectRun({ runId: Number(r.databaseId), repo: repo?.nameWithOwner || "" });
8060
+ }
8061
+ : undefined,
8062
+ },
8063
+ // 标题与外跳必须同行拆开:Actions 卡片过去整行就是一个 <a>,
8064
+ // 点击即跳浏览器;现在卡片本身要进弹窗,外跳必须独立成按钮并
8065
+ // stopPropagation,否则点外跳会连带把弹窗也打开。
8066
+ h("div", { className: "dsh-github-pr-title-row" }, h("span", {
6759
8067
  className: "dsh-github-item-title",
8068
+ title: r.createdAt ? `始于 ${r.createdAt}(点击在线查看)` : "点击在线查看该运行",
8069
+ }, r.name), h("a", {
8070
+ className: "dsh-github-pr-gh-btn",
6760
8071
  href: r.url,
6761
8072
  target: "_blank",
6762
- title: r.createdAt ? `始于 ${r.createdAt}` : undefined,
6763
- }, r.name), h("div", { className: "dsh-github-item-meta" }, h("span", null, `分支: ${r.headBranch}`), r.event ? h("span", null, eventLabel(r.event)) : null, formatRelativeTime(r.createdAt) ? h("span", null, formatRelativeTime(r.createdAt)) : null, r.conclusion === "success"
8073
+ rel: "noopener noreferrer",
8074
+ onClick: (e) => e.stopPropagation(),
8075
+ title: "在浏览器打开该运行(不进入在线详情)",
8076
+ }, "GitHub ↗")), h("div", { className: "dsh-github-item-meta" }, h("span", null, `分支: ${r.headBranch}`), r.event ? h("span", null, eventLabel(r.event)) : null, formatRelativeTime(r.createdAt) ? h("span", null, formatRelativeTime(r.createdAt)) : null, r.conclusion === "success"
6764
8077
  ? h("span", { className: "dsh-github-badge dsh-badge-green" }, "✓ 成功")
6765
8078
  : r.conclusion === "failure"
6766
8079
  ? h("span", { className: "dsh-github-badge dsh-badge-red" }, "✕ 失败")
@@ -6768,7 +8081,7 @@ function GithubHelpModal(props) {
6768
8081
  ? h("div", {
6769
8082
  className: "dsh-github-card-subtext",
6770
8083
  style: { fontSize: "11px", marginTop: "2px" },
6771
- }, `💡 让 Agent 调用 github_run(action:'log_failed', run_id:'${r.databaseId}') 提取失败日志定位原因`)
8084
+ }, `💡 点击卡片查看失败步骤与日志;或让 Agent 调用 github_task(action:'diagnose_ci', run_id:'${r.databaseId}')`)
6772
8085
  : null))));
6773
8086
  var authGuideEl = (!auth?.loggedIn && !auth?.networkError && syncStatus !== "failed")
6774
8087
  ? h(AuthGuideCard, { auth, onRefresh, isRightbar })
@@ -6776,9 +8089,11 @@ function GithubHelpModal(props) {
6776
8089
  var TAB_DEFS = [
6777
8090
  { key: "git", label: "Git", count: null, icon: renderGitIcon(13, "#1296db") },
6778
8091
  { key: "dirty", label: "📁 未提交", count: gitSt ? gitSt.dirtyCount : null },
6779
- { key: "prs", label: "🔀 PR", count: prs.length },
6780
- { key: "issues", label: "📋 Issues", count: issues.length },
6781
- { key: "runs", label: "⚡ Actions", count: runs.length },
8092
+ // Tab 上的数字是**仓库总数**(不是已加载条数):下滑加载更多时它必须纹丝不动
8093
+ { key: "prs", label: "🔀 PR", count: prTotalCount },
8094
+ { key: "issues", label: "📋 Issues", count: issueTotalCount },
8095
+ // Actions 没有可用的总数接口(gh/GraphQL 都不提供),保持已加载条数
8096
+ { key: "runs", label: "⚡ Actions", count: runTotalCount },
6782
8097
  ].filter(function (t) {
6783
8098
  if ((t.key === "git" || t.key === "dirty") && !gitSt)
6784
8099
  return false;
@@ -6807,7 +8122,7 @@ function GithubHelpModal(props) {
6807
8122
  },
6808
8123
  }, t.icon ? [t.icon, " ", labelText] : labelText);
6809
8124
  }));
6810
- var tabbedContentEl = h("div", { "data-active-tab": defaultTab }, tabsBarEl, h("div", { "data-tab-page": "prs", hidden: defaultTab !== "prs" }, prSectionEl), h("div", { "data-tab-page": "issues", hidden: defaultTab !== "issues" }, issueSectionEl), h("div", { "data-tab-page": "runs", hidden: defaultTab !== "runs" }, runSectionEl), gitSt ? h("div", { "data-tab-page": "git", hidden: defaultTab !== "git" }, gitSectionEl) : null, gitSt ? h("div", { "data-tab-page": "dirty", hidden: defaultTab !== "dirty" }, dirtySectionEl) : null);
8125
+ var tabbedContentEl = h("div", { "data-active-tab": defaultTab }, tabsBarEl, h("div", { "data-tab-page": "prs", hidden: defaultTab !== "prs" }, prSectionEl, renderListMore("prs", data && data.prsTruncated, prs.length, Boolean(listLoading && listLoading.prs)), renderListEnd("prs", data && data.prsTruncated, prs.length, data && data.prTotal)), h("div", { "data-tab-page": "issues", hidden: defaultTab !== "issues" }, issueSectionEl, renderListMore("issues", data && data.issuesTruncated, issues.length, Boolean(listLoading && listLoading.issues)), renderListEnd("issues", data && data.issuesTruncated, issues.length, data && data.issueTotal)), h("div", { "data-tab-page": "runs", hidden: defaultTab !== "runs" }, runSectionEl, renderListMore("runs", data && data.runsTruncated, runs.length, Boolean(listLoading && listLoading.runs)), renderListEnd("runs", data && data.runsTruncated, runs.length, data && data.runTotal)), gitSt ? h("div", { "data-tab-page": "git", hidden: defaultTab !== "git" }, gitSectionEl) : null, gitSt ? h("div", { "data-tab-page": "dirty", hidden: defaultTab !== "dirty" }, dirtySectionEl) : null);
6811
8126
  return [
6812
8127
  headerEl,
6813
8128
  networkAlertBannerEl,
@@ -6841,6 +8156,15 @@ function GithubHelpModal(props) {
6841
8156
  var [toastMsg, setToastMsg] = React.useState(null);
6842
8157
  var cloudTimerRef = React.useRef(null);
6843
8158
  var dataRef = React.useRef(data);
8159
+ // 列表分页游标按 Tab 独立维护:防止加载更多 PR 顺带把 Issue 和 Actions 也拉满膨胀
8160
+ var tabLimitsRef = React.useRef({
8161
+ prs: OVERVIEW_PAGE_SIZE,
8162
+ issues: OVERVIEW_PAGE_SIZE,
8163
+ runs: OVERVIEW_PAGE_SIZE,
8164
+ });
8165
+ var loadingMoreListsRef = React.useRef(false);
8166
+ // 各 Tab 的「正在加载更多」UI 标记(ref 只防重入,界面反馈必须走 state 才能重渲染)
8167
+ var [listLoading, setListLoading] = React.useState({ prs: false, issues: false, runs: false });
6844
8168
  dataRef.current = data;
6845
8169
  var lastCwdRef = React.useRef(cwd);
6846
8170
  var containerRef = React.useRef(null);
@@ -6852,6 +8176,11 @@ function GithubHelpModal(props) {
6852
8176
  if (lastCwdRef.current === cwd)
6853
8177
  return;
6854
8178
  lastCwdRef.current = cwd;
8179
+ tabLimitsRef.current = {
8180
+ prs: OVERVIEW_PAGE_SIZE,
8181
+ issues: OVERVIEW_PAGE_SIZE,
8182
+ runs: OVERVIEW_PAGE_SIZE,
8183
+ };
6855
8184
  var snap = getStoredRightbarSnapshot(cwd);
6856
8185
  setData(snap?.data || null);
6857
8186
  setSyncStatus(snap ? "cached" : "loading");
@@ -6916,6 +8245,10 @@ function GithubHelpModal(props) {
6916
8245
  if (cwd) {
6917
8246
  params.push("cwd=" + encodeURIComponent(cwd));
6918
8247
  }
8248
+ // 分页:PR / Issue / Actions 各自独立游标,避免相互膨胀
8249
+ params.push("prLimit=" + tabLimitsRef.current.prs);
8250
+ params.push("issueLimit=" + tabLimitsRef.current.issues);
8251
+ params.push("runLimit=" + tabLimitsRef.current.runs);
6919
8252
  if (params.length > 0) {
6920
8253
  url += "?" + params.join("&");
6921
8254
  }
@@ -7191,6 +8524,8 @@ function GithubHelpModal(props) {
7191
8524
  var [activePr, setActivePr] = React.useState(null);
7192
8525
  // Issue 在线查看弹窗状态:与 PR 同构
7193
8526
  var [activeIssue, setActiveIssue] = React.useState(null);
8527
+ // Run 在线详情弹窗状态:{ runId, repo };为 null 时不挂载弹窗(零常驻开销)
8528
+ var [activeRun, setActiveRun] = React.useState(null);
7194
8529
  var [extraCommits, setExtraCommits] = React.useState([]);
7195
8530
  var [loadingMore, setLoadingMore] = React.useState(false);
7196
8531
  var [hasMoreCommits, setHasMoreCommits] = React.useState(true);
@@ -7246,6 +8581,35 @@ function GithubHelpModal(props) {
7246
8581
  setPrStateFilter(stored.pr);
7247
8582
  setIssueStateFilter(stored.issue);
7248
8583
  }, [cwd]);
8584
+ /**
8585
+ * PR / Issue / Actions 列表的「加载更多」(独立游标维护)。
8586
+ *
8587
+ * 针对对应 Tab 单独累加步长,未处于当前焦点的 Tab 不增加 limit,
8588
+ * 从源头杜绝并发膨胀触发底层 125KB 截断。
8589
+ */
8590
+ var loadMoreTabList = function (kind) {
8591
+ if (loadingMoreListsRef.current)
8592
+ return;
8593
+ var validKey = kind === "prs" ? "prs" : kind === "issues" ? "issues" : kind === "runs" ? "runs" : null;
8594
+ if (!validKey)
8595
+ return;
8596
+ tabLimitsRef.current[validKey] = tabLimitsRef.current[validKey] + OVERVIEW_PAGE_SIZE;
8597
+ loadingMoreListsRef.current = true;
8598
+ // 立刻把该 Tab 的页脚切成「正在加载中」,请求结束后再切回来(成功失败都要复位)
8599
+ setListLoading(function (prev) {
8600
+ var next = Object.assign({}, prev);
8601
+ next[validKey] = true;
8602
+ return next;
8603
+ });
8604
+ loadData(true).finally(function () {
8605
+ loadingMoreListsRef.current = false;
8606
+ setListLoading(function (prev) {
8607
+ var next = Object.assign({}, prev);
8608
+ next[validKey] = false;
8609
+ return next;
8610
+ });
8611
+ });
8612
+ };
7249
8613
  var loadMoreCommits = function () {
7250
8614
  if (loadingMore || !hasMoreCommits)
7251
8615
  return;
@@ -7367,6 +8731,15 @@ function GithubHelpModal(props) {
7367
8731
  else if (activeTab === "dirty") {
7368
8732
  loadMoreDirtyFiles();
7369
8733
  }
8734
+ else if (activeTab === "prs" && data?.prsTruncated) {
8735
+ loadMoreTabList("prs");
8736
+ }
8737
+ else if (activeTab === "issues" && data?.issuesTruncated) {
8738
+ loadMoreTabList("issues");
8739
+ }
8740
+ else if (activeTab === "runs" && data?.runsTruncated) {
8741
+ loadMoreTabList("runs");
8742
+ }
7370
8743
  }
7371
8744
  };
7372
8745
  // 监听鼠标滚轮向下滚事件(即使用户收起卡片导致内容暂未撑出滚动条,只要向下滚轮也能瞬间响应!)
@@ -7384,6 +8757,15 @@ function GithubHelpModal(props) {
7384
8757
  else if (activeTab === "dirty") {
7385
8758
  loadMoreDirtyFiles();
7386
8759
  }
8760
+ else if (activeTab === "prs" && data?.prsTruncated) {
8761
+ loadMoreTabList("prs");
8762
+ }
8763
+ else if (activeTab === "issues" && data?.issuesTruncated) {
8764
+ loadMoreTabList("issues");
8765
+ }
8766
+ else if (activeTab === "runs" && data?.runsTruncated) {
8767
+ loadMoreTabList("runs");
8768
+ }
7387
8769
  }
7388
8770
  };
7389
8771
  el.addEventListener("scroll", onScroll, { passive: true });
@@ -7493,7 +8875,9 @@ function GithubHelpModal(props) {
7493
8875
  loadingMore: loadingMoreDirty,
7494
8876
  hasMore: hasMoreDirty,
7495
8877
  onLoadMore: loadMoreDirtyFiles,
7496
- }, activeDiffFile, (f) => setActiveDiffFile(f), () => setActiveDiffFile(null), openFileInDsh, headerCardsVisible, toggleHeaderCards, (target) => setActivePr(target), (target) => setActiveIssue(target), prStateFilter, issueStateFilter, changePrFilter, changeIssueFilter), h(GlobalSyncSettingsModal, {
8878
+ }, activeDiffFile, (f) => setActiveDiffFile(f), () => setActiveDiffFile(null), openFileInDsh, headerCardsVisible, toggleHeaderCards, (target) => setActivePr(target), (target) => setActiveIssue(target), prStateFilter, issueStateFilter, changePrFilter, changeIssueFilter,
8879
+ // 必须追加在形参末尾(该函数 30+ 个位置参数,插中间会静默错位)
8880
+ loadMoreTabList, listLoading, (target) => setActiveRun(target)), h(GlobalSyncSettingsModal, {
7497
8881
  isOpen: aboutModalOpen,
7498
8882
  initialSection: "about",
7499
8883
  onClose: () => setAboutModalOpen(false),
@@ -7536,6 +8920,14 @@ function GithubHelpModal(props) {
7536
8920
  onClose: function () { setActiveIssue(null); },
7537
8921
  showToast: showToast,
7538
8922
  })
8923
+ : null, activeRun
8924
+ ? h(RunDetailModal, {
8925
+ runId: activeRun.runId,
8926
+ repoNameWithOwner: activeRun.repo,
8927
+ cwd: cwd,
8928
+ onClose: function () { setActiveRun(null); },
8929
+ showToast: showToast,
8930
+ })
7539
8931
  : null);
7540
8932
  }
7541
8933
  // 2. 右侧栏 Tab 标题组件
@@ -7547,15 +8939,19 @@ function GithubHelpModal(props) {
7547
8939
  function GithubArtwork() {
7548
8940
  return renderOctocatBadge(32);
7549
8941
  }
7550
- // 4. 左侧栏全局入口与中央大面板
8942
+ // 4. 左侧栏全局入口
8943
+ //
8944
+ // 宿主的 sidebar.panellist 行渲染器会向本组件注入 { size, active }:
8945
+ // renderSlot("sidebar.panellist", { size: wide ? 16 : 18, active }, { only: id })
8946
+ // 且图标格 .panelGlyph 是 flex:none、不设宽高——宽度完全由内容决定。
8947
+ // 因此这里必须:① 用宿主给的 size;② 不再套任何固定宽高的盒子。
8948
+ // 早期版本套了 28px 内层盒子 + 深色渐变发光,结果是图标格被撑到 28px
8949
+ // (同级 16px)、标签右移 12px、选中态多出一块发光底(issue #9)。
7551
8950
  function GithubSidebarIcon(props) {
7552
- const active = Boolean(props && props.active);
7553
- return React.createElement("div", {
7554
- className: "dsh-github-icon" + (active ? " active" : ""),
7555
- title: "GitHub 全景驾驶舱(dsh-github-flow 插件)",
7556
- }, React.createElement("div", {
7557
- className: "dsh-github-icon-inner" + (active ? " active" : "")
7558
- }, renderOctocat(18, "currentColor")));
8951
+ var size = props && typeof props.size === "number" ? props.size : 16;
8952
+ // 选中态由宿主 .panelRow.panelActive 负责(行背景 + label-primary),这里不重复绘制;
8953
+ // 也不设 title:宿主已提供 aria-label 与 Tooltip,叠加会出现双提示。
8954
+ return renderOctocat(size, "currentColor");
7559
8955
  }
7560
8956
  var inject = ["slots", "sidebarRightTabs", "sessions", "layout", "theme", "sidebarRight"];
7561
8957
 
@@ -7590,6 +8986,12 @@ function GithubWorkspacePanel() {
7590
8986
  var [activeIssueModal, setActiveIssueModal] = React.useState(null);
7591
8987
  var cloudTimerRef = React.useRef(null);
7592
8988
  var dataRef = React.useRef(data);
8989
+ // 全局面板分页游标:PR 与 Issues 独立维护,防止点击加载更多相互膨胀
8990
+ var prLimitRef = React.useRef(15);
8991
+ var issueLimitRef = React.useRef(15);
8992
+ // 「正在加载更多」UI 标记:请求往返期间页脚必须给反馈,否则用户会连点触发重复请求
8993
+ var [loadingPrs, setLoadingPrs] = React.useState(false);
8994
+ var [loadingIssues, setLoadingIssues] = React.useState(false);
7593
8995
  dataRef.current = data;
7594
8996
  var showToast = (msg) => {
7595
8997
  setToastMsg(msg);
@@ -7672,8 +9074,14 @@ function GithubWorkspacePanel() {
7672
9074
  if (forceRefresh)
7673
9075
  await fetch("/api/github/refresh", { method: "POST", signal: controller.signal }).catch(() => { });
7674
9076
  var url = "/api/github/global-overview";
9077
+ var params = [];
7675
9078
  if (forceRefresh) {
7676
- url += "?force=true";
9079
+ params.push("force=true");
9080
+ }
9081
+ params.push("prLimit=" + prLimitRef.current);
9082
+ params.push("issueLimit=" + issueLimitRef.current);
9083
+ if (params.length > 0) {
9084
+ url += "?" + params.join("&");
7677
9085
  }
7678
9086
  // 接入 1s/2s/4s 阶梯退避重试,平滑吸收代理节点与网络的偶发抖动断流
7679
9087
  var { res, json } = await fetchWithRetry(url, {}, {
@@ -7909,6 +9317,21 @@ function GithubWorkspacePanel() {
7909
9317
  var workspaceMatrix = (data && data.workspaceMatrix) || [];
7910
9318
  var myPrs = (data && data.myPrs) || [];
7911
9319
  var myIssues = (data && data.myIssues) || [];
9320
+ // 主面板列表分页:PR 与 Issue 各自独立游标,每页步长 15,避免点击加载更多相互膨胀
9321
+ var loadMorePrs = function () {
9322
+ if (loadingPrs)
9323
+ return;
9324
+ prLimitRef.current = prLimitRef.current + 15;
9325
+ setLoadingPrs(true);
9326
+ loadData(true).finally(function () { setLoadingPrs(false); });
9327
+ };
9328
+ var loadMoreIssues = function () {
9329
+ if (loadingIssues)
9330
+ return;
9331
+ issueLimitRef.current = issueLimitRef.current + 15;
9332
+ setLoadingIssues(true);
9333
+ loadData(true).finally(function () { setLoadingIssues(false); });
9334
+ };
7912
9335
  var errorEl = error ? h("div", { style: { color: "#f85149", background: "rgba(248,81,73,0.1)", border: "1px solid rgba(248,81,73,0.3)", padding: "8px 12px", borderRadius: "6px", marginBottom: "16px", fontSize: "12px" } }, `⚠️ 加载失败: ${error}`) : null;
7913
9336
  var statsCardsEl = h("div", { className: "dsh-github-overview-cards" }, h("div", { className: "dsh-github-card" }, h("div", { className: "dsh-github-card-title" }, "CLI 认证账号"), h("div", { className: "dsh-github-card-value" }, auth?.networkError
7914
9337
  ? (auth.user ? `👤 ${auth.user} (离线)` : "🌐 网络连接受限")
@@ -7984,27 +9407,38 @@ function GithubWorkspacePanel() {
7984
9407
  return true;
7985
9408
  return String(pr.state || '').toLowerCase() === prStateFilter;
7986
9409
  });
7987
- var prPillsEl = h("div", { className: "dsh-github-pills" }, h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'all' ? 'active' : ''}`, onClick: () => setPrStateFilter('all') }, `全部 (${myPrs.length})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'open' ? 'active' : ''}`, onClick: () => setPrStateFilter('open') }, `🟢 Open (${prByState.open})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'merged' ? 'active' : ''}`, onClick: () => setPrStateFilter('merged') }, `🟣 Merged (${prByState.merged})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'closed' ? 'active' : ''}`, onClick: () => setPrStateFilter('closed') }, `⚪ Closed (${prByState.closed})`));
9410
+ var prPillsEl = h("div", { className: "dsh-github-pills" },
9411
+ // 计数优先用账号级真实总数(服务端 GraphQL search.issueCount),拿不到才回落已加载条数
9412
+ h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'all' ? 'active' : ''}`, onClick: () => setPrStateFilter('all') }, `全部 (${typeof data?.prTotal === 'number' ? data.prTotal : myPrs.length})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'open' ? 'active' : ''}`, onClick: () => setPrStateFilter('open') }, `🟢 Open (${typeof data?.prOpenTotal === 'number' ? data.prOpenTotal : prByState.open})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'merged' ? 'active' : ''}`, onClick: () => setPrStateFilter('merged') }, `🟣 Merged (${typeof data?.prMergedTotal === 'number' ? data.prMergedTotal : prByState.merged})`), h("button", { className: `dsh-github-pill-btn ${prStateFilter === 'closed' ? 'active' : ''}`, onClick: () => setPrStateFilter('closed') }, `⚪ Closed (${typeof data?.prClosedTotal === 'number' ? data.prClosedTotal : prByState.closed})`));
7988
9413
  // 空态必须区分「账号确实没有」与「当前筛选下没有」——否则用户会把筛空当成数据丢失
7989
9414
  var prEmptyEl = myPrs.length === 0
7990
9415
  ? h("div", { className: "dsh-github-empty" }, "该账号下暂无任何 Pull Request 记录")
7991
9416
  : h("div", { className: "dsh-github-empty" }, `当前筛选「${prStateFilter}」下无结果,共 ${myPrs.length} 条记录(切换到「全部」查看)`);
7992
- contentEl = h("div", null, h("h3", { className: "dsh-github-subheading" }, `🔀 我发起与参与的 Pull Requests (${filteredPrs.length} / ${myPrs.length})`), prPillsEl,
7993
- // 触顶提示:让用户知道列表被截断了,而不是以为历史 PR 凭空消失
9417
+ contentEl = h("div", null, h("h3", { className: "dsh-github-subheading" }, `🔀 我发起与参与的 Pull Requests(${filteredPrs.length} 条符合筛选 / 共 ${typeof data?.prTotal === 'number' ? data.prTotal : myPrs.length})`), prPillsEl,
9418
+ // 「加载更多」页脚:服务端说还有更多(本次拉满)时渲染,点击即 +15 重取。
9419
+ // 旧文案是「已触及检索上限,请到 GitHub 查看」——那是在没有分页能力时的兜底,
9420
+ // 现在数据可以继续拉,改成可点击的入口。
7994
9421
  data && data.prsTruncated
7995
9422
  ? h("div", {
9423
+ onClick: loadingPrs ? undefined : loadMorePrs,
9424
+ title: loadingPrs ? "正在加载更多 PR…" : "点击加载更多(滚动到底部也会自动加载)", "aria-busy": loadingPrs ? "true" : "false",
7996
9425
  style: {
7997
9426
  margin: "8px 0",
7998
- padding: "6px 12px",
9427
+ padding: "8px 12px",
7999
9428
  borderRadius: "6px",
8000
9429
  fontSize: "11px",
8001
9430
  lineHeight: 1.5,
9431
+ textAlign: "center",
9432
+ cursor: loadingPrs ? "default" : "pointer",
8002
9433
  background: "rgba(210, 153, 34, 0.12)",
8003
9434
  border: "1px solid rgba(210, 153, 34, 0.35)",
8004
9435
  color: "var(--dsw-alias-state-attention-primary, #9a6700)",
8005
9436
  },
8006
- }, `⚠️ 仅显示最近 ${myPrs.length} 条 PR(已触及检索上限)。更早的记录请到 GitHub 查看。`)
8007
- : null, filteredPrs.length === 0 ? prEmptyEl : h("div", { className: "dsh-github-list" }, filteredPrs.map((pr) => {
9437
+ }, loadingPrs ? "⏳ 正在加载中…" : `⬇️ 下滑或点击加载更多 PR(已显示 ${myPrs.length} 条)`)
9438
+ : (typeof data?.prTotal === 'number' && myPrs.length < data.prTotal
9439
+ // 顶到单次 100 条上限后不再有加载入口,必须说明「更早记录在哪」,否则用户以为界面坏了
9440
+ ? h("div", { style: { margin: "8px 0", fontSize: "11px", opacity: 0.6, textAlign: "center" } }, `已加载 ${myPrs.length} / 共 ${data.prTotal} 条,更早记录请到 GitHub 查看`)
9441
+ : null), filteredPrs.length === 0 ? prEmptyEl : h("div", { className: "dsh-github-list" }, filteredPrs.map((pr) => {
8008
9442
  // 点击卡片主体 → 在线审阅弹窗
8009
9443
  return h("div", {
8010
9444
  key: pr.number,
@@ -8029,14 +9463,11 @@ function GithubWorkspacePanel() {
8029
9463
  h("div", { className: "dsh-github-pr-title-row" }, h("span", { className: "dsh-github-item-title", style: { cursor: "pointer" } }, `${pr.isDraft ? "⚪ " : ""}[${pr.repository?.nameWithOwner || 'Repo'}] #${pr.number} ${pr.title}`), pr.isDraft ? h("span", { className: "dsh-github-badge dsh-badge-yellow", style: { flexShrink: 0 } }, "DRAFT") : null,
8030
9464
  // 状态徽章:列表含 merged/closed,必须标明才能一眼区分
8031
9465
  (() => {
8032
- var s = String(pr.state || '').toUpperCase();
8033
- if (s === 'MERGED')
8034
- return h("span", { className: "dsh-github-badge dsh-badge-purple", style: { flexShrink: 0 } }, "MERGED");
8035
- if (s === 'CLOSED')
8036
- return h("span", { className: "dsh-github-badge dsh-badge-red", style: { flexShrink: 0 } }, "CLOSED");
8037
- if (s === 'OPEN')
8038
- return h("span", { className: "dsh-github-badge dsh-badge-green", style: { flexShrink: 0 } }, "OPEN");
8039
- return null;
9466
+ // 状态→色调来自 shared/state-badges(与右侧栏、CLI 输出同一份)
9467
+ var b = prStateBadge(pr.state);
9468
+ if (!b || b.tone === 'neutral')
9469
+ return null;
9470
+ return h("span", { className: "dsh-github-badge dsh-badge-" + b.tone, style: { flexShrink: 0 } }, b.label);
8040
9471
  })(), h("a", {
8041
9472
  className: "dsh-github-pr-gh-btn",
8042
9473
  href: pr.url,
@@ -8072,7 +9503,7 @@ function GithubWorkspacePanel() {
8072
9503
  var issuePillsEl = h("div", { className: "dsh-github-pills" }, h("button", {
8073
9504
  className: `dsh-github-pill-btn ${issueFilter === 'all' ? 'active' : ''}`,
8074
9505
  onClick: () => setIssueFilter('all'),
8075
- }, `全部 (${issueStats.total || myIssues.length})`), h("button", {
9506
+ }, `全部 (${myIssues.length})`), h("button", {
8076
9507
  className: `dsh-github-pill-btn ${issueFilter === 'reported' ? 'active' : ''}`,
8077
9508
  onClick: () => setIssueFilter('reported'),
8078
9509
  }, `📥 用户提起的 (${issueStats.reported || 0})`), h("button", {
@@ -8082,7 +9513,28 @@ function GithubWorkspacePanel() {
8082
9513
  className: `dsh-github-pill-btn ${issueFilter === 'created' ? 'active' : ''}`,
8083
9514
  onClick: () => setIssueFilter('created'),
8084
9515
  }, `✍️ 我创建的 (${issueStats.created || 0})`));
8085
- contentEl = h("div", null, h("h3", { className: "dsh-github-subheading" }, `📝 我发起与参与的 Issues (${filteredIssues.length} / ${myIssues.length})`), issuePillsEl, filteredIssues.length === 0
9516
+ contentEl = h("div", null,
9517
+ // Issue 侧没有可信总数(三路并集无法用单条查询计数),老实标「已加载」,不假装是总数
9518
+ h("h3", { className: "dsh-github-subheading" }, `📝 我发起与参与的 Issues(${filteredIssues.length} 条符合筛选 / 已加载 ${myIssues.length})`), issuePillsEl,
9519
+ // 「加载更多」页脚:与 PR 列表同一口径(服务端 issuesTruncated = 还能继续拉)
9520
+ data && data.issuesTruncated
9521
+ ? h("div", {
9522
+ onClick: loadingIssues ? undefined : loadMoreIssues,
9523
+ title: loadingIssues ? "正在加载更多 Issue…" : "点击加载更多(滚动到底部也会自动加载)", "aria-busy": loadingIssues ? "true" : "false",
9524
+ style: {
9525
+ margin: "8px 0",
9526
+ padding: "8px 12px",
9527
+ borderRadius: "6px",
9528
+ fontSize: "11px",
9529
+ lineHeight: 1.5,
9530
+ textAlign: "center",
9531
+ cursor: loadingIssues ? "default" : "pointer",
9532
+ background: "rgba(210, 153, 34, 0.12)",
9533
+ border: "1px solid rgba(210, 153, 34, 0.35)",
9534
+ color: "var(--dsw-alias-state-attention-primary, #9a6700)",
9535
+ },
9536
+ }, loadingIssues ? "⏳ 正在加载中…" : `⬇️ 下滑或点击加载更多 Issue(已显示 ${myIssues.length} 条)`)
9537
+ : null, filteredIssues.length === 0
8086
9538
  ? (myIssues.length === 0
8087
9539
  ? h("div", { className: "dsh-github-empty" }, "该账号下暂无任何 Issue 记录")
8088
9540
  : h("div", { className: "dsh-github-empty" }, `当前分类「${issueFilter}」下无结果,共 ${myIssues.length} 条记录(切换到「全部」查看)`))
@@ -8118,12 +9570,10 @@ function GithubWorkspacePanel() {
8118
9570
  // 标题行:标题 → 分类徽章 → 状态徽章 → GitHub 外跳,全部同一行
8119
9571
  h("div", { className: "dsh-github-pr-title-row" }, h("span", { className: "dsh-github-item-title", style: { cursor: "pointer" } }, `[${is.repository?.nameWithOwner || 'Repo'}] #${is.number} ${is.title}`), ...badges, (() => {
8120
9572
  // 状态徽章:全局检索含 closed,不标注会让用户把已关闭议题当成待办
8121
- var s = String(is.state || '').toUpperCase();
8122
- if (s === 'CLOSED')
8123
- return h("span", { className: "dsh-github-badge dsh-badge-red", style: { flexShrink: 0 } }, "CLOSED");
8124
- if (s === 'OPEN')
8125
- return h("span", { className: "dsh-github-badge dsh-badge-green", style: { flexShrink: 0 } }, "OPEN");
8126
- return null;
9573
+ var b = issueStateBadge(is.state);
9574
+ if (!b || b.tone === 'neutral')
9575
+ return null;
9576
+ return h("span", { className: "dsh-github-badge dsh-badge-" + b.tone, style: { flexShrink: 0 } }, b.label);
8127
9577
  })(), h("a", {
8128
9578
  className: "dsh-github-pr-gh-btn",
8129
9579
  href: is.url,