dsh-github-flow 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +4 -0
- package/README.md +4 -0
- package/docs/UPGRADE-NOTES.md +8 -8
- package/docs/architecture.md +14 -4
- package/lib/api/routes-actions.js +51 -1
- package/lib/client.js +587 -293
- package/lib/executor.d.ts +48 -0
- package/lib/executor.js +81 -5
- package/lib/tools/agent-task.d.ts +4 -0
- package/lib/tools/agent-task.js +18 -5
- package/lib/tools/api.d.ts +4 -0
- package/lib/tools/api.js +30 -6
- package/lib/tools/body-input.d.ts +58 -0
- package/lib/tools/body-input.js +123 -0
- package/lib/tools/discussion.d.ts +4 -0
- package/lib/tools/discussion.js +29 -13
- package/lib/tools/help.js +15 -2
- package/lib/tools/issue.d.ts +4 -0
- package/lib/tools/issue.js +21 -10
- package/lib/tools/pr.d.ts +4 -0
- package/lib/tools/pr.js +36 -15
- package/lib/tools/release.d.ts +4 -0
- package/lib/tools/release.js +15 -4
- package/lib/tools/task.d.ts +4 -0
- package/lib/tools/task.js +9 -2
- package/lib/ui/commit-modal.js +108 -4
- package/lib/ui/diff-utils.d.ts +51 -0
- package/lib/ui/diff-utils.js +155 -0
- package/lib/ui/file-diff-modal.d.ts +33 -0
- package/lib/ui/file-diff-modal.js +250 -0
- package/lib/ui/help-modal.js +5 -3
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +6 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified diff 的切分与路径归一(纯函数:零 React、零 IO)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块:提交详情要支持「点文件看该文件的 diff」,而一次 `git show` 拿到的是
|
|
5
|
+
* 整包 patch,必须在前端按 `diff --git` 边界切段。切分规则(边界、重命名路径、二进制判定)
|
|
6
|
+
* 是纯逻辑,抽出来才能脱离 DOM 单测;放进组件里就只能靠肉眼看界面。
|
|
7
|
+
*
|
|
8
|
+
* 为什么不改成「点一次查一次 `git show <sha> -- <path>`」:`git show --stat` 把重命名显示成
|
|
9
|
+
* `src/{old => new}.ts`,拿它当 pathspec 必然查不到;带空格/引号的路径同样脆。整包 patch 的
|
|
10
|
+
* `diff --git a/… b/…` 行才是权威来源,按它切分天然免疫这些形态。
|
|
11
|
+
*/
|
|
12
|
+
export interface PatchSection {
|
|
13
|
+
/** b 侧路径(重命名后的新路径) */
|
|
14
|
+
path: string;
|
|
15
|
+
/** 仅重命名块有值(a 侧旧路径) */
|
|
16
|
+
oldPath?: string;
|
|
17
|
+
/** 该文件的完整补丁块(含 diff --git / index / --- / +++ / hunk 全部行) */
|
|
18
|
+
text: string;
|
|
19
|
+
isBinary: boolean;
|
|
20
|
+
additions: number;
|
|
21
|
+
deletions: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 解析 `diff --git a/<old> b/<new>` 头行。
|
|
25
|
+
*
|
|
26
|
+
* 路径含空格时这行本身是有歧义的(`a/a b.txt b/a b.txt`),所以按**最后一个** ` b/` 切:
|
|
27
|
+
* a 侧取前缀,b 侧取剩余全部——b 侧是我们要的路径,尾部截断不会丢字符。
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseDiffGitLine(line: string): {
|
|
30
|
+
aPath: string;
|
|
31
|
+
bPath: string;
|
|
32
|
+
} | null;
|
|
33
|
+
/**
|
|
34
|
+
* 把 `--stat` 里的重命名写法归一成新路径:
|
|
35
|
+
* - `src/{old => new}.ts` → `src/new.ts`
|
|
36
|
+
* - `old/name.ts => new/name.ts` → `new/name.ts`
|
|
37
|
+
* 非重命名原样返回。
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeRenamePath(raw: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* 按 `diff --git` 边界把整包 patch 切成逐文件块。
|
|
42
|
+
* 首个 `diff --git` 之前的内容(如 `--format=` 漏配时的提交头)直接丢弃:
|
|
43
|
+
* 它们不属于任何文件,混进第一段会污染差异渲染。
|
|
44
|
+
*/
|
|
45
|
+
export declare function splitUnifiedPatch(patch: string): PatchSection[];
|
|
46
|
+
/**
|
|
47
|
+
* 在切好的块里找目标文件。
|
|
48
|
+
* 匹配优先级:新路径 → 旧路径 → 归一化后的重命名写法(`--stat` 的原始形态)。
|
|
49
|
+
* 找不到返回 null,由调用方给「该文件在本次提交中无文本差异」的兜底文案。
|
|
50
|
+
*/
|
|
51
|
+
export declare function findPatchSection(sections: PatchSection[], targetPath: string): PatchSection | null;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified diff 的切分与路径归一(纯函数:零 React、零 IO)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块:提交详情要支持「点文件看该文件的 diff」,而一次 `git show` 拿到的是
|
|
5
|
+
* 整包 patch,必须在前端按 `diff --git` 边界切段。切分规则(边界、重命名路径、二进制判定)
|
|
6
|
+
* 是纯逻辑,抽出来才能脱离 DOM 单测;放进组件里就只能靠肉眼看界面。
|
|
7
|
+
*
|
|
8
|
+
* 为什么不改成「点一次查一次 `git show <sha> -- <path>`」:`git show --stat` 把重命名显示成
|
|
9
|
+
* `src/{old => new}.ts`,拿它当 pathspec 必然查不到;带空格/引号的路径同样脆。整包 patch 的
|
|
10
|
+
* `diff --git a/… b/…` 行才是权威来源,按它切分天然免疫这些形态。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 解析 `diff --git a/<old> b/<new>` 头行。
|
|
14
|
+
*
|
|
15
|
+
* 路径含空格时这行本身是有歧义的(`a/a b.txt b/a b.txt`),所以按**最后一个** ` b/` 切:
|
|
16
|
+
* a 侧取前缀,b 侧取剩余全部——b 侧是我们要的路径,尾部截断不会丢字符。
|
|
17
|
+
*/
|
|
18
|
+
export function parseDiffGitLine(line) {
|
|
19
|
+
if (typeof line !== "string" || line.indexOf("diff --git ") !== 0)
|
|
20
|
+
return null;
|
|
21
|
+
var rest = line.slice("diff --git ".length);
|
|
22
|
+
var sep = rest.lastIndexOf(" b/");
|
|
23
|
+
if (sep < 0)
|
|
24
|
+
return null;
|
|
25
|
+
var aPath = rest.slice(0, sep);
|
|
26
|
+
var bPath = rest.slice(sep + 1); // 保留 "b/" 前缀,与 git 输出一致
|
|
27
|
+
if (aPath.indexOf("a/") !== 0)
|
|
28
|
+
return null;
|
|
29
|
+
return { aPath: aPath.slice(2), bPath: bPath.slice(2) };
|
|
30
|
+
}
|
|
31
|
+
/** 去掉 git 对特殊路径加的引号(core.quotepath=false 后大多数情况不再出现) */
|
|
32
|
+
function unquote(raw) {
|
|
33
|
+
var s = String(raw == null ? "" : raw).trim();
|
|
34
|
+
if (s.length >= 2 && s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') {
|
|
35
|
+
return s.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
36
|
+
}
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 把 `--stat` 里的重命名写法归一成新路径:
|
|
41
|
+
* - `src/{old => new}.ts` → `src/new.ts`
|
|
42
|
+
* - `old/name.ts => new/name.ts` → `new/name.ts`
|
|
43
|
+
* 非重命名原样返回。
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeRenamePath(raw) {
|
|
46
|
+
var s = unquote(raw);
|
|
47
|
+
if (s.indexOf("=>") < 0)
|
|
48
|
+
return s;
|
|
49
|
+
var brace = s.match(/^(.*)\{([^{}]*)=>\s*([^{}]*)\}(.*)$/);
|
|
50
|
+
if (brace) {
|
|
51
|
+
var joined = (brace[1] + brace[3] + brace[4]).replace(/\/{2,}/g, "/");
|
|
52
|
+
return joined.replace(/^\s+|\s+$/g, "");
|
|
53
|
+
}
|
|
54
|
+
var parts = s.split("=>");
|
|
55
|
+
return parts[parts.length - 1].replace(/^\s+|\s+$/g, "");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 按 `diff --git` 边界把整包 patch 切成逐文件块。
|
|
59
|
+
* 首个 `diff --git` 之前的内容(如 `--format=` 漏配时的提交头)直接丢弃:
|
|
60
|
+
* 它们不属于任何文件,混进第一段会污染差异渲染。
|
|
61
|
+
*/
|
|
62
|
+
export function splitUnifiedPatch(patch) {
|
|
63
|
+
if (typeof patch !== "string" || !patch)
|
|
64
|
+
return [];
|
|
65
|
+
var lines = patch.split("\n");
|
|
66
|
+
var sections = [];
|
|
67
|
+
var buf = [];
|
|
68
|
+
var current = null;
|
|
69
|
+
/**
|
|
70
|
+
* 是否已进入第一个 hunk。
|
|
71
|
+
*
|
|
72
|
+
* 为什么必须有这个状态位:`--- `/`+++ ` 既是文件头,也可能是**正文**行
|
|
73
|
+
* (删掉一行 `---` 分隔线的补丁行是 `----`,Markdown 文档里很常见)。
|
|
74
|
+
* 只按行首前缀判断会把正文行当文件头,导致增删计数与配色全错。
|
|
75
|
+
* git 的格式保证:文件头只出现在第一个 `@@` 之前,`@@` 之后全是正文字节。
|
|
76
|
+
*/
|
|
77
|
+
var inHunk = false;
|
|
78
|
+
var flush = function () {
|
|
79
|
+
if (current && buf.length > 0) {
|
|
80
|
+
current.text = buf.join("\n");
|
|
81
|
+
sections.push(current);
|
|
82
|
+
}
|
|
83
|
+
buf = [];
|
|
84
|
+
};
|
|
85
|
+
for (var i = 0; i < lines.length; i++) {
|
|
86
|
+
var line = lines[i];
|
|
87
|
+
if (line.indexOf("diff --git ") === 0) {
|
|
88
|
+
flush();
|
|
89
|
+
var parsed = parseDiffGitLine(line);
|
|
90
|
+
current = {
|
|
91
|
+
path: parsed ? unquote(parsed.bPath) : "",
|
|
92
|
+
oldPath: parsed ? unquote(parsed.aPath) : undefined,
|
|
93
|
+
text: "",
|
|
94
|
+
isBinary: false,
|
|
95
|
+
additions: 0,
|
|
96
|
+
deletions: 0,
|
|
97
|
+
};
|
|
98
|
+
buf.push(line);
|
|
99
|
+
inHunk = false; // 新文件块:文件头区重新开始
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!current)
|
|
103
|
+
continue; // 首个 diff --git 之前的头部:丢弃
|
|
104
|
+
buf.push(line);
|
|
105
|
+
if (line.indexOf("@@") === 0) {
|
|
106
|
+
inHunk = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!inHunk) {
|
|
110
|
+
// 文件头区:只解析元信息,**绝不计数**——`+++ b/x` 不是新增行
|
|
111
|
+
if (line.indexOf("rename from ") === 0 && !current.oldPath) {
|
|
112
|
+
current.oldPath = unquote(line.slice("rename from ".length));
|
|
113
|
+
}
|
|
114
|
+
else if (line.indexOf("rename to ") === 0) {
|
|
115
|
+
current.path = unquote(line.slice("rename to ".length));
|
|
116
|
+
}
|
|
117
|
+
else if (line.indexOf("+++ b/") === 0 && !current.path) {
|
|
118
|
+
current.path = unquote(line.slice(6));
|
|
119
|
+
}
|
|
120
|
+
else if (line.indexOf("Binary files ") === 0 || line.indexOf("GIT binary patch") === 0) {
|
|
121
|
+
current.isBinary = true;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// hunk 内:行首 + / - 就是真实增删(`` 不计)
|
|
126
|
+
if (line.charAt(0) === "+")
|
|
127
|
+
current.additions++;
|
|
128
|
+
else if (line.charAt(0) === "-")
|
|
129
|
+
current.deletions++;
|
|
130
|
+
}
|
|
131
|
+
flush();
|
|
132
|
+
return sections;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 在切好的块里找目标文件。
|
|
136
|
+
* 匹配优先级:新路径 → 旧路径 → 归一化后的重命名写法(`--stat` 的原始形态)。
|
|
137
|
+
* 找不到返回 null,由调用方给「该文件在本次提交中无文本差异」的兜底文案。
|
|
138
|
+
*/
|
|
139
|
+
export function findPatchSection(sections, targetPath) {
|
|
140
|
+
if (!Array.isArray(sections) || sections.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
var target = unquote(targetPath);
|
|
143
|
+
if (!target)
|
|
144
|
+
return null;
|
|
145
|
+
var want = normalizeRenamePath(target);
|
|
146
|
+
for (var i = 0; i < sections.length; i++) {
|
|
147
|
+
if (sections[i].path === target || sections[i].path === want)
|
|
148
|
+
return sections[i];
|
|
149
|
+
}
|
|
150
|
+
for (var j = 0; j < sections.length; j++) {
|
|
151
|
+
if (sections[j].oldPath && (sections[j].oldPath === target || sections[j].oldPath === want))
|
|
152
|
+
return sections[j];
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件 Diff 预览弹窗(未提交清单 / 提交详情共用)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么合成一个组件:
|
|
5
|
+
* 1. 未提交清单原本把整个 section 换成「二级视图」(`activeDiffFile ? 二级视图 : 列表`),
|
|
6
|
+
* 观感像另开一页;改成弹窗后列表留在原位,返回成本从一次重渲染变成一次 Esc;
|
|
7
|
+
* 2. 提交详情也要「点文件看 diff」,两处只是**数据来源**不同(一个是 `/api/github/file-diff`,
|
|
8
|
+
* 一个是整包 patch 切出来的一段),外壳/配色/错误态/复制逻辑完全同构,分两份必然漂移。
|
|
9
|
+
*
|
|
10
|
+
* 404 降级保留:桌面端未重启时新路由不存在,先回退到常驻的 `/api/github/action`,
|
|
11
|
+
* 再不行才显式提示「重启桌面端」——这是 file-diff 链路既有的自愈策略。
|
|
12
|
+
*/
|
|
13
|
+
export declare function FileDiffModal(props: {
|
|
14
|
+
file: {
|
|
15
|
+
path: string;
|
|
16
|
+
status?: string;
|
|
17
|
+
additions?: number;
|
|
18
|
+
deletions?: number;
|
|
19
|
+
isBinary?: boolean;
|
|
20
|
+
};
|
|
21
|
+
cwd?: string | null;
|
|
22
|
+
/** 已有补丁正文(提交详情场景:由整包 patch 切出);缺省时按未提交路由自行拉取 */
|
|
23
|
+
diffText?: string | null;
|
|
24
|
+
/** 顶部副标题(如提交短 SHA) */
|
|
25
|
+
subtitle?: string | null;
|
|
26
|
+
/** 附加提示(如补丁被截断) */
|
|
27
|
+
note?: string | null;
|
|
28
|
+
loading?: boolean;
|
|
29
|
+
error?: string | null;
|
|
30
|
+
zIndex?: number;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onOpenInDsh?: ((filePath: string) => void) | null;
|
|
33
|
+
}): any;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件 Diff 预览弹窗(未提交清单 / 提交详情共用)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么合成一个组件:
|
|
5
|
+
* 1. 未提交清单原本把整个 section 换成「二级视图」(`activeDiffFile ? 二级视图 : 列表`),
|
|
6
|
+
* 观感像另开一页;改成弹窗后列表留在原位,返回成本从一次重渲染变成一次 Esc;
|
|
7
|
+
* 2. 提交详情也要「点文件看 diff」,两处只是**数据来源**不同(一个是 `/api/github/file-diff`,
|
|
8
|
+
* 一个是整包 patch 切出来的一段),外壳/配色/错误态/复制逻辑完全同构,分两份必然漂移。
|
|
9
|
+
*
|
|
10
|
+
* 404 降级保留:桌面端未重启时新路由不存在,先回退到常驻的 `/api/github/action`,
|
|
11
|
+
* 再不行才显式提示「重启桌面端」——这是 file-diff 链路既有的自愈策略。
|
|
12
|
+
*/
|
|
13
|
+
export function FileDiffModal(props) {
|
|
14
|
+
var h = React.createElement;
|
|
15
|
+
var f = props.file;
|
|
16
|
+
var selfFetch = props.diffText === undefined || props.diffText === null;
|
|
17
|
+
var [fetched, setFetched] = React.useState(null);
|
|
18
|
+
var [fetching, setFetching] = React.useState(selfFetch);
|
|
19
|
+
var [fetchError, setFetchError] = React.useState(null);
|
|
20
|
+
var [copied, setCopied] = React.useState(false);
|
|
21
|
+
React.useEffect(function () {
|
|
22
|
+
var onKeyDown = function (e) {
|
|
23
|
+
if (e.key === "Escape" || e.keyCode === 27)
|
|
24
|
+
props.onClose();
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener("keydown", onKeyDown);
|
|
27
|
+
return function () { window.removeEventListener("keydown", onKeyDown); };
|
|
28
|
+
}, [props.onClose]);
|
|
29
|
+
React.useEffect(function () {
|
|
30
|
+
if (!selfFetch)
|
|
31
|
+
return;
|
|
32
|
+
setFetching(true);
|
|
33
|
+
setFetchError(null);
|
|
34
|
+
setFetched(null);
|
|
35
|
+
var run = async function () {
|
|
36
|
+
var primaryUrl = "/api/github/file-diff?file=" + encodeURIComponent(f.path) + (props.cwd ? "&cwd=" + encodeURIComponent(props.cwd) : "");
|
|
37
|
+
var readJson = async function (res) {
|
|
38
|
+
var text = await res.text().catch(function () { return ""; });
|
|
39
|
+
var json = null;
|
|
40
|
+
if (text && (text.trim().charAt(0) === "{" || text.trim().charAt(0) === "[")) {
|
|
41
|
+
try {
|
|
42
|
+
json = JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch (e) { }
|
|
45
|
+
}
|
|
46
|
+
return { ok: res.ok, status: res.status, text: text, json: json };
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
var p1 = await readJson(await fetch(primaryUrl));
|
|
50
|
+
if (p1.ok && p1.json && p1.json.ok && p1.json.data) {
|
|
51
|
+
setFetched(p1.json.data);
|
|
52
|
+
setFetching(false);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// 主路由 404:后端常驻进程还没载入新路由 → 先退到既有 action 通道
|
|
56
|
+
if (p1.status === 404) {
|
|
57
|
+
var r2 = await fetch("/api/github/action", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ action: "get_file_diff", file: f.path, cwd: props.cwd || undefined }),
|
|
61
|
+
}).catch(function () { return null; });
|
|
62
|
+
if (r2) {
|
|
63
|
+
var p2 = await readJson(r2);
|
|
64
|
+
if (p2.ok && p2.json && p2.json.data) {
|
|
65
|
+
setFetched(p2.json.data);
|
|
66
|
+
setFetching(false);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
setFetching(false);
|
|
71
|
+
setFetchError("后端对比接口未激活 (404)。请重启 DSH 桌面主程序以载入最新对比引擎;您也可点击上方「在 DSH 中打开」直接预览文件源码。");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setFetching(false);
|
|
75
|
+
setFetchError(p1.json && p1.json.error ? p1.json.error : (p1.text || ("查询差异失败 (HTTP " + p1.status + ")")));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
setFetching(false);
|
|
79
|
+
setFetchError((err && err.message) || "网络请求异常");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
run();
|
|
83
|
+
}, [f.path, props.cwd, selfFetch]);
|
|
84
|
+
var diffText = selfFetch ? (fetched && fetched.diff) || "" : String(props.diffText || "");
|
|
85
|
+
var isBinary = Boolean(props.file.isBinary || (fetched && fetched.isBinary));
|
|
86
|
+
var additions = props.file.additions !== undefined ? props.file.additions : (fetched ? fetched.additions : undefined);
|
|
87
|
+
var deletions = props.file.deletions !== undefined ? props.file.deletions : (fetched ? fetched.deletions : undefined);
|
|
88
|
+
var loading = props.loading === true || (selfFetch && fetching);
|
|
89
|
+
var error = props.error || (selfFetch ? fetchError : null);
|
|
90
|
+
var status = String(props.file.status || "");
|
|
91
|
+
var statusColor = status === "??" ? "var(--dsw-alias-state-success-primary, #1a7f37)"
|
|
92
|
+
: status.indexOf("D") >= 0 ? "var(--dsw-alias-state-error-primary, #cf222e)"
|
|
93
|
+
: "var(--dsw-alias-state-attention-primary, #9a6700)";
|
|
94
|
+
var statusText = status === "??" ? "未跟踪新增"
|
|
95
|
+
: status.indexOf("D") >= 0 ? "已删除"
|
|
96
|
+
: status.indexOf("R") >= 0 ? "已重命名"
|
|
97
|
+
: status ? "已修改" : "";
|
|
98
|
+
var copyDiff = function () {
|
|
99
|
+
if (!diffText)
|
|
100
|
+
return;
|
|
101
|
+
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
102
|
+
navigator.clipboard.writeText(diffText).then(function () {
|
|
103
|
+
setCopied(true);
|
|
104
|
+
setTimeout(function () { setCopied(false); }, 2000);
|
|
105
|
+
}).catch(function () { });
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var renderLines = function () {
|
|
109
|
+
var lines = diffText.split("\n");
|
|
110
|
+
/**
|
|
111
|
+
* hunk 状态位:`--- ` / `+++ ` 既是文件头,也可能是正文(Markdown 的 `---` 分隔线
|
|
112
|
+
* 被删时补丁行是 `----`)。只按行首前缀判断会把正文染成灰色小字,且与真实增删不符。
|
|
113
|
+
* git 保证文件头只出现在第一个 `@@` 之前,据此切分才可靠。
|
|
114
|
+
*/
|
|
115
|
+
var inHunk = false;
|
|
116
|
+
return lines.map(function (line, idx) {
|
|
117
|
+
if (line.indexOf("diff --git") === 0)
|
|
118
|
+
inHunk = false;
|
|
119
|
+
var isHunk = line.indexOf("@@") === 0;
|
|
120
|
+
if (isHunk)
|
|
121
|
+
inHunk = true;
|
|
122
|
+
var isAdd = inHunk && !isHunk && line.charAt(0) === "+";
|
|
123
|
+
var isDel = inHunk && !isHunk && line.charAt(0) === "-";
|
|
124
|
+
var isHeader = !inHunk && !isHunk;
|
|
125
|
+
var lineStyle = { display: "flex", padding: "0 8px", whiteSpace: "pre", minWidth: "100%", boxSizing: "border-box" };
|
|
126
|
+
if (isAdd) {
|
|
127
|
+
lineStyle.backgroundColor = "var(--dsw-alias-state-success-subtle, rgba(46,160,67,0.15))";
|
|
128
|
+
lineStyle.color = "var(--dsw-alias-state-success-primary, #1a7f37)";
|
|
129
|
+
}
|
|
130
|
+
else if (isDel) {
|
|
131
|
+
lineStyle.backgroundColor = "var(--dsw-alias-state-error-subtle, rgba(248,81,73,0.15))";
|
|
132
|
+
lineStyle.color = "var(--dsw-alias-state-error-primary, #cf222e)";
|
|
133
|
+
}
|
|
134
|
+
else if (isHunk) {
|
|
135
|
+
lineStyle.backgroundColor = "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.04))";
|
|
136
|
+
lineStyle.color = "var(--dsw-alias-label-secondary, #656d76)";
|
|
137
|
+
lineStyle.fontWeight = 600;
|
|
138
|
+
lineStyle.margin = "2px 0";
|
|
139
|
+
}
|
|
140
|
+
else if (isHeader) {
|
|
141
|
+
lineStyle.color = "var(--dsw-alias-label-muted, #8c959f)";
|
|
142
|
+
lineStyle.fontSize = "10px";
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
lineStyle.color = "var(--dsw-alias-label-primary, #1f2328)";
|
|
146
|
+
}
|
|
147
|
+
return h("div", { key: idx, style: lineStyle }, line || " ");
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
return h("div", {
|
|
151
|
+
className: "dsh-github-modal-overlay",
|
|
152
|
+
onClick: function (e) {
|
|
153
|
+
// 叠在提交详情弹窗之上时,遮罩点击必须止住冒泡:
|
|
154
|
+
// 外层 overlay 的 onClick 是「关闭提交详情」,不拦就会一次点击关掉两层
|
|
155
|
+
if (e && typeof e.stopPropagation === "function")
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
props.onClose();
|
|
158
|
+
},
|
|
159
|
+
style: { zIndex: props.zIndex === undefined ? 100001 : props.zIndex },
|
|
160
|
+
}, h("div", {
|
|
161
|
+
className: "dsh-github-modal-dialog",
|
|
162
|
+
role: "dialog",
|
|
163
|
+
"aria-label": "文件差异预览",
|
|
164
|
+
style: { width: "94%", maxWidth: "920px", maxHeight: "88vh", display: "flex", flexDirection: "column", overflow: "hidden" },
|
|
165
|
+
onClick: function (e) { e.stopPropagation(); },
|
|
166
|
+
},
|
|
167
|
+
// 头部:状态 + 路径 + 增删统计 + 操作按钮
|
|
168
|
+
h("div", { className: "dsh-github-modal-header", style: { flexShrink: 0, gap: "8px" } }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, h("h3", { style: { margin: 0, fontSize: "14px", fontWeight: 600, flexShrink: 0 } }, "文件差异"), status
|
|
169
|
+
? h("span", {
|
|
170
|
+
className: "dsh-github-badge",
|
|
171
|
+
style: { backgroundColor: statusColor + "22", color: statusColor, fontWeight: 600, fontSize: "11px", flexShrink: 0 },
|
|
172
|
+
}, status + " " + statusText)
|
|
173
|
+
: null, additions !== undefined && deletions !== undefined
|
|
174
|
+
? h("div", { style: { display: "flex", gap: "4px", fontSize: "11px", fontFamily: "monospace", flexShrink: 0 } }, h("span", { style: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontWeight: 600 } }, "+" + additions), h("span", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600 } }, "-" + deletions))
|
|
175
|
+
: null, props.subtitle
|
|
176
|
+
? h("span", { style: { fontSize: "11px", opacity: 0.7, flexShrink: 0, fontFamily: "monospace" } }, props.subtitle)
|
|
177
|
+
: null), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, diffText
|
|
178
|
+
? h("button", {
|
|
179
|
+
className: "dsh-github-btn-sm",
|
|
180
|
+
style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
|
|
181
|
+
onClick: copyDiff,
|
|
182
|
+
title: "复制 Unified Diff 原文",
|
|
183
|
+
}, copied ? "✓ 已复制" : "📋 复制 Diff")
|
|
184
|
+
: null, props.onOpenInDsh
|
|
185
|
+
? h("button", {
|
|
186
|
+
className: "dsh-github-btn-sm",
|
|
187
|
+
style: { padding: "3px 8px", fontSize: "11px", cursor: "pointer" },
|
|
188
|
+
onClick: function () { if (props.onOpenInDsh)
|
|
189
|
+
props.onOpenInDsh(f.path); },
|
|
190
|
+
title: "在 DSH 侧边栏打开该文件完整源码",
|
|
191
|
+
}, "📄 在 DSH 中打开")
|
|
192
|
+
: null, h("button", {
|
|
193
|
+
style: { background: "none", border: "none", cursor: "pointer", fontSize: "18px", color: "var(--dsw-alias-label-secondary, #656d76)", padding: "0 4px" },
|
|
194
|
+
onClick: props.onClose,
|
|
195
|
+
title: "关闭 (Esc)",
|
|
196
|
+
}, "✕"))),
|
|
197
|
+
// 正文
|
|
198
|
+
h("div", {
|
|
199
|
+
className: "dsh-github-modal-body",
|
|
200
|
+
style: { padding: "12px 16px 16px", overflowY: "auto", flex: 1, minHeight: 0, display: "flex", flexDirection: "column", gap: "8px", boxSizing: "border-box" },
|
|
201
|
+
}, h("div", {
|
|
202
|
+
style: {
|
|
203
|
+
fontSize: "11px",
|
|
204
|
+
fontFamily: "monospace",
|
|
205
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
206
|
+
wordBreak: "break-all",
|
|
207
|
+
padding: "3px 6px",
|
|
208
|
+
backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
|
|
209
|
+
borderRadius: "4px",
|
|
210
|
+
flexShrink: 0,
|
|
211
|
+
},
|
|
212
|
+
title: f.path,
|
|
213
|
+
}, f.path), loading
|
|
214
|
+
? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.65, fontSize: "12px" } }, "⏳ 正在计算 Git 代码比对差异...")
|
|
215
|
+
: error
|
|
216
|
+
? h("div", {
|
|
217
|
+
style: {
|
|
218
|
+
padding: "12px 14px",
|
|
219
|
+
color: "var(--dsw-alias-label-primary, #1f2328)",
|
|
220
|
+
fontSize: "12px",
|
|
221
|
+
lineHeight: "1.6",
|
|
222
|
+
backgroundColor: "var(--dsw-alias-bg-subtle, rgba(0,0,0,0.03))",
|
|
223
|
+
borderRadius: "6px",
|
|
224
|
+
},
|
|
225
|
+
}, h("div", { style: { color: "var(--dsw-alias-state-error-primary, #cf222e)", fontWeight: 600, marginBottom: "6px" } }, "⚠️ " + error))
|
|
226
|
+
: isBinary
|
|
227
|
+
? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.8, fontSize: "12px" } }, "📦 该文件为二进制格式(图片或编译产物),无法直接展示文本差异。")
|
|
228
|
+
: !diffText
|
|
229
|
+
? h("div", { style: { padding: "32px 0", textAlign: "center", opacity: 0.7, fontSize: "12px" } }, "该文件在本次变更中没有可展示的文本差异。")
|
|
230
|
+
: h("div", {
|
|
231
|
+
className: "dsh-github-inline-diff-view",
|
|
232
|
+
style: { display: "flex", flexDirection: "column", gap: "8px", minHeight: "120px" },
|
|
233
|
+
}, h("div", {
|
|
234
|
+
className: "dsh-github-inline-diff-content",
|
|
235
|
+
style: {
|
|
236
|
+
margin: 0,
|
|
237
|
+
padding: "4px 0",
|
|
238
|
+
fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
|
|
239
|
+
fontSize: "11px",
|
|
240
|
+
lineHeight: "1.5",
|
|
241
|
+
overflowX: "auto",
|
|
242
|
+
maxHeight: "62vh",
|
|
243
|
+
borderRadius: "6px",
|
|
244
|
+
border: "1px solid var(--dsw-alias-border-subtle, rgba(0,0,0,0.08))",
|
|
245
|
+
backgroundColor: "var(--dsw-alias-bg-canvas, #ffffff)",
|
|
246
|
+
},
|
|
247
|
+
}, renderLines())), props.note
|
|
248
|
+
? h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary, #656d76)", lineHeight: "1.6" } }, "⚠️ " + props.note)
|
|
249
|
+
: null)));
|
|
250
|
+
}
|
package/lib/ui/help-modal.js
CHANGED
|
@@ -160,8 +160,8 @@ export function GithubHelpModal(props) {
|
|
|
160
160
|
title: "2. 会话右侧栏面板 (Sidebar Panel) —— 深度协同当前工作区 · 5 个 Tab",
|
|
161
161
|
items: [
|
|
162
162
|
"打开入口:位于每个会话工作区的右侧抽屉栏,深度锁定当前会话的物理 Git 仓库;",
|
|
163
|
-
"仓库身份与切换:顶部展示当前关联仓库、Fork 血缘行与权限徽章(只读/可写);若配置了多个 remote,可通过 RepoSelectorDropdown 下拉一键切换基准仓库;", "Git Tab:三段式提交时间线(待推送 / 待拉取 / 最近),默认 15 条、下滑触底自动加载;点任意提交行弹出完整 Commit
|
|
164
|
-
"未提交 Tab:实时监听本地未提交文件与状态码(?? 新增 / M 修改 / D 删除 / R
|
|
163
|
+
"仓库身份与切换:顶部展示当前关联仓库、Fork 血缘行与权限徽章(只读/可写);若配置了多个 remote,可通过 RepoSelectorDropdown 下拉一键切换基准仓库;", "Git Tab:三段式提交时间线(待推送 / 待拉取 / 最近),默认 15 条、下滑触底自动加载;点任意提交行弹出完整 Commit 详情;提交详情里点「📁 文件变更统计」的任意文件行,可再叠一层该文件的 Unified Diff 预览(Esc 先关 Diff、再关详情);",
|
|
164
|
+
"未提交 Tab:实时监听本地未提交文件与状态码(?? 新增 / M 修改 / D 删除 / R 重命名),点击文件弹出该文件的 Unified Diff 弹窗(清单保留原位,可复制原文或在 DSH 中打开源码);",
|
|
165
165
|
"PR Tab:展示全部 PR 并按状态过滤;带三态 CI 门禁、冲突检测、审核判定与 Draft 标示;点击卡片在线唤起 PR 审阅工作台(描述 / 评论 / 文件 Diff 双栏比对);",
|
|
166
166
|
"Issues Tab:展示全部 Issue 并按状态过滤;点击卡片在线查看详情(Markdown 正文 / 评论时间线 / 标签色块 / 里程碑 / 关联 PR / 隐藏评论标注);",
|
|
167
167
|
"Actions Tab:最近工作流运行记录与触发方式;点击卡片在线查看本次运行的 job / step 树(每步结论与耗时,失败步骤高亮,默认展开第一个失败 job),失败时可按需提取日志锚点片段(一次最多 3 个失败 job、服务端批次预算 90 秒);",
|
|
@@ -169,9 +169,11 @@ export function GithubHelpModal(props) {
|
|
|
169
169
|
],
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
|
-
title: "3.
|
|
172
|
+
title: "3. 详情弹窗的通用操作习惯",
|
|
173
173
|
items: [
|
|
174
174
|
"PR / Issue / Actions 三个弹窗都支持:按 Esc 或点击遮罩关闭、右上角「GitHub ↗」直达官方页面;",
|
|
175
|
+
"文件 Diff 弹窗可以叠在提交详情之上:Esc 或点遮罩先关 Diff,再按一次才关提交详情;弹窗内可「📋 复制 Diff」原文或「📄 在 DSH 中打开」查看文件源码;",
|
|
176
|
+
"Diff 弹窗对正文里的 `---` / `+++` 不会误判成文件头(按 hunk 解析),重命名文件按新路径展示、二进制文件给明确提示;",
|
|
175
177
|
"列表卡片上的「GitHub ↗」按钮是独立的浏览器外跳入口,点它不会触发弹窗(已做事件隔离);",
|
|
176
178
|
"PR 弹窗的文件区可拖拽分隔条调宽、双击或点 ◀ 折叠;布局偏好会记在本地,下次打开保持原样;",
|
|
177
179
|
"长评论默认折叠(显示「⌄ 展开全部」),PR/Issue 正文默认展开——避免一屏被一段超长文字占满;",
|