dsh-github-flow 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +16 -4
- package/README.md +16 -4
- package/docs/UPGRADE-NOTES.md +8 -8
- package/docs/architecture.md +33 -17
- package/docs/images/1.png +0 -0
- package/lib/api/routes-actions.js +139 -8
- package/lib/api/routes-overview.js +147 -29
- package/lib/cache.d.ts +77 -4
- package/lib/cache.js +114 -7
- package/lib/client.d.ts +7 -3
- package/lib/client.js +3309 -314
- package/lib/executor/error-classifier.d.ts +55 -0
- package/lib/executor/error-classifier.js +199 -0
- package/lib/executor/git-parser.d.ts +124 -0
- package/lib/executor/git-parser.js +397 -0
- package/lib/executor.d.ts +122 -4
- package/lib/executor.js +685 -80
- package/lib/tools/help.js +22 -7
- package/lib/tools/image-utils.d.ts +27 -0
- package/lib/tools/image-utils.js +73 -3
- package/lib/types.d.ts +331 -0
- package/lib/ui/help-modal.d.ts +6 -0
- package/lib/ui/help-modal.js +113 -15
- package/lib/ui/issue-modal.d.ts +14 -0
- package/lib/ui/issue-modal.js +366 -0
- package/lib/ui/markdown-lite.d.ts +66 -0
- package/lib/ui/markdown-lite.js +312 -0
- package/lib/ui/network-banner.d.ts +12 -0
- package/lib/ui/network-banner.js +35 -0
- package/lib/ui/pr-modal.d.ts +17 -0
- package/lib/ui/pr-modal.js +516 -0
- package/lib/ui/repo-badges.d.ts +43 -0
- package/lib/ui/repo-badges.js +82 -0
- package/lib/ui/repo-selector.d.ts +14 -0
- package/lib/ui/repo-selector.js +112 -0
- package/lib/ui/storage.d.ts +47 -0
- package/lib/ui/storage.js +139 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +960 -0
- package/lib/ui/utils.js +13 -1
- package/lib/ui/workspace-panel.js +169 -27
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -211,6 +211,29 @@ margin-top: 6px;
|
|
|
211
211
|
body[data-ds-dark-theme] .dsh-github-card-subtext {
|
|
212
212
|
color: var(--dsw-alias-label-tertiary, #888);
|
|
213
213
|
}
|
|
214
|
+
.dsh-github-fork-line {
|
|
215
|
+
font-size: 12px;
|
|
216
|
+
margin-top: 4px;
|
|
217
|
+
color: var(--dsw-alias-label-secondary, #656d76);
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 4px;
|
|
221
|
+
flex-wrap: wrap;
|
|
222
|
+
}
|
|
223
|
+
body[data-ds-dark-theme] .dsh-github-fork-line {
|
|
224
|
+
color: var(--dsw-alias-label-secondary, #bbb);
|
|
225
|
+
}
|
|
226
|
+
.dsh-github-fork-line a {
|
|
227
|
+
color: var(--dsw-alias-link, #0969da);
|
|
228
|
+
text-decoration: none;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
}
|
|
231
|
+
.dsh-github-fork-line a:hover {
|
|
232
|
+
text-decoration: underline;
|
|
233
|
+
}
|
|
234
|
+
body[data-ds-dark-theme] .dsh-github-fork-line a {
|
|
235
|
+
color: #58a6ff;
|
|
236
|
+
}
|
|
214
237
|
.dsh-github-badge {
|
|
215
238
|
display: inline-flex;
|
|
216
239
|
align-items: center;
|
|
@@ -235,6 +258,17 @@ background: rgba(154, 103, 0, 0.12);
|
|
|
235
258
|
color: #9a6700;
|
|
236
259
|
border: 1px solid rgba(154, 103, 0, 0.2);
|
|
237
260
|
}
|
|
261
|
+
.dsh-badge-readonly {
|
|
262
|
+
background: rgba(154, 103, 0, 0.12);
|
|
263
|
+
color: #9a6700;
|
|
264
|
+
border: 1px solid rgba(154, 103, 0, 0.35);
|
|
265
|
+
font-weight: 600;
|
|
266
|
+
}
|
|
267
|
+
body[data-ds-dark-theme] .dsh-badge-readonly {
|
|
268
|
+
background: rgba(210, 153, 34, 0.18);
|
|
269
|
+
color: #d29922;
|
|
270
|
+
border-color: rgba(210, 153, 34, 0.4);
|
|
271
|
+
}
|
|
238
272
|
.dsh-badge-blue {
|
|
239
273
|
background: rgba(9, 105, 218, 0.12);
|
|
240
274
|
color: #0969da;
|
|
@@ -1465,217 +1499,1021 @@ background: rgba(234, 179, 8, 0.2) !important;
|
|
|
1465
1499
|
color: #f0b72f !important;
|
|
1466
1500
|
border-color: #d29922 !important;
|
|
1467
1501
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1502
|
+
.dsh-github-repo-switch-trigger {
|
|
1503
|
+
display: inline-flex;
|
|
1504
|
+
align-items: center;
|
|
1505
|
+
gap: 3px;
|
|
1506
|
+
padding: 1px 7px;
|
|
1507
|
+
font-size: 11px;
|
|
1508
|
+
font-weight: 500;
|
|
1509
|
+
border-radius: 12px;
|
|
1510
|
+
border: 1px solid rgba(130, 80, 223, 0.35);
|
|
1511
|
+
background: rgba(130, 80, 223, 0.1);
|
|
1512
|
+
color: #8250df;
|
|
1513
|
+
cursor: pointer;
|
|
1514
|
+
transition: all 0.15s ease;
|
|
1515
|
+
user-select: none;
|
|
1516
|
+
line-height: 16px;
|
|
1481
1517
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
* 面板公共工具函数(纯函数,可被单元测试直接覆盖)
|
|
1486
|
-
*/
|
|
1487
|
-
function formatRelativeTime(iso) {
|
|
1488
|
-
if (!iso || typeof iso !== "string")
|
|
1489
|
-
return "";
|
|
1490
|
-
var t = Date.parse(iso);
|
|
1491
|
-
if (isNaN(t))
|
|
1492
|
-
return "";
|
|
1493
|
-
var diff = Date.now() - t;
|
|
1494
|
-
if (diff < 0)
|
|
1495
|
-
return "刚刚";
|
|
1496
|
-
var MIN = 60 * 1000, HOUR = 60 * MIN, DAY = 24 * HOUR;
|
|
1497
|
-
if (diff < MIN)
|
|
1498
|
-
return "刚刚";
|
|
1499
|
-
if (diff < HOUR)
|
|
1500
|
-
return Math.floor(diff / MIN) + "分钟前";
|
|
1501
|
-
if (diff < DAY)
|
|
1502
|
-
return Math.floor(diff / HOUR) + "小时前";
|
|
1503
|
-
if (diff < 7 * DAY)
|
|
1504
|
-
return Math.floor(diff / DAY) + "天前";
|
|
1505
|
-
var d = new Date(t);
|
|
1506
|
-
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
|
1518
|
+
.dsh-github-repo-switch-trigger:hover {
|
|
1519
|
+
background: rgba(130, 80, 223, 0.2);
|
|
1520
|
+
border-color: rgba(130, 80, 223, 0.6);
|
|
1507
1521
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
for (var i = 0; i < checks.length; i++) {
|
|
1513
|
-
var c = checks[i] || {};
|
|
1514
|
-
var status = String(c.status || "").toLowerCase();
|
|
1515
|
-
var conclusion = String(c.conclusion || "").toLowerCase();
|
|
1516
|
-
if (conclusion === "failure" || conclusion === "timed_out" || conclusion === "cancelled")
|
|
1517
|
-
hasFail = true;
|
|
1518
|
-
else if (conclusion === "success" || conclusion === "skipped" || conclusion === "neutral") { }
|
|
1519
|
-
else if (status === "in_progress" || status === "queued" || conclusion === "pending" || (!conclusion && status))
|
|
1520
|
-
hasPending = true;
|
|
1521
|
-
}
|
|
1522
|
-
if (hasFail)
|
|
1523
|
-
return "fail";
|
|
1524
|
-
if (hasPending)
|
|
1525
|
-
return "pending";
|
|
1526
|
-
return "pass";
|
|
1522
|
+
body[data-ds-dark-theme] .dsh-github-repo-switch-trigger {
|
|
1523
|
+
border-color: rgba(163, 113, 247, 0.4);
|
|
1524
|
+
background: rgba(163, 113, 247, 0.15);
|
|
1525
|
+
color: #a371f7;
|
|
1527
1526
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
return { text: "✔ 已批准", kind: "green" };
|
|
1532
|
-
if (d === "CHANGES_REQUESTED")
|
|
1533
|
-
return { text: "✋ 需修改", kind: "red" };
|
|
1534
|
-
if (d === "REVIEW_REQUIRED")
|
|
1535
|
-
return { text: "👀 待审查", kind: "yellow" };
|
|
1536
|
-
return null;
|
|
1527
|
+
body[data-ds-dark-theme] .dsh-github-repo-switch-trigger:hover {
|
|
1528
|
+
background: rgba(163, 113, 247, 0.25);
|
|
1529
|
+
border-color: #a371f7;
|
|
1537
1530
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1531
|
+
.dsh-github-repo-dropdown {
|
|
1532
|
+
position: absolute;
|
|
1533
|
+
top: 100%;
|
|
1534
|
+
left: 0;
|
|
1535
|
+
margin-top: 5px;
|
|
1536
|
+
width: 320px;
|
|
1537
|
+
max-height: 380px;
|
|
1538
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1539
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1540
|
+
border-radius: 8px;
|
|
1541
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
|
1542
|
+
z-index: 1000;
|
|
1543
|
+
display: flex;
|
|
1544
|
+
flex-direction: column;
|
|
1545
|
+
overflow: hidden;
|
|
1549
1546
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
* 1. 应对代理节点不稳定、TLS 握手卡顿、短时网络抖动(半秒至两秒微断流);
|
|
1555
|
-
* 2. 阶梯退避时序:第 1 次失败等 1s (1000ms),第 2 次等 2s (2000ms),第 3 次等 4s (4000ms),附加 0~200ms Jitter;
|
|
1556
|
-
* 3. 智能区分软硬错误:401/403/404/400 确定性错误坚决不重试,仅在网络超时、不可达、或带有 networkError 时重试;
|
|
1557
|
-
* 4. 严格响应外部 AbortSignal,组件卸载或用户手动打断时立即退出等待,绝无内存泄漏。
|
|
1558
|
-
*/
|
|
1559
|
-
async function fetchWithRetry(url, options = {}, config = {}) {
|
|
1560
|
-
var maxRetries = config.maxRetries !== undefined ? config.maxRetries : 3;
|
|
1561
|
-
var baseDelayMs = config.baseDelayMs !== undefined ? config.baseDelayMs : 1000;
|
|
1562
|
-
var signal = config.signal || options.signal;
|
|
1563
|
-
var attempt = 0;
|
|
1564
|
-
while (true) {
|
|
1565
|
-
attempt++;
|
|
1566
|
-
var currentRes = null;
|
|
1567
|
-
var currentJson = null;
|
|
1568
|
-
var failureReason = "";
|
|
1569
|
-
var shouldRetry = false;
|
|
1570
|
-
try {
|
|
1571
|
-
if (signal && signal.aborted) {
|
|
1572
|
-
throw new Error("Aborted");
|
|
1573
|
-
}
|
|
1574
|
-
currentRes = await fetch(url, { ...options, signal });
|
|
1575
|
-
// 硬错误守卫:客户端确定性错误(如未授权、权限拒绝、不存在、参数错误)坚决不重试
|
|
1576
|
-
if (currentRes.status === 401 || currentRes.status === 403 || currentRes.status === 404 || currentRes.status === 400) {
|
|
1577
|
-
currentJson = await currentRes.json().catch(() => ({}));
|
|
1578
|
-
return { res: currentRes, json: currentJson };
|
|
1579
|
-
}
|
|
1580
|
-
if (!currentRes.ok) {
|
|
1581
|
-
failureReason = "HTTP " + currentRes.status + " " + currentRes.statusText;
|
|
1582
|
-
shouldRetry = true;
|
|
1583
|
-
}
|
|
1584
|
-
else {
|
|
1585
|
-
currentJson = await currentRes.json().catch(() => null);
|
|
1586
|
-
// 业务层软错误检测(后端返回了 200,但标记了 networkError 或 partialFailure)
|
|
1587
|
-
var isNetworkError = Boolean(currentJson && (currentJson.partialFailure ||
|
|
1588
|
-
currentJson.data?.partialFailure ||
|
|
1589
|
-
currentJson.data?.networkError ||
|
|
1590
|
-
currentJson.data?.auth?.networkError));
|
|
1591
|
-
if (isNetworkError) {
|
|
1592
|
-
failureReason = currentJson.error || currentJson.data?.networkErrorReason || "云端接口网络抖动";
|
|
1593
|
-
shouldRetry = true;
|
|
1594
|
-
}
|
|
1595
|
-
else {
|
|
1596
|
-
// 成功获取完整且无网络错误的数据
|
|
1597
|
-
return { res: currentRes, json: currentJson };
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
catch (err) {
|
|
1602
|
-
if ((signal && signal.aborted) || err.name === "AbortError") {
|
|
1603
|
-
throw err;
|
|
1604
|
-
}
|
|
1605
|
-
failureReason = err.message || "网络连接中断或超时";
|
|
1606
|
-
shouldRetry = true;
|
|
1607
|
-
}
|
|
1608
|
-
// 判断是否已耗尽重试次数
|
|
1609
|
-
if (!shouldRetry || attempt > maxRetries) {
|
|
1610
|
-
if (currentRes) {
|
|
1611
|
-
return { res: currentRes, json: currentJson || {} };
|
|
1612
|
-
}
|
|
1613
|
-
throw new Error(failureReason || "网络请求失败");
|
|
1614
|
-
}
|
|
1615
|
-
// 计算阶梯退避等待时间:baseDelayMs * 2^(attempt - 1) + Jitter 自适应微扰
|
|
1616
|
-
var exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1);
|
|
1617
|
-
var maxJitter = Math.min(200, Math.floor(exponentialDelay * 0.2));
|
|
1618
|
-
var jitter = Math.floor(Math.random() * (maxJitter + 1));
|
|
1619
|
-
var totalDelay = exponentialDelay + jitter;
|
|
1620
|
-
if (config.onRetry) {
|
|
1621
|
-
config.onRetry(attempt, totalDelay, failureReason);
|
|
1622
|
-
}
|
|
1623
|
-
// 可中断的睡眠等待
|
|
1624
|
-
await new Promise((resolve, reject) => {
|
|
1625
|
-
var timer = setTimeout(resolve, totalDelay);
|
|
1626
|
-
if (signal) {
|
|
1627
|
-
signal.addEventListener("abort", () => {
|
|
1628
|
-
clearTimeout(timer);
|
|
1629
|
-
reject(new Error("Aborted"));
|
|
1630
|
-
}, { once: true });
|
|
1631
|
-
}
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1547
|
+
body[data-ds-dark-theme] .dsh-github-repo-dropdown {
|
|
1548
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1549
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
1550
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
|
|
1634
1551
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1552
|
+
.dsh-github-repo-dropdown-header {
|
|
1553
|
+
display: flex;
|
|
1554
|
+
justify-content: space-between;
|
|
1555
|
+
align-items: center;
|
|
1556
|
+
padding: 8px 12px;
|
|
1557
|
+
font-size: 11px;
|
|
1558
|
+
font-weight: 600;
|
|
1559
|
+
background: var(--dsw-alias-bg-layer-1, #f6f8fa);
|
|
1560
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1561
|
+
color: var(--dsw-alias-label-secondary, #57606a);
|
|
1562
|
+
}
|
|
1563
|
+
body[data-ds-dark-theme] .dsh-github-repo-dropdown-header {
|
|
1564
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1565
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
1566
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
1567
|
+
}
|
|
1568
|
+
.dsh-github-repo-dropdown-list {
|
|
1569
|
+
overflow-y: auto;
|
|
1570
|
+
max-height: 260px;
|
|
1571
|
+
padding: 4px 0;
|
|
1572
|
+
}
|
|
1573
|
+
.dsh-github-repo-item {
|
|
1574
|
+
padding: 7px 12px;
|
|
1575
|
+
font-size: 12px;
|
|
1576
|
+
cursor: pointer;
|
|
1577
|
+
transition: background 0.12s ease;
|
|
1578
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1579
|
+
}
|
|
1580
|
+
.dsh-github-repo-item:hover {
|
|
1581
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
1582
|
+
}
|
|
1583
|
+
body[data-ds-dark-theme] .dsh-github-repo-item {
|
|
1584
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
1585
|
+
}
|
|
1586
|
+
body[data-ds-dark-theme] .dsh-github-repo-item:hover {
|
|
1587
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.06));
|
|
1588
|
+
}
|
|
1589
|
+
.dsh-github-repo-item.selected {
|
|
1590
|
+
background: rgba(9, 105, 218, 0.08);
|
|
1591
|
+
}
|
|
1592
|
+
body[data-ds-dark-theme] .dsh-github-repo-item.selected {
|
|
1593
|
+
background: rgba(56, 139, 253, 0.15);
|
|
1594
|
+
}
|
|
1595
|
+
.dsh-github-repo-dropdown-footer {
|
|
1596
|
+
padding: 6px 10px;
|
|
1597
|
+
font-size: 10px;
|
|
1598
|
+
background: var(--dsw-alias-bg-layer-1, #f6f8fa);
|
|
1599
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1600
|
+
color: var(--dsw-alias-label-tertiary, #656d76);
|
|
1601
|
+
line-height: 1.4;
|
|
1602
|
+
}
|
|
1603
|
+
body[data-ds-dark-theme] .dsh-github-repo-dropdown-footer {
|
|
1604
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1605
|
+
border-top-color: var(--dsw-alias-border-l1, #30363d);
|
|
1606
|
+
color: var(--dsw-alias-label-tertiary, #8b949e);
|
|
1607
|
+
}
|
|
1608
|
+
.dsh-github-switch-quicklink {
|
|
1609
|
+
font-size: 11px;
|
|
1610
|
+
cursor: pointer;
|
|
1611
|
+
color: var(--dsw-alias-link, #0969da);
|
|
1612
|
+
text-decoration: none;
|
|
1613
|
+
font-weight: 500;
|
|
1614
|
+
display: inline-flex;
|
|
1615
|
+
align-items: center;
|
|
1616
|
+
gap: 3px;
|
|
1617
|
+
margin-left: 6px;
|
|
1618
|
+
padding: 1px 6px;
|
|
1619
|
+
border-radius: 4px;
|
|
1620
|
+
background: rgba(9, 105, 218, 0.08);
|
|
1621
|
+
border: 1px solid rgba(9, 105, 218, 0.2);
|
|
1622
|
+
}
|
|
1623
|
+
.dsh-github-switch-quicklink:hover {
|
|
1624
|
+
text-decoration: none;
|
|
1625
|
+
background: rgba(9, 105, 218, 0.16);
|
|
1626
|
+
border-color: rgba(9, 105, 218, 0.4);
|
|
1627
|
+
}
|
|
1628
|
+
body[data-ds-dark-theme] .dsh-github-switch-quicklink {
|
|
1629
|
+
color: #58a6ff;
|
|
1630
|
+
background: rgba(56, 139, 253, 0.15);
|
|
1631
|
+
border-color: rgba(56, 139, 253, 0.3);
|
|
1632
|
+
}
|
|
1633
|
+
body[data-ds-dark-theme] .dsh-github-switch-quicklink:hover {
|
|
1634
|
+
background: rgba(56, 139, 253, 0.25);
|
|
1635
|
+
border-color: rgba(56, 139, 253, 0.5);
|
|
1636
|
+
}
|
|
1637
|
+
.dsh-github-pr-modal-dialog {
|
|
1638
|
+
width: 95% !important;
|
|
1639
|
+
max-width: 1040px !important;
|
|
1640
|
+
height: 88vh !important;
|
|
1641
|
+
max-height: 88vh !important;
|
|
1642
|
+
display: flex !important;
|
|
1643
|
+
flex-direction: column !important;
|
|
1644
|
+
overflow: hidden !important;
|
|
1645
|
+
padding: 0 !important;
|
|
1646
|
+
}
|
|
1647
|
+
.dsh-github-pr-modal-header {
|
|
1648
|
+
padding: 12px 16px !important;
|
|
1649
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1650
|
+
background: var(--dsw-alias-bg-layer-1, #f6f8fa);
|
|
1651
|
+
flex-shrink: 0;
|
|
1652
|
+
}
|
|
1653
|
+
body[data-ds-dark-theme] .dsh-github-pr-modal-header {
|
|
1654
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
1655
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1656
|
+
}
|
|
1657
|
+
.dsh-github-pr-modal-tabs {
|
|
1658
|
+
display: flex;
|
|
1659
|
+
align-items: center;
|
|
1660
|
+
gap: 4px;
|
|
1661
|
+
padding: 6px 16px;
|
|
1662
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1663
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1664
|
+
flex-shrink: 0;
|
|
1665
|
+
}
|
|
1666
|
+
body[data-ds-dark-theme] .dsh-github-pr-modal-tabs {
|
|
1667
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1668
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
1669
|
+
}
|
|
1670
|
+
.dsh-github-pr-tab-btn {
|
|
1671
|
+
padding: 5px 12px;
|
|
1672
|
+
font-size: 12px;
|
|
1673
|
+
font-weight: 500;
|
|
1674
|
+
border-radius: 6px;
|
|
1675
|
+
border: 1px solid transparent;
|
|
1676
|
+
background: transparent;
|
|
1677
|
+
color: var(--dsw-alias-label-secondary, #656d76);
|
|
1678
|
+
cursor: pointer;
|
|
1679
|
+
transition: all 0.15s ease;
|
|
1680
|
+
}
|
|
1681
|
+
.dsh-github-pr-tab-btn:hover {
|
|
1682
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
1683
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1684
|
+
}
|
|
1685
|
+
.dsh-github-pr-tab-btn.active {
|
|
1686
|
+
background: var(--dsw-alias-interactive-bg-active, rgba(9, 105, 218, 0.1));
|
|
1687
|
+
color: var(--dsw-alias-link, #0969da);
|
|
1688
|
+
font-weight: 600;
|
|
1689
|
+
border-color: rgba(9, 105, 218, 0.25);
|
|
1690
|
+
}
|
|
1691
|
+
body[data-ds-dark-theme] .dsh-github-pr-tab-btn {
|
|
1692
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
1693
|
+
}
|
|
1694
|
+
body[data-ds-dark-theme] .dsh-github-pr-tab-btn:hover {
|
|
1695
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1696
|
+
color: #f0f6fc;
|
|
1697
|
+
}
|
|
1698
|
+
body[data-ds-dark-theme] .dsh-github-pr-tab-btn.active {
|
|
1699
|
+
background: rgba(56, 139, 253, 0.15);
|
|
1700
|
+
color: #58a6ff;
|
|
1701
|
+
border-color: rgba(56, 139, 253, 0.4);
|
|
1702
|
+
}
|
|
1703
|
+
.dsh-github-pr-conversation-pane {
|
|
1704
|
+
padding: 16px !important;
|
|
1705
|
+
overflow-y: auto !important;
|
|
1706
|
+
flex: 1;
|
|
1707
|
+
min-height: 0;
|
|
1708
|
+
display: flex;
|
|
1709
|
+
flex-direction: column;
|
|
1710
|
+
gap: 12px;
|
|
1711
|
+
overscroll-behavior: contain;
|
|
1712
|
+
}
|
|
1713
|
+
.dsh-github-pr-checks-bar {
|
|
1714
|
+
display: flex;
|
|
1715
|
+
align-items: center;
|
|
1716
|
+
justify-content: space-between;
|
|
1717
|
+
padding: 8px 12px;
|
|
1718
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
1719
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1720
|
+
border-radius: 6px;
|
|
1721
|
+
font-size: 11px;
|
|
1722
|
+
}
|
|
1723
|
+
body[data-ds-dark-theme] .dsh-github-pr-checks-bar {
|
|
1724
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1725
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
1726
|
+
}
|
|
1727
|
+
.dsh-github-pr-body-card {
|
|
1728
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1729
|
+
border-radius: 6px;
|
|
1730
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1731
|
+
overflow: hidden;
|
|
1732
|
+
flex: 0 0 auto;
|
|
1733
|
+
}
|
|
1734
|
+
body[data-ds-dark-theme] .dsh-github-pr-body-card {
|
|
1735
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
1736
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1737
|
+
}
|
|
1738
|
+
.dsh-github-pr-comment-card {
|
|
1739
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1740
|
+
border-radius: 6px;
|
|
1741
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1742
|
+
overflow: hidden;
|
|
1743
|
+
margin-bottom: 10px;
|
|
1744
|
+
flex: 0 0 auto;
|
|
1745
|
+
}
|
|
1746
|
+
body[data-ds-dark-theme] .dsh-github-pr-comment-card {
|
|
1747
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
1748
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1749
|
+
}
|
|
1750
|
+
.dsh-github-pr-comment-header {
|
|
1751
|
+
display: flex;
|
|
1752
|
+
justify-content: space-between;
|
|
1753
|
+
align-items: center;
|
|
1754
|
+
padding: 6px 12px;
|
|
1755
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
1756
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1757
|
+
font-size: 11px;
|
|
1758
|
+
}
|
|
1759
|
+
body[data-ds-dark-theme] .dsh-github-pr-comment-header {
|
|
1760
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1761
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
1762
|
+
}
|
|
1763
|
+
.dsh-github-pr-files-pane {
|
|
1764
|
+
display: flex;
|
|
1765
|
+
flex: 1;
|
|
1766
|
+
min-height: 0;
|
|
1767
|
+
overflow: hidden;
|
|
1768
|
+
}
|
|
1769
|
+
.dsh-github-pr-file-list {
|
|
1770
|
+
width: 280px;
|
|
1771
|
+
flex-shrink: 0;
|
|
1772
|
+
border-right: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1773
|
+
background: var(--dsw-alias-bg-layer-1, #f6f8fa);
|
|
1774
|
+
overflow-y: auto;
|
|
1775
|
+
padding: 6px 0;
|
|
1776
|
+
}
|
|
1777
|
+
body[data-ds-dark-theme] .dsh-github-pr-file-list {
|
|
1778
|
+
border-right-color: var(--dsw-alias-border-l1, #30363d);
|
|
1779
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1780
|
+
}
|
|
1781
|
+
.dsh-github-pr-splitter {
|
|
1782
|
+
position: relative;
|
|
1783
|
+
flex-shrink: 0;
|
|
1784
|
+
width: 6px;
|
|
1785
|
+
cursor: col-resize;
|
|
1786
|
+
background: var(--dsw-alias-border-l2, #e1e4e8);
|
|
1787
|
+
transition: background 0.15s ease;
|
|
1788
|
+
display: flex;
|
|
1789
|
+
align-items: center;
|
|
1790
|
+
justify-content: center;
|
|
1791
|
+
}
|
|
1792
|
+
.dsh-github-pr-splitter:hover {
|
|
1793
|
+
background: var(--dsw-alias-link, #0969da);
|
|
1794
|
+
}
|
|
1795
|
+
body[data-ds-dark-theme] .dsh-github-pr-splitter {
|
|
1796
|
+
background: var(--dsw-alias-border-l1, #30363d);
|
|
1797
|
+
}
|
|
1798
|
+
body[data-ds-dark-theme] .dsh-github-pr-splitter:hover {
|
|
1799
|
+
background: #58a6ff;
|
|
1800
|
+
}
|
|
1801
|
+
.dsh-github-pr-splitter-toggle {
|
|
1802
|
+
position: absolute;
|
|
1803
|
+
top: 50%;
|
|
1804
|
+
transform: translateY(-50%);
|
|
1805
|
+
width: 16px;
|
|
1806
|
+
height: 32px;
|
|
1807
|
+
padding: 0;
|
|
1808
|
+
border: 1px solid var(--dsw-alias-border-l2, #d0d7de);
|
|
1809
|
+
border-radius: 3px;
|
|
1810
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1811
|
+
color: var(--dsw-alias-label-secondary, #656d76);
|
|
1812
|
+
font-size: 8px;
|
|
1813
|
+
line-height: 1;
|
|
1814
|
+
cursor: pointer;
|
|
1815
|
+
display: flex;
|
|
1816
|
+
align-items: center;
|
|
1817
|
+
justify-content: center;
|
|
1818
|
+
z-index: 2;
|
|
1819
|
+
}
|
|
1820
|
+
.dsh-github-pr-splitter-toggle:hover {
|
|
1821
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.06));
|
|
1822
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1823
|
+
}
|
|
1824
|
+
body[data-ds-dark-theme] .dsh-github-pr-splitter-toggle {
|
|
1825
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
1826
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1827
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
1828
|
+
}
|
|
1829
|
+
body[data-ds-dark-theme] .dsh-github-pr-splitter-toggle:hover {
|
|
1830
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1831
|
+
color: #f0f6fc;
|
|
1832
|
+
}
|
|
1833
|
+
.dsh-github-pr-title-row {
|
|
1834
|
+
display: flex;
|
|
1835
|
+
align-items: center;
|
|
1836
|
+
gap: 10px;
|
|
1837
|
+
width: 100%;
|
|
1838
|
+
min-width: 0;
|
|
1839
|
+
}
|
|
1840
|
+
.dsh-github-pr-title-row .dsh-github-item-title {
|
|
1841
|
+
flex: 0 1 auto;
|
|
1842
|
+
min-width: 0;
|
|
1843
|
+
white-space: nowrap;
|
|
1844
|
+
overflow: hidden;
|
|
1845
|
+
text-overflow: ellipsis;
|
|
1846
|
+
text-decoration: none;
|
|
1847
|
+
}
|
|
1848
|
+
.dsh-github-pr-gh-btn {
|
|
1849
|
+
flex-shrink: 0;
|
|
1850
|
+
margin-left: auto;
|
|
1851
|
+
display: inline-flex;
|
|
1852
|
+
align-items: center;
|
|
1853
|
+
justify-content: center;
|
|
1854
|
+
gap: 4px;
|
|
1855
|
+
font-size: 12px;
|
|
1856
|
+
font-weight: 500;
|
|
1857
|
+
line-height: 18px;
|
|
1858
|
+
padding: 3px 10px;
|
|
1859
|
+
border-radius: 6px;
|
|
1860
|
+
border: 1px solid var(--dsw-alias-border-l2, #d0d7de);
|
|
1861
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
1862
|
+
color: var(--dsw-alias-label-secondary, #57606a);
|
|
1863
|
+
text-decoration: none;
|
|
1864
|
+
white-space: nowrap;
|
|
1865
|
+
transition: all 0.15s ease;
|
|
1866
|
+
}
|
|
1867
|
+
.dsh-github-pr-gh-btn:hover {
|
|
1868
|
+
color: var(--dsw-alias-link, #0969da);
|
|
1869
|
+
border-color: var(--dsw-alias-link, #0969da);
|
|
1870
|
+
background: var(--dsw-alias-interactive-bg-hover, #eaeef2);
|
|
1871
|
+
}
|
|
1872
|
+
body[data-ds-dark-theme] .dsh-github-pr-gh-btn {
|
|
1873
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
1874
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1875
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
1876
|
+
}
|
|
1877
|
+
body[data-ds-dark-theme] .dsh-github-pr-gh-btn:hover {
|
|
1878
|
+
color: #58a6ff;
|
|
1879
|
+
border-color: #58a6ff;
|
|
1880
|
+
background: rgba(56, 139, 253, 0.15);
|
|
1881
|
+
}
|
|
1882
|
+
.dsh-github-pr-file-item {
|
|
1883
|
+
display: flex;
|
|
1884
|
+
align-items: center;
|
|
1885
|
+
justify-content: space-between;
|
|
1886
|
+
gap: 8px;
|
|
1887
|
+
padding: 6px 12px;
|
|
1888
|
+
font-size: 11px;
|
|
1889
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
1890
|
+
cursor: pointer;
|
|
1891
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1892
|
+
transition: background 0.12s ease;
|
|
1893
|
+
}
|
|
1894
|
+
.dsh-github-pr-file-item:hover {
|
|
1895
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
1896
|
+
}
|
|
1897
|
+
.dsh-github-pr-file-item.selected {
|
|
1898
|
+
background: rgba(9, 105, 218, 0.1);
|
|
1899
|
+
color: var(--dsw-alias-link, #0969da);
|
|
1900
|
+
font-weight: 600;
|
|
1901
|
+
}
|
|
1902
|
+
body[data-ds-dark-theme] .dsh-github-pr-file-item {
|
|
1903
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
1904
|
+
}
|
|
1905
|
+
body[data-ds-dark-theme] .dsh-github-pr-file-item:hover {
|
|
1906
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1907
|
+
}
|
|
1908
|
+
body[data-ds-dark-theme] .dsh-github-pr-file-item.selected {
|
|
1909
|
+
background: rgba(56, 139, 253, 0.18);
|
|
1910
|
+
color: #58a6ff;
|
|
1911
|
+
}
|
|
1912
|
+
.dsh-github-pr-diff-viewer {
|
|
1913
|
+
flex: 1;
|
|
1914
|
+
min-width: 0;
|
|
1915
|
+
display: flex;
|
|
1916
|
+
flex-direction: column;
|
|
1917
|
+
overflow: hidden;
|
|
1918
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1919
|
+
}
|
|
1920
|
+
body[data-ds-dark-theme] .dsh-github-pr-diff-viewer {
|
|
1921
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1922
|
+
}
|
|
1923
|
+
.dsh-github-pr-diff-toolbar {
|
|
1924
|
+
display: flex;
|
|
1925
|
+
align-items: center;
|
|
1926
|
+
justify-content: space-between;
|
|
1927
|
+
padding: 6px 12px;
|
|
1928
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1929
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
1930
|
+
flex-shrink: 0;
|
|
1931
|
+
}
|
|
1932
|
+
body[data-ds-dark-theme] .dsh-github-pr-diff-toolbar {
|
|
1933
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
1934
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1935
|
+
}
|
|
1936
|
+
.dsh-github-pr-line-comments {
|
|
1937
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
1938
|
+
padding: 10px 12px 14px;
|
|
1939
|
+
background: var(--dsw-alias-bg-layer-1, #f6f8fa);
|
|
1940
|
+
}
|
|
1941
|
+
body[data-ds-dark-theme] .dsh-github-pr-line-comments {
|
|
1942
|
+
border-top-color: var(--dsw-alias-border-l1, #30363d);
|
|
1943
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
1944
|
+
}
|
|
1945
|
+
.dsh-github-pr-line-comments-title {
|
|
1946
|
+
font-size: 11px;
|
|
1947
|
+
font-weight: 600;
|
|
1948
|
+
margin-bottom: 8px;
|
|
1949
|
+
color: var(--dsw-alias-label-secondary, #656d76);
|
|
1950
|
+
}
|
|
1951
|
+
body[data-ds-dark-theme] .dsh-github-pr-line-comments-title {
|
|
1952
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
1953
|
+
}
|
|
1954
|
+
.dsh-github-pr-line-comment-item {
|
|
1955
|
+
border-left: 3px solid rgba(130, 80, 223, 0.5);
|
|
1956
|
+
border-radius: 4px;
|
|
1957
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1958
|
+
padding: 8px 10px;
|
|
1959
|
+
margin-bottom: 8px;
|
|
1960
|
+
}
|
|
1961
|
+
body[data-ds-dark-theme] .dsh-github-pr-line-comment-item {
|
|
1962
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
1963
|
+
border-left-color: rgba(163, 113, 247, 0.6);
|
|
1964
|
+
}
|
|
1965
|
+
.dsh-github-pr-line-comment-head {
|
|
1966
|
+
display: flex;
|
|
1967
|
+
align-items: center;
|
|
1968
|
+
gap: 8px;
|
|
1969
|
+
flex-wrap: wrap;
|
|
1970
|
+
font-size: 11px;
|
|
1971
|
+
margin-bottom: 5px;
|
|
1972
|
+
}
|
|
1973
|
+
.dsh-github-pr-line-comment-body {
|
|
1974
|
+
font-size: 12px;
|
|
1975
|
+
line-height: 1.6;
|
|
1976
|
+
white-space: pre-wrap;
|
|
1977
|
+
word-break: break-word;
|
|
1978
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1979
|
+
}
|
|
1980
|
+
body[data-ds-dark-theme] .dsh-github-pr-line-comment-body {
|
|
1981
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
1982
|
+
}
|
|
1983
|
+
.dsh-github-md-body {
|
|
1984
|
+
padding: 12px;
|
|
1985
|
+
font-size: 12px;
|
|
1986
|
+
line-height: 1.7;
|
|
1987
|
+
word-break: break-word;
|
|
1988
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
1989
|
+
}
|
|
1990
|
+
body[data-ds-dark-theme] .dsh-github-md-body {
|
|
1991
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
1992
|
+
}
|
|
1993
|
+
.dsh-github-md-body .dsh-github-md-heading {
|
|
1994
|
+
margin: 14px 0 8px 0;
|
|
1995
|
+
font-weight: 600;
|
|
1996
|
+
line-height: 1.4;
|
|
1997
|
+
}
|
|
1998
|
+
.dsh-github-md-body h1.dsh-github-md-heading { font-size: 17px; }
|
|
1999
|
+
.dsh-github-md-body h2.dsh-github-md-heading { font-size: 15px; }
|
|
2000
|
+
.dsh-github-md-body h3.dsh-github-md-heading { font-size: 14px; }
|
|
2001
|
+
.dsh-github-md-body h4.dsh-github-md-heading { font-size: 13px; }
|
|
2002
|
+
.dsh-github-md-body h5.dsh-github-md-heading,
|
|
2003
|
+
.dsh-github-md-body h6.dsh-github-md-heading { font-size: 12px; opacity: 0.9; }
|
|
2004
|
+
.dsh-github-md-body > .dsh-github-md-heading:first-child {
|
|
2005
|
+
margin-top: 0;
|
|
2006
|
+
}
|
|
2007
|
+
.dsh-github-md-body .dsh-github-md-p {
|
|
2008
|
+
margin: 0 0 10px 0;
|
|
2009
|
+
}
|
|
2010
|
+
.dsh-github-md-body .dsh-github-md-p:last-child {
|
|
2011
|
+
margin-bottom: 0;
|
|
2012
|
+
}
|
|
2013
|
+
.dsh-github-md-body .dsh-github-md-inline-code {
|
|
2014
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
2015
|
+
font-size: 0.92em;
|
|
2016
|
+
padding: 1px 5px;
|
|
2017
|
+
border-radius: 4px;
|
|
2018
|
+
background: var(--dsw-alias-bg-subtle, rgba(0, 0, 0, 0.06));
|
|
2019
|
+
border: 1px solid var(--dsw-alias-border-subtle, rgba(0, 0, 0, 0.08));
|
|
2020
|
+
word-break: break-all;
|
|
2021
|
+
}
|
|
2022
|
+
body[data-ds-dark-theme] .dsh-github-md-body .dsh-github-md-inline-code {
|
|
2023
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2024
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
2025
|
+
}
|
|
2026
|
+
.dsh-github-md-body .dsh-github-md-pre {
|
|
2027
|
+
margin: 10px 0;
|
|
2028
|
+
padding: 10px 12px;
|
|
2029
|
+
border-radius: 6px;
|
|
2030
|
+
overflow-x: auto;
|
|
2031
|
+
max-height: 420px;
|
|
2032
|
+
background: var(--dsw-alias-bg-subtle, rgba(0, 0, 0, 0.04));
|
|
2033
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
2034
|
+
}
|
|
2035
|
+
body[data-ds-dark-theme] .dsh-github-md-body .dsh-github-md-pre {
|
|
2036
|
+
background: rgba(0, 0, 0, 0.3);
|
|
2037
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
2038
|
+
}
|
|
2039
|
+
.dsh-github-md-body .dsh-github-md-pre code {
|
|
2040
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
2041
|
+
font-size: 11px;
|
|
2042
|
+
line-height: 1.55;
|
|
2043
|
+
white-space: pre;
|
|
2044
|
+
display: block;
|
|
2045
|
+
}
|
|
2046
|
+
.dsh-github-md-body .dsh-github-md-list {
|
|
2047
|
+
margin: 8px 0;
|
|
2048
|
+
padding-left: 22px;
|
|
2049
|
+
}
|
|
2050
|
+
.dsh-github-md-body .dsh-github-md-list li {
|
|
2051
|
+
margin: 4px 0;
|
|
2052
|
+
}
|
|
2053
|
+
.dsh-github-md-body .dsh-github-md-task {
|
|
2054
|
+
list-style: none;
|
|
2055
|
+
margin-left: -18px;
|
|
2056
|
+
display: flex;
|
|
2057
|
+
align-items: flex-start;
|
|
2058
|
+
gap: 6px;
|
|
2059
|
+
}
|
|
2060
|
+
.dsh-github-md-body .dsh-github-md-checkbox {
|
|
2061
|
+
flex-shrink: 0;
|
|
2062
|
+
opacity: 0.8;
|
|
2063
|
+
}
|
|
2064
|
+
.dsh-github-md-body .dsh-github-md-quote {
|
|
2065
|
+
margin: 10px 0;
|
|
2066
|
+
padding: 6px 12px;
|
|
2067
|
+
border-left: 3px solid var(--dsw-alias-border-l3, #d0d7de);
|
|
2068
|
+
color: var(--dsw-alias-label-secondary, #656d76);
|
|
2069
|
+
background: var(--dsw-alias-bg-subtle, rgba(0, 0, 0, 0.02));
|
|
2070
|
+
border-radius: 0 4px 4px 0;
|
|
2071
|
+
}
|
|
2072
|
+
.dsh-github-md-body .dsh-github-md-hr {
|
|
2073
|
+
margin: 14px 0;
|
|
2074
|
+
border: none;
|
|
2075
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
2076
|
+
}
|
|
2077
|
+
body[data-ds-dark-theme] .dsh-github-md-body .dsh-github-md-hr {
|
|
2078
|
+
border-top-color: var(--dsw-alias-border-l1, #30363d);
|
|
2079
|
+
}
|
|
2080
|
+
.dsh-github-md-body .dsh-github-md-link {
|
|
2081
|
+
color: var(--dsw-alias-link, #0969da);
|
|
2082
|
+
text-decoration: none;
|
|
2083
|
+
}
|
|
2084
|
+
.dsh-github-md-body .dsh-github-md-link:hover {
|
|
2085
|
+
text-decoration: underline;
|
|
2086
|
+
}
|
|
2087
|
+
body[data-ds-dark-theme] .dsh-github-md-body .dsh-github-md-link {
|
|
2088
|
+
color: #58a6ff;
|
|
2089
|
+
}
|
|
2090
|
+
.dsh-github-md-expand-btn {
|
|
2091
|
+
display: inline-flex;
|
|
2092
|
+
align-items: center;
|
|
2093
|
+
gap: 4px;
|
|
2094
|
+
margin: 0 0 4px 12px;
|
|
2095
|
+
padding: 3px 8px;
|
|
2096
|
+
font-size: 11px;
|
|
2097
|
+
font-weight: 500;
|
|
2098
|
+
border-radius: 5px;
|
|
2099
|
+
border: 1px solid var(--dsw-alias-border-l2, #d0d7de);
|
|
2100
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
2101
|
+
color: var(--dsw-alias-label-secondary, #57606a);
|
|
2102
|
+
cursor: pointer;
|
|
2103
|
+
transition: all 0.15s ease;
|
|
2104
|
+
}
|
|
2105
|
+
.dsh-github-md-expand-btn:hover {
|
|
2106
|
+
color: var(--dsw-alias-link, #0969da);
|
|
2107
|
+
border-color: var(--dsw-alias-link, #0969da);
|
|
2108
|
+
}
|
|
2109
|
+
body[data-ds-dark-theme] .dsh-github-md-expand-btn {
|
|
2110
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
2111
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
2112
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
2113
|
+
}
|
|
2114
|
+
body[data-ds-dark-theme] .dsh-github-md-expand-btn:hover {
|
|
2115
|
+
color: #58a6ff;
|
|
2116
|
+
border-color: #58a6ff;
|
|
2117
|
+
}
|
|
2118
|
+
.dsh-github-issue-modal-dialog {
|
|
2119
|
+
width: 92% !important;
|
|
2120
|
+
max-width: 880px !important;
|
|
2121
|
+
height: 84vh !important;
|
|
2122
|
+
max-height: 84vh !important;
|
|
2123
|
+
display: flex !important;
|
|
2124
|
+
flex-direction: column !important;
|
|
2125
|
+
overflow: hidden !important;
|
|
2126
|
+
padding: 0 !important;
|
|
2127
|
+
}
|
|
2128
|
+
.dsh-github-issue-modal-header {
|
|
2129
|
+
padding: 12px 16px !important;
|
|
2130
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
2131
|
+
background: var(--dsw-alias-bg-layer-1, #f6f8fa);
|
|
2132
|
+
flex-shrink: 0;
|
|
2133
|
+
}
|
|
2134
|
+
body[data-ds-dark-theme] .dsh-github-issue-modal-header {
|
|
2135
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
2136
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
2137
|
+
}
|
|
2138
|
+
.dsh-github-issue-conversation-pane {
|
|
2139
|
+
padding: 16px !important;
|
|
2140
|
+
overflow-y: auto !important;
|
|
2141
|
+
flex: 1;
|
|
2142
|
+
min-height: 0;
|
|
2143
|
+
display: flex;
|
|
2144
|
+
flex-direction: column;
|
|
2145
|
+
gap: 12px;
|
|
2146
|
+
overscroll-behavior: contain;
|
|
2147
|
+
}
|
|
2148
|
+
.dsh-github-issue-meta {
|
|
2149
|
+
display: flex;
|
|
2150
|
+
flex-wrap: wrap;
|
|
2151
|
+
align-items: center;
|
|
2152
|
+
gap: 6px;
|
|
2153
|
+
flex: 0 0 auto;
|
|
2154
|
+
}
|
|
2155
|
+
.dsh-github-issue-chip {
|
|
2156
|
+
display: inline-flex;
|
|
2157
|
+
align-items: center;
|
|
2158
|
+
gap: 5px;
|
|
2159
|
+
padding: 2px 8px;
|
|
2160
|
+
border-radius: 999px;
|
|
2161
|
+
border: 1px solid var(--dsw-alias-border-l2, #d0d7de);
|
|
2162
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
2163
|
+
color: var(--dsw-alias-label-secondary, #57606a);
|
|
2164
|
+
font-size: 11px;
|
|
2165
|
+
line-height: 18px;
|
|
2166
|
+
white-space: nowrap;
|
|
2167
|
+
max-width: 100%;
|
|
2168
|
+
overflow: hidden;
|
|
2169
|
+
text-overflow: ellipsis;
|
|
2170
|
+
}
|
|
2171
|
+
.dsh-github-issue-label-chip {
|
|
2172
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
2173
|
+
}
|
|
2174
|
+
.dsh-github-issue-chip-overdue {
|
|
2175
|
+
border-color: rgba(207, 34, 46, 0.5);
|
|
2176
|
+
color: var(--dsw-alias-state-error-primary, #cf222e);
|
|
2177
|
+
}
|
|
2178
|
+
body[data-ds-dark-theme] .dsh-github-issue-chip-overdue {
|
|
2179
|
+
border-color: rgba(248, 81, 73, 0.5);
|
|
2180
|
+
color: #f85149;
|
|
2181
|
+
}
|
|
2182
|
+
.dsh-github-issue-label-dot {
|
|
2183
|
+
width: 7px;
|
|
2184
|
+
height: 7px;
|
|
2185
|
+
border-radius: 50%;
|
|
2186
|
+
flex-shrink: 0;
|
|
2187
|
+
}
|
|
2188
|
+
.dsh-github-issue-chip-btn,
|
|
2189
|
+
.dsh-github-issue-chip-link {
|
|
2190
|
+
cursor: pointer;
|
|
2191
|
+
text-decoration: none;
|
|
2192
|
+
font-family: inherit;
|
|
2193
|
+
transition: border-color 0.15s ease, color 0.15s ease;
|
|
2194
|
+
}
|
|
2195
|
+
.dsh-github-issue-chip-btn:hover,
|
|
2196
|
+
.dsh-github-issue-chip-link:hover {
|
|
2197
|
+
color: var(--dsw-alias-link, #0969da);
|
|
2198
|
+
border-color: var(--dsw-alias-link, #0969da);
|
|
2199
|
+
}
|
|
2200
|
+
body[data-ds-dark-theme] .dsh-github-issue-chip {
|
|
2201
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
2202
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
2203
|
+
color: var(--dsw-alias-label-secondary, #8b949e);
|
|
2204
|
+
}
|
|
2205
|
+
body[data-ds-dark-theme] .dsh-github-issue-label-chip {
|
|
2206
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
2207
|
+
}
|
|
2208
|
+
body[data-ds-dark-theme] .dsh-github-issue-chip-btn:hover,
|
|
2209
|
+
body[data-ds-dark-theme] .dsh-github-issue-chip-link:hover {
|
|
2210
|
+
color: #58a6ff;
|
|
2211
|
+
border-color: #58a6ff;
|
|
2212
|
+
}
|
|
2213
|
+
.dsh-github-issue-image-badge {
|
|
2214
|
+
display: inline-flex;
|
|
2215
|
+
flex-direction: column;
|
|
2216
|
+
gap: 6px;
|
|
2217
|
+
max-width: 100%;
|
|
2218
|
+
}
|
|
2219
|
+
.dsh-github-issue-image-links {
|
|
2220
|
+
display: flex;
|
|
2221
|
+
flex-direction: column;
|
|
2222
|
+
gap: 3px;
|
|
2223
|
+
padding: 6px 8px;
|
|
2224
|
+
border-radius: 6px;
|
|
2225
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
2226
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
2227
|
+
}
|
|
2228
|
+
body[data-ds-dark-theme] .dsh-github-issue-image-links {
|
|
2229
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
2230
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
2231
|
+
}
|
|
2232
|
+
.dsh-github-issue-image-link {
|
|
2233
|
+
font-size: 11px;
|
|
2234
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
2235
|
+
color: var(--dsw-alias-link, #0969da);
|
|
2236
|
+
text-decoration: none;
|
|
2237
|
+
overflow: hidden;
|
|
2238
|
+
text-overflow: ellipsis;
|
|
2239
|
+
white-space: nowrap;
|
|
2240
|
+
}
|
|
2241
|
+
.dsh-github-issue-image-link:hover {
|
|
2242
|
+
text-decoration: underline;
|
|
2243
|
+
}
|
|
2244
|
+
.dsh-github-issue-body-card,
|
|
2245
|
+
.dsh-github-issue-comment-card {
|
|
2246
|
+
border: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
2247
|
+
border-radius: 6px;
|
|
2248
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
2249
|
+
overflow: hidden;
|
|
2250
|
+
flex: 0 0 auto;
|
|
2251
|
+
}
|
|
2252
|
+
.dsh-github-issue-comment-card {
|
|
2253
|
+
margin-bottom: 10px;
|
|
2254
|
+
}
|
|
2255
|
+
body[data-ds-dark-theme] .dsh-github-issue-body-card,
|
|
2256
|
+
body[data-ds-dark-theme] .dsh-github-issue-comment-card {
|
|
2257
|
+
border-color: var(--dsw-alias-border-l1, #30363d);
|
|
2258
|
+
background: var(--dsw-alias-bg-layer-2, #1e1e22);
|
|
2259
|
+
}
|
|
2260
|
+
.dsh-github-issue-comment-header {
|
|
2261
|
+
display: flex;
|
|
2262
|
+
justify-content: space-between;
|
|
2263
|
+
align-items: center;
|
|
2264
|
+
gap: 8px;
|
|
2265
|
+
padding: 6px 12px;
|
|
2266
|
+
background: var(--dsw-alias-bg-module-platform, #f6f8fa);
|
|
2267
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #e1e4e8);
|
|
2268
|
+
font-size: 11px;
|
|
2269
|
+
}
|
|
2270
|
+
body[data-ds-dark-theme] .dsh-github-issue-comment-header {
|
|
2271
|
+
background: var(--dsw-alias-bg-layer-1, #16161a);
|
|
2272
|
+
border-bottom-color: var(--dsw-alias-border-l1, #30363d);
|
|
2273
|
+
}
|
|
2274
|
+
.dsh-github-issue-body-content {
|
|
2275
|
+
padding: 0;
|
|
2276
|
+
}
|
|
2277
|
+
.dsh-github-issue-timeline-title {
|
|
2278
|
+
margin: 4px 0 0 0;
|
|
2279
|
+
font-size: 13px;
|
|
2280
|
+
font-weight: 600;
|
|
2281
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
2282
|
+
flex: 0 0 auto;
|
|
2283
|
+
}
|
|
2284
|
+
body[data-ds-dark-theme] .dsh-github-issue-timeline-title {
|
|
2285
|
+
color: var(--dsw-alias-label-primary, #f0f6fc);
|
|
2286
|
+
}
|
|
2287
|
+
.dsh-github-issue-empty-timeline {
|
|
2288
|
+
padding: 16px 0;
|
|
2289
|
+
text-align: center;
|
|
2290
|
+
font-size: 12px;
|
|
2291
|
+
opacity: 0.6;
|
|
2292
|
+
flex: 0 0 auto;
|
|
2293
|
+
}
|
|
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();
|
|
2307
|
+
}
|
|
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");
|
|
2333
|
+
}
|
|
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";
|
|
2353
|
+
}
|
|
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;
|
|
2363
|
+
}
|
|
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 || "";
|
|
2375
|
+
}
|
|
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) => {
|
|
2463
|
+
var timer = setTimeout(resolve, totalDelay);
|
|
2464
|
+
if (signal) {
|
|
2465
|
+
signal.addEventListener("abort", () => {
|
|
2466
|
+
clearTimeout(timer);
|
|
2467
|
+
reject(new Error("Aborted"));
|
|
2468
|
+
}, { once: true });
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
function parseGitStatText(statText) {
|
|
2474
|
+
if (!statText || typeof statText !== "string")
|
|
2475
|
+
return null;
|
|
2476
|
+
var lines = statText.split("\n");
|
|
2477
|
+
var files = [];
|
|
2478
|
+
var summary = null;
|
|
2479
|
+
for (var i = 0; i < lines.length; i++) {
|
|
2480
|
+
var line = lines[i].trim();
|
|
2481
|
+
if (!line)
|
|
2482
|
+
continue;
|
|
2483
|
+
var summaryMatch = line.match(/(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/i);
|
|
2484
|
+
if (summaryMatch) {
|
|
2485
|
+
summary = {
|
|
2486
|
+
filesChanged: parseInt(summaryMatch[1], 10),
|
|
2487
|
+
insertions: parseInt(summaryMatch[2] || "0", 10),
|
|
2488
|
+
deletions: parseInt(summaryMatch[3] || "0", 10),
|
|
2489
|
+
};
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
var pipeIdx = line.indexOf("|");
|
|
2493
|
+
if (pipeIdx > 0) {
|
|
2494
|
+
var left = line.slice(0, pipeIdx).trim();
|
|
2495
|
+
var right = line.slice(pipeIdx + 1).trim();
|
|
2496
|
+
if (left.startsWith('"') && left.endsWith('"') && left.length >= 2) {
|
|
2497
|
+
left = left.slice(1, -1);
|
|
2498
|
+
}
|
|
2499
|
+
if (/^Bin\b/i.test(right)) {
|
|
2500
|
+
files.push({ path: left, total: 0, additions: 0, deletions: 0, isBinary: true, rawRight: right });
|
|
2501
|
+
}
|
|
2502
|
+
else {
|
|
2503
|
+
var numMatch = right.match(/^(\d+)(.*)$/);
|
|
2504
|
+
if (numMatch) {
|
|
2505
|
+
var total = parseInt(numMatch[1], 10);
|
|
2506
|
+
var graph = numMatch[2].trim();
|
|
2507
|
+
var plusCount = 0;
|
|
2508
|
+
var minusCount = 0;
|
|
2509
|
+
for (var j = 0; j < graph.length; j++) {
|
|
2510
|
+
if (graph[j] === "+")
|
|
2511
|
+
plusCount++;
|
|
2512
|
+
else if (graph[j] === "-")
|
|
2513
|
+
minusCount++;
|
|
2514
|
+
}
|
|
2515
|
+
var additions = 0;
|
|
2516
|
+
var deletions = 0;
|
|
1679
2517
|
if (plusCount === 0 && minusCount === 0) {
|
|
1680
2518
|
additions = total;
|
|
1681
2519
|
}
|
|
@@ -1769,6 +2607,13 @@ function renderPluginBadge(hFactory) {
|
|
|
1769
2607
|
*/
|
|
1770
2608
|
const GLOBAL_OVERVIEW_CACHE_KEY = "dsh_github_global_overview_snapshot_v1";
|
|
1771
2609
|
const SYNC_SETTINGS_STORAGE_KEY = "dsh_github_sync_settings_v1";
|
|
2610
|
+
/** PR 审阅弹窗文件区布局偏好(宽度 + 折叠态) */
|
|
2611
|
+
const PR_FILE_PANE_STORAGE_KEY = "dsh_github_pr_file_pane_v1";
|
|
2612
|
+
/** 文件区默认宽度与边界(px)。下限保证路径可读,上限由容器宽度动态收敛 */
|
|
2613
|
+
const PR_FILE_PANE_DEFAULT_WIDTH = 280;
|
|
2614
|
+
const PR_FILE_PANE_MIN_WIDTH = 160;
|
|
2615
|
+
/** 文件区最多占容器宽度的比例——再宽就没地方看 diff 了 */
|
|
2616
|
+
const PR_FILE_PANE_MAX_RATIO = 0.6;
|
|
1772
2617
|
const DEFAULT_SYNC_SETTINGS = {
|
|
1773
2618
|
strategy: "swr", // 'swr' | 'auto' | 'manual'
|
|
1774
2619
|
cloudSyncIntervalMs: 600000, // 云端定时周期:默认 10 分钟 (600,000ms,下限 5分钟)
|
|
@@ -1922,6 +2767,138 @@ function saveStoredHeaderCardsVisible(visible) {
|
|
|
1922
2767
|
}
|
|
1923
2768
|
catch (e) { }
|
|
1924
2769
|
}
|
|
2770
|
+
/** 右侧栏 PR / Issue 状态筛选偏好(按 cwd 隔离,不同仓库各自记住选择) */
|
|
2771
|
+
const RIGHTBAR_STATE_FILTER_KEY = "dsh_github_rightbar_state_filter_v1";
|
|
2772
|
+
/** 合法筛选值;任何非法输入一律回落 all,绝不让脏数据把列表筛成空白 */
|
|
2773
|
+
const PR_STATE_FILTERS = ["all", "open", "merged", "closed"];
|
|
2774
|
+
const ISSUE_STATE_FILTERS = ["all", "open", "closed"];
|
|
2775
|
+
function sanitizeFilter(value, allowed) {
|
|
2776
|
+
var v = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
2777
|
+
return allowed.indexOf(v) >= 0 ? v : "all";
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* 把 cwd 映射为存储作用域键。
|
|
2781
|
+
*
|
|
2782
|
+
* 为什么不能只用 `replace(/[^a-zA-Z0-9_-]/g, "_")`:那是把不同路径**压成同一个键**
|
|
2783
|
+
* (实测 `/a/b` 与 `/a_b` 都会变成 `_a_b`),两个仓库的筛选会互相覆盖。
|
|
2784
|
+
* 这里改为「可读前缀 + 全路径哈希后缀」:既保留排查时可辨认的目录名,
|
|
2785
|
+
* 又让任意两条路径都落在不同键上。
|
|
2786
|
+
*
|
|
2787
|
+
* 散列只用于去重,不需要密码学强度;FNV-1a 足够且零依赖。
|
|
2788
|
+
*/
|
|
2789
|
+
function scopeKeyForCwd(cwd) {
|
|
2790
|
+
var raw = typeof cwd === "string" && cwd ? cwd : "default";
|
|
2791
|
+
var hash = 0x811c9dc5;
|
|
2792
|
+
for (var i = 0; i < raw.length; i++) {
|
|
2793
|
+
hash ^= raw.charCodeAt(i);
|
|
2794
|
+
// FNV-1a 32 位:Math.imul 保证不丢精度
|
|
2795
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
2796
|
+
}
|
|
2797
|
+
var readable = raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(-24);
|
|
2798
|
+
return readable + "~" + hash.toString(36);
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* 读取右侧栏 PR / Issue 的状态筛选偏好。
|
|
2802
|
+
*
|
|
2803
|
+
* 为什么按 cwd 分开存:同一个浏览器可能同时开着多个工作区,
|
|
2804
|
+
* PR 集中在 A 仓库、Issue 集中在 B 仓库时,用户对两者的关注档位不同,
|
|
2805
|
+
* 共用一个 key 会导致切仓库后筛选被另一个仓库的选择覆盖。
|
|
2806
|
+
*/
|
|
2807
|
+
function getStoredRightbarStateFilter(cwd) {
|
|
2808
|
+
var fallback = { pr: "all", issue: "all" };
|
|
2809
|
+
try {
|
|
2810
|
+
var raw = localStorage.getItem(RIGHTBAR_STATE_FILTER_KEY);
|
|
2811
|
+
if (!raw)
|
|
2812
|
+
return fallback;
|
|
2813
|
+
var parsed = JSON.parse(raw);
|
|
2814
|
+
if (!parsed || typeof parsed !== "object")
|
|
2815
|
+
return fallback;
|
|
2816
|
+
var entry = parsed[scopeKeyForCwd(cwd)];
|
|
2817
|
+
if (!entry || typeof entry !== "object")
|
|
2818
|
+
return fallback;
|
|
2819
|
+
return {
|
|
2820
|
+
pr: sanitizeFilter(entry.pr, PR_STATE_FILTERS),
|
|
2821
|
+
issue: sanitizeFilter(entry.issue, ISSUE_STATE_FILTERS),
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
catch (e) {
|
|
2825
|
+
return fallback;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
/** 保存右侧栏状态筛选偏好(写入失败静默——纯 UI 偏好不值得弹错) */
|
|
2829
|
+
function saveStoredRightbarStateFilter(cwd, next) {
|
|
2830
|
+
try {
|
|
2831
|
+
var raw = localStorage.getItem(RIGHTBAR_STATE_FILTER_KEY);
|
|
2832
|
+
var parsed = {};
|
|
2833
|
+
if (raw) {
|
|
2834
|
+
try {
|
|
2835
|
+
var p = JSON.parse(raw);
|
|
2836
|
+
if (p && typeof p === "object")
|
|
2837
|
+
parsed = p;
|
|
2838
|
+
}
|
|
2839
|
+
catch (e) {
|
|
2840
|
+
// 历史脏数据:整体重置,避免一条坏记录永久污染全部仓库的筛选
|
|
2841
|
+
parsed = {};
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
var scope = scopeKeyForCwd(cwd);
|
|
2845
|
+
var prev = parsed[scope] && typeof parsed[scope] === "object" ? parsed[scope] : {};
|
|
2846
|
+
parsed[scope] = {
|
|
2847
|
+
pr: sanitizeFilter(next.pr !== undefined ? next.pr : prev.pr, PR_STATE_FILTERS),
|
|
2848
|
+
issue: sanitizeFilter(next.issue !== undefined ? next.issue : prev.issue, ISSUE_STATE_FILTERS),
|
|
2849
|
+
};
|
|
2850
|
+
localStorage.setItem(RIGHTBAR_STATE_FILTER_KEY, JSON.stringify(parsed));
|
|
2851
|
+
}
|
|
2852
|
+
catch (e) { }
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* 把期望宽度收敛到合法区间。
|
|
2856
|
+
*
|
|
2857
|
+
* 抽成纯函数的原因:边界逻辑(未超限/低于下限/超过上限)是被单测断言的规则,
|
|
2858
|
+
* 埋在事件回调里就只能靠人工点点看。dnd 拖拽最容易出的 bug 就是能拖成负数
|
|
2859
|
+
* 或把 diff 区域挤成 0 宽度——这里一次性挡住。
|
|
2860
|
+
*
|
|
2861
|
+
* @param next 拖拽得到的候选宽度
|
|
2862
|
+
* @param containerWidth 容器实际宽度(未知时传 0,此时只做下限保护)
|
|
2863
|
+
*/
|
|
2864
|
+
function clampFilePaneWidth(next, containerWidth) {
|
|
2865
|
+
const min = PR_FILE_PANE_MIN_WIDTH;
|
|
2866
|
+
const max = containerWidth > 0
|
|
2867
|
+
? Math.max(min, Math.floor(containerWidth * PR_FILE_PANE_MAX_RATIO))
|
|
2868
|
+
: Number.MAX_SAFE_INTEGER;
|
|
2869
|
+
const value = Number.isFinite(next) ? Math.round(next) : PR_FILE_PANE_DEFAULT_WIDTH;
|
|
2870
|
+
return Math.min(Math.max(value, min), max);
|
|
2871
|
+
}
|
|
2872
|
+
/** 读取文件区偏好;任何异常或脏数据都退回默认值,绝不因偏好损坏打挂弹窗 */
|
|
2873
|
+
function getPrFilePanePrefs() {
|
|
2874
|
+
const fallback = { width: PR_FILE_PANE_DEFAULT_WIDTH, hidden: false };
|
|
2875
|
+
try {
|
|
2876
|
+
var raw = localStorage.getItem(PR_FILE_PANE_STORAGE_KEY);
|
|
2877
|
+
if (!raw)
|
|
2878
|
+
return fallback;
|
|
2879
|
+
var parsed = JSON.parse(raw);
|
|
2880
|
+
if (!parsed || typeof parsed !== "object")
|
|
2881
|
+
return fallback;
|
|
2882
|
+
var w = Number(parsed.width);
|
|
2883
|
+
return {
|
|
2884
|
+
width: Number.isFinite(w) ? clampFilePaneWidth(w, 0) : fallback.width,
|
|
2885
|
+
hidden: parsed.hidden === true,
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
catch (e) {
|
|
2889
|
+
return fallback;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
/** 持久化文件区偏好(写入失败静默——布局偏好不值得弹错) */
|
|
2893
|
+
function savePrFilePanePrefs(prefs) {
|
|
2894
|
+
try {
|
|
2895
|
+
localStorage.setItem(PR_FILE_PANE_STORAGE_KEY, JSON.stringify({
|
|
2896
|
+
width: clampFilePaneWidth(prefs.width, 0),
|
|
2897
|
+
hidden: prefs.hidden === true,
|
|
2898
|
+
}));
|
|
2899
|
+
}
|
|
2900
|
+
catch (e) { }
|
|
2901
|
+
}
|
|
1925
2902
|
|
|
1926
2903
|
// ---- [Bundled: branch-selector.js] ----
|
|
1927
2904
|
/**
|
|
@@ -2088,6 +3065,120 @@ function BranchSelectorDropdown(props) {
|
|
|
2088
3065
|
: null);
|
|
2089
3066
|
}
|
|
2090
3067
|
|
|
3068
|
+
// ---- [Bundled: repo-selector.js] ----
|
|
3069
|
+
/**
|
|
3070
|
+
* 关联仓库(上游源仓库 ↔ 个人复刻 Fork)切换下拉组件。
|
|
3071
|
+
*
|
|
3072
|
+
* 为什么单仓库时返回 null:单仓库场景没有切换意义,保持现有卡片外观 100% 纯净零噪音;
|
|
3073
|
+
* 仅在检测到当前工作区存在双 remote 或 Fork 关联拓扑时激活。
|
|
3074
|
+
*/
|
|
3075
|
+
function RepoSelectorDropdown(props) {
|
|
3076
|
+
var associatedRepos = Array.isArray(props.associatedRepos) ? props.associatedRepos : [];
|
|
3077
|
+
var currentRepo = (props.currentRepo || '').toLowerCase();
|
|
3078
|
+
var cwd = props.cwd;
|
|
3079
|
+
var onRepoSwitched = props.onRepoSwitched || (() => { });
|
|
3080
|
+
var showToast = props.showToast || (() => { });
|
|
3081
|
+
// 严格遵守 React Hooks 铁律:所有 useState / useRef / useEffect 必须无条件在顶层执行,
|
|
3082
|
+
// 严禁在任何条件分支/early return 之后调用 Hooks,杜绝触发
|
|
3083
|
+
// "Rendered fewer hooks than expected" 的致命运行时崩溃。
|
|
3084
|
+
var [isOpen, setIsOpen] = React.useState(false);
|
|
3085
|
+
var [switching, setSwitching] = React.useState(false);
|
|
3086
|
+
var containerRef = React.useRef(null);
|
|
3087
|
+
React.useEffect(() => {
|
|
3088
|
+
if (!isOpen)
|
|
3089
|
+
return;
|
|
3090
|
+
var handleClickOutside = (e) => {
|
|
3091
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
3092
|
+
setIsOpen(false);
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
3096
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
3097
|
+
}, [isOpen]);
|
|
3098
|
+
// Hooks 执行完毕后再进行单仓库零噪音短路返回
|
|
3099
|
+
if (associatedRepos.length <= 1) {
|
|
3100
|
+
return null;
|
|
3101
|
+
}
|
|
3102
|
+
var toggleOpen = (e) => {
|
|
3103
|
+
e.stopPropagation();
|
|
3104
|
+
setIsOpen(!isOpen);
|
|
3105
|
+
};
|
|
3106
|
+
var handleSwitch = async (targetRepo, targetRemote) => {
|
|
3107
|
+
if (switching)
|
|
3108
|
+
return;
|
|
3109
|
+
setSwitching(true);
|
|
3110
|
+
try {
|
|
3111
|
+
var res = await fetch('/api/github/action', {
|
|
3112
|
+
method: 'POST',
|
|
3113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3114
|
+
body: JSON.stringify({
|
|
3115
|
+
action: 'set_default_repo',
|
|
3116
|
+
cwd,
|
|
3117
|
+
repo: targetRepo,
|
|
3118
|
+
remote: targetRemote,
|
|
3119
|
+
}),
|
|
3120
|
+
});
|
|
3121
|
+
var json = await res.json().catch(() => ({}));
|
|
3122
|
+
if (!res.ok || !json.ok) {
|
|
3123
|
+
showToast(`❌ 切换基准仓库失败:${json.error || json.message || res.statusText}`);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
setIsOpen(false);
|
|
3127
|
+
showToast(`✅ 已将基准仓库切换为 ${targetRepo}`);
|
|
3128
|
+
onRepoSwitched(targetRepo);
|
|
3129
|
+
}
|
|
3130
|
+
catch (err) {
|
|
3131
|
+
showToast(`❌ 切换请求异常:${err?.message || err}`);
|
|
3132
|
+
}
|
|
3133
|
+
finally {
|
|
3134
|
+
setSwitching(false);
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
var roleLabel = (role) => {
|
|
3138
|
+
switch (role) {
|
|
3139
|
+
case 'fork': return '我的复刻';
|
|
3140
|
+
case 'upstream': return '上游源仓库';
|
|
3141
|
+
case 'parent': return '上游主干';
|
|
3142
|
+
case 'origin': return 'origin';
|
|
3143
|
+
default: return '远程关联';
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
var roleBadgeTone = (role) => {
|
|
3147
|
+
switch (role) {
|
|
3148
|
+
case 'fork': return 'dsh-badge-purple';
|
|
3149
|
+
case 'upstream':
|
|
3150
|
+
case 'parent': return 'dsh-badge-readonly';
|
|
3151
|
+
default: return 'dsh-badge-blue';
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
var h = React.createElement;
|
|
3155
|
+
return h('div', {
|
|
3156
|
+
ref: containerRef,
|
|
3157
|
+
style: { position: 'relative', display: 'inline-flex', alignItems: 'center' },
|
|
3158
|
+
className: 'dsh-github-repo-selector-container',
|
|
3159
|
+
}, h('button', {
|
|
3160
|
+
type: 'button',
|
|
3161
|
+
className: 'dsh-github-repo-switch-trigger',
|
|
3162
|
+
onClick: toggleOpen,
|
|
3163
|
+
title: `当前关联 ${associatedRepos.length} 个远程仓库(点击切换基准)`,
|
|
3164
|
+
disabled: switching,
|
|
3165
|
+
}, h('span', null, switching ? '⏳' : '⇄'), h('span', { style: { fontSize: '11px', fontWeight: 500 } }, '切换仓库'), h('span', { style: { fontSize: '9px', opacity: 0.7 } }, '▾')), isOpen
|
|
3166
|
+
? h('div', { className: 'dsh-github-repo-dropdown' }, h('div', { className: 'dsh-github-repo-dropdown-header' }, h('span', null, '选择当前工作区基准仓库'), h('span', { style: { fontSize: '11px', opacity: 0.6 } }, `${associatedRepos.length} 可用`)), h('div', { className: 'dsh-github-repo-dropdown-list' }, associatedRepos.map(function (item) {
|
|
3167
|
+
var isSelected = item.isCurrent || item.nameWithOwner.toLowerCase() === currentRepo;
|
|
3168
|
+
return h('div', {
|
|
3169
|
+
key: item.nameWithOwner,
|
|
3170
|
+
className: 'dsh-github-repo-item' + (isSelected ? ' selected' : ''),
|
|
3171
|
+
title: isSelected ? '当前正在使用的基准仓库' : `点击切换基准为 ${item.nameWithOwner}`,
|
|
3172
|
+
onClick: function () {
|
|
3173
|
+
if (!isSelected && !switching) {
|
|
3174
|
+
handleSwitch(item.nameWithOwner, item.remoteName);
|
|
3175
|
+
}
|
|
3176
|
+
},
|
|
3177
|
+
}, h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } }, h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } }, h('span', { style: { color: isSelected ? 'var(--dsw-alias-link, #0969da)' : 'inherit', fontWeight: isSelected ? 600 : 400 } }, item.nameWithOwner), h('span', { className: 'dsh-github-badge ' + roleBadgeTone(item.role), style: { fontSize: '10px', padding: '0 5px' } }, roleLabel(item.role))), isSelected ? h('span', { style: { color: 'var(--dsw-alias-state-success, #1a7f37)', fontWeight: 600 } }, '✓') : null));
|
|
3178
|
+
})), h('div', { className: 'dsh-github-repo-dropdown-footer' }, '💡 切换后系统将自动调用 gh repo set-default 对齐终端与面板'))
|
|
3179
|
+
: null);
|
|
3180
|
+
}
|
|
3181
|
+
|
|
2091
3182
|
// ---- [Bundled: auth-guide-card.js] ----
|
|
2092
3183
|
/**
|
|
2093
3184
|
* 认证与安装引导卡片组件(响应式自适应中央大面板与右侧栏)
|
|
@@ -2530,6 +3621,125 @@ function NetworkAlertBanner(props) {
|
|
|
2530
3621
|
style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
|
|
2531
3622
|
}, "⚙️ 配置代理"))), h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", lineHeight: 1.4 } }, reason + (lastSyncedAt ? `(当前数据快照同步于 ${formatTime(lastSyncedAt)})` : "")));
|
|
2532
3623
|
}
|
|
3624
|
+
/**
|
|
3625
|
+
* 确定性降级说明条(非错误)。
|
|
3626
|
+
*
|
|
3627
|
+
* 为什么要与 NetworkAlertBanner 分开:仓库关闭了 Issues、未启用 Discussions
|
|
3628
|
+
* 属于仓库的**稳定配置事实**,不是故障。把它塞进「网络连接失败」横幅会产生
|
|
3629
|
+
* 三重误导——用户去查网线、客户端启动阶梯重试、自动同步每 5 分钟空转一轮。
|
|
3630
|
+
* 这里只做安静陈述:一句话说明为什么列表是空的,不提供重试按钮。
|
|
3631
|
+
*/
|
|
3632
|
+
function RepoNoticeBar(props) {
|
|
3633
|
+
var notices = Array.isArray(props.notices) ? props.notices.filter(Boolean) : [];
|
|
3634
|
+
if (notices.length === 0)
|
|
3635
|
+
return null;
|
|
3636
|
+
var isRightbar = Boolean(props.isRightbar);
|
|
3637
|
+
var h = React.createElement;
|
|
3638
|
+
return h("div", {
|
|
3639
|
+
className: "dsh-github-repo-notice",
|
|
3640
|
+
style: {
|
|
3641
|
+
display: "flex",
|
|
3642
|
+
flexDirection: "column",
|
|
3643
|
+
gap: "3px",
|
|
3644
|
+
padding: isRightbar ? "8px 10px" : "10px 14px",
|
|
3645
|
+
marginBottom: "10px",
|
|
3646
|
+
borderRadius: "8px",
|
|
3647
|
+
background: "rgba(84, 174, 255, 0.10)",
|
|
3648
|
+
border: "1px solid rgba(84, 174, 255, 0.35)",
|
|
3649
|
+
fontSize: "12px",
|
|
3650
|
+
color: "var(--dsw-alias-label-primary, #1f2328)",
|
|
3651
|
+
},
|
|
3652
|
+
}, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", fontWeight: 600 } }, h("span", null, "ℹ️"), h("span", null, "仓库配置提示")), ...notices.map(function (n, i) {
|
|
3653
|
+
return h("div", {
|
|
3654
|
+
key: "notice-" + i,
|
|
3655
|
+
style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", lineHeight: 1.5 },
|
|
3656
|
+
}, n);
|
|
3657
|
+
}));
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// ---- [Bundled: repo-badges.js] ----
|
|
3661
|
+
/**
|
|
3662
|
+
* 仓库卡片徽章推导(纯函数,无 React 依赖,可被单测直接覆盖)。
|
|
3663
|
+
*
|
|
3664
|
+
* 为什么独立成模块:`client.ts` 是近 2000 行的巨石,其中的 UI 判定逻辑
|
|
3665
|
+
* 只能靠「bundle 里有没有这个字符串」这类弱断言覆盖。把判定规则抽成纯函数后,
|
|
3666
|
+
* 「什么情况出什么徽章」可以被真实断言,而不是靠人工目测。
|
|
3667
|
+
*/
|
|
3668
|
+
/** 只接受 http/https 的绝对 URL;其余(javascript:、data:、相对路径)一律拒收 */
|
|
3669
|
+
function safeHttpUrl(raw) {
|
|
3670
|
+
if (typeof raw !== 'string' || !raw)
|
|
3671
|
+
return null;
|
|
3672
|
+
try {
|
|
3673
|
+
const u = new URL(raw);
|
|
3674
|
+
return u.protocol === 'http:' || u.protocol === 'https:' ? u.toString() : null;
|
|
3675
|
+
}
|
|
3676
|
+
catch {
|
|
3677
|
+
return null;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* 依据仓库元数据推导展示信息。
|
|
3682
|
+
*
|
|
3683
|
+
* 三条规则(与上位方案 S4 展示契约对齐,但**不含上游 star**):
|
|
3684
|
+
* 1. `isFork` 为 true → `⑂ fork` 徽章;有 parent 时附带血缘行
|
|
3685
|
+
* 2. `viewerPermission` 为 READ / TRIAGE → `🔒 只读` 徽章
|
|
3686
|
+
* 3. 其余一律零徽章(保持现有外观)
|
|
3687
|
+
*
|
|
3688
|
+
* 为什么权限未知时不显示任何徽章:离线数据没有该字段,此时既不能说
|
|
3689
|
+
* 「可写」也不能说「只读」——沉默优于猜测。显示一个可能错误的徽章,
|
|
3690
|
+
* 比不显示更糟。
|
|
3691
|
+
*
|
|
3692
|
+
* 为什么不展示上游 star:`parent` 只返回骨架,拿不到 stargazerCount
|
|
3693
|
+
* (`--json` 不支持嵌套子字段,实测);补一次网络往返实测 3~6 秒,
|
|
3694
|
+
* 而 star 数不是决策信息。详见 s4 执行方案 §2.1。
|
|
3695
|
+
*
|
|
3696
|
+
* @param repo RepoMetadata 或 null/undefined(离线、非 Git 工作区)
|
|
3697
|
+
*/
|
|
3698
|
+
function resolveRepoBadges(repo) {
|
|
3699
|
+
const badges = [];
|
|
3700
|
+
let forkLine = null;
|
|
3701
|
+
if (!repo || typeof repo !== 'object') {
|
|
3702
|
+
return { badges, forkLine };
|
|
3703
|
+
}
|
|
3704
|
+
// ---- 规则 1:fork ----
|
|
3705
|
+
// 用 `=== true` 严格比较:字符串 "true" / 数字 1 都不算——
|
|
3706
|
+
// 这类来源不明的真值若被接受,会在数据形态漂移时静默产生错误徽章。
|
|
3707
|
+
if (repo.isFork === true) {
|
|
3708
|
+
const rawName = typeof repo.parent?.nameWithOwner === 'string' ? repo.parent.nameWithOwner.trim() : '';
|
|
3709
|
+
if (rawName) {
|
|
3710
|
+
// URL 只信 http/https:parent.url 来自 API 响应,直接塞进 href 会承接
|
|
3711
|
+
// 任意协议(javascript:/data:)。校验不过就从 nameWithOwner 重建——
|
|
3712
|
+
// 拼出来的地址一定是 https://github.com/<owner>/<repo>,天然安全。
|
|
3713
|
+
const parentUrl = safeHttpUrl(repo.parent?.url) || `https://github.com/${rawName}`;
|
|
3714
|
+
forkLine = { parentNameWithOwner: rawName, parentUrl };
|
|
3715
|
+
badges.push({
|
|
3716
|
+
text: '⑂ fork',
|
|
3717
|
+
tone: 'purple',
|
|
3718
|
+
title: `这是 ${rawName} 的 fork。改动默认提交到你的 fork,需要时再向上游发起 PR。`,
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
else {
|
|
3722
|
+
// 拿不到上游也必须告诉用户「这是 fork」——那是确定事实
|
|
3723
|
+
badges.push({
|
|
3724
|
+
text: '⑂ fork',
|
|
3725
|
+
tone: 'purple',
|
|
3726
|
+
title: '这是一个 fork 仓库(未能解析出上游仓库)。',
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
// ---- 规则 2:只读 ----
|
|
3731
|
+
// 只认 READ / TRIAGE:这两个档位必然无法写代码。MAINTAIN 及以上均可写。
|
|
3732
|
+
const perm = typeof repo.viewerPermission === 'string' ? repo.viewerPermission.toUpperCase() : '';
|
|
3733
|
+
if (perm === 'READ' || perm === 'TRIAGE') {
|
|
3734
|
+
badges.push({
|
|
3735
|
+
text: '🔒 只读',
|
|
3736
|
+
tone: 'yellow',
|
|
3737
|
+
title: '当前账号对该仓库仅有只读权限,写操作(创建 Issue / PR / 合并等)会被 GitHub 拒绝。' +
|
|
3738
|
+
'如需提交改动,请先 fork 到自己的账号(gh repo fork --clone)。',
|
|
3739
|
+
});
|
|
3740
|
+
}
|
|
3741
|
+
return { badges, forkLine };
|
|
3742
|
+
}
|
|
2533
3743
|
|
|
2534
3744
|
// ---- [Bundled: sync-status-badge.js] ----
|
|
2535
3745
|
/**
|
|
@@ -2597,6 +3807,320 @@ function SyncStatusBadge(props) {
|
|
|
2597
3807
|
}, text);
|
|
2598
3808
|
}
|
|
2599
3809
|
|
|
3810
|
+
// ---- [Bundled: markdown-lite.js] ----
|
|
3811
|
+
/**
|
|
3812
|
+
* 轻量 Markdown 解析与 React 渲染(零依赖)。
|
|
3813
|
+
*
|
|
3814
|
+
* 为什么必须自己写:本插件没有前端 dependencies,bundle 机制只内联
|
|
3815
|
+
* lib/ui/*.js 且不做 import 解析,因此 marked / markdown-it 都无法引入。
|
|
3816
|
+
* 服务端的 src/renderers/* 只产出给模型看的 Markdown 文本,不能复用为 DOM。
|
|
3817
|
+
*
|
|
3818
|
+
* 设计取舍:
|
|
3819
|
+
* - 只支持 GitHub PR/Issue 实际会用到的语法子集(标题/粗斜体/行内码/
|
|
3820
|
+
* 围栏代码块/列表/引用/分隔线/链接)。表格、图片、内嵌 HTML 明确不做。
|
|
3821
|
+
* - 无法识别的语法**降级为纯文本**,绝不抛错——弹窗是只读展示,
|
|
3822
|
+
* 因为一段奇怪文本崩掉整块内容是不可接受的。
|
|
3823
|
+
* - 全程 React.createElement,**从不使用 dangerouslySetInnerHTML**:
|
|
3824
|
+
* 用户写的 <script> 会被当作纯文本渲染,天然免疫 XSS。
|
|
3825
|
+
*/
|
|
3826
|
+
/**
|
|
3827
|
+
* 链接协议白名单。
|
|
3828
|
+
*
|
|
3829
|
+
* 为什么必须白名单:Markdown 的链接目标是**用户可控文本**。
|
|
3830
|
+
* `[点我](javascript:alert(1))` 若直接渲染成 href,点击即执行脚本。
|
|
3831
|
+
* 只放行 http/https/mailto,其余降级为纯文本(保留文字,不生成 <a>)。
|
|
3832
|
+
*/
|
|
3833
|
+
function isSafeHref(href) {
|
|
3834
|
+
if (typeof href !== 'string')
|
|
3835
|
+
return false;
|
|
3836
|
+
const trimmed = href.trim();
|
|
3837
|
+
if (!trimmed)
|
|
3838
|
+
return false;
|
|
3839
|
+
if (/^(https?:\/\/|mailto:)/i.test(trimmed))
|
|
3840
|
+
return true;
|
|
3841
|
+
// 站内相对路径(如 /owner/repo/issues/1)视为安全,但必须以单个 / 开头
|
|
3842
|
+
if (/^\/(?!\/)/.test(trimmed))
|
|
3843
|
+
return true;
|
|
3844
|
+
return false;
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* 解析行内语法:`code` / **strong** / *em* / [text](url)。
|
|
3848
|
+
*
|
|
3849
|
+
* 用单遍扫描而非多次 replace 的原因:
|
|
3850
|
+
* 行内代码里的内容**不应再被解析**(`**a**` 是字面量),
|
|
3851
|
+
* 分段替换很容易把代码块内的星号也吃掉。手写扫描能保证优先级正确:
|
|
3852
|
+
* 行内代码 > 链接 > 粗体 > 斜体。
|
|
3853
|
+
*/
|
|
3854
|
+
function parseInline(src) {
|
|
3855
|
+
const out = [];
|
|
3856
|
+
const text = String(src ?? '');
|
|
3857
|
+
let buf = '';
|
|
3858
|
+
let i = 0;
|
|
3859
|
+
const flush = () => {
|
|
3860
|
+
if (buf) {
|
|
3861
|
+
out.push({ type: 'text', text: buf });
|
|
3862
|
+
buf = '';
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
3865
|
+
while (i < text.length) {
|
|
3866
|
+
const ch = text[i];
|
|
3867
|
+
// ---- 行内代码:最高优先级,内部不再解析任何语法 ----
|
|
3868
|
+
if (ch === '`') {
|
|
3869
|
+
const close = text.indexOf('`', i + 1);
|
|
3870
|
+
if (close > i) {
|
|
3871
|
+
flush();
|
|
3872
|
+
out.push({ type: 'code', text: text.slice(i + 1, close) });
|
|
3873
|
+
i = close + 1;
|
|
3874
|
+
continue;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
// ---- 链接:[text](url) ----
|
|
3878
|
+
if (ch === '[') {
|
|
3879
|
+
const closeBracket = text.indexOf(']', i + 1);
|
|
3880
|
+
if (closeBracket > i && text[closeBracket + 1] === '(') {
|
|
3881
|
+
const closeParen = text.indexOf(')', closeBracket + 2);
|
|
3882
|
+
if (closeParen > closeBracket) {
|
|
3883
|
+
const label = text.slice(i + 1, closeBracket);
|
|
3884
|
+
const href = text.slice(closeBracket + 2, closeParen);
|
|
3885
|
+
flush();
|
|
3886
|
+
if (isSafeHref(href)) {
|
|
3887
|
+
out.push({ type: 'link', href: href.trim(), children: [{ type: 'text', text: label }] });
|
|
3888
|
+
}
|
|
3889
|
+
else {
|
|
3890
|
+
// 不安全协议:保留可见文字,但不生成可点链接
|
|
3891
|
+
out.push({ type: 'text', text: label });
|
|
3892
|
+
}
|
|
3893
|
+
i = closeParen + 1;
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
// ---- 粗体:**text** 或 __text__ ----
|
|
3899
|
+
if ((ch === '*' && text[i + 1] === '*') || (ch === '_' && text[i + 1] === '_')) {
|
|
3900
|
+
const mark = ch + ch;
|
|
3901
|
+
const close = text.indexOf(mark, i + 2);
|
|
3902
|
+
if (close > i + 2) {
|
|
3903
|
+
flush();
|
|
3904
|
+
out.push({ type: 'strong', children: parseInline(text.slice(i + 2, close)) });
|
|
3905
|
+
i = close + 2;
|
|
3906
|
+
continue;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
// ---- 斜体:*text* 或 _text_(单字符,且不能被上面粗体规则截获) ----
|
|
3910
|
+
if (ch === '*' || ch === '_') {
|
|
3911
|
+
// 避免把列表标记或孤立的 * 当斜体:要求闭合符前后都不是同一字符
|
|
3912
|
+
const close = text.indexOf(ch, i + 1);
|
|
3913
|
+
if (close > i + 1 && text[close + 1] !== ch && text[close - 1] !== ch) {
|
|
3914
|
+
flush();
|
|
3915
|
+
out.push({ type: 'em', children: parseInline(text.slice(i + 1, close)) });
|
|
3916
|
+
i = close + 1;
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
buf += ch;
|
|
3921
|
+
i++;
|
|
3922
|
+
}
|
|
3923
|
+
flush();
|
|
3924
|
+
return out;
|
|
3925
|
+
}
|
|
3926
|
+
/** 判断一行是否是围栏代码块标记,返回 { fence, lang } 或 null */
|
|
3927
|
+
function matchFence(line) {
|
|
3928
|
+
const m = /^\s*(```+|~~~+)\s*([A-Za-z0-9_+-]*)\s*$/.exec(line);
|
|
3929
|
+
if (!m)
|
|
3930
|
+
return null;
|
|
3931
|
+
return { fence: m[1], lang: m[2] || '' };
|
|
3932
|
+
}
|
|
3933
|
+
/** 判断一行是否是列表项,返回 { marker, content, ordered } 或 null */
|
|
3934
|
+
function matchListItem(line) {
|
|
3935
|
+
const m = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
3936
|
+
if (!m)
|
|
3937
|
+
return null;
|
|
3938
|
+
return { marker: m[2], content: m[3], ordered: /\d+\./.test(m[2]) };
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* 把 Markdown 源文本解析为块级 AST。
|
|
3942
|
+
*
|
|
3943
|
+
* 纯函数、无 I/O,可被单测直接覆盖——这是把「渲染正确性」从浏览器里
|
|
3944
|
+
* 剥离出来断言的关键。任何异常输入都返回空数组,绝不抛错。
|
|
3945
|
+
*/
|
|
3946
|
+
function parseMarkdownBlocks(src) {
|
|
3947
|
+
const blocks = [];
|
|
3948
|
+
if (typeof src !== 'string' || !src.trim())
|
|
3949
|
+
return blocks;
|
|
3950
|
+
const lines = src.replace(/\r\n/g, '\n').split('\n');
|
|
3951
|
+
let i = 0;
|
|
3952
|
+
while (i < lines.length) {
|
|
3953
|
+
const line = lines[i];
|
|
3954
|
+
// ---- 空行 ----
|
|
3955
|
+
if (!line.trim()) {
|
|
3956
|
+
i++;
|
|
3957
|
+
continue;
|
|
3958
|
+
}
|
|
3959
|
+
// ---- 围栏代码块 ----
|
|
3960
|
+
const fence = matchFence(line);
|
|
3961
|
+
if (fence) {
|
|
3962
|
+
const codeLines = [];
|
|
3963
|
+
i++;
|
|
3964
|
+
// 一直读到同种围栏闭合;未闭合时读到末尾(容错,不报错)
|
|
3965
|
+
while (i < lines.length) {
|
|
3966
|
+
const maybeClose = matchFence(lines[i]);
|
|
3967
|
+
if (maybeClose && maybeClose.fence[0] === fence.fence[0] && maybeClose.fence.length >= fence.fence.length) {
|
|
3968
|
+
i++;
|
|
3969
|
+
break;
|
|
3970
|
+
}
|
|
3971
|
+
codeLines.push(lines[i]);
|
|
3972
|
+
i++;
|
|
3973
|
+
}
|
|
3974
|
+
blocks.push({ type: 'code', lang: fence.lang, text: codeLines.join('\n') });
|
|
3975
|
+
continue;
|
|
3976
|
+
}
|
|
3977
|
+
// ---- 标题 ----
|
|
3978
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
3979
|
+
if (heading) {
|
|
3980
|
+
blocks.push({ type: 'heading', level: heading[1].length, children: parseInline(heading[2].trim()) });
|
|
3981
|
+
i++;
|
|
3982
|
+
continue;
|
|
3983
|
+
}
|
|
3984
|
+
// ---- 分隔线 ----
|
|
3985
|
+
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
3986
|
+
blocks.push({ type: 'hr' });
|
|
3987
|
+
i++;
|
|
3988
|
+
continue;
|
|
3989
|
+
}
|
|
3990
|
+
// ---- 引用:连续 > 行合并为一个 blockquote ----
|
|
3991
|
+
if (/^\s*>\s?/.test(line)) {
|
|
3992
|
+
const quoteLines = [];
|
|
3993
|
+
while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
|
|
3994
|
+
quoteLines.push(lines[i].replace(/^\s*>\s?/, ''));
|
|
3995
|
+
i++;
|
|
3996
|
+
}
|
|
3997
|
+
blocks.push({ type: 'quote', children: parseInline(quoteLines.join('\n').trim()) });
|
|
3998
|
+
continue;
|
|
3999
|
+
}
|
|
4000
|
+
// ---- 列表:连续同类型(有序/无序)合并为一个 list ----
|
|
4001
|
+
const firstItem = matchListItem(line);
|
|
4002
|
+
if (firstItem) {
|
|
4003
|
+
const ordered = firstItem.ordered;
|
|
4004
|
+
const items = [];
|
|
4005
|
+
while (i < lines.length) {
|
|
4006
|
+
const item = matchListItem(lines[i]);
|
|
4007
|
+
if (!item || item.ordered !== ordered)
|
|
4008
|
+
break;
|
|
4009
|
+
items.push({ marker: item.marker, children: parseInline(item.content) });
|
|
4010
|
+
i++;
|
|
4011
|
+
}
|
|
4012
|
+
blocks.push({ type: 'list', ordered, items });
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
// ---- 段落:连续非空行合并 ----
|
|
4016
|
+
const paraLines = [];
|
|
4017
|
+
while (i < lines.length) {
|
|
4018
|
+
const cur = lines[i];
|
|
4019
|
+
if (!cur.trim())
|
|
4020
|
+
break;
|
|
4021
|
+
// 遇到块级语法起始就停下,交给下一轮解析
|
|
4022
|
+
if (matchFence(cur) || /^(#{1,6})\s+/.test(cur) || /^\s*>\s?/.test(cur) || matchListItem(cur) || /^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(cur)) {
|
|
4023
|
+
break;
|
|
4024
|
+
}
|
|
4025
|
+
paraLines.push(cur);
|
|
4026
|
+
i++;
|
|
4027
|
+
}
|
|
4028
|
+
// 段落内的硬换行保留为 text 中的 \n,渲染时用 white-space: pre-wrap 呈现
|
|
4029
|
+
if (paraLines.length > 0) {
|
|
4030
|
+
blocks.push({ type: 'paragraph', children: parseInline(paraLines.join('\n')) });
|
|
4031
|
+
}
|
|
4032
|
+
else {
|
|
4033
|
+
// 兜底:当前行以某种方式无法消费时跳过,防止死循环
|
|
4034
|
+
i++;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return blocks;
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* 把 AST 渲染为 React 元素数组。
|
|
4041
|
+
*
|
|
4042
|
+
* @param h React.createElement
|
|
4043
|
+
* @param blocks parseMarkdownBlocks 的产物
|
|
4044
|
+
*/
|
|
4045
|
+
function renderMarkdownBlocks(h, blocks) {
|
|
4046
|
+
if (!Array.isArray(blocks))
|
|
4047
|
+
return [];
|
|
4048
|
+
const renderInline = (nodes, keyPrefix) => {
|
|
4049
|
+
if (!Array.isArray(nodes))
|
|
4050
|
+
return [];
|
|
4051
|
+
return nodes.map((n, idx) => {
|
|
4052
|
+
const key = `${keyPrefix}-${idx}`;
|
|
4053
|
+
switch (n.type) {
|
|
4054
|
+
case 'code':
|
|
4055
|
+
return h('code', { key, className: 'dsh-github-md-inline-code' }, n.text ?? '');
|
|
4056
|
+
case 'strong':
|
|
4057
|
+
return h('strong', { key }, renderInline(n.children, key));
|
|
4058
|
+
case 'em':
|
|
4059
|
+
return h('em', { key }, renderInline(n.children, key));
|
|
4060
|
+
case 'link':
|
|
4061
|
+
return h('a', {
|
|
4062
|
+
key,
|
|
4063
|
+
href: n.href,
|
|
4064
|
+
target: '_blank',
|
|
4065
|
+
rel: 'noopener noreferrer',
|
|
4066
|
+
className: 'dsh-github-md-link',
|
|
4067
|
+
}, renderInline(n.children, key));
|
|
4068
|
+
default:
|
|
4069
|
+
return h('span', { key }, n.text ?? '');
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
};
|
|
4073
|
+
return blocks.map((b, idx) => {
|
|
4074
|
+
const key = `md-${idx}`;
|
|
4075
|
+
switch (b.type) {
|
|
4076
|
+
case 'heading': {
|
|
4077
|
+
const tag = `h${Math.min(Math.max(b.level || 1, 1), 6)}`;
|
|
4078
|
+
return h(tag, { key, className: 'dsh-github-md-heading' }, renderInline(b.children, key));
|
|
4079
|
+
}
|
|
4080
|
+
case 'code':
|
|
4081
|
+
return h('pre', { key, className: 'dsh-github-md-pre' }, h('code', { className: b.lang ? `language-${b.lang}` : undefined }, b.text ?? ''));
|
|
4082
|
+
case 'list': {
|
|
4083
|
+
const tag = b.ordered ? 'ol' : 'ul';
|
|
4084
|
+
return h(tag, { key, className: 'dsh-github-md-list' }, (b.items || []).map((item, ii) => {
|
|
4085
|
+
const itemKey = `${key}-${ii}`;
|
|
4086
|
+
// 任务列表:- [ ] / - [x]
|
|
4087
|
+
const raw = (item.children || []).map((c) => c.text || '').join('');
|
|
4088
|
+
const task = /^\[( |x|X)\]\s*/.exec(raw);
|
|
4089
|
+
if (task) {
|
|
4090
|
+
const checked = task[1].toLowerCase() === 'x';
|
|
4091
|
+
const rest = raw.replace(/^\[( |x|X)\]\s*/, '');
|
|
4092
|
+
return h('li', { key: itemKey, className: 'dsh-github-md-task' }, h('span', { className: 'dsh-github-md-checkbox' }, checked ? '☑' : '☐'), h('span', null, rest));
|
|
4093
|
+
}
|
|
4094
|
+
return h('li', { key: itemKey }, renderInline(item.children, itemKey));
|
|
4095
|
+
}));
|
|
4096
|
+
}
|
|
4097
|
+
case 'quote':
|
|
4098
|
+
return h('blockquote', { key, className: 'dsh-github-md-quote' }, renderInline(b.children, key));
|
|
4099
|
+
case 'hr':
|
|
4100
|
+
return h('hr', { key, className: 'dsh-github-md-hr' });
|
|
4101
|
+
default:
|
|
4102
|
+
return h('p', { key, className: 'dsh-github-md-p' }, renderInline(b.children, key));
|
|
4103
|
+
}
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* 估算一段 Markdown 渲染后的行数,用于决定是否折叠。
|
|
4108
|
+
*
|
|
4109
|
+
* 为什么用估算而不是测量 DOM:getBoundingClientRect 需要 ref + 二次渲染,
|
|
4110
|
+
* 在弹窗内会造成可见的布局抖动。按换行数 + 长行折行数估算已足够准确,
|
|
4111
|
+
* 且成本为零。阈值取保守值——宁可多折一次,也不要漏折导致内容淹没页面。
|
|
4112
|
+
*/
|
|
4113
|
+
function estimateContentLines(src, charsPerLine = 80) {
|
|
4114
|
+
if (typeof src !== 'string' || !src)
|
|
4115
|
+
return 0;
|
|
4116
|
+
let lines = 0;
|
|
4117
|
+
for (const raw of src.replace(/\r\n/g, '\n').split('\n')) {
|
|
4118
|
+
const len = raw.length;
|
|
4119
|
+
lines += len === 0 ? 1 : Math.ceil(len / charsPerLine);
|
|
4120
|
+
}
|
|
4121
|
+
return lines;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
2600
4124
|
// ---- [Bundled: commit-modal.js] ----
|
|
2601
4125
|
/**
|
|
2602
4126
|
* 提交详情模态弹窗组件(与设置弹窗同构)
|
|
@@ -2798,24 +4322,910 @@ function CommitDetailModal(props) {
|
|
|
2798
4322
|
: h("pre", {
|
|
2799
4323
|
className: "dsh-github-commit-pre",
|
|
2800
4324
|
style: {
|
|
2801
|
-
fontSize: "11px",
|
|
2802
|
-
lineHeight: "1.5",
|
|
2803
|
-
whiteSpace: "pre-wrap",
|
|
2804
|
-
wordBreak: "break-word",
|
|
2805
|
-
margin: 0,
|
|
2806
|
-
maxHeight: "34vh",
|
|
2807
|
-
overflowY: "auto",
|
|
2808
|
-
padding: "8px 10px",
|
|
2809
|
-
borderRadius: "6px",
|
|
2810
|
-
background: "var(--dsw-alias-bg-module-platform, #f6f8fa)",
|
|
2811
|
-
border: "1px solid var(--dsw-alias-border-l2, #e1e4e8)",
|
|
4325
|
+
fontSize: "11px",
|
|
4326
|
+
lineHeight: "1.5",
|
|
4327
|
+
whiteSpace: "pre-wrap",
|
|
4328
|
+
wordBreak: "break-word",
|
|
4329
|
+
margin: 0,
|
|
4330
|
+
maxHeight: "34vh",
|
|
4331
|
+
overflowY: "auto",
|
|
4332
|
+
padding: "8px 10px",
|
|
4333
|
+
borderRadius: "6px",
|
|
4334
|
+
background: "var(--dsw-alias-bg-module-platform, #f6f8fa)",
|
|
4335
|
+
border: "1px solid var(--dsw-alias-border-l2, #e1e4e8)",
|
|
4336
|
+
},
|
|
4337
|
+
}, effectiveStat))
|
|
4338
|
+
: null, !effectiveBody && !effectiveStat
|
|
4339
|
+
? (fetchingDetail
|
|
4340
|
+
? h("div", { style: { fontSize: "12px", opacity: 0.7, padding: "8px 0" } }, "⏳ 正在获取详细变更说明…")
|
|
4341
|
+
: h("div", { style: { fontSize: "11px", opacity: 0.6, fontStyle: "italic", padding: "6px 0" } }, "(该提交未包含多行正文;重启 DSH 桌面端可同步最新格式并直接呈现变更统计)"))
|
|
4342
|
+
: null)));
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
// ---- [Bundled: pr-modal.js] ----
|
|
4346
|
+
/**
|
|
4347
|
+
* 可折叠的 Markdown 内容块。
|
|
4348
|
+
*
|
|
4349
|
+
* 【为什么必须定义在模块顶层,不能写在 PrDetailModal 函数体内】
|
|
4350
|
+
* 函数体内定义的组件每次父级渲染都会产生**新的函数引用**,
|
|
4351
|
+
* React 按引用比较类型,会把 h(CollapsibleBody, ...) 当成**另一个组件**,
|
|
4352
|
+
* 于是卸载重建、useState 的展开状态被重置。
|
|
4353
|
+
* 表现就是:用户刚点开一条长评论,一拖分隔条(触发父级重渲染)评论又缩回去了。
|
|
4354
|
+
* 提到模块级后引用恒定,状态才能跨父级渲染存活。
|
|
4355
|
+
*/
|
|
4356
|
+
function CollapsibleBody(props) {
|
|
4357
|
+
var h = React.createElement;
|
|
4358
|
+
var raw = typeof props.text === "string" ? props.text : "";
|
|
4359
|
+
var totalLines = estimateContentLines(raw);
|
|
4360
|
+
// 阈值取保守值(8 行 / 400 字符):宁可多给一个展开按钮,
|
|
4361
|
+
// 也不要让长评论把后续内容淹掉。
|
|
4362
|
+
var collapsible = totalLines > 8 || raw.length > 400;
|
|
4363
|
+
var st = React.useState(props.defaultExpanded);
|
|
4364
|
+
var expanded = st[0];
|
|
4365
|
+
var setExpanded = st[1];
|
|
4366
|
+
var inner = null;
|
|
4367
|
+
if (raw.trim()) {
|
|
4368
|
+
inner = renderMarkdownBlocks(h, parseMarkdownBlocks(raw));
|
|
4369
|
+
}
|
|
4370
|
+
if (!inner || inner.length === 0) {
|
|
4371
|
+
return h("div", { style: { padding: "10px 12px", fontSize: "12px", opacity: 0.5, fontStyle: "italic" } }, "(无正文)");
|
|
4372
|
+
}
|
|
4373
|
+
if (!collapsible) {
|
|
4374
|
+
return h("div", { className: "dsh-github-md-body" }, inner);
|
|
4375
|
+
}
|
|
4376
|
+
return h("div", null, h("div", {
|
|
4377
|
+
className: "dsh-github-md-body",
|
|
4378
|
+
style: {
|
|
4379
|
+
maxHeight: expanded ? "none" : "132px",
|
|
4380
|
+
overflow: expanded ? "visible" : "hidden",
|
|
4381
|
+
position: "relative",
|
|
4382
|
+
},
|
|
4383
|
+
}, inner,
|
|
4384
|
+
// 折叠时底部渐隐,暗示内容被截断而不是到此为止
|
|
4385
|
+
!expanded
|
|
4386
|
+
? h("div", {
|
|
4387
|
+
style: {
|
|
4388
|
+
position: "absolute",
|
|
4389
|
+
left: 0,
|
|
4390
|
+
right: 0,
|
|
4391
|
+
bottom: 0,
|
|
4392
|
+
height: "24px",
|
|
4393
|
+
pointerEvents: "none",
|
|
4394
|
+
background: "linear-gradient(to bottom, transparent, var(--dsw-alias-bg-base, #ffffff))",
|
|
4395
|
+
},
|
|
4396
|
+
})
|
|
4397
|
+
: null), h("button", {
|
|
4398
|
+
type: "button",
|
|
4399
|
+
className: "dsh-github-md-expand-btn",
|
|
4400
|
+
onClick: function (e) {
|
|
4401
|
+
e.stopPropagation();
|
|
4402
|
+
setExpanded(!expanded);
|
|
4403
|
+
},
|
|
4404
|
+
title: expanded ? "收起内容" : "展开全部内容",
|
|
4405
|
+
}, expanded ? "⌃ 收起" : "⌄ 展开全部(约 " + totalLines + " 行)"));
|
|
4406
|
+
}
|
|
4407
|
+
/**
|
|
4408
|
+
* PR 在线完整审阅与代码比对工作台模态弹窗组件。
|
|
4409
|
+
*
|
|
4410
|
+
* 架构设计:
|
|
4411
|
+
* 1. 单次网络往返拉取元数据与 Diff 切片,按文件切换比对 0ms 瞬间响应;
|
|
4412
|
+
* 2. 覆盖作者卡片、状态徽章、分支流向、正文描述、CI 与审查决定;
|
|
4413
|
+
* 3. 评论回复流 Timeline(Issue Comments + Review 评注);
|
|
4414
|
+
* 4. 变动文件清单与行级高保真 Unified Diff 比对(绿色新增、红色删除、灰底 Hunk)。
|
|
4415
|
+
*/
|
|
4416
|
+
function PrDetailModal(props) {
|
|
4417
|
+
var prNumber = props.prNumber;
|
|
4418
|
+
var repoNameWithOwner = props.repoNameWithOwner || "";
|
|
4419
|
+
var cwd = props.cwd;
|
|
4420
|
+
var onClose = props.onClose;
|
|
4421
|
+
var showToast = props.showToast || (() => { });
|
|
4422
|
+
// 严格遵守 React Hooks 铁律:所有 Hooks 必须无条件在顶层执行,
|
|
4423
|
+
// 严禁在 early return 之后调用。否则「prNumber 由空变有值」时
|
|
4424
|
+
// 两次渲染的 Hook 数量不一致,会触发
|
|
4425
|
+
// "Rendered fewer hooks than expected" 的致命运行时崩溃。
|
|
4426
|
+
var [data, setData] = React.useState(null);
|
|
4427
|
+
var [loading, setLoading] = React.useState(true);
|
|
4428
|
+
var [error, setError] = React.useState(null);
|
|
4429
|
+
var [activeTab, setActiveTab] = React.useState("conversation");
|
|
4430
|
+
var [selectedFilePath, setSelectedFilePath] = React.useState(null);
|
|
4431
|
+
var [copied, setCopied] = React.useState(false);
|
|
4432
|
+
// 文件区布局:宽度可拖拽、可整块折叠。初值从 localStorage 偏好恢复。
|
|
4433
|
+
// 同样必须无条件在顶层执行(与上面几个 Hook 一并满足调用顺序铁律)。
|
|
4434
|
+
var initialPanePrefs = getPrFilePanePrefs();
|
|
4435
|
+
var [filePaneWidth, setFilePaneWidth] = React.useState(initialPanePrefs.width);
|
|
4436
|
+
var [filePaneHidden, setFilePaneHidden] = React.useState(initialPanePrefs.hidden);
|
|
4437
|
+
var paneContainerRef = React.useRef(null);
|
|
4438
|
+
// 宽度镜像 ref:供异步回调(如 setState updater)读取最新值,
|
|
4439
|
+
// 避免闭包捕获渲染期快照导致的偏好回跳。
|
|
4440
|
+
var widthRef = React.useRef(initialPanePrefs.width);
|
|
4441
|
+
widthRef.current = filePaneWidth;
|
|
4442
|
+
// 拖拽起点存 ref 而非 state:mousemove 每秒触发几十次,
|
|
4443
|
+
// 把起点塞进 state 会引发无意义的重渲染风暴。
|
|
4444
|
+
var dragStateRef = React.useRef(null);
|
|
4445
|
+
// 拖拽进行中的清理函数:组件卸载(如拖到一半按 Esc)时必须兜底解绑,
|
|
4446
|
+
// 否则 document 上的 mousemove 与 body 的 col-resize 光标会永久残留。
|
|
4447
|
+
var cleanupDragRef = React.useRef(null);
|
|
4448
|
+
// Hooks 执行完毕后再做空值短路
|
|
4449
|
+
if (!prNumber)
|
|
4450
|
+
return null;
|
|
4451
|
+
// 卸载兜底:拖拽中途关闭弹窗(Esc / 点遮罩)时清理 document 监听与 body 光标
|
|
4452
|
+
React.useEffect(() => {
|
|
4453
|
+
return () => {
|
|
4454
|
+
if (cleanupDragRef.current) {
|
|
4455
|
+
cleanupDragRef.current();
|
|
4456
|
+
cleanupDragRef.current = null;
|
|
4457
|
+
}
|
|
4458
|
+
};
|
|
4459
|
+
}, []);
|
|
4460
|
+
// Esc 键监听关闭
|
|
4461
|
+
React.useEffect(() => {
|
|
4462
|
+
var onKeyDown = (e) => {
|
|
4463
|
+
if (e.key === "Escape" || e.keyCode === 27)
|
|
4464
|
+
onClose();
|
|
4465
|
+
};
|
|
4466
|
+
window.addEventListener("keydown", onKeyDown);
|
|
4467
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
4468
|
+
}, [onClose]);
|
|
4469
|
+
// 拉取 PR 详情与差异数据
|
|
4470
|
+
React.useEffect(() => {
|
|
4471
|
+
// 无有效编号时不发请求(Hooks 已无条件执行,这里只做副作用守卫)
|
|
4472
|
+
if (!prNumber) {
|
|
4473
|
+
setLoading(false);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
setLoading(true);
|
|
4477
|
+
setError(null);
|
|
4478
|
+
var url = "/api/github/pr-detail?number=" +
|
|
4479
|
+
encodeURIComponent(String(prNumber)) +
|
|
4480
|
+
(repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
|
|
4481
|
+
(cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
|
|
4482
|
+
fetch(url)
|
|
4483
|
+
.then(async (res) => {
|
|
4484
|
+
if (!res.ok) {
|
|
4485
|
+
var errText = await res.text().catch(() => "");
|
|
4486
|
+
var j = null;
|
|
4487
|
+
try {
|
|
4488
|
+
j = JSON.parse(errText);
|
|
4489
|
+
}
|
|
4490
|
+
catch (e) { }
|
|
4491
|
+
throw new Error(j?.error || j?.message || errText || `服务响应异常 (${res.status})`);
|
|
4492
|
+
}
|
|
4493
|
+
return res.json();
|
|
4494
|
+
})
|
|
4495
|
+
.then((json) => {
|
|
4496
|
+
setLoading(false);
|
|
4497
|
+
if (json && json.ok && json.data) {
|
|
4498
|
+
setData(json.data);
|
|
4499
|
+
// 默认选中第一个修改的文件
|
|
4500
|
+
if (Array.isArray(json.data.files) && json.data.files.length > 0) {
|
|
4501
|
+
setSelectedFilePath(json.data.files[0].path);
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
else {
|
|
4505
|
+
setError(json?.error || "获取 PR 详情失败");
|
|
4506
|
+
}
|
|
4507
|
+
})
|
|
4508
|
+
.catch((err) => {
|
|
4509
|
+
setLoading(false);
|
|
4510
|
+
setError(err?.message || "网络请求异常");
|
|
4511
|
+
});
|
|
4512
|
+
}, [prNumber, repoNameWithOwner, cwd]);
|
|
4513
|
+
var h = React.createElement;
|
|
4514
|
+
/**
|
|
4515
|
+
* 开始拖拽分隔条。
|
|
4516
|
+
*
|
|
4517
|
+
* 为什么把监听挂到 document 而不是分隔条自身:鼠标移动速度快于元素跟随速度时,
|
|
4518
|
+
* 指针会脱离分隔条,挂在元素上的 mousemove 立刻丢失,拖拽表现为"卡住"。
|
|
4519
|
+
* 这也是必须成对解绑(mouseup 里 removeEventListener)的原因,否则监听泄漏。
|
|
4520
|
+
*/
|
|
4521
|
+
var beginResize = (e) => {
|
|
4522
|
+
if (filePaneHidden)
|
|
4523
|
+
return;
|
|
4524
|
+
e.preventDefault();
|
|
4525
|
+
dragStateRef.current = { startX: e.clientX, startWidth: filePaneWidth };
|
|
4526
|
+
var onMove = (ev) => {
|
|
4527
|
+
var drag = dragStateRef.current;
|
|
4528
|
+
if (!drag)
|
|
4529
|
+
return;
|
|
4530
|
+
var container = paneContainerRef.current;
|
|
4531
|
+
var containerWidth = container ? container.getBoundingClientRect().width : 0;
|
|
4532
|
+
// 宽度收敛统一走纯函数,避免在事件回调里散落边界判断
|
|
4533
|
+
setFilePaneWidth(clampFilePaneWidth(drag.startWidth + (ev.clientX - drag.startX), containerWidth));
|
|
4534
|
+
};
|
|
4535
|
+
var onUp = () => {
|
|
4536
|
+
dragStateRef.current = null;
|
|
4537
|
+
document.removeEventListener("mousemove", onMove);
|
|
4538
|
+
document.removeEventListener("mouseup", onUp);
|
|
4539
|
+
document.body.style.cursor = "";
|
|
4540
|
+
document.body.style.userSelect = "";
|
|
4541
|
+
// 拖拽真正的清理函数置空,避免卸载时重复清理
|
|
4542
|
+
cleanupDragRef.current = null;
|
|
4543
|
+
// 只在松手时落盘一次:拖拽过程中每像素写一次 localStorage 既慢又无意义
|
|
4544
|
+
setFilePaneWidth((current) => {
|
|
4545
|
+
savePrFilePanePrefs({ width: current, hidden: false });
|
|
4546
|
+
return current;
|
|
4547
|
+
});
|
|
4548
|
+
};
|
|
4549
|
+
document.addEventListener("mousemove", onMove);
|
|
4550
|
+
document.addEventListener("mouseup", onUp);
|
|
4551
|
+
// 拖拽期间禁用文本选择并统一光标,否则会意外选中 diff 文本、光标闪烁
|
|
4552
|
+
document.body.style.cursor = "col-resize";
|
|
4553
|
+
document.body.style.userSelect = "none";
|
|
4554
|
+
// 登记清理函数供组件卸载时兜底调用。
|
|
4555
|
+
// 为什么必须有:拖拽过程中按下 Esc 会直接卸载整个弹窗,此时 mouseup 永远不会触发,
|
|
4556
|
+
// 挂在 document 上的 mousemove 与 body 上的 col-resize 光标会永久残留——
|
|
4557
|
+
// 表现为「弹窗关了,但整个页面鼠标仍是拖拽光标、选中文本失效」。
|
|
4558
|
+
cleanupDragRef.current = () => {
|
|
4559
|
+
dragStateRef.current = null;
|
|
4560
|
+
document.removeEventListener("mousemove", onMove);
|
|
4561
|
+
document.removeEventListener("mouseup", onUp);
|
|
4562
|
+
document.body.style.cursor = "";
|
|
4563
|
+
document.body.style.userSelect = "";
|
|
4564
|
+
};
|
|
4565
|
+
};
|
|
4566
|
+
/**
|
|
4567
|
+
* 折叠/展开文件区,并立即持久化(这是低频操作,无需等松手)。
|
|
4568
|
+
*
|
|
4569
|
+
* 为什么宽度要从 ref 读而不是直接闭包捕获 filePaneWidth:
|
|
4570
|
+
* setFilePaneHidden 的回调执行时机由 React 调度,此时闭包里的 filePaneWidth
|
|
4571
|
+
* 是**渲染时的快照**。若用户刚拖完宽度、紧接着点折叠,
|
|
4572
|
+
* 落盘的会是拖动前的旧宽度,下次打开弹窗宽度回跳。
|
|
4573
|
+
* ref 始终指向最新值,避免这类「看起来偶发」的偏好丢失。
|
|
4574
|
+
*/
|
|
4575
|
+
var toggleFilePane = () => {
|
|
4576
|
+
setFilePaneHidden((prev) => {
|
|
4577
|
+
var next = !prev;
|
|
4578
|
+
savePrFilePanePrefs({ width: widthRef.current, hidden: next });
|
|
4579
|
+
return next;
|
|
4580
|
+
});
|
|
4581
|
+
};
|
|
4582
|
+
/**
|
|
4583
|
+
* 渲染一段 Markdown 为 React 元素。
|
|
4584
|
+
*
|
|
4585
|
+
* 用 useMemo 按原文缓存:解析结果只依赖输入文本,
|
|
4586
|
+
* 而弹窗内的拖拽/折叠会触发大量重渲染,不缓存等于每次都重新解析整个 PR 描述。
|
|
4587
|
+
*/
|
|
4588
|
+
var renderMd = function (src) {
|
|
4589
|
+
var text = typeof src === "string" ? src : "";
|
|
4590
|
+
if (!text.trim())
|
|
4591
|
+
return null;
|
|
4592
|
+
var blocks = parseMarkdownBlocks(text);
|
|
4593
|
+
return renderMarkdownBlocks(h, blocks);
|
|
4594
|
+
};
|
|
4595
|
+
var copyText = (text, label) => {
|
|
4596
|
+
if (navigator?.clipboard?.writeText) {
|
|
4597
|
+
navigator.clipboard
|
|
4598
|
+
.writeText(text)
|
|
4599
|
+
.then(() => {
|
|
4600
|
+
setCopied(true);
|
|
4601
|
+
showToast(`✅ ${label} 已复制到剪贴板`);
|
|
4602
|
+
setTimeout(() => setCopied(false), 2000);
|
|
4603
|
+
})
|
|
4604
|
+
.catch(() => { });
|
|
4605
|
+
}
|
|
4606
|
+
};
|
|
4607
|
+
// 渲染单行 Diff 高亮
|
|
4608
|
+
var renderDiffContent = (diffText) => {
|
|
4609
|
+
if (!diffText) {
|
|
4610
|
+
return h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.65, fontSize: "12px" } }, "该文件未产生可识别的文本差异,或已被清空。");
|
|
4611
|
+
}
|
|
4612
|
+
return h("div", {
|
|
4613
|
+
className: "dsh-github-inline-diff-content",
|
|
4614
|
+
style: {
|
|
4615
|
+
margin: 0,
|
|
4616
|
+
padding: "4px 0",
|
|
4617
|
+
fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
|
|
4618
|
+
fontSize: "11px",
|
|
4619
|
+
lineHeight: "1.5",
|
|
4620
|
+
overflowX: "auto",
|
|
4621
|
+
borderRadius: "6px",
|
|
4622
|
+
border: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))",
|
|
4623
|
+
backgroundColor: "var(--dsw-alias-bg-canvas, #ffffff)",
|
|
4624
|
+
},
|
|
4625
|
+
}, diffText.split("\n").map(function (line, idx) {
|
|
4626
|
+
var isAdd = line.startsWith("+") && !line.startsWith("+++");
|
|
4627
|
+
var isDel = line.startsWith("-") && !line.startsWith("---");
|
|
4628
|
+
var isHunk = line.startsWith("@@");
|
|
4629
|
+
var isHeader = line.startsWith("diff --git") ||
|
|
4630
|
+
line.startsWith("index ") ||
|
|
4631
|
+
line.startsWith("---") ||
|
|
4632
|
+
line.startsWith("+++");
|
|
4633
|
+
var lineStyle = {
|
|
4634
|
+
display: "flex",
|
|
4635
|
+
padding: "0 8px",
|
|
4636
|
+
whiteSpace: "pre",
|
|
4637
|
+
minWidth: "100%",
|
|
4638
|
+
boxSizing: "border-box",
|
|
4639
|
+
};
|
|
4640
|
+
if (isAdd) {
|
|
4641
|
+
lineStyle.backgroundColor = "var(--dsw-alias-state-success-subtle, rgba(46,160,67,0.15))";
|
|
4642
|
+
lineStyle.color = "var(--dsw-alias-state-success-primary, #1a7f37)";
|
|
4643
|
+
}
|
|
4644
|
+
else if (isDel) {
|
|
4645
|
+
lineStyle.backgroundColor = "var(--dsw-alias-state-error-subtle, rgba(248,81,73,0.15))";
|
|
4646
|
+
lineStyle.color = "var(--dsw-alias-state-error-primary, #cf222e)";
|
|
4647
|
+
}
|
|
4648
|
+
else if (isHunk) {
|
|
4649
|
+
lineStyle.backgroundColor = "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.04))";
|
|
4650
|
+
lineStyle.color = "var(--dsw-alias-label-secondary, #656d76)";
|
|
4651
|
+
lineStyle.fontWeight = 600;
|
|
4652
|
+
lineStyle.margin = "2px 0";
|
|
4653
|
+
}
|
|
4654
|
+
else if (isHeader) {
|
|
4655
|
+
lineStyle.color = "var(--dsw-alias-label-muted, #8c959f)";
|
|
4656
|
+
lineStyle.fontSize = "10px";
|
|
4657
|
+
}
|
|
4658
|
+
else {
|
|
4659
|
+
lineStyle.color = "var(--dsw-alias-label-primary, #1f2328)";
|
|
4660
|
+
}
|
|
4661
|
+
return h("div", { key: idx, style: lineStyle }, line || " ");
|
|
4662
|
+
}));
|
|
4663
|
+
};
|
|
4664
|
+
return h("div", {
|
|
4665
|
+
className: "dsh-github-modal-overlay",
|
|
4666
|
+
onClick: onClose,
|
|
4667
|
+
style: { zIndex: 100000 },
|
|
4668
|
+
}, h("div", {
|
|
4669
|
+
className: "dsh-github-modal-dialog dsh-github-pr-modal-dialog",
|
|
4670
|
+
onClick: (e) => e.stopPropagation(),
|
|
4671
|
+
},
|
|
4672
|
+
// 头部 Header
|
|
4673
|
+
h("div", { className: "dsh-github-modal-header dsh-github-pr-modal-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, renderOctocat(18, "#8250df"), h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 } }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("h3", {
|
|
4674
|
+
style: {
|
|
4675
|
+
margin: 0,
|
|
4676
|
+
fontSize: "14px",
|
|
4677
|
+
fontWeight: 600,
|
|
4678
|
+
overflow: "hidden",
|
|
4679
|
+
textOverflow: "ellipsis",
|
|
4680
|
+
whiteSpace: "nowrap",
|
|
4681
|
+
},
|
|
4682
|
+
}, data ? `#${data.number} ${data.title}` : `PR #${prNumber}`), data
|
|
4683
|
+
? h("span", {
|
|
4684
|
+
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"),
|
|
4692
|
+
style: { fontSize: "10px", padding: "1px 6px" },
|
|
4693
|
+
}, data.isDraft ? "Draft" : data.state)
|
|
4694
|
+
: 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})`))
|
|
4696
|
+
: null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, data?.url
|
|
4697
|
+
? h("button", {
|
|
4698
|
+
className: "dsh-github-btn-sm",
|
|
4699
|
+
style: { padding: "4px 8px", fontSize: "11px" },
|
|
4700
|
+
onClick: () => copyText(data.url, "PR 链接"),
|
|
4701
|
+
title: "复制该 Pull Request 网页链接",
|
|
4702
|
+
}, copied ? "✓ 已复制" : "📋 复制链接")
|
|
4703
|
+
: null, data?.url
|
|
4704
|
+
? h("a", {
|
|
4705
|
+
href: data.url,
|
|
4706
|
+
target: "_blank",
|
|
4707
|
+
rel: "noopener noreferrer",
|
|
4708
|
+
className: "dsh-github-btn-sm",
|
|
4709
|
+
style: { padding: "4px 8px", fontSize: "11px", textDecoration: "none", color: "inherit" },
|
|
4710
|
+
title: "在系统默认浏览器中打开该 PR 官方页面",
|
|
4711
|
+
}, "↗ 浏览器打开")
|
|
4712
|
+
: null, h("button", {
|
|
4713
|
+
style: {
|
|
4714
|
+
background: "none",
|
|
4715
|
+
border: "none",
|
|
4716
|
+
cursor: "pointer",
|
|
4717
|
+
fontSize: "18px",
|
|
4718
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
4719
|
+
padding: "0 6px",
|
|
4720
|
+
},
|
|
4721
|
+
onClick: onClose,
|
|
4722
|
+
title: "关闭 (Esc)",
|
|
4723
|
+
}, "✕"))),
|
|
4724
|
+
// 状态加载或异常
|
|
4725
|
+
loading
|
|
4726
|
+
? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 PR 详情与变更代码比对…")
|
|
4727
|
+
: error
|
|
4728
|
+
? h("div", { style: { padding: "24px", margin: "16px", borderRadius: "8px", background: "rgba(248,81,73,0.08)", border: "1px solid rgba(248,81,73,0.3)" } }, h("div", { style: { fontWeight: 600, color: "#f85149", marginBottom: "6px" } }, "⚠️ 读取 PR 详情失败"), h("div", { style: { fontSize: "12px", opacity: 0.85 } }, error))
|
|
4729
|
+
: data
|
|
4730
|
+
? h("div", { style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" } },
|
|
4731
|
+
// Tab 切换导航
|
|
4732
|
+
h("div", { className: "dsh-github-pr-modal-tabs" }, h("button", {
|
|
4733
|
+
className: `dsh-github-pr-tab-btn ${activeTab === "conversation" ? "active" : ""}`,
|
|
4734
|
+
onClick: () => setActiveTab("conversation"),
|
|
4735
|
+
}, `💬 描述与讨论 (${data.comments ? data.comments.length : 0})`), h("button", {
|
|
4736
|
+
className: `dsh-github-pr-tab-btn ${activeTab === "files" ? "active" : ""}`,
|
|
4737
|
+
onClick: () => setActiveTab("files"),
|
|
4738
|
+
}, `📁 变动文件 (${data.files ? data.files.length : 0} 文件 · +${data.additions || 0} / -${data.deletions || 0})`)),
|
|
4739
|
+
// Tab 1: 描述与讨论
|
|
4740
|
+
activeTab === "conversation"
|
|
4741
|
+
? h("div", { className: "dsh-github-modal-body dsh-github-pr-conversation-pane" },
|
|
4742
|
+
// 审查与 CI 门禁卡片
|
|
4743
|
+
h("div", { className: "dsh-github-pr-checks-bar" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, h("span", { style: { fontWeight: 600 } }, "审查状态:"), h("span", {
|
|
4744
|
+
className: "dsh-github-badge " +
|
|
4745
|
+
(data.reviewDecision === "APPROVED"
|
|
4746
|
+
? "dsh-badge-green"
|
|
4747
|
+
: data.reviewDecision === "CHANGES_REQUESTED"
|
|
4748
|
+
? "dsh-badge-red"
|
|
4749
|
+
: "dsh-badge-yellow"),
|
|
4750
|
+
}, data.reviewDecision || "REVIEW_REQUIRED")), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", fontFamily: "monospace" } }, h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontWeight: 600 } }, `+${data.additions}`), h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600 } }, `-${data.deletions}`))),
|
|
4751
|
+
// PR 正文描述
|
|
4752
|
+
h("div", { className: "dsh-github-pr-body-card" }, h("div", { className: "dsh-github-pr-comment-header" }, h("strong", null, `@${data.author.login}`), h("span", { style: { fontSize: "11px", opacity: 0.6 } }, "提单说明 (PR Body)")), h("div", { style: { padding: "12px" } }, data.body
|
|
4753
|
+
? h(CollapsibleBody, { text: data.body, defaultExpanded: true, idPrefix: "pr-body" })
|
|
4754
|
+
: h("span", { style: { opacity: 0.5, fontStyle: "italic" } }, "(该 PR 未填写描述正文)"))),
|
|
4755
|
+
// 讨论与评审评论流
|
|
4756
|
+
h("h4", { style: { margin: "14px 0 8px 0", fontSize: "13px", fontWeight: 600 } }, `讨论与评论时间线 (${(data.comments?.length || 0) + (data.reviews?.filter((r) => r.body)?.length || 0)})`), data.comments && data.comments.length > 0
|
|
4757
|
+
? data.comments.map(function (c) {
|
|
4758
|
+
return h("div", { key: c.id, className: "dsh-github-pr-comment-card" }, h("div", { className: "dsh-github-pr-comment-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, h("strong", null, `@${c.author.login}`), c.authorAssociation && c.authorAssociation !== "NONE"
|
|
4759
|
+
? h("span", { className: "dsh-github-badge dsh-badge-blue", style: { fontSize: "9px" } }, c.authorAssociation)
|
|
4760
|
+
: null), h("span", { style: { fontSize: "11px", opacity: 0.6 } }, formatRelativeTime(c.createdAt))), h("div", { style: { padding: "10px 12px" } }, h(CollapsibleBody, { text: c.body, defaultExpanded: false, idPrefix: "cmt-" + c.id })));
|
|
4761
|
+
})
|
|
4762
|
+
: h("div", { style: { textAlign: "center", padding: "16px 0", fontSize: "12px", opacity: 0.6 } }, "💬 暂无讨论与评论"))
|
|
4763
|
+
: null,
|
|
4764
|
+
// Tab 2: 变动文件与 Diff 比对
|
|
4765
|
+
activeTab === "files"
|
|
4766
|
+
? h("div", { className: "dsh-github-pr-files-pane", ref: paneContainerRef },
|
|
4767
|
+
// 左/上侧:变动文件列表(可拖拽调宽、可整块折叠)
|
|
4768
|
+
filePaneHidden
|
|
4769
|
+
? null
|
|
4770
|
+
: h("div", {
|
|
4771
|
+
className: "dsh-github-pr-file-list",
|
|
4772
|
+
style: { width: filePaneWidth + "px", flexShrink: 0 },
|
|
4773
|
+
}, data.files && data.files.length > 0
|
|
4774
|
+
? data.files.map(function (f) {
|
|
4775
|
+
var isSelected = f.path === selectedFilePath;
|
|
4776
|
+
return h("div", {
|
|
4777
|
+
key: f.path,
|
|
4778
|
+
className: "dsh-github-pr-file-item" + (isSelected ? " selected" : ""),
|
|
4779
|
+
onClick: () => setSelectedFilePath(f.path),
|
|
4780
|
+
title: `${f.path} (+${f.additions}, -${f.deletions})`,
|
|
4781
|
+
}, h("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, minWidth: 0 } }, f.path), h("div", { style: { display: "flex", gap: "4px", fontFamily: "monospace", fontSize: "10px", flexShrink: 0 } }, f.additions > 0 ? h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)" } }, `+${f.additions}`) : null, f.deletions > 0 ? h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)" } }, `-${f.deletions}`) : null));
|
|
4782
|
+
})
|
|
4783
|
+
: h("div", { style: { padding: "16px", opacity: 0.6 } }, "暂无变动文件记录")),
|
|
4784
|
+
// 拖拽分隔条:仅文件区展开时出现。折叠按钮常驻于此,
|
|
4785
|
+
// 保证「折叠后一定找得到恢复入口」——否则一旦折叠就再也点不回来。
|
|
4786
|
+
h("div", {
|
|
4787
|
+
className: "dsh-github-pr-splitter",
|
|
4788
|
+
onMouseDown: beginResize,
|
|
4789
|
+
title: filePaneHidden ? "点击展开文件列表" : "拖动调整文件列表宽度(双击可折叠)",
|
|
4790
|
+
onDoubleClick: toggleFilePane,
|
|
4791
|
+
}, h("button", {
|
|
4792
|
+
type: "button",
|
|
4793
|
+
className: "dsh-github-pr-splitter-toggle",
|
|
4794
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
4795
|
+
onClick: (e) => { e.stopPropagation(); toggleFilePane(); },
|
|
4796
|
+
title: filePaneHidden ? "展开文件列表" : "折叠文件列表",
|
|
4797
|
+
}, filePaneHidden ? "▶" : "◀")),
|
|
4798
|
+
// 右/下侧:当前选中文件的代码 Diff 视图
|
|
4799
|
+
h("div", { className: "dsh-github-pr-diff-viewer" }, selectedFilePath
|
|
4800
|
+
? h("div", { style: { display: "flex", flexDirection: "column", height: "100%" } }, h("div", { className: "dsh-github-pr-diff-toolbar" },
|
|
4801
|
+
// 文件区折叠后,这里是恢复入口的第一触点(分隔条上的
|
|
4802
|
+
// ◀/▶ 按钮是第二触点)。两个入口都保留,避免用户找不到。
|
|
4803
|
+
filePaneHidden
|
|
4804
|
+
? h("button", {
|
|
4805
|
+
className: "dsh-github-btn-sm",
|
|
4806
|
+
onClick: toggleFilePane,
|
|
4807
|
+
style: { padding: "2px 8px", fontSize: "10px", flexShrink: 0 },
|
|
4808
|
+
title: "展开左侧文件列表",
|
|
4809
|
+
}, `📁 文件列表 (${(data.files && data.files.length) || 0})`)
|
|
4810
|
+
: null, h("span", { style: { fontFamily: "monospace", fontWeight: 600, fontSize: "11px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, selectedFilePath), data.fileDiffs && data.fileDiffs[selectedFilePath]
|
|
4811
|
+
? h("button", {
|
|
4812
|
+
className: "dsh-github-btn-sm",
|
|
4813
|
+
onClick: () => copyText(data.fileDiffs[selectedFilePath], "文件 Diff"),
|
|
4814
|
+
style: { padding: "2px 8px", fontSize: "10px" },
|
|
4815
|
+
}, copied ? "✓ 已复制" : "📋 复制文件 Diff")
|
|
4816
|
+
: null), h("div", { style: { flex: 1, overflowY: "auto", minHeight: 0 } },
|
|
4817
|
+
// 截断警告必须显式呈现:否则用户会把「拿不到 diff」误读成「代码没改动」
|
|
4818
|
+
data.diffTruncated
|
|
4819
|
+
? h("div", {
|
|
4820
|
+
style: {
|
|
4821
|
+
margin: "8px",
|
|
4822
|
+
padding: "8px 12px",
|
|
4823
|
+
borderRadius: "6px",
|
|
4824
|
+
fontSize: "11px",
|
|
4825
|
+
lineHeight: 1.5,
|
|
4826
|
+
background: "rgba(210, 153, 34, 0.12)",
|
|
4827
|
+
border: "1px solid rgba(210, 153, 34, 0.35)",
|
|
4828
|
+
color: "var(--dsw-alias-state-attention-primary, #9a6700)",
|
|
4829
|
+
},
|
|
4830
|
+
}, "⚠️ 该 PR 的 diff 体量过大已触达输出上限,部分文件可能无法展示差异。可点击右上角「↗ 浏览器打开」查看完整内容。")
|
|
4831
|
+
: null, data.fileDiffs && data.fileDiffs[selectedFilePath]
|
|
4832
|
+
? renderDiffContent(data.fileDiffs[selectedFilePath])
|
|
4833
|
+
: renderDiffContent(""),
|
|
4834
|
+
// 行级审查评论:挂在具体代码行上的 review comment。
|
|
4835
|
+
// comments 字段只含对话区,这些必须来自独立通道;
|
|
4836
|
+
// 放在文件 diff 下方并按行号标注——不嵌进 hunk 中间,
|
|
4837
|
+
// 避免多行/折叠上下文下错位到错误的代码行。
|
|
4838
|
+
(() => {
|
|
4839
|
+
var fileComments = (Array.isArray(data.lineComments) ? data.lineComments : []).filter((c) => c.path === selectedFilePath);
|
|
4840
|
+
if (fileComments.length === 0)
|
|
4841
|
+
return null;
|
|
4842
|
+
return h("div", { className: "dsh-github-pr-line-comments" }, h("div", { className: "dsh-github-pr-line-comments-title" }, `💬 该文件的行级审查评论 (${fileComments.length})`), fileComments.map(function (c) {
|
|
4843
|
+
return h("div", { key: c.id, className: "dsh-github-pr-line-comment-item" }, h("div", { className: "dsh-github-pr-line-comment-head" }, h("strong", null, `@${c.author?.login || "unknown"}`), c.line
|
|
4844
|
+
? h("span", {
|
|
4845
|
+
style: {
|
|
4846
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
4847
|
+
fontSize: "10px",
|
|
4848
|
+
padding: "0 5px",
|
|
4849
|
+
borderRadius: "8px",
|
|
4850
|
+
background: "rgba(130, 80, 223, 0.12)",
|
|
4851
|
+
color: "#8250df",
|
|
4852
|
+
},
|
|
4853
|
+
title: `评论锚定在${c.side === "LEFT" ? "旧" : "新"}文件第 ${c.line} 行`,
|
|
4854
|
+
}, `L${c.line}${c.side === "LEFT" ? " (旧)" : ""}`)
|
|
4855
|
+
: null, h("span", { style: { fontSize: "10px", opacity: 0.6 } }, formatRelativeTime(c.createdAt))), h("div", { className: "dsh-github-pr-line-comment-body" }, h(CollapsibleBody, { text: c.body, defaultExpanded: false, idPrefix: "lc-" + c.id })));
|
|
4856
|
+
}));
|
|
4857
|
+
})()))
|
|
4858
|
+
: h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.6 } }, "请从左侧选择需要审阅的文件")))
|
|
4859
|
+
: null)
|
|
4860
|
+
: null));
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
// ---- [Bundled: issue-modal.js] ----
|
|
4864
|
+
/**
|
|
4865
|
+
* GitHub 关闭原因的状态语义映射。
|
|
4866
|
+
* 实测 `stateReason` 取值域:COMPLETED / NOT_PLANNED / REOPENED / ''(空串)。
|
|
4867
|
+
* 空串在关闭态下表示「未给出原因」,不渲染徽章即可。
|
|
4868
|
+
*/
|
|
4869
|
+
var ISSUE_STATE_REASON_LABEL = {
|
|
4870
|
+
COMPLETED: '已完成',
|
|
4871
|
+
NOT_PLANNED: '不计划处理',
|
|
4872
|
+
REOPENED: '已重开',
|
|
4873
|
+
};
|
|
4874
|
+
/** `#d73a4a` 形态的标签色转 rgba;非法色值退回中性灰,绝不让颜色拼错导致整块渲染失败 */
|
|
4875
|
+
function labelRgba(hex, alpha) {
|
|
4876
|
+
var raw = typeof hex === 'string' ? hex.replace('#', '').trim() : '';
|
|
4877
|
+
if (!/^[0-9a-fA-F]{6}$/.test(raw))
|
|
4878
|
+
return 'rgba(140, 149, 159, ' + alpha + ')';
|
|
4879
|
+
var r = parseInt(raw.slice(0, 2), 16);
|
|
4880
|
+
var g = parseInt(raw.slice(2, 4), 16);
|
|
4881
|
+
var b = parseInt(raw.slice(4, 6), 16);
|
|
4882
|
+
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')';
|
|
4883
|
+
}
|
|
4884
|
+
/**
|
|
4885
|
+
* 格式化里程碑截止日(未来 / 过去双向)。
|
|
4886
|
+
*
|
|
4887
|
+
* 为什么不能直接用 formatRelativeTime:它把**所有未来时间**都归一成「刚刚」
|
|
4888
|
+
* (见 utils.ts 的 `if (diff < 0) return "刚刚"`,那是为更新时间/创建时间这类
|
|
4889
|
+
* 永不为未来的字段设计的)。而里程碑 dueOn 绝大多数都在未来,
|
|
4890
|
+
* 直接套用会把「30 天后到期」渲染成「刚刚截止」——语义完全反过来。
|
|
4891
|
+
*/
|
|
4892
|
+
function formatDueDate(iso) {
|
|
4893
|
+
var t = typeof iso === 'string' ? Date.parse(iso) : NaN;
|
|
4894
|
+
if (isNaN(t))
|
|
4895
|
+
return '';
|
|
4896
|
+
var diff = t - Date.now();
|
|
4897
|
+
var DAY = 86400000;
|
|
4898
|
+
var days = Math.ceil(Math.abs(diff) / DAY);
|
|
4899
|
+
if (diff >= 0) {
|
|
4900
|
+
if (days <= 0)
|
|
4901
|
+
return '今天到期';
|
|
4902
|
+
if (days === 1)
|
|
4903
|
+
return '明天到期';
|
|
4904
|
+
if (days < 30)
|
|
4905
|
+
return days + ' 天后到期';
|
|
4906
|
+
if (days < 365)
|
|
4907
|
+
return Math.round(days / 30) + ' 个月后到期';
|
|
4908
|
+
return Math.round(days / 365) + ' 年后到期';
|
|
4909
|
+
}
|
|
4910
|
+
if (days <= 0)
|
|
4911
|
+
return '今天已到期';
|
|
4912
|
+
if (days === 1)
|
|
4913
|
+
return '昨天已到期';
|
|
4914
|
+
if (days < 30)
|
|
4915
|
+
return '已逾期 ' + days + ' 天';
|
|
4916
|
+
if (days < 365)
|
|
4917
|
+
return '已逾期 ' + Math.round(days / 30) + ' 个月';
|
|
4918
|
+
return '已逾期 ' + Math.round(days / 365) + ' 年';
|
|
4919
|
+
}
|
|
4920
|
+
/**
|
|
4921
|
+
* 图片附件清单。
|
|
4922
|
+
*
|
|
4923
|
+
* 为什么不在弹窗里直接 <img src>:图片可能来自任意 https 外域,直连会把
|
|
4924
|
+
* 宿主页面的 Referer 与来源暴露给第三方,也绕过插件的域名白名单防线。
|
|
4925
|
+
* 这里只给可点外链,由用户显式决定是否打开。
|
|
4926
|
+
*/
|
|
4927
|
+
function IssueImageList(props) {
|
|
4928
|
+
var h = React.createElement;
|
|
4929
|
+
var urls = Array.isArray(props.urls) ? props.urls : [];
|
|
4930
|
+
var st = React.useState(false);
|
|
4931
|
+
var open = st[0];
|
|
4932
|
+
var setOpen = st[1];
|
|
4933
|
+
if (urls.length === 0)
|
|
4934
|
+
return null;
|
|
4935
|
+
return h("span", { className: "dsh-github-issue-image-badge" }, h("button", {
|
|
4936
|
+
type: "button",
|
|
4937
|
+
className: "dsh-github-issue-chip dsh-github-issue-chip-btn",
|
|
4938
|
+
onClick: function () { setOpen(!open); },
|
|
4939
|
+
title: open ? "收起图片清单" : "展开图片链接清单(不在面板内直连渲染)",
|
|
4940
|
+
"aria-expanded": open ? "true" : "false",
|
|
4941
|
+
}, "📎 " + urls.length + " 张图片"), open
|
|
4942
|
+
? h(
|
|
4943
|
+
// 用 span 而非 div:整个元数据条本身是 span 级 content 容器,
|
|
4944
|
+
// div 嵌在 span 里虽然浏览器会容错,但属于非法嵌套。
|
|
4945
|
+
"span", { className: "dsh-github-issue-image-links" }, urls.map(function (u) {
|
|
4946
|
+
var name = u.split("?")[0].split("/").pop() || u;
|
|
4947
|
+
return h("a", {
|
|
4948
|
+
key: u,
|
|
4949
|
+
href: u,
|
|
4950
|
+
target: "_blank",
|
|
4951
|
+
rel: "noopener noreferrer",
|
|
4952
|
+
className: "dsh-github-issue-image-link",
|
|
4953
|
+
title: u,
|
|
4954
|
+
}, "🖼 " + (name.length > 42 ? name.slice(0, 42) + "…" : name));
|
|
4955
|
+
}))
|
|
4956
|
+
: null);
|
|
4957
|
+
}
|
|
4958
|
+
/**
|
|
4959
|
+
* 单条评论卡片。
|
|
4960
|
+
*
|
|
4961
|
+
* 为什么提到模块顶层:函数体内定义的组件每次父级渲染都会产生新引用,
|
|
4962
|
+
* React 按引用比较类型会卸载重建,CollapsibleBody 的展开状态随之重置。
|
|
4963
|
+
* 这是 pr-modal 上已经踩过的坑,Issue 侧不走第二遍。
|
|
4964
|
+
*/
|
|
4965
|
+
function IssueCommentCard(props) {
|
|
4966
|
+
var h = React.createElement;
|
|
4967
|
+
var c = props.comment;
|
|
4968
|
+
var author = c && c.author ? c.author.login : "ghost";
|
|
4969
|
+
var hidden = Boolean(c && c.isMinimized);
|
|
4970
|
+
return h("div", { className: "dsh-github-issue-comment-card" }, h("div", { className: "dsh-github-issue-comment-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap", minWidth: 0 } }, h("strong", null, "@" + author), c && c.authorAssociation && c.authorAssociation !== "NONE"
|
|
4971
|
+
? h("span", { className: "dsh-github-badge dsh-badge-blue", style: { fontSize: "9px" } }, c.authorAssociation)
|
|
4972
|
+
: null, c && c.includesCreatedEdit ? h("span", { style: { fontSize: "10px", opacity: 0.6 } }, "· 已编辑") : null, hidden
|
|
4973
|
+
? h("span", {
|
|
4974
|
+
className: "dsh-github-badge dsh-badge-red",
|
|
4975
|
+
style: { fontSize: "9px" },
|
|
4976
|
+
title: "GitHub 判定该评论被折叠(常见原因:垃圾信息 / 攻击性内容)",
|
|
4977
|
+
}, "已隐藏" + (c.minimizedReason ? "(" + c.minimizedReason + ")" : ""))
|
|
4978
|
+
: null), h("span", { style: { fontSize: "11px", opacity: 0.6, flexShrink: 0 } }, formatRelativeTime(c.createdAt))), h("div", { style: { padding: "10px 12px" } }, h(CollapsibleBody, { text: c.body, defaultExpanded: false, idPrefix: "ic-" + c.id })));
|
|
4979
|
+
}
|
|
4980
|
+
/**
|
|
4981
|
+
* Issue 在线查看弹窗(详情 + 元数据 + 评论时间线)。
|
|
4982
|
+
*
|
|
4983
|
+
* 与 PrDetailModal 的差异:Issue 没有 diff 载荷,因此不做双栏文件区,
|
|
4984
|
+
* 单列纵向滚动即可——多出来的宽度全部让给 Markdown 正文,可读性更好。
|
|
4985
|
+
*/
|
|
4986
|
+
function IssueDetailModal(props) {
|
|
4987
|
+
var issueNumber = props.issueNumber;
|
|
4988
|
+
var repoNameWithOwner = props.repoNameWithOwner || "";
|
|
4989
|
+
var cwd = props.cwd;
|
|
4990
|
+
var onClose = props.onClose;
|
|
4991
|
+
var showToast = props.showToast || (function () { });
|
|
4992
|
+
// Hooks 必须先于任何 early return 无条件执行(见 pr-modal 的同类注释):
|
|
4993
|
+
// 否则「issueNumber 由空变有值」时两次渲染的 Hook 数量不一致,直接崩。
|
|
4994
|
+
// 注意 useState 与 useEffect 都必须在这个位置之前——early return 卡在两者
|
|
4995
|
+
// 中间时,首帧(无编号)会跳过全部 effect,有编号的首帧却多出两个 effect,
|
|
4996
|
+
// 同样违反「每次渲染 Hook 序列一致」。
|
|
4997
|
+
var [data, setData] = React.useState(null);
|
|
4998
|
+
var [loading, setLoading] = React.useState(true);
|
|
4999
|
+
var [error, setError] = React.useState(null);
|
|
5000
|
+
var [copied, setCopied] = React.useState(false);
|
|
5001
|
+
React.useEffect(function () {
|
|
5002
|
+
var onKeyDown = function (e) {
|
|
5003
|
+
if (e.key === "Escape" || e.keyCode === 27)
|
|
5004
|
+
onClose();
|
|
5005
|
+
};
|
|
5006
|
+
window.addEventListener("keydown", onKeyDown);
|
|
5007
|
+
return function () { window.removeEventListener("keydown", onKeyDown); };
|
|
5008
|
+
}, [onClose]);
|
|
5009
|
+
React.useEffect(function () {
|
|
5010
|
+
if (!issueNumber) {
|
|
5011
|
+
setLoading(false);
|
|
5012
|
+
return;
|
|
5013
|
+
}
|
|
5014
|
+
setLoading(true);
|
|
5015
|
+
setError(null);
|
|
5016
|
+
var url = "/api/github/issue-detail?number=" +
|
|
5017
|
+
encodeURIComponent(String(issueNumber)) +
|
|
5018
|
+
(repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
|
|
5019
|
+
(cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
|
|
5020
|
+
fetch(url)
|
|
5021
|
+
.then(async function (res) {
|
|
5022
|
+
if (!res.ok) {
|
|
5023
|
+
var errText = await res.text().catch(function () { return ""; });
|
|
5024
|
+
var j = null;
|
|
5025
|
+
try {
|
|
5026
|
+
j = JSON.parse(errText);
|
|
5027
|
+
}
|
|
5028
|
+
catch (e) { }
|
|
5029
|
+
throw new Error((j && j.error) || errText || ("服务响应异常 (" + res.status + ")"));
|
|
5030
|
+
}
|
|
5031
|
+
return res.json();
|
|
5032
|
+
})
|
|
5033
|
+
.then(function (json) {
|
|
5034
|
+
setLoading(false);
|
|
5035
|
+
if (json && json.ok && json.data) {
|
|
5036
|
+
setData(json.data);
|
|
5037
|
+
}
|
|
5038
|
+
else {
|
|
5039
|
+
setError((json && json.error) || "获取 Issue 详情失败");
|
|
5040
|
+
}
|
|
5041
|
+
})
|
|
5042
|
+
.catch(function (err) {
|
|
5043
|
+
setLoading(false);
|
|
5044
|
+
setError((err && err.message) || "网络请求异常");
|
|
5045
|
+
});
|
|
5046
|
+
}, [issueNumber, repoNameWithOwner, cwd]);
|
|
5047
|
+
if (!issueNumber)
|
|
5048
|
+
return null;
|
|
5049
|
+
var h = React.createElement;
|
|
5050
|
+
var copyLink = function (text) {
|
|
5051
|
+
if (!text)
|
|
5052
|
+
return;
|
|
5053
|
+
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
5054
|
+
navigator.clipboard
|
|
5055
|
+
.writeText(text)
|
|
5056
|
+
.then(function () {
|
|
5057
|
+
setCopied(true);
|
|
5058
|
+
showToast("✅ Issue 链接已复制到剪贴板");
|
|
5059
|
+
setTimeout(function () { setCopied(false); }, 2000);
|
|
5060
|
+
})
|
|
5061
|
+
.catch(function () { });
|
|
5062
|
+
}
|
|
5063
|
+
};
|
|
5064
|
+
var stateBadge = function (d) {
|
|
5065
|
+
if (!d)
|
|
5066
|
+
return null;
|
|
5067
|
+
var isOpen = String(d.state).toUpperCase() === "OPEN";
|
|
5068
|
+
return h("span", {
|
|
5069
|
+
className: "dsh-github-badge " + (isOpen ? "dsh-badge-green" : "dsh-badge-red"),
|
|
5070
|
+
style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 },
|
|
5071
|
+
}, isOpen ? "OPEN" : "CLOSED");
|
|
5072
|
+
};
|
|
5073
|
+
/**
|
|
5074
|
+
* 元数据条:只渲染真实存在的字段。
|
|
5075
|
+
* 空的「无标签 / 无指派 / 无里程碑」占位没有信息量,只会把正文推得更远。
|
|
5076
|
+
*/
|
|
5077
|
+
var metaChips = function (d) {
|
|
5078
|
+
var chips = [];
|
|
5079
|
+
var reason = d.stateReason ? ISSUE_STATE_REASON_LABEL[String(d.stateReason).toUpperCase()] : "";
|
|
5080
|
+
if (reason) {
|
|
5081
|
+
chips.push(h("span", {
|
|
5082
|
+
key: "reason",
|
|
5083
|
+
className: "dsh-github-issue-chip",
|
|
5084
|
+
title: "关闭原因 (stateReason): " + d.stateReason,
|
|
5085
|
+
}, "✓ " + reason));
|
|
5086
|
+
}
|
|
5087
|
+
if (d.isPinned) {
|
|
5088
|
+
chips.push(h("span", { key: "pin", className: "dsh-github-issue-chip" }, "📌 置顶"));
|
|
5089
|
+
}
|
|
5090
|
+
if (d.issueType) {
|
|
5091
|
+
chips.push(h("span", { key: "type", className: "dsh-github-issue-chip" }, "🏷️ " + d.issueType));
|
|
5092
|
+
}
|
|
5093
|
+
(Array.isArray(d.labels) ? d.labels : []).forEach(function (l, i) {
|
|
5094
|
+
chips.push(h("span", {
|
|
5095
|
+
key: "label-" + i + "-" + l.name,
|
|
5096
|
+
className: "dsh-github-issue-chip dsh-github-issue-label-chip",
|
|
5097
|
+
style: {
|
|
5098
|
+
background: labelRgba(l.color, 0.14),
|
|
5099
|
+
borderColor: labelRgba(l.color, 0.45),
|
|
2812
5100
|
},
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
5101
|
+
title: l.description || l.name,
|
|
5102
|
+
}, h("span", {
|
|
5103
|
+
className: "dsh-github-issue-label-dot",
|
|
5104
|
+
style: { background: labelRgba(l.color, 0.9) },
|
|
5105
|
+
}), l.name));
|
|
5106
|
+
});
|
|
5107
|
+
(Array.isArray(d.assignees) ? d.assignees : []).forEach(function (a, i) {
|
|
5108
|
+
chips.push(h("span", { key: "asg-" + i, className: "dsh-github-issue-chip" }, "👤 " + a.login));
|
|
5109
|
+
});
|
|
5110
|
+
if (d.milestone && d.milestone.title) {
|
|
5111
|
+
var dueText = d.milestone.dueOn ? formatDueDate(d.milestone.dueOn) : '';
|
|
5112
|
+
chips.push(h("span", {
|
|
5113
|
+
key: "milestone",
|
|
5114
|
+
className: "dsh-github-issue-chip" + (dueText.indexOf('已逾期') >= 0 ? " dsh-github-issue-chip-overdue" : ""),
|
|
5115
|
+
title: d.milestone.dueOn ? "截止 " + d.milestone.dueOn : "未设置截止日期",
|
|
5116
|
+
}, "🏁 " + d.milestone.title + (dueText ? " · " + dueText : "")));
|
|
5117
|
+
}
|
|
5118
|
+
if (d.subIssues && Number(d.subIssues.total) > 0) {
|
|
5119
|
+
chips.push(h("span", { key: "sub", className: "dsh-github-issue-chip" }, "🧩 子议题 " + d.subIssues.completed + "/" + d.subIssues.total));
|
|
5120
|
+
}
|
|
5121
|
+
(Array.isArray(d.linkedPrs) ? d.linkedPrs : []).forEach(function (p, i) {
|
|
5122
|
+
chips.push(h("a", {
|
|
5123
|
+
key: "pr-" + i,
|
|
5124
|
+
className: "dsh-github-issue-chip dsh-github-issue-chip-link",
|
|
5125
|
+
href: p.url,
|
|
5126
|
+
target: "_blank",
|
|
5127
|
+
rel: "noopener noreferrer",
|
|
5128
|
+
title: "该 Issue 被此 Pull Request 关联/关闭",
|
|
5129
|
+
}, "🔗 PR #" + p.number));
|
|
5130
|
+
});
|
|
5131
|
+
chips.push(h(IssueImageList, { key: "images", urls: d.images || [] }));
|
|
5132
|
+
return chips.length > 0 ? h("div", { className: "dsh-github-issue-meta" }, ...chips) : null;
|
|
5133
|
+
};
|
|
5134
|
+
var bodyCard = function (d) {
|
|
5135
|
+
return h("div", { className: "dsh-github-issue-body-card" }, h("div", { className: "dsh-github-issue-comment-header" }, h("strong", null, "@" + d.author.login), h("span", { style: { fontSize: "11px", opacity: 0.6 } }, "提单说明 (Issue Body)")), h("div", { className: "dsh-github-issue-body-content" }, d.body
|
|
5136
|
+
? h(CollapsibleBody, { text: d.body, defaultExpanded: true, idPrefix: "issue-body" })
|
|
5137
|
+
: h("span", { style: { opacity: 0.5, fontStyle: "italic" } }, "(该 Issue 未填写正文)")));
|
|
5138
|
+
};
|
|
5139
|
+
return h("div", { className: "dsh-github-modal-overlay", onClick: onClose, style: { zIndex: 100000 } }, h("div", {
|
|
5140
|
+
className: "dsh-github-modal-dialog dsh-github-issue-modal-dialog",
|
|
5141
|
+
role: "dialog",
|
|
5142
|
+
"aria-modal": "true",
|
|
5143
|
+
"aria-label": "Issue #" + issueNumber + " 详情",
|
|
5144
|
+
onClick: function (e) { e.stopPropagation(); },
|
|
5145
|
+
},
|
|
5146
|
+
// 头部
|
|
5147
|
+
h("div", { className: "dsh-github-modal-header dsh-github-issue-modal-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, renderOctocat(18, "#1f883d"), h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 } }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("h3", {
|
|
5148
|
+
style: {
|
|
5149
|
+
margin: 0,
|
|
5150
|
+
fontSize: "14px",
|
|
5151
|
+
fontWeight: 600,
|
|
5152
|
+
overflow: "hidden",
|
|
5153
|
+
textOverflow: "ellipsis",
|
|
5154
|
+
whiteSpace: "nowrap",
|
|
5155
|
+
},
|
|
5156
|
+
title: data ? "#" + data.number + " " + data.title : "Issue #" + issueNumber,
|
|
5157
|
+
}, data ? "#" + data.number + " " + data.title : "Issue #" + issueNumber), stateBadge(data)), data
|
|
5158
|
+
? h("div", {
|
|
5159
|
+
style: {
|
|
5160
|
+
fontSize: "11px",
|
|
5161
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
5162
|
+
display: "flex",
|
|
5163
|
+
gap: "6px",
|
|
5164
|
+
alignItems: "center",
|
|
5165
|
+
flexWrap: "wrap",
|
|
5166
|
+
},
|
|
5167
|
+
}, 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
|
+
? h("span", { style: { fontFamily: "monospace", opacity: 0.85 } }, repoNameWithOwner)
|
|
5169
|
+
: 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
|
|
5179
|
+
? h("a", {
|
|
5180
|
+
href: data.url,
|
|
5181
|
+
target: "_blank",
|
|
5182
|
+
rel: "noopener noreferrer",
|
|
5183
|
+
className: "dsh-github-btn-sm",
|
|
5184
|
+
style: { padding: "4px 8px", fontSize: "11px", textDecoration: "none", color: "inherit" },
|
|
5185
|
+
title: "在系统默认浏览器中打开该 Issue",
|
|
5186
|
+
}, "↗ 浏览器打开")
|
|
5187
|
+
: null, h("button", {
|
|
5188
|
+
type: "button",
|
|
5189
|
+
"aria-label": "关闭 (Esc)",
|
|
5190
|
+
style: {
|
|
5191
|
+
background: "none",
|
|
5192
|
+
border: "none",
|
|
5193
|
+
cursor: "pointer",
|
|
5194
|
+
fontSize: "18px",
|
|
5195
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
5196
|
+
padding: "0 6px",
|
|
5197
|
+
},
|
|
5198
|
+
onClick: onClose,
|
|
5199
|
+
title: "关闭 (Esc)",
|
|
5200
|
+
}, "✕"))),
|
|
5201
|
+
// 主体
|
|
5202
|
+
loading
|
|
5203
|
+
? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 Issue 详情与讨论…")
|
|
5204
|
+
: 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));
|
|
2819
5229
|
}
|
|
2820
5230
|
|
|
2821
5231
|
// ---- [Bundled: sync-settings-modal.js] ----
|
|
@@ -3180,6 +5590,12 @@ function GlobalSyncSettingsModal(props) {
|
|
|
3180
5590
|
* 3. 拒绝技术自嗨清单,从用户行动视角组织:3分钟快速上手、Agent实战Prompt、双界面图解与常见排错;
|
|
3181
5591
|
* 4. 宽屏卡片排列,配备 Prompt 一键复制到剪贴板与短暂反馈气泡;
|
|
3182
5592
|
* 5. 监听键盘 Esc 键与遮罩点击秒关,复制完指令直接回到聊天输入框发送,零打扰、不中断工作流。
|
|
5593
|
+
*
|
|
5594
|
+
* 【内容与代码同步的纪律】
|
|
5595
|
+
* 本文件里出现的工具数量、action 数量、Tab 名称、按钮文案,全部以当前代码为准:
|
|
5596
|
+
* 工具矩阵 = src/main.ts 的 18 处 ctx.tools.register;
|
|
5597
|
+
* Tab 布局 = src/client.ts 的 TAB_DEFS 与 src/ui/workspace-panel.ts 的四个标签;
|
|
5598
|
+
* 若后续增删工具或 Tab,必须同步改这里,否则用户会照着过时说明去找不存在的入口。
|
|
3183
5599
|
*/
|
|
3184
5600
|
function GithubHelpModal(props) {
|
|
3185
5601
|
if (!props.isOpen)
|
|
@@ -3216,6 +5632,7 @@ function GithubHelpModal(props) {
|
|
|
3216
5632
|
{ id: "quickstart", label: "🚀 3分钟极速上手" },
|
|
3217
5633
|
{ id: "prompts", label: "🤖 Agent 提示词实战" },
|
|
3218
5634
|
{ id: "panels", label: "🖥️ 两大界面操作图解" },
|
|
5635
|
+
{ id: "tools", label: "🧰 18 大工具速查" },
|
|
3219
5636
|
{ id: "faq", label: "🔑 常见自愈排错" },
|
|
3220
5637
|
];
|
|
3221
5638
|
// 2. 渲染各 Tab 独立视图
|
|
@@ -3232,7 +5649,7 @@ function GithubHelpModal(props) {
|
|
|
3232
5649
|
step: "2",
|
|
3233
5650
|
title: "打开你的代码仓库",
|
|
3234
5651
|
desc: "在 DSH 中打开任何包含 .git 的项目文件夹,插件将自动毫秒级绑定当前仓库上下文。",
|
|
3235
|
-
actionText: "无需每次告诉 Agent 仓库叫什么名字,插件底层已自动完成 CWD
|
|
5652
|
+
actionText: "无需每次告诉 Agent 仓库叫什么名字,插件底层已自动完成 CWD 工作区绑定,提单与审查均精准面向当前项目。\n若当前是 Fork 仓库,右侧栏顶部会显示血缘行,可一键在「我的复刻」与「上游主干」之间切换基准仓库。",
|
|
3236
5653
|
},
|
|
3237
5654
|
{
|
|
3238
5655
|
step: "3",
|
|
@@ -3242,7 +5659,9 @@ function GithubHelpModal(props) {
|
|
|
3242
5659
|
},
|
|
3243
5660
|
].map(function (s, i) {
|
|
3244
5661
|
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)));
|
|
3245
|
-
})
|
|
5662
|
+
}),
|
|
5663
|
+
// 能力总览:让用户三秒钟知道这插件到底能干什么,而不是去翻 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 个标签的全景驾驶舱。")));
|
|
3246
5665
|
}
|
|
3247
5666
|
if (activeTab === "prompts") {
|
|
3248
5667
|
var promptCards = [
|
|
@@ -3271,13 +5690,25 @@ function GithubHelpModal(props) {
|
|
|
3271
5690
|
aiAction: "Agent 自动获取改动文件与行级差异,结合工程最佳实践逐项排查边界条件、内存泄漏、空指针与安全漏洞。",
|
|
3272
5691
|
},
|
|
3273
5692
|
{
|
|
3274
|
-
title: "
|
|
5693
|
+
title: "📝 场景五:从用户报障截图直接定位问题",
|
|
5694
|
+
desc: "用户提的 Issue 里贴了崩溃截图,Agent 可以直接「看」图,不用你手抄报错信息。",
|
|
5695
|
+
prompt: "查看 Issue #42 的正文与截图,判断是哪个模块出的问题并给出修复方案",
|
|
5696
|
+
aiAction: "Agent 调用 github_issue(view_images) 把正文与评论里的图片下载到本地并转为可读图像内容块,结合文字描述一起分析。",
|
|
5697
|
+
},
|
|
5698
|
+
{
|
|
5699
|
+
title: "🏷️ 场景六:批量标签治理与跨仓库同步",
|
|
5700
|
+
desc: "新仓库要配一套和主仓库一致的标签体系,手点太慢。",
|
|
5701
|
+
prompt: "把 A 仓库的标签体系克隆到当前仓库,然后给所有 open 的 bug Issue 打上 triage 标签",
|
|
5702
|
+
aiAction: "Agent 调用 github_label(clone) 复制标签颜色与描述,再按条件批量追加标签;删除类高危操作前会先返回影响评估要求你确认。",
|
|
5703
|
+
},
|
|
5704
|
+
{
|
|
5705
|
+
title: "💬 场景七:团队方案提案与开源 RFC 研讨",
|
|
3275
5706
|
desc: "需要和团队讨论架构思路或征集想法时,直接将讨论沉淀在 GitHub Discussions 知识库中。",
|
|
3276
5707
|
prompt: "在当前仓库发起一个关于 '优化前端构建性能' 的 RFC 讨论方案",
|
|
3277
5708
|
aiAction: "Agent 自动调用 Discussions 工具在指定分类下发起主题,格式化排版技术方案正文,并返回可供团队直达探讨的链接。",
|
|
3278
5709
|
},
|
|
3279
5710
|
];
|
|
3280
|
-
return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "14px" } }, "以下精选了开发者日常研发中最高频的
|
|
5711
|
+
return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "14px" } }, "以下精选了开发者日常研发中最高频的 7 大真实协作场景。点击卡片右侧的「📋 一键复制」按钮,直接粘贴到聊天输入框发给 Agent 即可:"), promptCards.map(function (c, idx) {
|
|
3281
5712
|
var isCopied = copiedIdx === idx;
|
|
3282
5713
|
return h("div", { key: idx, className: "dsh-github-help-card" }, h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" } }, h("div", { className: "dsh-github-help-title" }, c.title), h("button", {
|
|
3283
5714
|
className: "dsh-github-btn-sm",
|
|
@@ -3297,29 +5728,97 @@ function GithubHelpModal(props) {
|
|
|
3297
5728
|
if (activeTab === "panels") {
|
|
3298
5729
|
return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "16px" } }, "插件为人类开发者量身打造了两套互补的可视化工作台,消灭频繁在 VS Code、终端与浏览器切屏的上下文丢失痛点:"), [
|
|
3299
5730
|
{
|
|
3300
|
-
title: "1. 全景驾驶舱 (Main Dashboard) —— 掌控全局资产",
|
|
5731
|
+
title: "1. 全景驾驶舱 (Main Dashboard) —— 掌控全局资产 · 4 个标签",
|
|
3301
5732
|
items: [
|
|
3302
5733
|
"打开入口:点击 DSH 最左侧主导航栏的 Octocat 猫咪徽标,瞬间全屏展开;",
|
|
3303
5734
|
"SWR 0ms 瞬间秒开:采用本地持久化快照体系,打开界面零转圈、零等待;",
|
|
3304
|
-
"
|
|
3305
|
-
"
|
|
3306
|
-
"
|
|
5735
|
+
"📂 本地工作区矩阵:一屏透视本机所有项目与远程仓库的关联状态、分支领先/落后、未提交文件数;",
|
|
5736
|
+
"☁️ 我的 GitHub 仓库:名下全部仓库、默认分支与 Star 状态,支持关键词搜索;",
|
|
5737
|
+
"🔀 Pull Requests:默认展示「全部」,顶部胶囊可切 🟢 Open / 🟣 Merged / ⚪ Closed 并显示各档计数;点击任一卡片在线唤起 PR 审阅工作台;",
|
|
5738
|
+
"📝 任务与待办 Issues:三维分类(📥 用户提单 / 🎯 指派待办 / ✍️ 我发起的),点击卡片在线查看 Issue 详情;",
|
|
5739
|
+
"右下角长期驻留 使用指南 / ⚙️ 设置 两个入口,设置内含数据同步、网络代理、更新源与关于四个分区。",
|
|
3307
5740
|
],
|
|
3308
5741
|
},
|
|
3309
5742
|
{
|
|
3310
|
-
title: "2. 会话右侧栏面板 (Sidebar Panel) —— 深度协同当前工作区",
|
|
5743
|
+
title: "2. 会话右侧栏面板 (Sidebar Panel) —— 深度协同当前工作区 · 5 个 Tab",
|
|
3311
5744
|
items: [
|
|
3312
5745
|
"打开入口:位于每个会话工作区的右侧抽屉栏,深度锁定当前会话的物理 Git 仓库;",
|
|
3313
|
-
"
|
|
3314
|
-
"
|
|
3315
|
-
"
|
|
5746
|
+
"仓库身份与切换:顶部展示当前关联仓库、Fork 血缘行与权限徽章(只读/可写);若配置了多个 remote,可通过 RepoSelectorDropdown 下拉一键切换基准仓库;", "Git Tab:三段式提交时间线(待推送 / 待拉取 / 最近),默认 15 条、下滑触底自动加载;点任意提交行弹出完整 Commit 详情;",
|
|
5747
|
+
"未提交 Tab:实时监听本地未提交文件与状态码(?? 新增 / M 修改 / D 删除 / R 重命名),点击文件可就地下钻查看该文件的 Unified Diff;",
|
|
5748
|
+
"PR Tab:展示全部 PR 并按状态过滤;带三态 CI 门禁、冲突检测、审核判定与 Draft 标示;点击卡片在线唤起 PR 审阅工作台(描述 / 评论 / 文件 Diff 双栏比对);",
|
|
5749
|
+
"Issues Tab:展示全部 Issue 并按状态过滤;点击卡片在线查看详情(Markdown 正文 / 评论时间线 / 标签色块 / 里程碑 / 关联 PR / 隐藏评论标注);",
|
|
5750
|
+
"Actions Tab:最近工作流运行记录与触发方式,失败时给出可直接交给 Agent 的诊断指引;",
|
|
3316
5751
|
"动静解耦架构:修改本地代码只定向查询本地 .git 核心元数据(耗时仅 5ms),绝对不发起远端网络请求,杜绝网络风暴。",
|
|
3317
5752
|
],
|
|
3318
5753
|
},
|
|
5754
|
+
{
|
|
5755
|
+
title: "3. 两个弹窗的通用操作习惯",
|
|
5756
|
+
items: [
|
|
5757
|
+
"PR / Issue 弹窗都支持:按 Esc 或点击遮罩关闭、右上角「📋 复制链接」、「↗ 浏览器打开」直达 GitHub;",
|
|
5758
|
+
"列表卡片上的「GitHub ↗」按钮是独立的浏览器外跳入口,点它不会触发弹窗(已做事件隔离);",
|
|
5759
|
+
"PR 弹窗的文件区可拖拽分隔条调宽、双击或点 ◀ 折叠;布局偏好会记在本地,下次打开保持原样;",
|
|
5760
|
+
"长评论默认折叠(显示「⌄ 展开全部」),PR/Issue 正文默认展开——避免一屏被一段超长文字占满。",
|
|
5761
|
+
],
|
|
5762
|
+
},
|
|
3319
5763
|
].map(function (sec, idx) {
|
|
3320
5764
|
return h("div", { key: idx, className: "dsh-github-help-card" }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "8px" } }, sec.title), h("ul", { style: { margin: 0, paddingLeft: "18px", fontSize: "12px", lineHeight: "1.7", color: "var(--dsw-alias-label-secondary, #8b949e)" } }, sec.items.map(function (it, i) { return h("li", { key: i }, it); })));
|
|
3321
5765
|
}));
|
|
3322
5766
|
}
|
|
5767
|
+
if (activeTab === "tools") {
|
|
5768
|
+
var toolGroups = [
|
|
5769
|
+
{
|
|
5770
|
+
group: "日常研发主力",
|
|
5771
|
+
items: [
|
|
5772
|
+
{ name: "github_pr", desc: "PR 全生命周期:列表/详情/diff/checks/create/merge/review/ready/update_branch/revert/preview/checkout,含行级评论与附件上传" },
|
|
5773
|
+
{ 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 启停" },
|
|
5775
|
+
{ name: "github_repo", desc: "仓库元数据、免 clone 读文件(read_file/read_dir)、跨仓检索(code/issues/prs/commits)、Fork 同步" },
|
|
5776
|
+
],
|
|
5777
|
+
},
|
|
5778
|
+
{
|
|
5779
|
+
group: "交付与配置",
|
|
5780
|
+
items: [
|
|
5781
|
+
{ name: "github_release", desc: "发版历史、自动生成说明、创建 Release、上传/下载构建资产" },
|
|
5782
|
+
{ name: "github_secret", desc: "Actions Secrets 与 Variables:列表/设置/删除(密文经 stdin 通道,绝不进命令行)" },
|
|
5783
|
+
{ name: "github_label", desc: "标签体系 CRUD 与跨仓库一键克隆(删除需 confirm)" },
|
|
5784
|
+
{ name: "github_ruleset", desc: "分支保护规则集查看与「为什么这个 PR 不能合」的策略排障" },
|
|
5785
|
+
{ name: "github_cache", desc: "Actions 构建缓存盘点、按 Key/ID 删除与全量清空(高危需 confirm)" },
|
|
5786
|
+
],
|
|
5787
|
+
},
|
|
5788
|
+
{
|
|
5789
|
+
group: "生态与协作",
|
|
5790
|
+
items: [
|
|
5791
|
+
{ name: "github_skill", desc: "Agent Skills 生态:跨仓检索/预览/安装/发布自检/批量更新" },
|
|
5792
|
+
{ name: "github_status", desc: "账号维度跨仓待办大盘 + 运行时健康度与 API 降级率审计" },
|
|
5793
|
+
{ name: "github_agent_task", desc: "Copilot 云端编码智能体:派活、查状态、拉会话日志" },
|
|
5794
|
+
{ name: "github_discussion", desc: "社区讨论与 RFC:列表/详情/发起/编辑/评论" },
|
|
5795
|
+
{ name: "github_project", desc: "Projects v2 看板:项目列表、条目增删与草稿任务" },
|
|
5796
|
+
{ name: "github_codespace", desc: "云端开发机:列表/详情/停止(防计费)/销毁" },
|
|
5797
|
+
{ name: "github_gist", desc: "代码片段与排障留痕:创建 Secret Gist、查看、编辑、删除" },
|
|
5798
|
+
],
|
|
5799
|
+
},
|
|
5800
|
+
{
|
|
5801
|
+
group: "兜底与元能力",
|
|
5802
|
+
items: [
|
|
5803
|
+
{ name: "github_api", desc: "REST / GraphQL 逃生门,支持 --jq 过滤与 raw 原文模式" },
|
|
5804
|
+
{ name: "github_help", desc: "全景命令映射、官方语法速查与 gh 报错自愈指引" },
|
|
5805
|
+
],
|
|
5806
|
+
},
|
|
5807
|
+
];
|
|
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) {
|
|
5809
|
+
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
|
+
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
|
+
style: {
|
|
5812
|
+
flexShrink: 0,
|
|
5813
|
+
width: "132px",
|
|
5814
|
+
fontSize: "11px",
|
|
5815
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
5816
|
+
color: "var(--dsw-alias-link, #0969da)",
|
|
5817
|
+
},
|
|
5818
|
+
}, t.name), h("span", { style: { fontSize: "12px", lineHeight: "1.6", color: "var(--dsw-alias-label-secondary, #8b949e)" } }, t.desc));
|
|
5819
|
+
}));
|
|
5820
|
+
}), h("div", { className: "dsh-github-help-card" }, h("div", { className: "dsh-github-help-title", style: { marginBottom: "8px" } }, "⌨️ 人类 Slash 指令"), h("div", { style: { fontSize: "12px", lineHeight: "1.9", color: "var(--dsw-alias-label-secondary, #8b949e)" } }, h("code", { className: "dsh-github-md-inline-code" }, "/gh"), " 或 ", h("code", { className: "dsh-github-md-inline-code" }, "/gh status"), " —— 账号与仓库健康度诊断(登录态、权限、网络、版本);", h("br"), h("code", { className: "dsh-github-md-inline-code" }, "/gh upgrade"), " —— 一键跨平台自动升级插件;", h("br"), h("code", { className: "dsh-github-md-inline-code" }, "/gh help"), " —— 在聊天流中打印精简版指令与范例;", h("br"), "想看点状图文说明,就是当前这个窗口:右下角「📖 使用指南」。")));
|
|
5821
|
+
}
|
|
3323
5822
|
if (activeTab === "faq") {
|
|
3324
5823
|
var faqs = [
|
|
3325
5824
|
{
|
|
@@ -3328,16 +5827,24 @@ function GithubHelpModal(props) {
|
|
|
3328
5827
|
},
|
|
3329
5828
|
{
|
|
3330
5829
|
q: "Q2: 提示「缺少权限 (Scope)」或「SCOPE_REQUIRED」怎么办?",
|
|
3331
|
-
a: "当调用项目看板(Projects v2)或云端开发机(Codespaces)时,GitHub 要求专有的 Token Scope。在系统终端执行对应的一键自愈命令完成增量授权即可(无需重新登录):\n-
|
|
5830
|
+
a: "当调用项目看板(Projects v2)或云端开发机(Codespaces)时,GitHub 要求专有的 Token Scope。在系统终端执行对应的一键自愈命令完成增量授权即可(无需重新登录):\n- 项目看板(只读):`gh auth refresh -s read:project`\n- 项目看板(读写):`gh auth refresh -s project`\n- 云端开发机:`gh auth refresh -h github.com -s codespace`",
|
|
3332
5831
|
},
|
|
3333
5832
|
{
|
|
3334
|
-
q: "Q3:
|
|
5833
|
+
q: "Q3: 点了一键升级提示「处于 24h 安全保护期 (防投毒策略)」怎么办?",
|
|
3335
5834
|
a: "这是 DSH 官方内置包管理器针对刚发布未满 24 小时新版本的供应链防投毒安全锁机制。\n- 立即体验:请先【完全退出关闭 DSH 应用程序】,然后在系统终端执行复制的手动安装命令;\n- 稍后升级:无需任何操作,新版本发版满 24 小时后,再次点击一键升级即可自动安装。",
|
|
3336
5835
|
},
|
|
3337
5836
|
{
|
|
3338
5837
|
q: "Q4: 使用代理节点时偶尔出现网络波动怎么办?",
|
|
3339
5838
|
a: "插件内置了 1s ➔ 2s ➔ 4s 的阶梯指数退避重试机制。代理节点若发生短时抖动断流,系统会自动在后台静默发起自愈重试,在节点恢复后瞬间平滑拉取最新数据,不会立刻亮出大红横幅打扰你。同时电脑联网恢复瞬间也会自动触发静默更新。",
|
|
3340
5839
|
},
|
|
5840
|
+
{
|
|
5841
|
+
q: "Q5: 右侧栏 PR / Issues 列表是空的,但 GitHub 网页上明明有记录?",
|
|
5842
|
+
a: "先看顶部状态胶囊是不是停在某个具体档位(例如「Merged」)而该档确实为零——点「全部」即可看到所有记录。\n若「全部」也是空的,通常是两类原因:\n- 仓库关闭了 Issues 功能:面板会给出「该仓库未开启 Issues」的确定性提示,这不是故障;\n- 当前工作区没绑定对仓库:看右侧栏顶部显示的是哪个仓库,若不对可在仓库下拉里切换基准,或检查 `.git/config` 的 remote 配置。",
|
|
5843
|
+
},
|
|
5844
|
+
{
|
|
5845
|
+
q: "Q6: 为什么改了代码,插件界面要重启 DSH 才生效?",
|
|
5846
|
+
a: "如果你是在开发这个插件本身:后端 HTTP 路由是在插件 `apply()` 时一次性注册到宿主的 WebServer 上的,`pnpm run build` 只更新磁盘产物,不会给已运行的进程热注册新路由。改完代码需重启 DSH 才能挂上新端点。\n若只是普通使用,请忽略此条——正常升级走一键升级即可。",
|
|
5847
|
+
},
|
|
3341
5848
|
];
|
|
3342
5849
|
return h("div", null, h("div", { className: "dsh-github-help-desc", style: { fontSize: "13px", marginBottom: "14px" } }, "常见环境、权限与网络问题的排查与自愈指引:"), faqs.map(function (f, idx) {
|
|
3343
5850
|
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));
|
|
@@ -3353,9 +5860,10 @@ function GithubHelpModal(props) {
|
|
|
3353
5860
|
h("div", { className: "dsh-github-help-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px" } }, h("span", { style: { fontSize: "20px" } }, "📖"), h("h3", { style: { margin: 0, fontSize: "16px", fontWeight: 600, color: "inherit" } }, "DSH GitHub Flow · 使用指南与实战手册")), h("button", {
|
|
3354
5861
|
style: { background: "none", border: "none", cursor: "pointer", fontSize: "18px", color: "var(--dsw-alias-label-secondary, #8b949e)", padding: "0 4px" },
|
|
3355
5862
|
onClick: props.onClose,
|
|
5863
|
+
"aria-label": "关闭 (Esc)",
|
|
3356
5864
|
title: "关闭 (Esc)",
|
|
3357
5865
|
}, "✕")),
|
|
3358
|
-
//
|
|
5866
|
+
// 顶部分类 Segment 导航栏
|
|
3359
5867
|
h("div", { className: "dsh-github-help-nav" }, tabs.map(function (t) {
|
|
3360
5868
|
var isActive = activeTab === t.id;
|
|
3361
5869
|
return h("button", {
|
|
@@ -3626,7 +6134,16 @@ function GithubHelpModal(props) {
|
|
|
3626
6134
|
return h("div", { key: idx, style: lineStyle }, line || " ");
|
|
3627
6135
|
})));
|
|
3628
6136
|
}
|
|
3629
|
-
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
|
|
6137
|
+
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
|
+
// 必须追加在形参末尾:该函数有 17 个位置参数,插在中间会静默错位
|
|
6139
|
+
onSelectIssue = null,
|
|
6140
|
+
/**
|
|
6141
|
+
* 状态筛选(默认 all)。
|
|
6142
|
+
* 为什么由组件传入而不是在本函数内 useState:本函数是每次渲染都重新执行的
|
|
6143
|
+
* 普通函数,内部调用 useState 会被 React 当成新的 Hook 序列直接抛错。
|
|
6144
|
+
* 状态必须住在 GithubRightbarBody 组件体内,这里只接收值与 setter。
|
|
6145
|
+
*/
|
|
6146
|
+
prStateFilter = "all", issueStateFilter = "all", onChangePrFilter = null, onChangeIssueFilter = null) {
|
|
3630
6147
|
var auth = data ? data.auth : null;
|
|
3631
6148
|
var repo = data ? data.repo : null;
|
|
3632
6149
|
var prs = (data && data.pullRequests) || [];
|
|
@@ -3650,6 +6167,12 @@ function GithubHelpModal(props) {
|
|
|
3650
6167
|
},
|
|
3651
6168
|
})
|
|
3652
6169
|
: null;
|
|
6170
|
+
// 仓库配置提示条:与网络告警互斥展示。
|
|
6171
|
+
// 仓库关了 Issues 时不该同时出现「网络连接失败」——那是两件性质相反的事
|
|
6172
|
+
// (一个是稳定配置事实,一个是瞬时传输故障)。有真网络错误时以错误优先。
|
|
6173
|
+
var repoNoticeBarEl = networkAlertBannerEl
|
|
6174
|
+
? null
|
|
6175
|
+
: h(RepoNoticeBar, { isRightbar, notices: data?.notices });
|
|
3653
6176
|
var headerEl = h("div", { className: "dsh-github-header" }, h("div", { className: "dsh-github-title" }, renderOctocatBadge(isRightbar ? 26 : 30), h("h2", null, isRightbar ? "GitHub 仓库" : "GitHub 工作台"), renderPluginBadge(h), syncStatus ? h(SyncStatusBadge, { syncStatus, lastSyncedAt, isRightbar }) : null), h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } }, isRightbar && onToggleHeaderCards ? h("button", {
|
|
3654
6177
|
className: "dsh-github-btn-sm" + (!headerCardsVisible ? " active" : ""),
|
|
3655
6178
|
onClick: onToggleHeaderCards,
|
|
@@ -3682,40 +6205,154 @@ function GithubHelpModal(props) {
|
|
|
3682
6205
|
var gitSt = data && data.gitStatus;
|
|
3683
6206
|
var relInfo = data && data.releaseInfo;
|
|
3684
6207
|
var branchPr = data && data.branchPr;
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
},
|
|
3700
|
-
title: `当前仓库 Star 数量: ${repo.stargazerCount}(点击前往 GitHub 查看)`,
|
|
3701
|
-
}, "⭐ ", String(repo.stargazerCount))
|
|
3702
|
-
: null), h("div", { className: "dsh-github-card-value" }, repo
|
|
3703
|
-
? h("a", { href: repo.url, target: "_blank", className: "dsh-github-repo-link" }, repo.nameWithOwner)
|
|
3704
|
-
: h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)" } }, "未检测到关联的 GitHub 仓库"), repo
|
|
3705
|
-
? h(BranchSelectorDropdown, {
|
|
3706
|
-
currentBranch: gitSt?.branch || repo.defaultBranch || "main",
|
|
3707
|
-
cwd,
|
|
3708
|
-
branchesData: data?.branches,
|
|
3709
|
-
onBranchSwitched: () => onRefresh(),
|
|
3710
|
-
showToast: onShowToast || (() => { }),
|
|
6208
|
+
// 徽章推导交给纯函数(src/ui/repo-badges.ts):
|
|
6209
|
+
// 判定规则需要可断言,而写在 JSX 里只能靠「bundle 里有没有这个词」检查。
|
|
6210
|
+
var repoPresentation = resolveRepoBadges(repo);
|
|
6211
|
+
var forkLine = repoPresentation.forkLine;
|
|
6212
|
+
var toneClass = function (tone) {
|
|
6213
|
+
return tone === "yellow" ? "dsh-badge-readonly" : "dsh-badge-" + tone;
|
|
6214
|
+
};
|
|
6215
|
+
var repoBadgesEl = repoPresentation.badges.length > 0
|
|
6216
|
+
? repoPresentation.badges.map(function (b, i) {
|
|
6217
|
+
return h("span", {
|
|
6218
|
+
key: "repo-badge-" + i,
|
|
6219
|
+
className: "dsh-github-badge " + toneClass(b.tone),
|
|
6220
|
+
style: { fontSize: "10px", padding: "0 6px", lineHeight: "16px", flexShrink: 0 },
|
|
6221
|
+
title: b.title,
|
|
6222
|
+
}, b.text);
|
|
3711
6223
|
})
|
|
3712
|
-
: null
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
:
|
|
6224
|
+
: null;
|
|
6225
|
+
var associatedRepos = repo && Array.isArray(repo.associatedRepos) ? repo.associatedRepos : [];
|
|
6226
|
+
var myForkCandidate = associatedRepos.find(function (r) {
|
|
6227
|
+
return !r.isCurrent && r.role === 'fork';
|
|
6228
|
+
});
|
|
6229
|
+
var quickSwitchForkEl = myForkCandidate
|
|
6230
|
+
? h("div", { className: "dsh-github-fork-line", style: { color: "var(--dsw-alias-link, #0969da)" } }, "⑂ 本地复刻仓库: ", h("strong", { style: { color: "inherit" } }, myForkCandidate.nameWithOwner), h("a", {
|
|
6231
|
+
href: "#",
|
|
6232
|
+
className: "dsh-github-switch-quicklink",
|
|
6233
|
+
title: `切换默认基准仓库为 ${myForkCandidate.nameWithOwner}`,
|
|
6234
|
+
onClick: function (e) {
|
|
6235
|
+
e.preventDefault();
|
|
6236
|
+
fetch("/api/github/action", {
|
|
6237
|
+
method: "POST",
|
|
6238
|
+
headers: { "Content-Type": "application/json" },
|
|
6239
|
+
body: JSON.stringify({
|
|
6240
|
+
action: "set_default_repo",
|
|
6241
|
+
cwd,
|
|
6242
|
+
repo: myForkCandidate.nameWithOwner,
|
|
6243
|
+
remote: myForkCandidate.remoteName,
|
|
6244
|
+
}),
|
|
6245
|
+
})
|
|
6246
|
+
.then(function (r) { return r.json(); })
|
|
6247
|
+
.then(function (j) {
|
|
6248
|
+
if (j && j.ok) {
|
|
6249
|
+
if (typeof onShowToast === "function")
|
|
6250
|
+
onShowToast(`✅ 已切换为我的复刻: ${myForkCandidate.nameWithOwner}`);
|
|
6251
|
+
onRefresh();
|
|
6252
|
+
}
|
|
6253
|
+
else {
|
|
6254
|
+
if (typeof onShowToast === "function")
|
|
6255
|
+
onShowToast(`❌ 切换失败: ${j.error || j.message || "未知错误"}`);
|
|
6256
|
+
}
|
|
6257
|
+
})
|
|
6258
|
+
.catch(function (err) {
|
|
6259
|
+
if (typeof onShowToast === "function")
|
|
6260
|
+
onShowToast(`❌ 切换请求异常: ${err?.message || err}`);
|
|
6261
|
+
});
|
|
6262
|
+
},
|
|
6263
|
+
}, "⇄ 设为当前基准仓库"))
|
|
6264
|
+
: null;
|
|
6265
|
+
var repoCardEl = (function () {
|
|
6266
|
+
const activeForkLine = forkLine;
|
|
6267
|
+
return h("div", { className: "dsh-github-card" }, h("div", { className: "dsh-github-card-title", style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, h("span", null, "当前工作区 GitHub 仓库"), repo && repo.stargazerCount !== undefined
|
|
6268
|
+
? h("a", {
|
|
6269
|
+
href: `${repo.url}/stargazers`,
|
|
6270
|
+
target: "_blank",
|
|
6271
|
+
className: "dsh-github-badge",
|
|
6272
|
+
style: {
|
|
6273
|
+
textDecoration: "none",
|
|
6274
|
+
display: "inline-flex",
|
|
6275
|
+
alignItems: "center",
|
|
6276
|
+
gap: "3px",
|
|
6277
|
+
fontWeight: 600,
|
|
6278
|
+
fontSize: "11px",
|
|
6279
|
+
cursor: "pointer",
|
|
6280
|
+
padding: "1px 6px",
|
|
6281
|
+
},
|
|
6282
|
+
title: `当前仓库 Star 数量: ${repo.stargazerCount}(点击前往 GitHub 查看)`,
|
|
6283
|
+
}, "⭐ ", String(repo.stargazerCount))
|
|
6284
|
+
: null), h("div", { className: "dsh-github-card-value" }, repo
|
|
6285
|
+
? h("a", { href: repo.url, target: "_blank", className: "dsh-github-repo-link" }, repo.nameWithOwner)
|
|
6286
|
+
: h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)" } }, "未检测到关联的 GitHub 仓库"), repoBadgesEl, repo
|
|
6287
|
+
? h(RepoSelectorDropdown, {
|
|
6288
|
+
associatedRepos: repo.associatedRepos,
|
|
6289
|
+
currentRepo: repo.nameWithOwner,
|
|
6290
|
+
cwd,
|
|
6291
|
+
onRepoSwitched: () => onRefresh(),
|
|
6292
|
+
showToast: onShowToast || (() => { }),
|
|
6293
|
+
})
|
|
6294
|
+
: null, repo
|
|
6295
|
+
? h(BranchSelectorDropdown, {
|
|
6296
|
+
currentBranch: gitSt?.branch || repo.defaultBranch || "main",
|
|
6297
|
+
cwd,
|
|
6298
|
+
branchesData: data?.branches,
|
|
6299
|
+
onBranchSwitched: () => onRefresh(),
|
|
6300
|
+
showToast: onShowToast || (() => { }),
|
|
6301
|
+
})
|
|
6302
|
+
: null),
|
|
6303
|
+
// 快捷切换行:针对“当前展示源仓库但本地检测到个人复刻”的一键切换交互
|
|
6304
|
+
quickSwitchForkEl,
|
|
6305
|
+
// fork 血缘行:告诉用户「这个 fork 的上游是谁」,上游名可点击
|
|
6306
|
+
activeForkLine
|
|
6307
|
+
? h("div", { className: "dsh-github-fork-line" }, "⑂ fork 自 ", h("a", {
|
|
6308
|
+
href: activeForkLine.parentUrl,
|
|
6309
|
+
target: "_blank",
|
|
6310
|
+
rel: "noopener noreferrer",
|
|
6311
|
+
title: `打开上游仓库 ${activeForkLine.parentNameWithOwner}`,
|
|
6312
|
+
}, activeForkLine.parentNameWithOwner), associatedRepos.some((r) => !r.isCurrent && r.nameWithOwner.toLowerCase() === activeForkLine.parentNameWithOwner.toLowerCase())
|
|
6313
|
+
? h("a", {
|
|
6314
|
+
href: "#",
|
|
6315
|
+
className: "dsh-github-switch-quicklink",
|
|
6316
|
+
title: `切换为上游基准仓库 ${activeForkLine.parentNameWithOwner}`,
|
|
6317
|
+
onClick: function (e) {
|
|
6318
|
+
e.preventDefault();
|
|
6319
|
+
fetch("/api/github/action", {
|
|
6320
|
+
method: "POST",
|
|
6321
|
+
headers: { "Content-Type": "application/json" },
|
|
6322
|
+
body: JSON.stringify({
|
|
6323
|
+
action: "set_default_repo",
|
|
6324
|
+
cwd,
|
|
6325
|
+
repo: activeForkLine.parentNameWithOwner,
|
|
6326
|
+
remote: "upstream",
|
|
6327
|
+
}),
|
|
6328
|
+
})
|
|
6329
|
+
.then(function (r) { return r.json(); })
|
|
6330
|
+
.then(function (j) {
|
|
6331
|
+
if (j && j.ok) {
|
|
6332
|
+
if (typeof onShowToast === "function")
|
|
6333
|
+
onShowToast(`✅ 已切为上游视角: ${activeForkLine.parentNameWithOwner}`);
|
|
6334
|
+
onRefresh();
|
|
6335
|
+
}
|
|
6336
|
+
else {
|
|
6337
|
+
if (typeof onShowToast === "function")
|
|
6338
|
+
onShowToast(`❌ 切换失败: ${j.error || j.message || "未知错误"}`);
|
|
6339
|
+
}
|
|
6340
|
+
})
|
|
6341
|
+
.catch(function (err) {
|
|
6342
|
+
if (typeof onShowToast === "function")
|
|
6343
|
+
onShowToast(`❌ 切换请求异常: ${err?.message || err}`);
|
|
6344
|
+
});
|
|
6345
|
+
},
|
|
6346
|
+
}, "⇄ 切为基准")
|
|
6347
|
+
: null)
|
|
6348
|
+
: null, repo && repo.description
|
|
6349
|
+
? h("div", { className: "dsh-github-card-desc" }, repo.description)
|
|
6350
|
+
: null, auth && auth.loggedIn
|
|
6351
|
+
? h("div", { className: "dsh-github-card-subtext" }, `CLI 认证账号: ${auth.user} (${auth.scopes?.join(", ") || "已登录"})`)
|
|
6352
|
+
: null, !repo
|
|
6353
|
+
? h("div", { className: "dsh-github-card-subtext" }, "若本地已有 git 仓库,请运行 `git remote add github <url>` 或 `git remote add origin <url>` 关联远程仓库。")
|
|
6354
|
+
: null);
|
|
6355
|
+
})();
|
|
3719
6356
|
var commitLog = data && data.commitLog;
|
|
3720
6357
|
var renderCommitRow = function (c, tone) {
|
|
3721
6358
|
return h("div", {
|
|
@@ -3933,18 +6570,91 @@ function GithubHelpModal(props) {
|
|
|
3933
6570
|
}
|
|
3934
6571
|
},
|
|
3935
6572
|
}, "📝 新建 Issue")) : null;
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
6573
|
+
/*
|
|
6574
|
+
* PR 状态分组统计与筛选。
|
|
6575
|
+
*
|
|
6576
|
+
* 为什么计数必须在过滤之前算:胶囊上的数字要让用户知道「切过去有几条」,
|
|
6577
|
+
* 用过滤后的列表长度会得到「点了 Merged 之后 Open 显示 0」这种废信息。
|
|
6578
|
+
*/
|
|
6579
|
+
var prByState = { open: 0, merged: 0, closed: 0 };
|
|
6580
|
+
prs.forEach(function (pr) {
|
|
6581
|
+
var s = String(pr.state || "").toLowerCase();
|
|
6582
|
+
if (s === "open" || s === "merged" || s === "closed")
|
|
6583
|
+
prByState[s]++;
|
|
6584
|
+
});
|
|
6585
|
+
var filteredPrs = prs.filter(function (pr) {
|
|
6586
|
+
if (prStateFilter === "all")
|
|
6587
|
+
return true;
|
|
6588
|
+
// 未知状态(旧快照缺 state)只出现在「全部」里,不会被误塞进具体档
|
|
6589
|
+
return String(pr.state || "").toLowerCase() === prStateFilter;
|
|
6590
|
+
});
|
|
6591
|
+
// 空态文案要给人看,不能把内部档位键(merged)直接抛给用户
|
|
6592
|
+
var PR_STATE_LABEL = { all: "全部", open: "Open", merged: "Merged", closed: "Closed" };
|
|
6593
|
+
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})`));
|
|
6595
|
+
// 空态必须区分「仓库确实没有」与「当前筛选下没有」——否则用户会把筛空当成数据丢失
|
|
6596
|
+
var prEmptyEl = prs.length === 0
|
|
6597
|
+
? h("div", { className: "dsh-github-empty" }, "该仓库暂无 Pull Request 记录")
|
|
6598
|
+
: 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
|
|
6615
|
+
? prEmptyEl
|
|
6616
|
+
: h("div", { className: "dsh-github-list" }, filteredPrs.map((pr) => {
|
|
3939
6617
|
var ci = rollupCiStatus(pr.statusCheckRollup);
|
|
3940
6618
|
var rv = reviewDecisionBadge(pr.reviewDecision);
|
|
3941
6619
|
var relTime = formatRelativeTime(pr.updatedAt);
|
|
3942
|
-
|
|
6620
|
+
// 点击卡片 → 在线审阅弹窗;标题旁 ↗ 保留浏览器直达(stopPropagation 防冒泡)
|
|
6621
|
+
return h("div", {
|
|
6622
|
+
key: pr.number,
|
|
6623
|
+
className: "dsh-github-item",
|
|
6624
|
+
style: onSelectPr ? { cursor: "pointer" } : undefined,
|
|
6625
|
+
title: onSelectPr ? "点击在线审阅该 PR(描述 / 评论 / 文件 Diff)" : undefined,
|
|
6626
|
+
onClick: onSelectPr
|
|
6627
|
+
? function () {
|
|
6628
|
+
onSelectPr({ number: Number(pr.number), repo: repo?.nameWithOwner || "" });
|
|
6629
|
+
}
|
|
6630
|
+
: undefined,
|
|
6631
|
+
},
|
|
6632
|
+
// 标题与跳转按钮必须同行:.dsh-github-item 本身是竖排 flex,
|
|
6633
|
+
// 直接放按钮会让它独占一行、只剩一个裸 ↗ 字符(曾经的丑样)。
|
|
6634
|
+
h("div", { className: "dsh-github-pr-title-row" }, h("span", {
|
|
3943
6635
|
className: "dsh-github-item-title",
|
|
6636
|
+
title: relTime ? `更新于 ${pr.updatedAt}(点击在线审阅)` : "点击在线审阅该 PR",
|
|
6637
|
+
}, `${pr.isDraft ? "⚪ " : ""}#${pr.number} ${pr.title}`),
|
|
6638
|
+
// 独立的浏览器直达入口:文字标签 + 边框,明确表达「外部跳转」
|
|
6639
|
+
h("a", {
|
|
6640
|
+
className: "dsh-github-pr-gh-btn",
|
|
3944
6641
|
href: pr.url,
|
|
3945
6642
|
target: "_blank",
|
|
3946
|
-
|
|
3947
|
-
|
|
6643
|
+
rel: "noopener noreferrer",
|
|
6644
|
+
onClick: (e) => e.stopPropagation(),
|
|
6645
|
+
title: "在浏览器打开该 PR(不进入在线审阅)",
|
|
6646
|
+
}, "GitHub ↗")), h("div", { className: "dsh-github-item-meta" }, h("span", null, `${pr.headRefName} → ${pr.baseRefName}`),
|
|
6647
|
+
// 状态徽章:与主面板保持一致,否则同一 PR 在两处显示不同信息
|
|
6648
|
+
(() => {
|
|
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;
|
|
6657
|
+
})(), pr.mergeable === "CONFLICTING"
|
|
3948
6658
|
? h("span", { className: "dsh-github-badge dsh-badge-red" }, "代码冲突")
|
|
3949
6659
|
: h("span", { className: "dsh-github-badge dsh-badge-green" }, "可合并"), ci === "fail"
|
|
3950
6660
|
? h("span", { className: "dsh-github-badge dsh-badge-red" }, "❌ CI 失败")
|
|
@@ -3956,15 +6666,46 @@ function GithubHelpModal(props) {
|
|
|
3956
6666
|
? h("span", { className: `dsh-github-badge dsh-badge-${rv.kind}` }, rv.text)
|
|
3957
6667
|
: null, relTime ? h("span", null, relTime) : null));
|
|
3958
6668
|
})));
|
|
3959
|
-
|
|
6669
|
+
// Issue 状态分组统计与筛选(与 PR 同一套口径:先统计全量再过滤)
|
|
6670
|
+
var issueByState = { open: 0, closed: 0 };
|
|
6671
|
+
issues.forEach(function (issue) {
|
|
6672
|
+
var s = String(issue.state || "").toLowerCase();
|
|
6673
|
+
if (s === "open" || s === "closed")
|
|
6674
|
+
issueByState[s]++;
|
|
6675
|
+
});
|
|
6676
|
+
var filteredIssues = issues.filter(function (issue) {
|
|
6677
|
+
if (issueStateFilter === "all")
|
|
6678
|
+
return true;
|
|
6679
|
+
return String(issue.state || "").toLowerCase() === issueStateFilter;
|
|
6680
|
+
});
|
|
6681
|
+
var ISSUE_STATE_LABEL = { all: "全部", open: "Open", closed: "Closed" };
|
|
6682
|
+
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})`));
|
|
6684
|
+
var issueEmptyEl = issues.length === 0
|
|
6685
|
+
? h("div", { className: "dsh-github-empty" }, "该仓库暂无 Issue 记录")
|
|
6686
|
+
: 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
|
|
3960
6688
|
? h("a", {
|
|
3961
6689
|
href: `${repo.url}/issues`,
|
|
3962
6690
|
target: "_blank",
|
|
3963
6691
|
style: { fontSize: "11px", color: "var(--dsw-alias-link, #0969da)", textDecoration: "none" },
|
|
3964
6692
|
}, "全部 ↗")
|
|
3965
|
-
: null),
|
|
3966
|
-
? h("div", {
|
|
3967
|
-
|
|
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
|
|
6707
|
+
? issueEmptyEl
|
|
6708
|
+
: h("div", { className: "dsh-github-list" }, filteredIssues.map((issue) => {
|
|
3968
6709
|
var cats = issue.categories || [];
|
|
3969
6710
|
var badges = [];
|
|
3970
6711
|
if (cats.includes("reported")) {
|
|
@@ -3976,7 +6717,37 @@ function GithubHelpModal(props) {
|
|
|
3976
6717
|
if (cats.includes("created")) {
|
|
3977
6718
|
badges.push(h("span", { key: "crt", className: "dsh-github-badge dsh-badge-green" }, "我发起的"));
|
|
3978
6719
|
}
|
|
3979
|
-
return h("div", {
|
|
6720
|
+
return h("div", {
|
|
6721
|
+
key: issue.number,
|
|
6722
|
+
className: "dsh-github-item",
|
|
6723
|
+
// 只加 cursor:内联 display:flex 会顶掉 .dsh-github-item 的 column 布局
|
|
6724
|
+
style: onSelectIssue ? { cursor: "pointer" } : undefined,
|
|
6725
|
+
title: onSelectIssue ? "点击在线查看该 Issue(正文 / 评论时间线 / 标签与里程碑)" : undefined,
|
|
6726
|
+
onClick: onSelectIssue
|
|
6727
|
+
? function () {
|
|
6728
|
+
onSelectIssue({ number: Number(issue.number), repo: repo?.nameWithOwner || "" });
|
|
6729
|
+
}
|
|
6730
|
+
: undefined,
|
|
6731
|
+
},
|
|
6732
|
+
// 标题行:标题 → 分类徽章 → 状态徽章 → GitHub 外跳,同一行
|
|
6733
|
+
h("div", { className: "dsh-github-pr-title-row" }, h("span", {
|
|
6734
|
+
className: "dsh-github-item-title",
|
|
6735
|
+
title: onSelectIssue ? "点击在线查看该 Issue" : undefined,
|
|
6736
|
+
}, `#${issue.number} ${issue.title}`), ...badges, (() => {
|
|
6737
|
+
var s = String(issue.state || "").toUpperCase();
|
|
6738
|
+
if (s === "CLOSED")
|
|
6739
|
+
return h("span", { className: "dsh-github-badge dsh-badge-red" }, "CLOSED");
|
|
6740
|
+
if (s === "OPEN")
|
|
6741
|
+
return h("span", { className: "dsh-github-badge dsh-badge-green" }, "OPEN");
|
|
6742
|
+
return null;
|
|
6743
|
+
})(), h("a", {
|
|
6744
|
+
className: "dsh-github-pr-gh-btn",
|
|
6745
|
+
href: issue.url,
|
|
6746
|
+
target: "_blank",
|
|
6747
|
+
rel: "noopener noreferrer",
|
|
6748
|
+
onClick: (e) => e.stopPropagation(),
|
|
6749
|
+
title: "在浏览器打开该 Issue(不进入在线查看)",
|
|
6750
|
+
}, "GitHub ↗")), h("div", { className: "dsh-github-item-meta" }, h("span", null, `由 ${issue.author?.login || "未知"} 提出`), issue.assignees && issue.assignees.length > 0
|
|
3980
6751
|
? h("span", null, `指派: ${issue.assignees.map((a) => a.login).join(", ")}`)
|
|
3981
6752
|
: null, issue.labels && issue.labels.length > 0
|
|
3982
6753
|
? issue.labels.map((lbl) => h("span", { key: lbl.name, className: "dsh-github-badge dsh-badge-yellow" }, lbl.name))
|
|
@@ -4040,6 +6811,7 @@ function GithubHelpModal(props) {
|
|
|
4040
6811
|
return [
|
|
4041
6812
|
headerEl,
|
|
4042
6813
|
networkAlertBannerEl,
|
|
6814
|
+
repoNoticeBarEl,
|
|
4043
6815
|
upgradeBannerEl,
|
|
4044
6816
|
authGuideEl,
|
|
4045
6817
|
headerCardsVisible ? repoCardEl : null,
|
|
@@ -4181,11 +6953,39 @@ function GithubHelpModal(props) {
|
|
|
4181
6953
|
if (isFail) {
|
|
4182
6954
|
setSyncStatus("failed");
|
|
4183
6955
|
setError(failReason);
|
|
6956
|
+
// 失败响应也可能携带「本次新鲜解析」的仓库身份与认证态(后端在
|
|
6957
|
+
// 回退旧快照时已用新鲜 repo 覆盖旧值)。身份字段永远采纳,
|
|
6958
|
+
// 业务列表才走「非空才覆盖」的防冲刷策略。
|
|
6959
|
+
//
|
|
6960
|
+
// 为什么必须拆开:旧逻辑把「要不要整体替换」和「列表是否有内容」
|
|
6961
|
+
// 绑在一起,于是仓库身份修正会被空列表连坐否决——用户换了 remote
|
|
6962
|
+
// 之后面板永远显示旧仓库,刷新也救不回来。
|
|
4184
6963
|
var curData = dataRef.current;
|
|
4185
|
-
if (curData
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
6964
|
+
if (curData) {
|
|
6965
|
+
var hasFreshList = (json.data.pullRequests?.length > 0 || json.data.issues?.length > 0);
|
|
6966
|
+
setData(function (prev) {
|
|
6967
|
+
if (!prev)
|
|
6968
|
+
return json.data;
|
|
6969
|
+
var merged = {
|
|
6970
|
+
...prev,
|
|
6971
|
+
// 身份 / 认证 / 通知:始终采纳最新(这些字段不可能是「空数据假象」)
|
|
6972
|
+
repo: json.data.repo || prev.repo,
|
|
6973
|
+
auth: json.data.auth || prev.auth,
|
|
6974
|
+
notices: json.data.notices,
|
|
6975
|
+
updateInfo: json.data.updateInfo || prev.updateInfo,
|
|
6976
|
+
lastUpdated: json.data.lastUpdated || prev.lastUpdated,
|
|
6977
|
+
};
|
|
6978
|
+
// 业务列表:仅有新内容时才替换,防止把「好数据」冲刷成空数组
|
|
6979
|
+
if (hasFreshList) {
|
|
6980
|
+
merged.pullRequests = json.data.pullRequests;
|
|
6981
|
+
merged.issues = json.data.issues;
|
|
6982
|
+
merged.issueStats = json.data.issueStats;
|
|
6983
|
+
}
|
|
6984
|
+
if (json.data.runs?.length > 0) {
|
|
6985
|
+
merged.runs = json.data.runs;
|
|
6986
|
+
}
|
|
6987
|
+
return merged;
|
|
6988
|
+
});
|
|
4189
6989
|
}
|
|
4190
6990
|
else {
|
|
4191
6991
|
setData(json.data);
|
|
@@ -4196,6 +6996,8 @@ function GithubHelpModal(props) {
|
|
|
4196
6996
|
}
|
|
4197
6997
|
else {
|
|
4198
6998
|
setData(json.data);
|
|
6999
|
+
// 落快照前统一判定:带离线标记的数据不写(saveStoredRightbarData 内部还有一道),
|
|
7000
|
+
// 且身份变更后的快照会覆盖旧键——localStorage 的 key 是 cwd,天然随路径对齐。
|
|
4199
7001
|
saveStoredRightbarData(cwd, json.data);
|
|
4200
7002
|
var syncTs = json.fromCache && json.data.lastUpdated
|
|
4201
7003
|
? (Date.parse(json.data.lastUpdated) || Date.now())
|
|
@@ -4385,6 +7187,10 @@ function GithubHelpModal(props) {
|
|
|
4385
7187
|
var [rightbarModalSection, setRightbarModalSection] = React.useState("sync");
|
|
4386
7188
|
var [helpModalOpen, setHelpModalOpen] = React.useState(false);
|
|
4387
7189
|
var [selectedCommit, setSelectedCommit] = React.useState(null);
|
|
7190
|
+
// PR 在线审阅弹窗状态:{ number, repo };为 null 时不挂载弹窗(零常驻开销)
|
|
7191
|
+
var [activePr, setActivePr] = React.useState(null);
|
|
7192
|
+
// Issue 在线查看弹窗状态:与 PR 同构
|
|
7193
|
+
var [activeIssue, setActiveIssue] = React.useState(null);
|
|
4388
7194
|
var [extraCommits, setExtraCommits] = React.useState([]);
|
|
4389
7195
|
var [loadingMore, setLoadingMore] = React.useState(false);
|
|
4390
7196
|
var [hasMoreCommits, setHasMoreCommits] = React.useState(true);
|
|
@@ -4394,6 +7200,26 @@ function GithubHelpModal(props) {
|
|
|
4394
7200
|
var [hasMoreDirty, setHasMoreDirty] = React.useState(true);
|
|
4395
7201
|
var [activeDiffFile, setActiveDiffFile] = React.useState(null);
|
|
4396
7202
|
var [headerCardsVisible, setHeaderCardsVisible] = React.useState(() => getStoredHeaderCardsVisible());
|
|
7203
|
+
/*
|
|
7204
|
+
* 状态筛选:默认 all(= 能看见 closed/merged)。
|
|
7205
|
+
* 初值来自按 cwd 隔离的本地偏好,切仓库时在下面的 effect 里重新对齐。
|
|
7206
|
+
* 放在这里而不是 renderGithubRepoView 内部:那是普通函数,
|
|
7207
|
+
* 每次渲染都会重新执行,函数内 useState 会被当成新的 Hook 序列直接抛错。
|
|
7208
|
+
*/
|
|
7209
|
+
var [prStateFilter, setPrStateFilter] = React.useState(() => getStoredRightbarStateFilter(cwd).pr);
|
|
7210
|
+
var [issueStateFilter, setIssueStateFilter] = React.useState(() => getStoredRightbarStateFilter(cwd).issue);
|
|
7211
|
+
var changePrFilter = React.useCallback(function (v) {
|
|
7212
|
+
if (PR_STATE_FILTERS.indexOf(v) < 0)
|
|
7213
|
+
return;
|
|
7214
|
+
setPrStateFilter(v);
|
|
7215
|
+
saveStoredRightbarStateFilter(cwd, { pr: v });
|
|
7216
|
+
}, [cwd]);
|
|
7217
|
+
var changeIssueFilter = React.useCallback(function (v) {
|
|
7218
|
+
if (ISSUE_STATE_FILTERS.indexOf(v) < 0)
|
|
7219
|
+
return;
|
|
7220
|
+
setIssueStateFilter(v);
|
|
7221
|
+
saveStoredRightbarStateFilter(cwd, { issue: v });
|
|
7222
|
+
}, [cwd]);
|
|
4397
7223
|
var toggleHeaderCards = React.useCallback(() => {
|
|
4398
7224
|
setHeaderCardsVisible(function (prev) {
|
|
4399
7225
|
var next = !prev;
|
|
@@ -4409,6 +7235,17 @@ function GithubHelpModal(props) {
|
|
|
4409
7235
|
setHasMoreDirty(true);
|
|
4410
7236
|
setActiveDiffFile(null);
|
|
4411
7237
|
}, [cwd, data]);
|
|
7238
|
+
/*
|
|
7239
|
+
* 切仓库时把筛选对齐到该仓库自己的偏好。
|
|
7240
|
+
*
|
|
7241
|
+
* 只依赖 cwd:若依赖 data 会在每次数据刷新时把用户刚点的筛选重置回上次
|
|
7242
|
+
* 落盘值——用户点「Merged」后恰逢后台 SWR 拉新,选择立刻跳回「全部」。
|
|
7243
|
+
*/
|
|
7244
|
+
React.useEffect(function () {
|
|
7245
|
+
var stored = getStoredRightbarStateFilter(cwd);
|
|
7246
|
+
setPrStateFilter(stored.pr);
|
|
7247
|
+
setIssueStateFilter(stored.issue);
|
|
7248
|
+
}, [cwd]);
|
|
4412
7249
|
var loadMoreCommits = function () {
|
|
4413
7250
|
if (loadingMore || !hasMoreCommits)
|
|
4414
7251
|
return;
|
|
@@ -4656,7 +7493,7 @@ function GithubHelpModal(props) {
|
|
|
4656
7493
|
loadingMore: loadingMoreDirty,
|
|
4657
7494
|
hasMore: hasMoreDirty,
|
|
4658
7495
|
onLoadMore: loadMoreDirtyFiles,
|
|
4659
|
-
}, activeDiffFile, (f) => setActiveDiffFile(f), () => setActiveDiffFile(null), openFileInDsh, headerCardsVisible, toggleHeaderCards), h(GlobalSyncSettingsModal, {
|
|
7496
|
+
}, activeDiffFile, (f) => setActiveDiffFile(f), () => setActiveDiffFile(null), openFileInDsh, headerCardsVisible, toggleHeaderCards, (target) => setActivePr(target), (target) => setActiveIssue(target), prStateFilter, issueStateFilter, changePrFilter, changeIssueFilter), h(GlobalSyncSettingsModal, {
|
|
4660
7497
|
isOpen: aboutModalOpen,
|
|
4661
7498
|
initialSection: "about",
|
|
4662
7499
|
onClose: () => setAboutModalOpen(false),
|
|
@@ -4683,6 +7520,22 @@ function GithubHelpModal(props) {
|
|
|
4683
7520
|
onClose: function () { setSelectedCommit(null); },
|
|
4684
7521
|
cwd: cwd,
|
|
4685
7522
|
})
|
|
7523
|
+
: null, activePr
|
|
7524
|
+
? h(PrDetailModal, {
|
|
7525
|
+
prNumber: activePr.number,
|
|
7526
|
+
repoNameWithOwner: activePr.repo,
|
|
7527
|
+
cwd: cwd,
|
|
7528
|
+
onClose: function () { setActivePr(null); },
|
|
7529
|
+
showToast: showToast,
|
|
7530
|
+
})
|
|
7531
|
+
: null, activeIssue
|
|
7532
|
+
? h(IssueDetailModal, {
|
|
7533
|
+
issueNumber: activeIssue.number,
|
|
7534
|
+
repoNameWithOwner: activeIssue.repo,
|
|
7535
|
+
cwd: cwd,
|
|
7536
|
+
onClose: function () { setActiveIssue(null); },
|
|
7537
|
+
showToast: showToast,
|
|
7538
|
+
})
|
|
4686
7539
|
: null);
|
|
4687
7540
|
}
|
|
4688
7541
|
// 2. 右侧栏 Tab 标题组件
|
|
@@ -4726,7 +7579,15 @@ function GithubWorkspacePanel() {
|
|
|
4726
7579
|
var [activeTab, setActiveTab] = React.useState("workspaces");
|
|
4727
7580
|
var [searchQuery, setSearchQuery] = React.useState("");
|
|
4728
7581
|
var [issueFilter, setIssueFilter] = React.useState("all");
|
|
7582
|
+
// PR 状态筛选:默认「全部」而不是「Open」。
|
|
7583
|
+
// 实测账号可能零 open PR(当前账号 7 个 PR 全为 merged/closed),
|
|
7584
|
+
// 默认 Open 会让列表恒为空白,等于没修「看不到数据」这个原始问题。
|
|
7585
|
+
var [prStateFilter, setPrStateFilter] = React.useState("all");
|
|
4729
7586
|
var [toastMsg, setToastMsg] = React.useState(null);
|
|
7587
|
+
// PR 在线审阅弹窗状态:{ number, repo } 二元组;为 null 时弹窗不渲染
|
|
7588
|
+
var [activePrModal, setActivePrModal] = React.useState(null);
|
|
7589
|
+
// Issue 在线查看弹窗状态:与 PR 完全同构的 { number, repo } 二元组
|
|
7590
|
+
var [activeIssueModal, setActiveIssueModal] = React.useState(null);
|
|
4730
7591
|
var cloudTimerRef = React.useRef(null);
|
|
4731
7592
|
var dataRef = React.useRef(data);
|
|
4732
7593
|
dataRef.current = data;
|
|
@@ -5067,7 +7928,7 @@ function GithubWorkspacePanel() {
|
|
|
5067
7928
|
var authGuideEl = (!auth?.loggedIn && !auth?.networkError && syncStatus !== "failed")
|
|
5068
7929
|
? h(AuthGuideCard, { auth, onRefresh: () => loadData(true), isRightbar: false })
|
|
5069
7930
|
: null;
|
|
5070
|
-
var tabsEl = h("div", { className: "dsh-github-tabs" }, h("button", { className: `dsh-github-tab-btn ${activeTab === 'workspaces' ? 'active' : ''}`, onClick: () => setActiveTab('workspaces') }, `📂 本地工作区矩阵 (${workspaceMatrix.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'repos' ? 'active' : ''}`, onClick: () => setActiveTab('repos') }, `☁️ 我的 GitHub 仓库 (${userRepos.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === '
|
|
7931
|
+
var tabsEl = h("div", { className: "dsh-github-tabs" }, h("button", { className: `dsh-github-tab-btn ${activeTab === 'workspaces' ? 'active' : ''}`, onClick: () => setActiveTab('workspaces') }, `📂 本地工作区矩阵 (${workspaceMatrix.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'repos' ? 'active' : ''}`, onClick: () => setActiveTab('repos') }, `☁️ 我的 GitHub 仓库 (${userRepos.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'prs' ? 'active' : ''}`, onClick: () => setActiveTab('prs') }, `🔀 Pull Requests (${myPrs.length})`), h("button", { className: `dsh-github-tab-btn ${activeTab === 'issues' ? 'active' : ''}`, onClick: () => setActiveTab('issues') }, `📝 任务与待办 Issues (${myIssues.length})`));
|
|
5071
7932
|
var contentEl = null;
|
|
5072
7933
|
if (activeTab === 'workspaces') {
|
|
5073
7934
|
contentEl = h("div", { className: "dsh-github-list" }, workspaceMatrix.map((ws) => h("div", { key: ws.id, className: "dsh-github-card dsh-github-workspace-card" }, h("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } }, h("div", { className: "dsh-github-ws-title" }, ws.title), h("div", { className: "dsh-github-ws-path" }, ws.path), h("div", { style: { fontSize: "12px", marginTop: "4px", display: "flex", gap: "8px", alignItems: "center" } }, ws.repo
|
|
@@ -5105,6 +7966,92 @@ function GithubWorkspacePanel() {
|
|
|
5105
7966
|
: null)))));
|
|
5106
7967
|
}
|
|
5107
7968
|
}
|
|
7969
|
+
else if (activeTab === 'prs') {
|
|
7970
|
+
// ---- PR 独立标签页:完整大屏空间,点击卡片在线唤起审阅工作台 ----
|
|
7971
|
+
if (!auth?.loggedIn) {
|
|
7972
|
+
contentEl = h("div", { className: "dsh-github-empty" }, "当前尚未认证登录 GitHub 账号,无法读取个人 PR 列表。请先完成上方的安装或登录指引。");
|
|
7973
|
+
}
|
|
7974
|
+
else {
|
|
7975
|
+
// 状态分组统计(供胶囊计数与空态判断)
|
|
7976
|
+
var prByState = { open: 0, merged: 0, closed: 0 };
|
|
7977
|
+
myPrs.forEach(function (pr) {
|
|
7978
|
+
var s = String(pr.state || '').toLowerCase();
|
|
7979
|
+
if (s === 'open' || s === 'merged' || s === 'closed')
|
|
7980
|
+
prByState[s]++;
|
|
7981
|
+
});
|
|
7982
|
+
var filteredPrs = myPrs.filter(function (pr) {
|
|
7983
|
+
if (prStateFilter === 'all')
|
|
7984
|
+
return true;
|
|
7985
|
+
return String(pr.state || '').toLowerCase() === prStateFilter;
|
|
7986
|
+
});
|
|
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})`));
|
|
7988
|
+
// 空态必须区分「账号确实没有」与「当前筛选下没有」——否则用户会把筛空当成数据丢失
|
|
7989
|
+
var prEmptyEl = myPrs.length === 0
|
|
7990
|
+
? h("div", { className: "dsh-github-empty" }, "该账号下暂无任何 Pull Request 记录")
|
|
7991
|
+
: 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 凭空消失
|
|
7994
|
+
data && data.prsTruncated
|
|
7995
|
+
? h("div", {
|
|
7996
|
+
style: {
|
|
7997
|
+
margin: "8px 0",
|
|
7998
|
+
padding: "6px 12px",
|
|
7999
|
+
borderRadius: "6px",
|
|
8000
|
+
fontSize: "11px",
|
|
8001
|
+
lineHeight: 1.5,
|
|
8002
|
+
background: "rgba(210, 153, 34, 0.12)",
|
|
8003
|
+
border: "1px solid rgba(210, 153, 34, 0.35)",
|
|
8004
|
+
color: "var(--dsw-alias-state-attention-primary, #9a6700)",
|
|
8005
|
+
},
|
|
8006
|
+
}, `⚠️ 仅显示最近 ${myPrs.length} 条 PR(已触及检索上限)。更早的记录请到 GitHub 查看。`)
|
|
8007
|
+
: null, filteredPrs.length === 0 ? prEmptyEl : h("div", { className: "dsh-github-list" }, filteredPrs.map((pr) => {
|
|
8008
|
+
// 点击卡片主体 → 在线审阅弹窗
|
|
8009
|
+
return h("div", {
|
|
8010
|
+
key: pr.number,
|
|
8011
|
+
className: "dsh-github-item",
|
|
8012
|
+
/*
|
|
8013
|
+
* 只加 cursor,绝不覆盖 display/alignItems/flexWrap。
|
|
8014
|
+
* 这里的 inline style 曾写成 display:flex + alignItems:center + flexWrap:wrap,
|
|
8015
|
+
* 直接把 .dsh-github-item 的 flex-direction:column 顶掉,
|
|
8016
|
+
* 导致标题、状态徽章、时间全部横向挤成一行并整体居中——
|
|
8017
|
+
* 卡片布局就是被这一行内联样式毁掉的。
|
|
8018
|
+
*/
|
|
8019
|
+
style: { cursor: "pointer" },
|
|
8020
|
+
title: "点击在线审阅该 PR(描述 / 评论 / 文件 Diff)",
|
|
8021
|
+
onClick: () => {
|
|
8022
|
+
setActivePrModal({
|
|
8023
|
+
number: Number(pr.number),
|
|
8024
|
+
repo: pr.repository?.nameWithOwner || '',
|
|
8025
|
+
});
|
|
8026
|
+
},
|
|
8027
|
+
},
|
|
8028
|
+
// 标题行:标题 → 状态徽章 → GitHub 跳转按钮,全部同一行
|
|
8029
|
+
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
|
+
// 状态徽章:列表含 merged/closed,必须标明才能一眼区分
|
|
8031
|
+
(() => {
|
|
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;
|
|
8040
|
+
})(), h("a", {
|
|
8041
|
+
className: "dsh-github-pr-gh-btn",
|
|
8042
|
+
href: pr.url,
|
|
8043
|
+
target: "_blank",
|
|
8044
|
+
rel: "noopener noreferrer",
|
|
8045
|
+
onClick: (e) => e.stopPropagation(),
|
|
8046
|
+
title: "在浏览器打开该 PR(不进入在线审阅)",
|
|
8047
|
+
}, "GitHub ↗")),
|
|
8048
|
+
// 第二行只保留时间,避免与标题行的徽章重复
|
|
8049
|
+
formatRelativeTime(pr.updatedAt)
|
|
8050
|
+
? h("div", { className: "dsh-github-item-meta" }, h("span", { style: { whiteSpace: "nowrap" } }, formatRelativeTime(pr.updatedAt)))
|
|
8051
|
+
: null);
|
|
8052
|
+
})));
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
5108
8055
|
else {
|
|
5109
8056
|
if (!auth?.loggedIn) {
|
|
5110
8057
|
contentEl = h("div", { className: "dsh-github-empty" }, "当前尚未认证登录 GitHub 账号,无法读取个人待办 PR 与 Issues。请先完成上方的安装或登录指引。");
|
|
@@ -5135,31 +8082,61 @@ function GithubWorkspacePanel() {
|
|
|
5135
8082
|
className: `dsh-github-pill-btn ${issueFilter === 'created' ? 'active' : ''}`,
|
|
5136
8083
|
onClick: () => setIssueFilter('created'),
|
|
5137
8084
|
}, `✍️ 我创建的 (${issueStats.created || 0})`));
|
|
5138
|
-
contentEl = h("div", null, h("h3", { className: "dsh-github-subheading" },
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
8085
|
+
contentEl = h("div", null, h("h3", { className: "dsh-github-subheading" }, `📝 我发起与参与的 Issues (${filteredIssues.length} / ${myIssues.length})`), issuePillsEl, filteredIssues.length === 0
|
|
8086
|
+
? (myIssues.length === 0
|
|
8087
|
+
? h("div", { className: "dsh-github-empty" }, "该账号下暂无任何 Issue 记录")
|
|
8088
|
+
: h("div", { className: "dsh-github-empty" }, `当前分类「${issueFilter}」下无结果,共 ${myIssues.length} 条记录(切换到「全部」查看)`))
|
|
8089
|
+
: h("div", { className: "dsh-github-list" }, filteredIssues.map((is) => {
|
|
8090
|
+
var cats = is.categories || [];
|
|
8091
|
+
var badges = [];
|
|
8092
|
+
if (cats.includes("reported")) {
|
|
8093
|
+
badges.push(h("span", { key: "rep", className: "dsh-github-badge dsh-badge-purple" }, "用户提单"));
|
|
8094
|
+
}
|
|
8095
|
+
if (cats.includes("assigned")) {
|
|
8096
|
+
badges.push(h("span", { key: "asn", className: "dsh-github-badge dsh-badge-blue" }, "分配待办"));
|
|
8097
|
+
}
|
|
8098
|
+
if (cats.includes("created")) {
|
|
8099
|
+
badges.push(h("span", { key: "crt", className: "dsh-github-badge dsh-badge-green" }, "我发起的"));
|
|
8100
|
+
}
|
|
8101
|
+
return h("div", {
|
|
8102
|
+
key: is.url || is.number,
|
|
8103
|
+
className: "dsh-github-item",
|
|
8104
|
+
/*
|
|
8105
|
+
* 只加 cursor,绝不覆盖 display/alignItems/flexWrap。
|
|
8106
|
+
* .dsh-github-item 是 flex-direction:column,任何内联 flex 布局
|
|
8107
|
+
* 都会把标题、徽章、元信息横向挤成一行(PR 卡片上踩过的坑)。
|
|
8108
|
+
*/
|
|
8109
|
+
style: { cursor: "pointer" },
|
|
8110
|
+
title: "点击在线查看该 Issue(正文 / 评论时间线 / 标签与里程碑)",
|
|
8111
|
+
onClick: () => {
|
|
8112
|
+
setActiveIssueModal({
|
|
8113
|
+
number: Number(is.number),
|
|
8114
|
+
repo: is.repository?.nameWithOwner || '',
|
|
8115
|
+
});
|
|
8116
|
+
},
|
|
8117
|
+
},
|
|
8118
|
+
// 标题行:标题 → 分类徽章 → 状态徽章 → GitHub 外跳,全部同一行
|
|
8119
|
+
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
|
+
// 状态徽章:全局检索含 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;
|
|
8127
|
+
})(), h("a", {
|
|
8128
|
+
className: "dsh-github-pr-gh-btn",
|
|
8129
|
+
href: is.url,
|
|
8130
|
+
target: "_blank",
|
|
8131
|
+
rel: "noopener noreferrer",
|
|
8132
|
+
onClick: (e) => e.stopPropagation(),
|
|
8133
|
+
title: "在浏览器打开该 Issue(不进入在线查看)",
|
|
8134
|
+
}, "GitHub ↗")), h("div", { className: "dsh-github-item-meta" }, is.author ? h("span", null, `由 @${is.author.login} 提出`) : null, is.assignees && is.assignees.length > 0
|
|
8135
|
+
? h("span", null, `指派: ${is.assignees.map((a) => '@' + a.login).join(', ')}`)
|
|
8136
|
+
: null, is.labels && is.labels.length > 0
|
|
8137
|
+
? is.labels.map((lbl) => h("span", { key: lbl.name, className: "dsh-github-badge dsh-badge-yellow" }, lbl.name))
|
|
8138
|
+
: null, is.updatedAt ? h("span", null, `更新于: ${new Date(is.updatedAt).toLocaleDateString()}`) : null));
|
|
8139
|
+
})));
|
|
5163
8140
|
}
|
|
5164
8141
|
}
|
|
5165
8142
|
var toastEl = toastMsg
|
|
@@ -5194,7 +8171,25 @@ function GithubWorkspacePanel() {
|
|
|
5194
8171
|
return h("div", { className: "dsh-github-panel", ref: panelRef }, toastEl, headerEl, networkAlertBannerEl, upgradeBannerEl, errorEl, statsCardsEl, authGuideEl, tabsEl, contentEl, settingsModalEl, h(GithubHelpModal, {
|
|
5195
8172
|
isOpen: isHelpModalOpen,
|
|
5196
8173
|
onClose: () => setIsHelpModalOpen(false),
|
|
5197
|
-
})
|
|
8174
|
+
}),
|
|
8175
|
+
// PR 在线审阅工作台:仅在用户点击某个 PR 时挂载,关闭即卸载(零常驻开销)
|
|
8176
|
+
activePrModal
|
|
8177
|
+
? h(PrDetailModal, {
|
|
8178
|
+
prNumber: activePrModal.number,
|
|
8179
|
+
repoNameWithOwner: activePrModal.repo,
|
|
8180
|
+
onClose: () => setActivePrModal(null),
|
|
8181
|
+
showToast: showToast,
|
|
8182
|
+
})
|
|
8183
|
+
: null,
|
|
8184
|
+
// Issue 在线查看弹窗:同上,点击即挂载、关闭即卸载
|
|
8185
|
+
activeIssueModal
|
|
8186
|
+
? h(IssueDetailModal, {
|
|
8187
|
+
issueNumber: activeIssueModal.number,
|
|
8188
|
+
repoNameWithOwner: activeIssueModal.repo,
|
|
8189
|
+
onClose: () => setActiveIssueModal(null),
|
|
8190
|
+
showToast: showToast,
|
|
8191
|
+
})
|
|
8192
|
+
: null);
|
|
5198
8193
|
}
|
|
5199
8194
|
|
|
5200
8195
|
|