diffninja 0.1.1 → 0.2.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.md +84 -228
- package/dist/review/connected-analysis.d.ts +10 -2
- package/dist/review/connected-analysis.js +5 -0
- package/dist/review/connected-html.js +293 -28
- package/dist/review/connected.d.ts +12 -0
- package/dist/review/connected.js +21 -1
- package/dist/review/markdown.d.ts +68 -0
- package/dist/review/markdown.js +339 -0
- package/dist/review/mcp.js +31 -7
- package/dist/review/report-pages.d.ts +21 -3
- package/dist/review/report-pages.js +47 -2
- package/dist/review/types.d.ts +19 -0
- package/package.json +2 -1
|
@@ -36,6 +36,16 @@ export function renderConnectedPage(csrf) {
|
|
|
36
36
|
`<p class="brand">${BRAND_MARK}<span>diffninja</span></p>`,
|
|
37
37
|
'<h1 id="page-title">Pull request review</h1>',
|
|
38
38
|
'<p id="page-meta" class="page-meta" hidden></p>',
|
|
39
|
+
'<section id="pr-goal" class="pr-goal" aria-labelledby="pr-goal-heading" hidden>',
|
|
40
|
+
'<h2 id="pr-goal-heading" class="pr-goal-heading">Goal</h2>',
|
|
41
|
+
'<p id="pr-goal-text" class="pr-goal-text"></p>',
|
|
42
|
+
'<p id="pr-goal-by" class="pr-goal-by" hidden></p>',
|
|
43
|
+
"</section>",
|
|
44
|
+
'<details id="pr-description" class="pr-description" hidden>',
|
|
45
|
+
'<summary>Original PR description</summary>',
|
|
46
|
+
'<p class="note">The author’s own words, as written, including any generated notes. Raw HTML is shown as text, images as links, and nothing here is loaded from another host. It is the author’s claim, not evidence that the changes achieve it.</p>',
|
|
47
|
+
'<div id="pr-description-body" class="pr-description-body"></div>',
|
|
48
|
+
"</details>",
|
|
39
49
|
'<p id="lede" class="lede">Load a GitHub pull request, read its diff in a recommended order, and post your own review through the <span class="mono">gh</span> CLI. Nothing is posted until you press Submit.</p>',
|
|
40
50
|
'<p id="message-note" class="note" role="status" aria-live="polite" hidden></p>',
|
|
41
51
|
"</header>",
|
|
@@ -840,6 +850,8 @@ function script(csrf) {
|
|
|
840
850
|
show(el.lede, !snap);
|
|
841
851
|
show(el.loadSection, !snap || showLoad);
|
|
842
852
|
show(el.pageMeta, Boolean(snap));
|
|
853
|
+
show(el.prDescription, Boolean(snap));
|
|
854
|
+
renderDescription();
|
|
843
855
|
el.pageMeta.textContent = '';
|
|
844
856
|
el.snapshotBody.textContent = '';
|
|
845
857
|
if (!snap) {
|
|
@@ -854,10 +866,6 @@ function script(csrf) {
|
|
|
854
866
|
el.pageMeta.appendChild(make('span', 'meta-item sha', shortSha(snap.headSha)));
|
|
855
867
|
var login = reviewerLogin();
|
|
856
868
|
if (login !== '') el.pageMeta.appendChild(make('span', 'meta-item', 'Reviewing as ' + login));
|
|
857
|
-
var description = make('details', '');
|
|
858
|
-
description.appendChild(make('summary', '', 'Description (the author\u2019s claims)'));
|
|
859
|
-
description.appendChild(make('pre', '', typeof snap.body === 'string' && snap.body ? snap.body : 'No description.'));
|
|
860
|
-
el.snapshotBody.appendChild(description);
|
|
861
869
|
var dl = make('dl', 'facts');
|
|
862
870
|
addLinkFact(dl, 'Pull request', snap.url, typeof snap.url === 'string' ? snap.url : 'unknown');
|
|
863
871
|
addFact(dl, 'GitHub state', typeof snap.state === 'string' ? snap.state : 'unknown');
|
|
@@ -905,6 +913,224 @@ function script(csrf) {
|
|
|
905
913
|
return wrap;
|
|
906
914
|
}
|
|
907
915
|
|
|
916
|
+
/* ---------------------------------------------------------- description -- */
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* The author's description, rendered from the node tree the server parsed out
|
|
920
|
+
* of their Markdown. This page may not set markup directly, so every node
|
|
921
|
+
* becomes a whitelisted element and every string a text node: raw HTML
|
|
922
|
+
* arrives as text, an image arrives as a link, and an anchor is created only
|
|
923
|
+
* for an http(s) URL. The body is never summarized, shortened or rewritten
|
|
924
|
+
* here — the snapshot's own body text stays the untouched source, and the
|
|
925
|
+
* goal above it is the agent's sentence about the goal, not a rewrite of
|
|
926
|
+
* this text.
|
|
927
|
+
*/
|
|
928
|
+
function inlineNodesInto(host, nodes, depth) {
|
|
929
|
+
if (!Array.isArray(nodes) || depth > 16) return;
|
|
930
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
931
|
+
var node = nodes[i];
|
|
932
|
+
if (!node || typeof node !== 'object') continue;
|
|
933
|
+
var type = typeof node.t === 'string' ? node.t : '';
|
|
934
|
+
if (type === 'text') {
|
|
935
|
+
if (typeof node.v === 'string') host.appendChild(document.createTextNode(node.v));
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (type === 'code') {
|
|
939
|
+
host.appendChild(make('code', 'pr-inline-code', typeof node.v === 'string' ? node.v : ''));
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (type === 'br') { host.appendChild(document.createElement('br')); continue; }
|
|
943
|
+
if (type === 'strong' || type === 'em' || type === 'del') {
|
|
944
|
+
var wrapper = document.createElement(type);
|
|
945
|
+
inlineNodesInto(wrapper, node.c, depth + 1);
|
|
946
|
+
host.appendChild(wrapper);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (type === 'link') {
|
|
950
|
+
var inner = document.createDocumentFragment();
|
|
951
|
+
inlineNodesInto(inner, node.c, depth + 1);
|
|
952
|
+
// The URL is checked here too: no anchor is built for anything but http(s).
|
|
953
|
+
if (typeof node.href === 'string' && /^https?:\\/\\//i.test(node.href)) {
|
|
954
|
+
var link = document.createElement('a');
|
|
955
|
+
link.href = node.href;
|
|
956
|
+
link.target = '_blank';
|
|
957
|
+
link.rel = 'noopener noreferrer';
|
|
958
|
+
link.appendChild(inner);
|
|
959
|
+
host.appendChild(link);
|
|
960
|
+
} else {
|
|
961
|
+
host.appendChild(inner);
|
|
962
|
+
}
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
// Anything this page does not know is shown as its own text.
|
|
966
|
+
if (typeof node.v === 'string') host.appendChild(document.createTextNode(node.v));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function blockNodesInto(host, nodes, depth) {
|
|
971
|
+
if (!Array.isArray(nodes) || depth > 16) return;
|
|
972
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
973
|
+
var node = nodes[i];
|
|
974
|
+
if (!node || typeof node !== 'object') continue;
|
|
975
|
+
var type = typeof node.t === 'string' ? node.t : '';
|
|
976
|
+
if (type === 'para') {
|
|
977
|
+
var para = document.createElement('p');
|
|
978
|
+
inlineNodesInto(para, node.c, depth + 1);
|
|
979
|
+
host.appendChild(para);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (type === 'heading') {
|
|
983
|
+
// Demoted: the page owns h1 and h2, so a description's headings stay below them.
|
|
984
|
+
var level = typeof node.d === 'number' && isFinite(node.d) ? Math.min(Math.max(Math.floor(node.d) + 2, 3), 6) : 3;
|
|
985
|
+
var heading = document.createElement('h' + String(level));
|
|
986
|
+
inlineNodesInto(heading, node.c, depth + 1);
|
|
987
|
+
host.appendChild(heading);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (type === 'quote') {
|
|
991
|
+
var quote = document.createElement('blockquote');
|
|
992
|
+
blockNodesInto(quote, node.c, depth + 1);
|
|
993
|
+
host.appendChild(quote);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (type === 'code') {
|
|
997
|
+
var lang = typeof node.lang === 'string' ? node.lang : '';
|
|
998
|
+
var pre = make('pre', 'pr-code', typeof node.v === 'string' ? node.v : '');
|
|
999
|
+
if (lang !== '') pre.setAttribute('data-lang', lang);
|
|
1000
|
+
host.appendChild(pre);
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (type === 'raw') {
|
|
1004
|
+
// Markup in the description is shown verbatim, as text.
|
|
1005
|
+
host.appendChild(make('pre', 'pr-raw', typeof node.v === 'string' ? node.v : ''));
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (type === 'rule') { host.appendChild(document.createElement('hr')); continue; }
|
|
1009
|
+
if (type === 'list') {
|
|
1010
|
+
var ordered = node.ordered === true;
|
|
1011
|
+
var list = document.createElement(ordered ? 'ol' : 'ul');
|
|
1012
|
+
if (ordered && typeof node.start === 'number' && isFinite(node.start) && node.start > 1) list.start = Math.floor(node.start);
|
|
1013
|
+
var items = Array.isArray(node.items) ? node.items : [];
|
|
1014
|
+
for (var j = 0; j < items.length; j += 1) {
|
|
1015
|
+
var item = items[j] && typeof items[j] === 'object' ? items[j] : {};
|
|
1016
|
+
var entry = document.createElement('li');
|
|
1017
|
+
if (item.task === true) {
|
|
1018
|
+
entry.className = 'pr-task';
|
|
1019
|
+
var box = document.createElement('input');
|
|
1020
|
+
box.type = 'checkbox';
|
|
1021
|
+
box.disabled = true;
|
|
1022
|
+
box.checked = item.checked === true;
|
|
1023
|
+
box.setAttribute('aria-label', item.checked === true ? 'Task done' : 'Task not done');
|
|
1024
|
+
entry.appendChild(box);
|
|
1025
|
+
}
|
|
1026
|
+
blockNodesInto(entry, item.c, depth + 1);
|
|
1027
|
+
list.appendChild(entry);
|
|
1028
|
+
if (!entry.hasChildNodes()) entry.appendChild(document.createTextNode('(empty)'));
|
|
1029
|
+
}
|
|
1030
|
+
host.appendChild(list);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (type === 'table') {
|
|
1034
|
+
var align = Array.isArray(node.align) ? node.align : [];
|
|
1035
|
+
var table = make('table', 'pr-table');
|
|
1036
|
+
var head = Array.isArray(node.head) ? node.head : [];
|
|
1037
|
+
if (head.length > 0) {
|
|
1038
|
+
var headRow = document.createElement('tr');
|
|
1039
|
+
for (var h = 0; h < head.length; h += 1) {
|
|
1040
|
+
var th = make('th', 'pr-align-' + (align[h] === 'center' || align[h] === 'right' ? align[h] : 'left'));
|
|
1041
|
+
inlineNodesInto(th, head[h], depth + 1);
|
|
1042
|
+
headRow.appendChild(th);
|
|
1043
|
+
}
|
|
1044
|
+
var thead = document.createElement('thead');
|
|
1045
|
+
thead.appendChild(headRow);
|
|
1046
|
+
table.appendChild(thead);
|
|
1047
|
+
}
|
|
1048
|
+
var rows = Array.isArray(node.rows) ? node.rows : [];
|
|
1049
|
+
var body = document.createElement('tbody');
|
|
1050
|
+
for (var r = 0; r < rows.length; r += 1) {
|
|
1051
|
+
var row = Array.isArray(rows[r]) ? rows[r] : [];
|
|
1052
|
+
var rowElement = document.createElement('tr');
|
|
1053
|
+
for (var c = 0; c < row.length; c += 1) {
|
|
1054
|
+
var td = make('td', 'pr-align-' + (align[c] === 'center' || align[c] === 'right' ? align[c] : 'left'));
|
|
1055
|
+
inlineNodesInto(td, row[c], depth + 1);
|
|
1056
|
+
rowElement.appendChild(td);
|
|
1057
|
+
}
|
|
1058
|
+
body.appendChild(rowElement);
|
|
1059
|
+
}
|
|
1060
|
+
table.appendChild(body);
|
|
1061
|
+
host.appendChild(table);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
// An unknown block that carries children or text is still readable as that.
|
|
1065
|
+
if (Array.isArray(node.c)) { blockNodesInto(host, node.c, depth + 1); continue; }
|
|
1066
|
+
if (typeof node.v === 'string' && node.v !== '') host.appendChild(make('p', null, node.v));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function renderDescription() {
|
|
1071
|
+
var snap = snapshot();
|
|
1072
|
+
var host = el.prDescriptionBody;
|
|
1073
|
+
host.textContent = '';
|
|
1074
|
+
var blocks = snap && Array.isArray(snap.bodyBlocks) ? snap.bodyBlocks : null;
|
|
1075
|
+
var plain = snap && typeof snap.body === 'string' ? snap.body.trim() : '';
|
|
1076
|
+
host.className = 'pr-description-body';
|
|
1077
|
+
if (blocks && blocks.length > 0) { blockNodesInto(host, blocks, 0); return; }
|
|
1078
|
+
// No parsed form in this response, or one this server would not hold in full:
|
|
1079
|
+
// the author's own bytes, unwrapped, rather than a cut-off rendering.
|
|
1080
|
+
if (plain !== '') {
|
|
1081
|
+
host.appendChild(make('div', 'pr-plain', plain));
|
|
1082
|
+
if (snap.bodyTooLarge === true) {
|
|
1083
|
+
host.appendChild(make('p', 'note', 'This description is longer than this page will format, so it is shown as the author wrote it, in full and unformatted.'));
|
|
1084
|
+
}
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
setText(host, 'The author has not provided a PR description. No expected outcome was stated.');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* The goal the reviewing agent recorded for this revision, at the top of the
|
|
1092
|
+
* page: the reading the review was done against, in the agent's own words. It
|
|
1093
|
+
* belongs to the revision on screen — an analysis of another revision shows
|
|
1094
|
+
* nothing rather than a stale goal — and when none was recorded the block
|
|
1095
|
+
* says so instead of describing the pull request itself.
|
|
1096
|
+
*/
|
|
1097
|
+
function renderGoal() {
|
|
1098
|
+
var snap = snapshot();
|
|
1099
|
+
// A revision that cannot be reviewed gets no goal block: there is nothing to aim the reading at.
|
|
1100
|
+
var reviewable = Boolean(snap) && !(typeof snap.unavailableReason === 'string' && snap.unavailableReason !== '');
|
|
1101
|
+
show(el.prGoal, reviewable);
|
|
1102
|
+
if (!reviewable) return;
|
|
1103
|
+
var current = currentAnalysis();
|
|
1104
|
+
var source = current && current.summary && typeof current.summary === 'object' ? current.summary : null;
|
|
1105
|
+
var text = source && typeof source.text === 'string' ? source.text.trim() : '';
|
|
1106
|
+
setText(el.prGoalText, '');
|
|
1107
|
+
setText(el.prGoalBy, '');
|
|
1108
|
+
show(el.prGoalBy, false);
|
|
1109
|
+
if (text !== '') {
|
|
1110
|
+
el.prGoal.className = 'pr-goal';
|
|
1111
|
+
el.prGoalText.className = 'pr-goal-text';
|
|
1112
|
+
setText(el.prGoalText, text);
|
|
1113
|
+
var by = typeof source.summarizedBy === 'string' ? source.summarizedBy.trim() : '';
|
|
1114
|
+
setText(el.prGoalBy, 'Recorded by ' + (by === '' ? 'your review agent' : by) + ' from the pull request title and description. It is the agent\u2019s reading of the goal, not a check that the changes reach it.');
|
|
1115
|
+
show(el.prGoalBy, true);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
el.prGoal.className = 'pr-goal is-empty';
|
|
1119
|
+
el.prGoalText.className = 'pr-goal-note';
|
|
1120
|
+
setText(el.prGoalText, goalAbsentReason(snap));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Why no goal is on screen: the analysis is still coming, is of another revision, or recorded none. */
|
|
1124
|
+
function goalAbsentReason(snap) {
|
|
1125
|
+
if (!analysis || analysisFor !== snap.id) return 'Waiting for the local analysis of this revision\u2026';
|
|
1126
|
+
if (analysis.available !== true) {
|
|
1127
|
+
var reason = typeof analysis.reason === 'string' && analysis.reason !== '' ? analysis.reason : 'no local analysis is attached to this session.';
|
|
1128
|
+
return 'No agent-stated goal for this revision: ' + reason;
|
|
1129
|
+
}
|
|
1130
|
+
if (analysis.snapshotId !== snap.id) return 'The pull request changed; waiting for the analysis of this revision before any goal is shown.';
|
|
1131
|
+
return 'No goal was recorded for this revision. The reviewing agent records one when it finishes its review; the original description below is the author\u2019s own words.';
|
|
1132
|
+
}
|
|
1133
|
+
|
|
908
1134
|
/* ------------------------------------------------------------ analysis -- */
|
|
909
1135
|
|
|
910
1136
|
var STATUS_ORDER = ['attention', 'uncertain', 'low', 'passed'];
|
|
@@ -915,17 +1141,12 @@ function script(csrf) {
|
|
|
915
1141
|
return analysis;
|
|
916
1142
|
}
|
|
917
1143
|
|
|
918
|
-
function
|
|
1144
|
+
function fileHasUncertainty(path) {
|
|
919
1145
|
var current = currentAnalysis();
|
|
920
|
-
if (!current || !Array.isArray(current.hunks)) return
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (!hunk || hunk.file !== path) continue;
|
|
925
|
-
var rank = STATUS_ORDER.indexOf(hunk.status);
|
|
926
|
-
if (rank >= 0 && rank < best) best = rank;
|
|
927
|
-
}
|
|
928
|
-
return best < STATUS_ORDER.length ? STATUS_ORDER[best] : '';
|
|
1146
|
+
if (!current || !Array.isArray(current.hunks)) return false;
|
|
1147
|
+
return current.hunks.some(function (hunk) {
|
|
1148
|
+
return hunk && hunk.file === path && hunk.status === 'uncertain';
|
|
1149
|
+
});
|
|
929
1150
|
}
|
|
930
1151
|
|
|
931
1152
|
function scheduleAnalysis(delay) {
|
|
@@ -956,7 +1177,9 @@ function script(csrf) {
|
|
|
956
1177
|
var pendingAnswers = analysis && analysis.available === true && analysis.questions && analysis.questions.answered < analysis.questions.total;
|
|
957
1178
|
var pendingOrder = analysis && analysis.available === true && !(analysis.order && analysis.order.source === 'agent');
|
|
958
1179
|
var pendingSuggestions = analysis && analysis.available === true && !analysis.suggestions;
|
|
959
|
-
|
|
1180
|
+
// The goal of the revision is recorded when the agent finishes, which can be after its order arrived.
|
|
1181
|
+
var pendingGoal = analysis && analysis.available === true && !analysis.summary;
|
|
1182
|
+
if (pendingAnswers || pendingOrder || pendingSuggestions || pendingGoal) scheduleAnalysis(10000);
|
|
960
1183
|
});
|
|
961
1184
|
}
|
|
962
1185
|
|
|
@@ -979,12 +1202,8 @@ function script(csrf) {
|
|
|
979
1202
|
return out;
|
|
980
1203
|
}
|
|
981
1204
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
/** A status as a small tinted label; the word is the text, the tint only repeats it. */
|
|
985
|
-
function statusTag(status) {
|
|
986
|
-
var known = STATUS_ORDER.indexOf(status) >= 0 ? status : 'uncertain';
|
|
987
|
-
return make('span', 'status-tag status-' + known, STATUS_LABEL[known]);
|
|
1205
|
+
function uncertaintyTag() {
|
|
1206
|
+
return make('span', 'status-tag status-uncertain', 'Uncertain');
|
|
988
1207
|
}
|
|
989
1208
|
|
|
990
1209
|
/** A path with its directory muted, so the file name reads first. */
|
|
@@ -1013,7 +1232,7 @@ function script(csrf) {
|
|
|
1013
1232
|
return go;
|
|
1014
1233
|
}
|
|
1015
1234
|
|
|
1016
|
-
/** One station of the rail: where the change is
|
|
1235
|
+
/** One station of the rail: where the change is; what to look at lives beside the code. */
|
|
1017
1236
|
function renderHunkEntry(hunk, rank) {
|
|
1018
1237
|
var entry = make('li', 'order-item');
|
|
1019
1238
|
entry.dataset.action = 'goto-stop';
|
|
@@ -1028,8 +1247,7 @@ function script(csrf) {
|
|
|
1028
1247
|
where.appendChild(make('span', 'path-line', ':' + String(hunk.line)));
|
|
1029
1248
|
main.appendChild(where);
|
|
1030
1249
|
var meta = make('div', 'order-meta');
|
|
1031
|
-
|
|
1032
|
-
if (hunk.status === 'attention' || hunk.status === 'uncertain') meta.appendChild(statusTag(hunk.status));
|
|
1250
|
+
if (hunk.status === 'uncertain') meta.appendChild(uncertaintyTag());
|
|
1033
1251
|
meta.appendChild(sizeNode(hunk.added, hunk.removed));
|
|
1034
1252
|
main.appendChild(meta);
|
|
1035
1253
|
var jump = make('a', 'sr-only', 'Go to change ' + rank + ', ' + hunk.file + ' line ' + hunk.line);
|
|
@@ -1117,6 +1335,8 @@ function script(csrf) {
|
|
|
1117
1335
|
|
|
1118
1336
|
function renderAnalysis() {
|
|
1119
1337
|
var snap = snapshot();
|
|
1338
|
+
// The goal rides on the analysis, so it re-renders wherever the analysis does — every poll included.
|
|
1339
|
+
renderGoal();
|
|
1120
1340
|
show(el.analysisSection, Boolean(snap) && !(typeof snap.unavailableReason === 'string' && snap.unavailableReason !== ''));
|
|
1121
1341
|
el.analysisBody.textContent = '';
|
|
1122
1342
|
el.analysisActions.textContent = '';
|
|
@@ -1867,7 +2087,7 @@ function script(csrf) {
|
|
|
1867
2087
|
where.appendChild(make('span', 'path-line', ':' + String(lead.line)));
|
|
1868
2088
|
head.appendChild(where);
|
|
1869
2089
|
head.appendChild(sizeOf([stop.segment]));
|
|
1870
|
-
if (lead.status === '
|
|
2090
|
+
if (lead.status === 'uncertain') head.appendChild(uncertaintyTag());
|
|
1871
2091
|
var tools = make('span', 'file-tools');
|
|
1872
2092
|
if (hasCallFlow(stop.path)) tools.appendChild(flowButton(stop.path, 'Call flow'));
|
|
1873
2093
|
head.appendChild(tools);
|
|
@@ -1891,8 +2111,7 @@ function script(csrf) {
|
|
|
1891
2111
|
var head = make('summary', 'file-head');
|
|
1892
2112
|
head.appendChild(pathNode(group.path, 'file-path'));
|
|
1893
2113
|
head.appendChild(sizeOf(segments));
|
|
1894
|
-
|
|
1895
|
-
if (worst === 'attention' || worst === 'uncertain') head.appendChild(statusTag(worst));
|
|
2114
|
+
if (fileHasUncertainty(group.path)) head.appendChild(uncertaintyTag());
|
|
1896
2115
|
var tools = make('span', 'file-tools');
|
|
1897
2116
|
if (hasCallFlow(group.path)) tools.appendChild(flowButton(group.path, 'Call flow'));
|
|
1898
2117
|
head.appendChild(tools);
|
|
@@ -2334,6 +2553,11 @@ function script(csrf) {
|
|
|
2334
2553
|
el.refreshButton = byId('refresh-button');
|
|
2335
2554
|
el.pageTitle = byId('page-title');
|
|
2336
2555
|
el.pageMeta = byId('page-meta');
|
|
2556
|
+
el.prGoal = byId('pr-goal');
|
|
2557
|
+
el.prGoalText = byId('pr-goal-text');
|
|
2558
|
+
el.prGoalBy = byId('pr-goal-by');
|
|
2559
|
+
el.prDescription = byId('pr-description');
|
|
2560
|
+
el.prDescriptionBody = byId('pr-description-body');
|
|
2337
2561
|
el.lede = byId('lede');
|
|
2338
2562
|
el.loadSection = byId('load-section');
|
|
2339
2563
|
el.detailsSection = byId('details-section');
|
|
@@ -2451,6 +2675,48 @@ ${BRAND_MARK_STYLES}
|
|
|
2451
2675
|
.brand .brand-mark { width: 20px; height: 20px; }
|
|
2452
2676
|
.page-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 13px; color: var(--ink-soft); }
|
|
2453
2677
|
.page-meta a { font-weight: 600; }
|
|
2678
|
+
/* The agent's stated goal, first thing under the masthead: one sentence, with its attribution. */
|
|
2679
|
+
.pr-goal { margin-top: 6px; padding: 14px 16px; border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius); background: var(--panel); }
|
|
2680
|
+
.pr-goal.is-empty { border-left-color: var(--line-strong); background: var(--sunken); }
|
|
2681
|
+
.pr-goal-heading { font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
|
|
2682
|
+
.pr-goal-text { margin: 6px 0 0; font-size: 15.5px; font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; max-width: 90ch; }
|
|
2683
|
+
.pr-goal-note { margin: 6px 0 0; font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; max-width: 90ch; }
|
|
2684
|
+
.pr-goal-by { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-faint); overflow-wrap: anywhere; max-width: 90ch; }
|
|
2685
|
+
.pr-description { margin-top: 6px; padding: 12px 16px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
|
|
2686
|
+
.pr-description > summary { cursor: pointer; font-size: 14px; font-weight: 600; }
|
|
2687
|
+
.pr-description > .note { margin-top: 10px; }
|
|
2688
|
+
.pr-description-body { margin-top: 10px; overflow-wrap: anywhere; font: 14px/1.6 var(--sans); max-width: 90ch; }
|
|
2689
|
+
/* No parsed form in the response, or one too large to hold: the author's own bytes, unwrapped. */
|
|
2690
|
+
.pr-description-body .pr-plain { white-space: pre-wrap; }
|
|
2691
|
+
/* The author's Markdown, laid out as the document it is; every size stays below the page's own headings. */
|
|
2692
|
+
.pr-description-body > *:first-child { margin-top: 0; }
|
|
2693
|
+
.pr-description-body > *:last-child { margin-bottom: 0; }
|
|
2694
|
+
.pr-description-body p, .pr-description-body blockquote, .pr-description-body table, .pr-description-body pre, .pr-description-body ul, .pr-description-body ol { margin: 0 0 10px; }
|
|
2695
|
+
.pr-description-body h3, .pr-description-body h4, .pr-description-body h5, .pr-description-body h6 { margin: 16px 0 6px; line-height: 1.3; }
|
|
2696
|
+
.pr-description-body h3 { font-size: 15px; }
|
|
2697
|
+
.pr-description-body h4 { font-size: 14px; }
|
|
2698
|
+
.pr-description-body h5, .pr-description-body h6 { font-size: 13px; color: var(--ink-soft); }
|
|
2699
|
+
.pr-description-body ul, .pr-description-body ol { padding-left: 22px; }
|
|
2700
|
+
.pr-description-body li { margin: 2px 0; }
|
|
2701
|
+
.pr-description-body li > ul, .pr-description-body li > ol { margin: 2px 0 0; }
|
|
2702
|
+
.pr-description-body li > p { margin: 0 0 6px; }
|
|
2703
|
+
.pr-description-body blockquote { padding: 2px 0 2px 12px; border-left: 3px solid var(--line-strong); color: var(--ink-soft); }
|
|
2704
|
+
.pr-description-body hr { margin: 14px 0; border: 0; border-top: 1px solid var(--line); }
|
|
2705
|
+
.pr-description-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
2706
|
+
.pr-description-body code { font: 12.5px var(--mono); background: var(--sunken); border: 1px solid var(--line-soft); border-radius: 4px; padding: 1px 4px; }
|
|
2707
|
+
.pr-description-body pre { padding: 9px 11px; background: var(--sunken); border: 1px solid var(--line); border-radius: 6px; overflow-x: auto; font: 12.5px/1.5 var(--mono); white-space: pre; }
|
|
2708
|
+
.pr-description-body pre code { padding: 0; border: 0; background: none; font: inherit; }
|
|
2709
|
+
/* Raw HTML in a description is shown, not run: dimmed, so it reads as text the author wrote. */
|
|
2710
|
+
.pr-description-body .pr-raw { color: var(--ink-soft); white-space: pre-wrap; }
|
|
2711
|
+
.pr-description-body .pr-task { list-style: none; }
|
|
2712
|
+
.pr-description-body .pr-task input { margin-right: 6px; vertical-align: baseline; }
|
|
2713
|
+
/* The marker sits beside the item's first line, not above the paragraph it labels. */
|
|
2714
|
+
.pr-description-body .pr-task > p:first-of-type { display: inline; margin: 0; }
|
|
2715
|
+
.pr-description-body table.pr-table { width: 100%; border-collapse: collapse; font-size: 13px; display: block; overflow-x: auto; }
|
|
2716
|
+
.pr-description-body .pr-table th, .pr-description-body .pr-table td { border: 1px solid var(--line); padding: 4px 8px; text-align: left; vertical-align: top; }
|
|
2717
|
+
.pr-description-body .pr-table th { background: var(--panel-head, var(--sunken)); font-weight: 650; }
|
|
2718
|
+
.pr-description-body .pr-align-center { text-align: center; }
|
|
2719
|
+
.pr-description-body .pr-align-right { text-align: right; }
|
|
2454
2720
|
.meta-item.sha { font: 12px var(--mono); padding: 2px 7px; border-radius: 6px; background: var(--neutral-soft); color: var(--ink); }
|
|
2455
2721
|
.state-badge {
|
|
2456
2722
|
display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px;
|
|
@@ -2529,7 +2795,6 @@ body.is-busy button { cursor: progress; }
|
|
|
2529
2795
|
font-size: 11.5px; font-weight: 600; white-space: nowrap;
|
|
2530
2796
|
color: var(--ink-soft); background: var(--neutral-soft);
|
|
2531
2797
|
}
|
|
2532
|
-
.status-tag.status-attention { color: var(--alarm); background: var(--alarm-bg); }
|
|
2533
2798
|
.status-tag.status-uncertain { color: var(--warn); background: var(--warn-soft); }
|
|
2534
2799
|
.tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
|
|
2535
2800
|
.tag {
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
2
|
import { ConnectedReview } from "./github.js";
|
|
3
|
+
import { type MarkdownBlock } from "./markdown.js";
|
|
3
4
|
import type { ConnectedAnalysisView } from "./connected-analysis.js";
|
|
4
5
|
export interface ConnectedSession {
|
|
5
6
|
server: Server;
|
|
6
7
|
url: string;
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* What `GET /api/state` adds to the snapshot: the author's description already
|
|
11
|
+
* parsed into the nodes the page renders. The snapshot's own `body` is left
|
|
12
|
+
* exactly as GitHub returned it; these nodes are how the page shows it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ConnectedDescription {
|
|
15
|
+
/** The description as nodes. Absent when the body is empty, or too large to hold in full. */
|
|
16
|
+
readonly bodyBlocks?: readonly MarkdownBlock[];
|
|
17
|
+
/** The body was larger than this parser will hold: the page shows it as plain text rather than cut off. */
|
|
18
|
+
readonly bodyTooLarge?: true;
|
|
19
|
+
}
|
|
8
20
|
/** Read-only extras a connected session can serve beside the review itself. */
|
|
9
21
|
export interface ConnectedOptions {
|
|
10
22
|
/**
|
package/dist/review/connected.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ConnectedReview } from "./github.js";
|
|
5
5
|
import { renderConnectedPage } from "./connected-html.js";
|
|
6
|
+
import { markdownBlocks } from "./markdown.js";
|
|
6
7
|
import { reportPolicy } from "./report-pages.js";
|
|
7
8
|
const MAX_BODY = 256 * 1024;
|
|
8
9
|
const loadSchema = z.object({ url: z.string() }).strict();
|
|
@@ -12,6 +13,25 @@ const reviewSchema = z.object({
|
|
|
12
13
|
}).strict();
|
|
13
14
|
const emptySchema = z.object({}).strict();
|
|
14
15
|
const tokenSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
16
|
+
function described(state) {
|
|
17
|
+
const snap = state.snapshot;
|
|
18
|
+
if (snap === undefined)
|
|
19
|
+
return state;
|
|
20
|
+
const body = snap.body;
|
|
21
|
+
if (body === undefined || body.trim() === "")
|
|
22
|
+
return state;
|
|
23
|
+
const parsed = markdownBlocks(body);
|
|
24
|
+
if (parsed.truncated)
|
|
25
|
+
return { ...state, snapshot: { ...snap, bodyTooLarge: true } };
|
|
26
|
+
return { ...state, snapshot: { ...snap, bodyBlocks: parsed.blocks } };
|
|
27
|
+
}
|
|
28
|
+
function withDescription(response) {
|
|
29
|
+
if ("status" in response)
|
|
30
|
+
return described(response);
|
|
31
|
+
if ("state" in response && response.state !== undefined)
|
|
32
|
+
return { ...response, state: described(response.state) };
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
15
35
|
const NO_ANALYSIS = {
|
|
16
36
|
available: false,
|
|
17
37
|
reason: "No local analysis is attached to this session.",
|
|
@@ -52,7 +72,7 @@ export async function serveConnected(review = new ConnectedReview(), options = {
|
|
|
52
72
|
res.setHeader("X-Frame-Options", "DENY");
|
|
53
73
|
res.setHeader("Referrer-Policy", "no-referrer");
|
|
54
74
|
res.setHeader("Content-Security-Policy", `default-src 'none'; script-src 'nonce-${csrf}'; style-src 'nonce-${csrf}'; connect-src 'self'; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`);
|
|
55
|
-
const json = (code, value) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(value)); };
|
|
75
|
+
const json = (code, value) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(withDescription(value))); };
|
|
56
76
|
if (!requestIsTrusted(req, origin)) {
|
|
57
77
|
json(403, { error: "Untrusted Host or Origin." });
|
|
58
78
|
return;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** End of the inline nodes a description may contain. */
|
|
2
|
+
export type MarkdownInline = {
|
|
3
|
+
readonly t: "text";
|
|
4
|
+
readonly v: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly t: "code";
|
|
7
|
+
readonly v: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly t: "br";
|
|
10
|
+
} | {
|
|
11
|
+
readonly t: "strong" | "em" | "del";
|
|
12
|
+
readonly c: readonly MarkdownInline[];
|
|
13
|
+
} | {
|
|
14
|
+
readonly t: "link";
|
|
15
|
+
readonly href: string;
|
|
16
|
+
readonly c: readonly MarkdownInline[];
|
|
17
|
+
};
|
|
18
|
+
export type MarkdownAlign = "left" | "center" | "right";
|
|
19
|
+
/** Inline content of one table cell. */
|
|
20
|
+
export type MarkdownCell = readonly MarkdownInline[];
|
|
21
|
+
export interface MarkdownItem {
|
|
22
|
+
readonly task: boolean;
|
|
23
|
+
readonly checked: boolean;
|
|
24
|
+
readonly c: readonly MarkdownBlock[];
|
|
25
|
+
}
|
|
26
|
+
/** End of the block nodes a description may contain. */
|
|
27
|
+
export type MarkdownBlock = {
|
|
28
|
+
readonly t: "para";
|
|
29
|
+
readonly c: readonly MarkdownInline[];
|
|
30
|
+
} | {
|
|
31
|
+
readonly t: "heading";
|
|
32
|
+
readonly d: number;
|
|
33
|
+
readonly c: readonly MarkdownInline[];
|
|
34
|
+
} | {
|
|
35
|
+
readonly t: "quote";
|
|
36
|
+
readonly c: readonly MarkdownBlock[];
|
|
37
|
+
} | {
|
|
38
|
+
readonly t: "code";
|
|
39
|
+
readonly lang: string;
|
|
40
|
+
readonly v: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly t: "raw";
|
|
43
|
+
readonly v: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly t: "rule";
|
|
46
|
+
} | {
|
|
47
|
+
readonly t: "list";
|
|
48
|
+
readonly ordered: boolean;
|
|
49
|
+
readonly start: number;
|
|
50
|
+
readonly items: readonly MarkdownItem[];
|
|
51
|
+
} | {
|
|
52
|
+
readonly t: "table";
|
|
53
|
+
readonly align: readonly MarkdownAlign[];
|
|
54
|
+
readonly head: readonly MarkdownCell[];
|
|
55
|
+
readonly rows: readonly (readonly MarkdownCell[])[];
|
|
56
|
+
};
|
|
57
|
+
/** A parsed description, and whether the walk stopped before the body ended. */
|
|
58
|
+
export interface ParsedDescription {
|
|
59
|
+
readonly blocks: readonly MarkdownBlock[];
|
|
60
|
+
/** True when the description was larger than this parser will hold: `blocks` is only its beginning. */
|
|
61
|
+
readonly truncated: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The description as renderable nodes, memoized on the last source: the page
|
|
65
|
+
* polls the same state every few seconds, and a description never changes
|
|
66
|
+
* within one snapshot.
|
|
67
|
+
*/
|
|
68
|
+
export declare function markdownBlocks(source: string): ParsedDescription;
|