paseo-req-kit 1.14.0-beta.3 → 1.14.0-beta.4
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/CHANGELOG.md +14 -0
- package/bin/req.mjs +8 -0
- package/contracts/cli-json.md +1 -1
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +8 -0
- package/engine/prompts/06-code-review.md +12 -1
- package/engine/prompts/incremental.md +2 -0
- package/engine/src/api.ts +14 -2
- package/engine/src/cli-args.ts +4 -1
- package/engine/src/code-review.ts +419 -0
- package/engine/src/codeowners.ts +87 -0
- package/engine/src/main.ts +13 -3
- package/engine/src/preflight.ts +10 -1
- package/engine/src/review-assets/code.css +93 -0
- package/engine/src/review-assets/code.html +43 -0
- package/engine/src/review-assets/code.js +402 -0
- package/engine/src/review.ts +83 -1
- package/package.json +1 -1
- package/paseo-plugin/client/data.ts +2 -2
- package/paseo-plugin/client/detail.tsx +7 -2
- package/paseo-plugin/client/nav.ts +5 -0
- package/paseo-plugin/client/review.tsx +16 -6
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +1 -1
- package/paseo-plugin/shared/contracts.ts +4 -2
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.BpC99f5a.js → app.Cp7WwPWE.js} +1 -1
- package/site-dist/assets/changelog.md.Zi-_3Rjh.js +1 -0
- package/site-dist/assets/chunks/@localSearchIndexroot.COxp5Rrc.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.CI43rhTA.js → VPLocalSearchBox.CMTXQK-Q.js} +1 -1
- package/site-dist/assets/chunks/{theme.S6pNEF23.js → theme.DvVUfKr3.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.js → guide_getting-started.md.D8XlIQOg.js} +1 -1
- package/site-dist/assets/{guide_user.md.D9GpGJ7y.js → guide_user.md.B4N9ywmr.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.js → reference_contracts_cli-json.md.CCvKLasM.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.lean.js → reference_contracts_cli-json.md.CCvKLasM.lean.js} +1 -1
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +4 -4
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +4 -4
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +5 -5
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +4 -4
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +4 -4
- package/site-dist/reference/contracts/state.html +4 -4
- package/site-dist/reference/contracts/usage-record.html +4 -4
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/skills/req/SKILL.md +1 -0
- package/site-dist/assets/changelog.md.m4cSU5e4.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.D6ywcTJ5.js +0 -1
- /package/site-dist/assets/{changelog.md.m4cSU5e4.lean.js → changelog.md.Zi-_3Rjh.lean.js} +0 -0
- /package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.lean.js → guide_getting-started.md.D8XlIQOg.lean.js} +0 -0
- /package/site-dist/assets/{guide_user.md.D9GpGJ7y.lean.js → guide_user.md.B4N9ywmr.lean.js} +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// The code review page (1.14). Plain script, no framework: the server serves it next to its JSON
|
|
2
|
+
// API (api/info, api/draft, api/submit). Everything shown is set with textContent.
|
|
3
|
+
(function () {
|
|
4
|
+
"use strict";
|
|
5
|
+
var state = { info: null, comments: [], decisions: {}, reviewer: "", reviewers: "", note: "", saveTimer: null, busy: false, open: null };
|
|
6
|
+
var $ = function (id) {
|
|
7
|
+
return document.getElementById(id);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function el(tag, attrs, children) {
|
|
11
|
+
var node = document.createElement(tag);
|
|
12
|
+
Object.keys(attrs || {}).forEach(function (k) {
|
|
13
|
+
if (attrs[k] === null || attrs[k] === undefined) return;
|
|
14
|
+
if (k === "text") node.textContent = attrs[k];
|
|
15
|
+
else if (k === "className") node.className = attrs[k];
|
|
16
|
+
else if (k.indexOf("on") === 0) node.addEventListener(k.slice(2), attrs[k]);
|
|
17
|
+
else node.setAttribute(k, attrs[k]);
|
|
18
|
+
});
|
|
19
|
+
(children || []).forEach(function (c) {
|
|
20
|
+
if (c) node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
|
|
21
|
+
});
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toast(text) {
|
|
26
|
+
var t = $("toast");
|
|
27
|
+
t.textContent = text;
|
|
28
|
+
t.hidden = false;
|
|
29
|
+
clearTimeout(toast.timer);
|
|
30
|
+
toast.timer = setTimeout(function () {
|
|
31
|
+
t.hidden = true;
|
|
32
|
+
}, 8000);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function api(pathname, method, body) {
|
|
36
|
+
return fetch(pathname, { method: method || "GET", headers: body ? { "content-type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined }).then(function (r) {
|
|
37
|
+
return r.json().then(function (data) {
|
|
38
|
+
if (!r.ok && data && data.message) throw new Error(data.message);
|
|
39
|
+
return data;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function reviewersList() {
|
|
45
|
+
return state.reviewers
|
|
46
|
+
.split(/[,,、\s]+/)
|
|
47
|
+
.map(function (s) {
|
|
48
|
+
return s.trim();
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function save() {
|
|
54
|
+
if (state.info.readOnly) return;
|
|
55
|
+
clearTimeout(state.saveTimer);
|
|
56
|
+
state.saveTimer = setTimeout(function () {
|
|
57
|
+
api("api/draft", "PUT", { comments: state.comments, decisions: state.decisions, reviewer: state.reviewer, reviewers: state.reviewers, note: state.note }).catch(function () {});
|
|
58
|
+
}, 600);
|
|
59
|
+
count();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fixes() {
|
|
63
|
+
return Object.keys(state.decisions)
|
|
64
|
+
.filter(function (id) {
|
|
65
|
+
return state.decisions[id].mark === "fix";
|
|
66
|
+
})
|
|
67
|
+
.map(function (id) {
|
|
68
|
+
return { id: id, note: state.decisions[id].note || "" };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function count() {
|
|
73
|
+
var comments = state.comments.filter(function (c) {
|
|
74
|
+
return c.text.trim();
|
|
75
|
+
}).length;
|
|
76
|
+
var fix = fixes().length;
|
|
77
|
+
var total = comments + fix + (state.note.trim() ? 1 : 0);
|
|
78
|
+
$("count").textContent = "代码行上的意见 " + comments + " 条,AI 意见标为要改 " + fix + " 条";
|
|
79
|
+
var blocked = state.info.readOnly || state.info.running || state.busy;
|
|
80
|
+
$("changes").disabled = blocked || total === 0;
|
|
81
|
+
$("changes").textContent = total ? "退回修改(" + total + " 条意见)" : "退回修改";
|
|
82
|
+
$("approve").disabled = blocked;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var SEVERITY_CLASS = { "阻塞": "danger", "重要": "warn", "建议": "", "待核实": "" };
|
|
86
|
+
var STATUS_TEXT = { fixed: "已修复", rejected: "修复时判为不成立", wontfix: "决定不修复", open: "未处理" };
|
|
87
|
+
|
|
88
|
+
function findingNode(f, compact) {
|
|
89
|
+
var d = state.decisions[f.id] || {};
|
|
90
|
+
var note = el("input", { className: "note-input", placeholder: "补充一句(可不填)", value: d.note || "", hidden: d.mark === "fix" ? null : "" });
|
|
91
|
+
note.addEventListener("input", function (e) {
|
|
92
|
+
state.decisions[f.id] = { mark: "fix", note: e.target.value };
|
|
93
|
+
save();
|
|
94
|
+
});
|
|
95
|
+
function mark(m) {
|
|
96
|
+
return function () {
|
|
97
|
+
var cur = state.decisions[f.id] && state.decisions[f.id].mark;
|
|
98
|
+
if (cur === m) delete state.decisions[f.id];
|
|
99
|
+
else state.decisions[f.id] = { mark: m, note: (state.decisions[f.id] && state.decisions[f.id].note) || "" };
|
|
100
|
+
render();
|
|
101
|
+
save();
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
var where = f.file ? f.file + (f.line ? ":" + f.line : "") : "";
|
|
105
|
+
return el("div", { className: "finding " + (SEVERITY_CLASS[f.severity] || "") + (d.mark ? " decided-" + d.mark : "") }, [
|
|
106
|
+
el("div", { className: "finding-head" }, [
|
|
107
|
+
el("span", { className: "badge " + (SEVERITY_CLASS[f.severity] || ""), text: "AI · " + f.severity }),
|
|
108
|
+
el("b", { text: f.id }),
|
|
109
|
+
f.status !== "open" ? el("span", { className: "badge " + (f.status === "fixed" ? "ok" : ""), text: STATUS_TEXT[f.status] }) : null,
|
|
110
|
+
!compact && where ? el("a", { href: "#", className: "where", text: where, onclick: function (e) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
jump(f.file, f.line);
|
|
113
|
+
} }) : null,
|
|
114
|
+
]),
|
|
115
|
+
el("div", { className: "finding-text", text: f.title }),
|
|
116
|
+
f.suggestion ? el("div", { className: "muted small", text: "建议:" + f.suggestion }) : null,
|
|
117
|
+
state.info.readOnly
|
|
118
|
+
? null
|
|
119
|
+
: el("div", { className: "marks" }, [
|
|
120
|
+
el("button", { className: "btn small" + (d.mark === "fix" ? " on-fix" : ""), text: "要改", onclick: mark("fix") }),
|
|
121
|
+
el("button", { className: "btn small" + (d.mark === "ok" ? " on-ok" : ""), text: "不用改", onclick: mark("ok") }),
|
|
122
|
+
]),
|
|
123
|
+
note,
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function jump(file, line) {
|
|
128
|
+
var target = document.querySelector('[data-file="' + CSS.escape(file) + '"]' + (line ? '[data-n="' + line + '"]' : ""));
|
|
129
|
+
if (!target) target = document.querySelector('[data-file-head="' + CSS.escape(file) + '"]');
|
|
130
|
+
if (target) {
|
|
131
|
+
var box = target.closest(".file");
|
|
132
|
+
if (box && box.classList.contains("folded")) box.classList.remove("folded");
|
|
133
|
+
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
134
|
+
target.classList.add("flash");
|
|
135
|
+
setTimeout(function () {
|
|
136
|
+
target.classList.remove("flash");
|
|
137
|
+
}, 1600);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function commentBox(file, line) {
|
|
142
|
+
var existing = state.comments.find(function (c) {
|
|
143
|
+
return c.file === file && c.line === line;
|
|
144
|
+
});
|
|
145
|
+
if (!existing) {
|
|
146
|
+
existing = { file: file, line: line, text: "" };
|
|
147
|
+
state.comments.push(existing);
|
|
148
|
+
}
|
|
149
|
+
var ta = el("textarea", { rows: "2", placeholder: "这一行哪里不对、应该怎么改" });
|
|
150
|
+
ta.value = existing.text;
|
|
151
|
+
ta.addEventListener("input", function (e) {
|
|
152
|
+
existing.text = e.target.value;
|
|
153
|
+
save();
|
|
154
|
+
});
|
|
155
|
+
var remove = el("button", { className: "btn small", text: "删除", onclick: function () {
|
|
156
|
+
state.comments = state.comments.filter(function (c) {
|
|
157
|
+
return c !== existing;
|
|
158
|
+
});
|
|
159
|
+
state.open = null;
|
|
160
|
+
render();
|
|
161
|
+
save();
|
|
162
|
+
} });
|
|
163
|
+
return el("tr", { className: "comment-row" }, [el("td", { colspan: "3" }, [el("div", { className: "comment" }, [el("div", { className: "muted small", text: "你的意见(" + file + ":" + line + ")" }), ta, state.info.readOnly ? null : remove])])]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderFile(f, findingsByFile) {
|
|
167
|
+
var mine = findingsByFile[f.path] || [];
|
|
168
|
+
var owners = (state.info.reviewers.byFile || {})[f.path] || [];
|
|
169
|
+
var head = el("div", { className: "file-head", "data-file-head": f.path, onclick: function () {
|
|
170
|
+
box.classList.toggle("folded");
|
|
171
|
+
} }, [
|
|
172
|
+
el("span", { className: "path", text: f.path + (f.oldPath ? "(原 " + f.oldPath + ")" : "") }),
|
|
173
|
+
el("span", { className: "badge", text: { added: "新增", deleted: "删除", modified: "修改", renamed: "改名" }[f.status] }),
|
|
174
|
+
el("span", { className: "plus", text: "+" + f.additions }),
|
|
175
|
+
el("span", { className: "minus", text: "−" + f.deletions }),
|
|
176
|
+
f.doc ? el("span", { className: "badge", text: "需求文档" }) : null,
|
|
177
|
+
mine.length ? el("span", { className: "badge warn", text: "AI 意见 " + mine.length }) : null,
|
|
178
|
+
owners.length ? el("span", { className: "muted small", text: "负责人 " + owners.join(" ") }) : null,
|
|
179
|
+
]);
|
|
180
|
+
var body = el("div", { className: "file-body" });
|
|
181
|
+
var unplaced = mine.filter(function (x) {
|
|
182
|
+
return !x.line || !f.hunks.some(function (h) {
|
|
183
|
+
return h.lines.some(function (l) {
|
|
184
|
+
return l.n === x.line;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
unplaced.forEach(function (x) {
|
|
189
|
+
body.appendChild(findingNode(x, true));
|
|
190
|
+
});
|
|
191
|
+
if (f.binary) body.appendChild(el("div", { className: "muted pad", text: "二进制文件,不显示内容。" }));
|
|
192
|
+
f.hunks.forEach(function (h) {
|
|
193
|
+
var table = el("table", { className: "diff" });
|
|
194
|
+
table.appendChild(el("tr", { className: "hunk" }, [el("td", { colspan: "3", text: h.header })]));
|
|
195
|
+
h.lines.forEach(function (l) {
|
|
196
|
+
var line = l.n || l.o;
|
|
197
|
+
var row = el("tr", { className: "line " + (l.t === "+" ? "add" : l.t === "-" ? "del" : ""), "data-file": f.path, "data-n": l.n || "", title: state.info.readOnly ? "" : "点这一行写意见" }, [
|
|
198
|
+
el("td", { className: "num", text: l.o === null ? "" : String(l.o) }),
|
|
199
|
+
el("td", { className: "num", text: l.n === null ? "" : String(l.n) }),
|
|
200
|
+
el("td", { className: "code", text: (l.t === " " ? " " : l.t) + " " + l.s }),
|
|
201
|
+
]);
|
|
202
|
+
if (!state.info.readOnly) {
|
|
203
|
+
row.addEventListener("click", function () {
|
|
204
|
+
state.open = f.path + ":" + line;
|
|
205
|
+
render();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
table.appendChild(row);
|
|
209
|
+
mine
|
|
210
|
+
.filter(function (x) {
|
|
211
|
+
return x.line && l.n === x.line;
|
|
212
|
+
})
|
|
213
|
+
.forEach(function (x) {
|
|
214
|
+
table.appendChild(el("tr", { className: "finding-row" }, [el("td", { colspan: "3" }, [findingNode(x, true)])]));
|
|
215
|
+
});
|
|
216
|
+
var hasComment = state.comments.some(function (c) {
|
|
217
|
+
return c.file === f.path && c.line === line;
|
|
218
|
+
});
|
|
219
|
+
if (hasComment || state.open === f.path + ":" + line) table.appendChild(commentBox(f.path, line));
|
|
220
|
+
});
|
|
221
|
+
body.appendChild(table);
|
|
222
|
+
});
|
|
223
|
+
if (f.truncated) body.appendChild(el("div", { className: "muted pad", text: "改动太多,只显示了一部分。完整改动请在开发工具里看。" }));
|
|
224
|
+
var box = el("section", { className: "file" + (f.doc && !mine.length ? " folded" : ""), id: "file-" + f.path }, [head, body]);
|
|
225
|
+
return box;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function render() {
|
|
229
|
+
var info = state.info;
|
|
230
|
+
var byFile = {};
|
|
231
|
+
info.findings.forEach(function (x) {
|
|
232
|
+
if (x.file && info.files.some(function (f) {
|
|
233
|
+
return f.path === x.file;
|
|
234
|
+
})) (byFile[x.file] = byFile[x.file] || []).push(x);
|
|
235
|
+
});
|
|
236
|
+
var folded = {};
|
|
237
|
+
document.querySelectorAll(".file").forEach(function (n) {
|
|
238
|
+
folded[n.id] = n.classList.contains("folded");
|
|
239
|
+
});
|
|
240
|
+
var diffs = $("diffs");
|
|
241
|
+
var y = window.scrollY;
|
|
242
|
+
diffs.textContent = "";
|
|
243
|
+
if (!info.files.length) diffs.appendChild(el("div", { className: "card muted", text: info.base.commit ? "没有找到改动。" : "找不到这个需求是从哪个提交开始的,没法列出改动。" }));
|
|
244
|
+
info.files.forEach(function (f) {
|
|
245
|
+
var node = renderFile(f, byFile);
|
|
246
|
+
if (node.id in folded) node.classList.toggle("folded", folded[node.id]);
|
|
247
|
+
diffs.appendChild(node);
|
|
248
|
+
});
|
|
249
|
+
// The AI findings not on a changed file: listed at the top.
|
|
250
|
+
var ai = $("ai");
|
|
251
|
+
ai.textContent = "";
|
|
252
|
+
var elsewhere = info.findings.filter(function (x) {
|
|
253
|
+
return !byFile[x.file] || byFile[x.file].indexOf(x) < 0;
|
|
254
|
+
});
|
|
255
|
+
if (info.findings.length) {
|
|
256
|
+
ai.hidden = false;
|
|
257
|
+
var open = info.findings.filter(function (x) {
|
|
258
|
+
return x.status === "open";
|
|
259
|
+
}).length;
|
|
260
|
+
ai.appendChild(el("h2", { text: "AI 评审意见(" + info.findings.length + " 条,未处理 " + open + " 条)" }));
|
|
261
|
+
ai.appendChild(el("p", { className: "muted small", text: "在代码旁边也能看到。阻塞、重要的未处理意见建议标为要改;拿不准的请开发同事看一下。" }));
|
|
262
|
+
info.findings.forEach(function (x) {
|
|
263
|
+
if (elsewhere.indexOf(x) >= 0 || x.severity === "阻塞" || x.severity === "重要") ai.appendChild(findingNode(x, false));
|
|
264
|
+
});
|
|
265
|
+
} else if (info.rounds.length) {
|
|
266
|
+
ai.hidden = false;
|
|
267
|
+
ai.appendChild(el("h2", { text: "AI 评审" }));
|
|
268
|
+
ai.appendChild(el("p", { className: "muted", text: "AI 评审没有留下结构化的意见,详细内容见 code-review.md。" }));
|
|
269
|
+
}
|
|
270
|
+
var nav = $("files");
|
|
271
|
+
nav.textContent = "";
|
|
272
|
+
nav.appendChild(el("div", { className: "nav-title", text: "改动的文件(" + info.files.length + ")" }));
|
|
273
|
+
info.files.forEach(function (f) {
|
|
274
|
+
nav.appendChild(el("a", { href: "#", className: f.doc ? "doc" : "", onclick: function (e) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
jump(f.path, null);
|
|
277
|
+
} }, [el("span", { text: f.path }), (byFile[f.path] || []).length ? el("span", { className: "badge warn", text: String(byFile[f.path].length) }) : null]));
|
|
278
|
+
});
|
|
279
|
+
window.scrollTo(0, y);
|
|
280
|
+
count();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function submit(verdict) {
|
|
284
|
+
if (state.busy) return;
|
|
285
|
+
var text = verdict === "approve" ? "确认评审通过?会记入 code-review.md 并提交。" : "确认退回修改?流水线会按这些意见修改(需求说明不变),改完再评审、再验证。";
|
|
286
|
+
if (!window.confirm(text)) return;
|
|
287
|
+
state.busy = true;
|
|
288
|
+
count();
|
|
289
|
+
api("api/submit", "POST", {
|
|
290
|
+
verdict: verdict,
|
|
291
|
+
reviewer: state.reviewer,
|
|
292
|
+
reviewers: reviewersList(),
|
|
293
|
+
comments: state.comments.filter(function (c) {
|
|
294
|
+
return c.text.trim();
|
|
295
|
+
}),
|
|
296
|
+
fix: fixes(),
|
|
297
|
+
note: state.note,
|
|
298
|
+
})
|
|
299
|
+
.then(function (r) {
|
|
300
|
+
toast(r.message);
|
|
301
|
+
return load();
|
|
302
|
+
})
|
|
303
|
+
.catch(function (e) {
|
|
304
|
+
toast(e.message);
|
|
305
|
+
})
|
|
306
|
+
.then(function () {
|
|
307
|
+
state.busy = false;
|
|
308
|
+
count();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function load() {
|
|
313
|
+
return api("api/info").then(function (info) {
|
|
314
|
+
state.info = info;
|
|
315
|
+
$("project").textContent = info.featureDir;
|
|
316
|
+
$("title").textContent = "代码审查:" + info.title;
|
|
317
|
+
var adds = 0,
|
|
318
|
+
dels = 0;
|
|
319
|
+
info.files.forEach(function (f) {
|
|
320
|
+
if (!f.doc) {
|
|
321
|
+
adds += f.additions;
|
|
322
|
+
dels += f.deletions;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
var last = info.rounds.length ? info.rounds[info.rounds.length - 1] : null;
|
|
326
|
+
$("sub").textContent =
|
|
327
|
+
(info.branch ? "分支 " + info.branch : "") +
|
|
328
|
+
(info.base.ref ? ",相对 " + info.base.ref : "") +
|
|
329
|
+
":改了 " + info.files.filter(function (f) {
|
|
330
|
+
return !f.doc;
|
|
331
|
+
}).length + " 个代码文件(+" + adds + " −" + dels + ")" +
|
|
332
|
+
(last ? ";AI 评审 " + info.rounds.length + " 轮,最后一轮" + (last.verdict === "pass" ? "通过" : "需要修改") : "");
|
|
333
|
+
var banner = $("status");
|
|
334
|
+
banner.hidden = true;
|
|
335
|
+
banner.className = "banner";
|
|
336
|
+
if (info.running) {
|
|
337
|
+
banner.hidden = false;
|
|
338
|
+
banner.textContent = "流水线正在运行,改动还会变化。可以先看,结束后再提交评审。";
|
|
339
|
+
} else if (info.human) {
|
|
340
|
+
banner.hidden = false;
|
|
341
|
+
var h = info.human;
|
|
342
|
+
if (h.status === "approved") {
|
|
343
|
+
banner.className = "banner " + (h.stale ? "warn" : "ok");
|
|
344
|
+
banner.textContent = (h.stale ? "评审通过后代码又有改动,需要重新评审。" : "已评审通过") + "(" + h.reviewer + "," + h.at.slice(0, 10) + (h.reviewers.length ? ",审核人 " + h.reviewers.join(" ") : "") + ")";
|
|
345
|
+
} else {
|
|
346
|
+
banner.className = "banner warn";
|
|
347
|
+
banner.textContent = "上次退回了 " + h.items.length + " 条意见(" + h.reviewer + "," + h.at.slice(0, 10) + ")" + (h.consumedAttempt ? ",第 " + h.consumedAttempt + " 次尝试在处理" : ",还没开始修改");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
var sum = $("summary");
|
|
351
|
+
sum.textContent = "";
|
|
352
|
+
if (info.summary.length) {
|
|
353
|
+
sum.hidden = false;
|
|
354
|
+
sum.appendChild(el("h2", { text: "这次改了什么(来自提测说明)" }));
|
|
355
|
+
info.summary.forEach(function (l) {
|
|
356
|
+
sum.appendChild(el("div", { text: l.replace(/^\s*[-*]\s+/, "• ") }));
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
var d = info.draft || {};
|
|
360
|
+
state.comments = Array.isArray(d.comments) ? d.comments : [];
|
|
361
|
+
state.decisions = d.decisions && typeof d.decisions === "object" ? d.decisions : {};
|
|
362
|
+
state.reviewer = d.reviewer || "";
|
|
363
|
+
state.reviewers = typeof d.reviewers === "string" ? d.reviewers : info.reviewers.suggested.join(", ");
|
|
364
|
+
state.note = d.note || "";
|
|
365
|
+
$("reviewer").value = state.reviewer;
|
|
366
|
+
$("reviewers").value = state.reviewers;
|
|
367
|
+
$("reviewers").title = info.reviewers.source ? "默认取自 " + info.reviewers.source + ",可以改" : "项目里没有 CODEOWNERS,请填写";
|
|
368
|
+
$("note").value = state.note;
|
|
369
|
+
if (info.readOnly) ["reviewer", "reviewers", "note"].forEach(function (id) {
|
|
370
|
+
$(id).disabled = true;
|
|
371
|
+
});
|
|
372
|
+
render();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function start() {
|
|
377
|
+
load().catch(function (e) {
|
|
378
|
+
$("title").textContent = "打不开代码审查页";
|
|
379
|
+
$("sub").textContent = e.message;
|
|
380
|
+
});
|
|
381
|
+
$("reviewer").addEventListener("input", function (e) {
|
|
382
|
+
state.reviewer = e.target.value;
|
|
383
|
+
save();
|
|
384
|
+
});
|
|
385
|
+
$("reviewers").addEventListener("input", function (e) {
|
|
386
|
+
state.reviewers = e.target.value;
|
|
387
|
+
save();
|
|
388
|
+
});
|
|
389
|
+
$("note").addEventListener("input", function (e) {
|
|
390
|
+
state.note = e.target.value;
|
|
391
|
+
save();
|
|
392
|
+
});
|
|
393
|
+
$("changes").addEventListener("click", function () {
|
|
394
|
+
submit("changes");
|
|
395
|
+
});
|
|
396
|
+
$("approve").addEventListener("click", function () {
|
|
397
|
+
submit("approve");
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
start();
|
|
402
|
+
})();
|
package/engine/src/review.ts
CHANGED
|
@@ -15,6 +15,7 @@ import os from "node:os";
|
|
|
15
15
|
import http from "node:http";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
|
|
18
|
+
import { buildCodeReview, recordHumanReview, startReviewFix, type Submission } from "./code-review.js";
|
|
18
19
|
import { type Answer, acceptanceFailMessage, acceptanceText, buildChecklist, recordAcceptance, verdictOf } from "./checklist.js";
|
|
19
20
|
import { readIndex } from "./records.js";
|
|
20
21
|
import { readState, statePath } from "./state.js";
|
|
@@ -204,6 +205,8 @@ export interface ReviewServerOptions {
|
|
|
204
205
|
env?: NodeJS.ProcessEnv;
|
|
205
206
|
/** Runs `req approve|reply …` in the requirement's checkout; tests replace it. */
|
|
206
207
|
run?: (command: "approve" | "reply", repo: string, featureDir: string, extra: string[]) => { ok: boolean; message: string };
|
|
208
|
+
/** 1.14: `req review-fix <需求>` after the code review page asked for changes; tests replace it. */
|
|
209
|
+
codeFix?: (repo: string, featureDir: string) => { ok: boolean; message: string };
|
|
207
210
|
/** 1.14: `req try <需求> --json [--stop]` for the checklist page's "试用" button; tests replace it. */
|
|
208
211
|
tryRun?: (repo: string, featureDir: string, stop: boolean) => { ok: boolean; message: string; services?: unknown[]; howTo?: string[] };
|
|
209
212
|
onRequest?: () => void;
|
|
@@ -329,6 +332,7 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
|
329
332
|
const env = opts.env ?? process.env;
|
|
330
333
|
const run = opts.run ?? defaultRun;
|
|
331
334
|
const tryRun = opts.tryRun ?? defaultTry;
|
|
335
|
+
const codeFixRun = opts.codeFix ?? startReviewFix;
|
|
332
336
|
return http.createServer((req, res) => {
|
|
333
337
|
opts.onRequest?.();
|
|
334
338
|
handle(req, res).catch((error: Error) => json(res, 500, { ok: false, message: error.message }));
|
|
@@ -359,6 +363,11 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
|
359
363
|
return res.end();
|
|
360
364
|
}
|
|
361
365
|
if (rest.startsWith("check/")) return handleChecklist(req, res, rest.slice("check/".length), entry, featureAbs, full);
|
|
366
|
+
if (rest === "code") {
|
|
367
|
+
res.writeHead(301, { location: `${url.pathname}/` });
|
|
368
|
+
return res.end();
|
|
369
|
+
}
|
|
370
|
+
if (rest.startsWith("code/")) return handleCodeReview(req, res, rest.slice("code/".length), entry, featureAbs, full);
|
|
362
371
|
if (rest === "") return sendFile(res, path.join(ASSETS, "app.html"));
|
|
363
372
|
if (rest === "app.js" || rest === "app.css") return sendFile(res, path.join(ASSETS, rest));
|
|
364
373
|
if (rest.startsWith("proto/cur/")) return sendFile(res, inside(prototypeDir, rest.slice("proto/cur/".length)), PROTOTYPE_CSP);
|
|
@@ -428,6 +437,63 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
|
428
437
|
return send(res, 404, "没有这个页面");
|
|
429
438
|
}
|
|
430
439
|
|
|
440
|
+
// 1.14: the code review page, /r/<token>/f/<key>/code/…
|
|
441
|
+
async function handleCodeReview(req: http.IncomingMessage, res: http.ServerResponse, rest: string, entry: { repo: string; featureDir: string }, featureAbs: string, full: boolean) {
|
|
442
|
+
const draftFile = path.join(featureAbs, ".pipeline", "code-review", "draft.json");
|
|
443
|
+
if (rest === "") return sendFile(res, path.join(ASSETS, "code.html"));
|
|
444
|
+
if (rest === "code.js" || rest === "code.css") return sendFile(res, path.join(ASSETS, rest));
|
|
445
|
+
if (rest === "api/info" && req.method === "GET") {
|
|
446
|
+
let draft: unknown = null;
|
|
447
|
+
try {
|
|
448
|
+
draft = JSON.parse(fs.readFileSync(draftFile, "utf8"));
|
|
449
|
+
} catch {
|
|
450
|
+
draft = null;
|
|
451
|
+
}
|
|
452
|
+
return json(res, 200, { ...buildCodeReview(entry.repo, entry.featureDir), readOnly: !full, draft });
|
|
453
|
+
}
|
|
454
|
+
if (rest === "api/draft" && req.method === "PUT") {
|
|
455
|
+
if (!full) return json(res, 403, { ok: false, message: "这是只读链接" });
|
|
456
|
+
const body = await readBody(req);
|
|
457
|
+
if (!body || typeof body !== "object") return json(res, 400, { ok: false, message: "格式不对" });
|
|
458
|
+
fs.mkdirSync(path.dirname(draftFile), { recursive: true });
|
|
459
|
+
fs.writeFileSync(draftFile, JSON.stringify({ ...(body as object), savedAt: new Date().toISOString() }, null, 2));
|
|
460
|
+
return json(res, 200, { ok: true });
|
|
461
|
+
}
|
|
462
|
+
if (rest === "api/submit" && req.method === "POST") {
|
|
463
|
+
if (!full) return json(res, 403, { ok: false, message: "这是只读链接,不能提交" });
|
|
464
|
+
const body = (await readBody(req)) as Partial<Submission> | null;
|
|
465
|
+
const verdict = body?.verdict === "approve" ? "approve" : body?.verdict === "changes" ? "changes" : null;
|
|
466
|
+
if (!verdict) return json(res, 400, { ok: false, message: "请选择评审通过或退回修改" });
|
|
467
|
+
const data = buildCodeReview(entry.repo, entry.featureDir);
|
|
468
|
+
if (data.running) return json(res, 409, { ok: false, message: "流水线正在运行,等它结束后再提交评审。" });
|
|
469
|
+
const sub: Submission = {
|
|
470
|
+
verdict,
|
|
471
|
+
reviewer: typeof body?.reviewer === "string" ? body.reviewer : "",
|
|
472
|
+
reviewers: Array.isArray(body?.reviewers) ? body.reviewers.filter((r): r is string => typeof r === "string") : [],
|
|
473
|
+
comments: (Array.isArray(body?.comments) ? body.comments : []).filter((c) => c && typeof c.text === "string").map((c) => ({ file: typeof c.file === "string" ? c.file : null, line: typeof c.line === "number" ? c.line : null, text: c.text })),
|
|
474
|
+
fix: (Array.isArray(body?.fix) ? body.fix : []).filter((f) => f && typeof f.id === "string").map((f) => ({ id: f.id, note: typeof f.note === "string" ? f.note : undefined })),
|
|
475
|
+
note: typeof body?.note === "string" ? body.note : "",
|
|
476
|
+
};
|
|
477
|
+
if (verdict === "changes" && sub.comments.length === 0 && sub.fix.length === 0 && !sub.note.trim()) {
|
|
478
|
+
return json(res, 400, { ok: false, message: "退回修改需要至少一条意见:在代码行上写意见,或把 AI 的意见标为\"要改\"。" });
|
|
479
|
+
}
|
|
480
|
+
const { review, committed } = recordHumanReview(entry.repo, entry.featureDir, sub);
|
|
481
|
+
if (verdict === "approve") {
|
|
482
|
+
return json(res, 200, { ok: true, verdict, committed, message: `已记录:评审通过${committed ? ",已写入 code-review.md 并提交" : ""}。${review.reviewers.length ? `审核人:${review.reviewers.join("、")}。` : ""}合并请求接入后会用这些审核人。` });
|
|
483
|
+
}
|
|
484
|
+
const started = codeFixRun(entry.repo, entry.featureDir);
|
|
485
|
+
return json(res, 200, {
|
|
486
|
+
ok: true,
|
|
487
|
+
verdict,
|
|
488
|
+
committed,
|
|
489
|
+
message: started.ok
|
|
490
|
+
? `已记录 ${review.items.length} 条修改意见,流水线开始按意见修改(需求说明不变,从方案设计开始只处理这些意见),进展会发回需求的对话。`
|
|
491
|
+
: `已记录 ${review.items.length} 条修改意见,但没能开始修改:${started.message.split("\n")[0]}。可以在需求的对话里说"按代码评审意见修改",或执行 req review-fix ${path.basename(entry.featureDir)}。`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return send(res, 404, "没有这个页面");
|
|
495
|
+
}
|
|
496
|
+
|
|
431
497
|
// 1.14: the acceptance checklist, /r/<token>/f/<key>/check/…
|
|
432
498
|
async function handleChecklist(req: http.IncomingMessage, res: http.ServerResponse, rest: string, entry: { repo: string; featureDir: string; key: string; project: string }, featureAbs: string, full: boolean) {
|
|
433
499
|
const checkDir = path.join(featureAbs, ".pipeline", "check");
|
|
@@ -641,7 +707,23 @@ async function main(argv: string[]): Promise<number> {
|
|
|
641
707
|
}
|
|
642
708
|
const open = !argv.includes("--no-open");
|
|
643
709
|
const readOnly = argv.includes("--read-only");
|
|
644
|
-
|
|
710
|
+
// 1.14: --page code opens the code review page of the same server.
|
|
711
|
+
const pageAt = argv.indexOf("--page");
|
|
712
|
+
const page = pageAt >= 0 ? argv[pageAt + 1] : null;
|
|
713
|
+
const feature = argv.find((a, i) => !a.startsWith("--") && !(pageAt >= 0 && i === pageAt + 1));
|
|
714
|
+
if (page === "code" && feature && !argv.includes("--help")) {
|
|
715
|
+
const { locate } = await import("./api.js");
|
|
716
|
+
const { repo, featureDir } = locate(process.cwd(), feature);
|
|
717
|
+
const key = keyOf(repo, featureDir);
|
|
718
|
+
if (!key) {
|
|
719
|
+
console.error(`${featureDir} 不在本机的需求列表中,先运行一次 req index --rebuild`);
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
const url = `${reviewUrl(await ensureReviewServer(), key, readOnly)}code/`;
|
|
723
|
+
console.log(`代码审查页:${url}`);
|
|
724
|
+
if (open) openBrowser(url);
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
645
727
|
if (!feature || argv.includes("--help") || argv.includes("-h")) {
|
|
646
728
|
console.log("用法:req review <需求> [--no-open] [--read-only]\n\n在浏览器中打开这个需求的原型审查页:点选要改的地方、直接改文字,然后提交修改意见或确认方案。");
|
|
647
729
|
return feature ? 0 : 2;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.14.0-beta.
|
|
3
|
+
"version": "1.14.0-beta.4",
|
|
4
4
|
"description": "req-kit: a requirement-development workflow for Paseo. /req aligns a requirement in chat; the engine runs design, implementation, review and verification as Paseo agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -74,11 +74,11 @@ export function useConfigView(repo: string | null) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** The prototype review page's address (1.13); it stays valid while its server runs. */
|
|
77
|
-
export function useReviewUrl(repo: string, featureDir: string, enabled: boolean, page: "prototype" | "check" = "prototype") {
|
|
77
|
+
export function useReviewUrl(repo: string, featureDir: string, enabled: boolean, page: "prototype" | "check" | "code" = "prototype") {
|
|
78
78
|
const reviewUrl = useRpc(reviewUrlRpc);
|
|
79
79
|
return useQuery({
|
|
80
80
|
queryKey: ["req", "review-url", repo, featureDir, page],
|
|
81
|
-
queryFn: () => reviewUrl({ repo, featureDir, ...(page
|
|
81
|
+
queryFn: () => reviewUrl({ repo, featureDir, ...(page !== "prototype" ? { page } : {}) }),
|
|
82
82
|
enabled: enabled && Boolean(repo && featureDir),
|
|
83
83
|
retry: false,
|
|
84
84
|
});
|
|
@@ -9,7 +9,7 @@ import type { Action, RequirementDetail, Step } from "../shared/contracts";
|
|
|
9
9
|
import { useAction, useDocument, usePreferences, useRequirement, useRequirements } from "./data";
|
|
10
10
|
import { ACTION_TEXT, actionsFor, ago, ATTENTION_LABELS, facts, FILE_LABELS, minutes, tokens } from "./format";
|
|
11
11
|
import { Markdown, tsvToMarkdown } from "./markdown";
|
|
12
|
-
import { ChecklistButton, ReviewButton } from "./review";
|
|
12
|
+
import { ChecklistButton, CodeReviewButton, ReviewButton } from "./review";
|
|
13
13
|
import { Badge, Button, Chip, Status, useStyles } from "./ui";
|
|
14
14
|
|
|
15
15
|
type Theme = PluginHostProps["theme"];
|
|
@@ -170,6 +170,10 @@ export function RequirementView({
|
|
|
170
170
|
const actions = readOnly ? [] : actionsFor(d);
|
|
171
171
|
// 1.14: finished (it has a report) and not running: the requester can try it and accept it.
|
|
172
172
|
const canAccept = d.files.includes("report.md") && !d.running;
|
|
173
|
+
// 1.14: code written and reviewed by the AI: a person can review it too.
|
|
174
|
+
const canReviewCode = (d.files.includes("code-review.md") || d.files.includes("implementation.md")) && !d.running;
|
|
175
|
+
const hr = d.humanReview ?? null;
|
|
176
|
+
const reviewLabel = !hr ? "代码审查" : hr.status === "approved" ? (hr.stale ? "代码审查(有新改动,需重新评审)" : "代码审查(已通过)") : "代码审查(已退回修改)";
|
|
173
177
|
return (
|
|
174
178
|
<View style={{ gap: 16 }}>
|
|
175
179
|
<View style={{ gap: 4 }}>
|
|
@@ -196,8 +200,9 @@ export function RequirementView({
|
|
|
196
200
|
{d.attention === "business" ? <Text style={s.muted}>需求方的决定请在发起需求的对话中回答,回答后再继续。</Text> : null}
|
|
197
201
|
</View>
|
|
198
202
|
|
|
199
|
-
{actions.length > 0 || d.hasPrototype || canAccept ? (
|
|
203
|
+
{actions.length > 0 || d.hasPrototype || canAccept || canReviewCode ? (
|
|
200
204
|
<View style={s.row}>
|
|
205
|
+
{canReviewCode ? <CodeReviewButton theme={theme} repo={d.sourceRepo ?? d.repo} featureDir={d.featureDir} title={d.title} label={reviewLabel} primary={!!hr && hr.stale} /> : null}
|
|
201
206
|
{canAccept ? <ChecklistButton theme={theme} repo={d.sourceRepo ?? d.repo} featureDir={d.featureDir} title={d.title} primary={d.stage.startsWith("待验收") || d.stage === "验收未通过"} /> : null}
|
|
202
207
|
{d.hasPrototype ? <ReviewButton theme={theme} repo={d.sourceRepo ?? d.repo} featureDir={d.featureDir} title={d.title} waiting={d.attention === "approval"} /> : null}
|
|
203
208
|
{actions.map((a) => (
|
|
@@ -15,3 +15,8 @@ export function openReview(input: { repo: string; featureDir: string; title: str
|
|
|
15
15
|
export function openChecklist(input: { repo: string; featureDir: string; title: string }): void {
|
|
16
16
|
opener?.({ screenId: "review", params: { repo: input.repo, featureDir: input.featureDir, title: input.title, page: "check" } });
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/** 1.14: the code review page of a requirement (the same screen, another page). */
|
|
20
|
+
export function openCodeReview(input: { repo: string; featureDir: string; title: string }): void {
|
|
21
|
+
opener?.({ screenId: "review", params: { repo: input.repo, featureDir: input.featureDir, title: input.title, page: "code" } });
|
|
22
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { PluginHostProps, PluginScreenProps } from "@getpaseo/plugin/client
|
|
|
4
4
|
import { ExternalLink } from "@getpaseo/plugin/client/ui";
|
|
5
5
|
import { Text, View } from "react-native";
|
|
6
6
|
import { useReviewUrl } from "./data";
|
|
7
|
-
import { openChecklist, openReview } from "./nav";
|
|
7
|
+
import { openChecklist, openCodeReview, openReview } from "./nav";
|
|
8
8
|
import { Button, Status, useStyles } from "./ui";
|
|
9
9
|
import { canEmbedPages, embeddedPage, reviewUnreachable } from "./web";
|
|
10
10
|
|
|
@@ -15,6 +15,11 @@ export function ChecklistButton({ theme, repo, featureDir, title, primary }: { t
|
|
|
15
15
|
return <Button theme={theme} primary={primary} label="验收清单(可一键试用)" onPress={() => openChecklist({ repo, featureDir, title })} />;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** 1.14: the code review page: AI findings next to the code, the person's comments, pass or send back. */
|
|
19
|
+
export function CodeReviewButton({ theme, repo, featureDir, title, primary, label }: { theme: Theme; repo: string; featureDir: string; title: string; primary: boolean; label: string }) {
|
|
20
|
+
return <Button theme={theme} primary={primary} label={label} onPress={() => openCodeReview({ repo, featureDir, title })} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
/** The button on a requirement with a prototype: primary while the design waits for confirmation. */
|
|
19
24
|
export function ReviewButton({ theme, repo, featureDir, title, waiting }: { theme: Theme; repo: string; featureDir: string; title: string; waiting: boolean }) {
|
|
20
25
|
return <Button theme={theme} primary={waiting} label={waiting ? "审查原型并确认方案" : "审查原型"} onPress={() => openReview({ repo, featureDir, title })} />;
|
|
@@ -24,7 +29,7 @@ export function ReviewScreen({ theme, layout, params }: PluginScreenProps) {
|
|
|
24
29
|
const s = useStyles(theme, layout.compact);
|
|
25
30
|
const repo = params.repo ?? "";
|
|
26
31
|
const featureDir = params.featureDir ?? "";
|
|
27
|
-
const page = params.page === "check" ? "check" : "prototype";
|
|
32
|
+
const page = params.page === "check" ? "check" : params.page === "code" ? "code" : "prototype";
|
|
28
33
|
const review = useReviewUrl(repo, featureDir, true, page);
|
|
29
34
|
if (!review.data) {
|
|
30
35
|
return (
|
|
@@ -44,12 +49,17 @@ export function ReviewScreen({ theme, layout, params }: PluginScreenProps) {
|
|
|
44
49
|
</View>
|
|
45
50
|
);
|
|
46
51
|
}
|
|
47
|
-
const
|
|
52
|
+
const name = page === "check" ? "验收清单" : page === "code" ? "代码审查" : "原型审查";
|
|
53
|
+
const frame = canEmbedPages ? embeddedPage(url, `${name}:${params.title ?? featureDir}`) : null;
|
|
48
54
|
return (
|
|
49
55
|
<View style={[s.screen, { gap: 6 }]}>
|
|
50
56
|
<View style={[s.row, { paddingHorizontal: 12, paddingTop: 8, justifyContent: "space-between" }]}>
|
|
51
57
|
<Text style={s.muted}>
|
|
52
|
-
{page === "
|
|
58
|
+
{page === "code"
|
|
59
|
+
? readOnly
|
|
60
|
+
? "插件设为只读:可以看改动和 AI 意见,不能提交评审。"
|
|
61
|
+
: "点代码行写意见,AI 的意见标要改或不用改;看完在底部退回修改或评审通过。"
|
|
62
|
+
: page === "check"
|
|
53
63
|
? readOnly
|
|
54
64
|
? "插件设为只读:可以看清单,不能提交。"
|
|
55
65
|
: "逐条核对:通过、不对(写下看到了什么)、先不验;需要时点右上角把它跑起来。"
|
|
@@ -61,8 +71,8 @@ export function ReviewScreen({ theme, layout, params }: PluginScreenProps) {
|
|
|
61
71
|
</View>
|
|
62
72
|
{frame ?? (
|
|
63
73
|
<View style={[s.card, { margin: 12, gap: 8 }]}>
|
|
64
|
-
<Text style={s.text}>{page === "
|
|
65
|
-
<ExternalLink href={url}>{page === "
|
|
74
|
+
<Text style={s.text}>{page === "prototype" ? "原型审查页" : name}在浏览器中打开效果最好(手机上屏幕较小,建议用电脑)。</Text>
|
|
75
|
+
<ExternalLink href={url}>{page === "prototype" ? "打开原型审查页" : `打开${name}`}</ExternalLink>
|
|
66
76
|
</View>
|
|
67
77
|
)}
|
|
68
78
|
</View>
|