diffninja 0.1.1 → 0.3.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 +92 -228
- package/dist/review/call-flow-html.d.ts +1 -1
- package/dist/review/call-flow-html.js +115 -21
- package/dist/review/connected-analysis.d.ts +29 -2
- package/dist/review/connected-analysis.js +28 -0
- package/dist/review/connected-html.js +372 -32
- package/dist/review/connected.d.ts +17 -3
- package/dist/review/connected.js +23 -2
- package/dist/review/explanation.d.ts +143 -0
- package/dist/review/explanation.js +310 -0
- package/dist/review/html.d.ts +7 -0
- package/dist/review/html.js +98 -4
- package/dist/review/markdown.d.ts +68 -0
- package/dist/review/markdown.js +339 -0
- package/dist/review/mcp.js +102 -13
- package/dist/review/process-html.d.ts +93 -0
- package/dist/review/process-html.js +525 -0
- package/dist/review/report-pages.d.ts +37 -3
- package/dist/review/report-pages.js +69 -3
- package/dist/review/service.js +3 -1
- package/dist/review/types.d.ts +34 -0
- package/package.json +2 -1
|
@@ -36,6 +36,26 @@ 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
|
+
'<section id="pr-how" class="pr-how" aria-labelledby="pr-how-heading" hidden>',
|
|
45
|
+
'<details class="pr-how-fold" open>',
|
|
46
|
+
'<summary class="pr-how-head"><h2 id="pr-how-heading" class="pr-goal-heading">How it works</h2></summary>',
|
|
47
|
+
'<div class="pr-how-tools">',
|
|
48
|
+
'<p id="pr-how-by" class="pr-goal-by"></p>',
|
|
49
|
+
'<button type="button" id="pr-how-open" class="btn btn-quiet btn-sm" data-action="open-how" aria-label="Open how it works beside the diff">Open beside the diff</button>',
|
|
50
|
+
"</div>",
|
|
51
|
+
'<iframe id="pr-how-frame" class="pr-how-frame" title="How it works: the business processes and rules this change touches"></iframe>',
|
|
52
|
+
"</details>",
|
|
53
|
+
"</section>",
|
|
54
|
+
'<details id="pr-description" class="pr-description" hidden>',
|
|
55
|
+
'<summary>Original PR description</summary>',
|
|
56
|
+
'<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>',
|
|
57
|
+
'<div id="pr-description-body" class="pr-description-body"></div>',
|
|
58
|
+
"</details>",
|
|
39
59
|
'<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
60
|
'<p id="message-note" class="note" role="status" aria-live="polite" hidden></p>',
|
|
41
61
|
"</header>",
|
|
@@ -840,6 +860,8 @@ function script(csrf) {
|
|
|
840
860
|
show(el.lede, !snap);
|
|
841
861
|
show(el.loadSection, !snap || showLoad);
|
|
842
862
|
show(el.pageMeta, Boolean(snap));
|
|
863
|
+
show(el.prDescription, Boolean(snap));
|
|
864
|
+
renderDescription();
|
|
843
865
|
el.pageMeta.textContent = '';
|
|
844
866
|
el.snapshotBody.textContent = '';
|
|
845
867
|
if (!snap) {
|
|
@@ -854,10 +876,6 @@ function script(csrf) {
|
|
|
854
876
|
el.pageMeta.appendChild(make('span', 'meta-item sha', shortSha(snap.headSha)));
|
|
855
877
|
var login = reviewerLogin();
|
|
856
878
|
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
879
|
var dl = make('dl', 'facts');
|
|
862
880
|
addLinkFact(dl, 'Pull request', snap.url, typeof snap.url === 'string' ? snap.url : 'unknown');
|
|
863
881
|
addFact(dl, 'GitHub state', typeof snap.state === 'string' ? snap.state : 'unknown');
|
|
@@ -905,9 +923,248 @@ function script(csrf) {
|
|
|
905
923
|
return wrap;
|
|
906
924
|
}
|
|
907
925
|
|
|
926
|
+
/* ---------------------------------------------------------- description -- */
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* The author's description, rendered from the node tree the server parsed out
|
|
930
|
+
* of their Markdown. This page may not set markup directly, so every node
|
|
931
|
+
* becomes a whitelisted element and every string a text node: raw HTML
|
|
932
|
+
* arrives as text, an image arrives as a link, and an anchor is created only
|
|
933
|
+
* for an http(s) URL. The body is never summarized, shortened or rewritten
|
|
934
|
+
* here — the snapshot's own body text stays the untouched source, and the
|
|
935
|
+
* goal above it is the agent's sentence about the goal, not a rewrite of
|
|
936
|
+
* this text.
|
|
937
|
+
*/
|
|
938
|
+
function inlineNodesInto(host, nodes, depth) {
|
|
939
|
+
if (!Array.isArray(nodes) || depth > 16) return;
|
|
940
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
941
|
+
var node = nodes[i];
|
|
942
|
+
if (!node || typeof node !== 'object') continue;
|
|
943
|
+
var type = typeof node.t === 'string' ? node.t : '';
|
|
944
|
+
if (type === 'text') {
|
|
945
|
+
if (typeof node.v === 'string') host.appendChild(document.createTextNode(node.v));
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (type === 'code') {
|
|
949
|
+
host.appendChild(make('code', 'pr-inline-code', typeof node.v === 'string' ? node.v : ''));
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (type === 'br') { host.appendChild(document.createElement('br')); continue; }
|
|
953
|
+
if (type === 'strong' || type === 'em' || type === 'del') {
|
|
954
|
+
var wrapper = document.createElement(type);
|
|
955
|
+
inlineNodesInto(wrapper, node.c, depth + 1);
|
|
956
|
+
host.appendChild(wrapper);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (type === 'link') {
|
|
960
|
+
var inner = document.createDocumentFragment();
|
|
961
|
+
inlineNodesInto(inner, node.c, depth + 1);
|
|
962
|
+
// The URL is checked here too: no anchor is built for anything but http(s).
|
|
963
|
+
if (typeof node.href === 'string' && /^https?:\\/\\//i.test(node.href)) {
|
|
964
|
+
var link = document.createElement('a');
|
|
965
|
+
link.href = node.href;
|
|
966
|
+
link.target = '_blank';
|
|
967
|
+
link.rel = 'noopener noreferrer';
|
|
968
|
+
link.appendChild(inner);
|
|
969
|
+
host.appendChild(link);
|
|
970
|
+
} else {
|
|
971
|
+
host.appendChild(inner);
|
|
972
|
+
}
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
// Anything this page does not know is shown as its own text.
|
|
976
|
+
if (typeof node.v === 'string') host.appendChild(document.createTextNode(node.v));
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function blockNodesInto(host, nodes, depth) {
|
|
981
|
+
if (!Array.isArray(nodes) || depth > 16) return;
|
|
982
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
983
|
+
var node = nodes[i];
|
|
984
|
+
if (!node || typeof node !== 'object') continue;
|
|
985
|
+
var type = typeof node.t === 'string' ? node.t : '';
|
|
986
|
+
if (type === 'para') {
|
|
987
|
+
var para = document.createElement('p');
|
|
988
|
+
inlineNodesInto(para, node.c, depth + 1);
|
|
989
|
+
host.appendChild(para);
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (type === 'heading') {
|
|
993
|
+
// Demoted: the page owns h1 and h2, so a description's headings stay below them.
|
|
994
|
+
var level = typeof node.d === 'number' && isFinite(node.d) ? Math.min(Math.max(Math.floor(node.d) + 2, 3), 6) : 3;
|
|
995
|
+
var heading = document.createElement('h' + String(level));
|
|
996
|
+
inlineNodesInto(heading, node.c, depth + 1);
|
|
997
|
+
host.appendChild(heading);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (type === 'quote') {
|
|
1001
|
+
var quote = document.createElement('blockquote');
|
|
1002
|
+
blockNodesInto(quote, node.c, depth + 1);
|
|
1003
|
+
host.appendChild(quote);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (type === 'code') {
|
|
1007
|
+
var lang = typeof node.lang === 'string' ? node.lang : '';
|
|
1008
|
+
var pre = make('pre', 'pr-code', typeof node.v === 'string' ? node.v : '');
|
|
1009
|
+
if (lang !== '') pre.setAttribute('data-lang', lang);
|
|
1010
|
+
host.appendChild(pre);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (type === 'raw') {
|
|
1014
|
+
// Markup in the description is shown verbatim, as text.
|
|
1015
|
+
host.appendChild(make('pre', 'pr-raw', typeof node.v === 'string' ? node.v : ''));
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (type === 'rule') { host.appendChild(document.createElement('hr')); continue; }
|
|
1019
|
+
if (type === 'list') {
|
|
1020
|
+
var ordered = node.ordered === true;
|
|
1021
|
+
var list = document.createElement(ordered ? 'ol' : 'ul');
|
|
1022
|
+
if (ordered && typeof node.start === 'number' && isFinite(node.start) && node.start > 1) list.start = Math.floor(node.start);
|
|
1023
|
+
var items = Array.isArray(node.items) ? node.items : [];
|
|
1024
|
+
for (var j = 0; j < items.length; j += 1) {
|
|
1025
|
+
var item = items[j] && typeof items[j] === 'object' ? items[j] : {};
|
|
1026
|
+
var entry = document.createElement('li');
|
|
1027
|
+
if (item.task === true) {
|
|
1028
|
+
entry.className = 'pr-task';
|
|
1029
|
+
var box = document.createElement('input');
|
|
1030
|
+
box.type = 'checkbox';
|
|
1031
|
+
box.disabled = true;
|
|
1032
|
+
box.checked = item.checked === true;
|
|
1033
|
+
box.setAttribute('aria-label', item.checked === true ? 'Task done' : 'Task not done');
|
|
1034
|
+
entry.appendChild(box);
|
|
1035
|
+
}
|
|
1036
|
+
blockNodesInto(entry, item.c, depth + 1);
|
|
1037
|
+
list.appendChild(entry);
|
|
1038
|
+
if (!entry.hasChildNodes()) entry.appendChild(document.createTextNode('(empty)'));
|
|
1039
|
+
}
|
|
1040
|
+
host.appendChild(list);
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (type === 'table') {
|
|
1044
|
+
var align = Array.isArray(node.align) ? node.align : [];
|
|
1045
|
+
var table = make('table', 'pr-table');
|
|
1046
|
+
var head = Array.isArray(node.head) ? node.head : [];
|
|
1047
|
+
if (head.length > 0) {
|
|
1048
|
+
var headRow = document.createElement('tr');
|
|
1049
|
+
for (var h = 0; h < head.length; h += 1) {
|
|
1050
|
+
var th = make('th', 'pr-align-' + (align[h] === 'center' || align[h] === 'right' ? align[h] : 'left'));
|
|
1051
|
+
inlineNodesInto(th, head[h], depth + 1);
|
|
1052
|
+
headRow.appendChild(th);
|
|
1053
|
+
}
|
|
1054
|
+
var thead = document.createElement('thead');
|
|
1055
|
+
thead.appendChild(headRow);
|
|
1056
|
+
table.appendChild(thead);
|
|
1057
|
+
}
|
|
1058
|
+
var rows = Array.isArray(node.rows) ? node.rows : [];
|
|
1059
|
+
var body = document.createElement('tbody');
|
|
1060
|
+
for (var r = 0; r < rows.length; r += 1) {
|
|
1061
|
+
var row = Array.isArray(rows[r]) ? rows[r] : [];
|
|
1062
|
+
var rowElement = document.createElement('tr');
|
|
1063
|
+
for (var c = 0; c < row.length; c += 1) {
|
|
1064
|
+
var td = make('td', 'pr-align-' + (align[c] === 'center' || align[c] === 'right' ? align[c] : 'left'));
|
|
1065
|
+
inlineNodesInto(td, row[c], depth + 1);
|
|
1066
|
+
rowElement.appendChild(td);
|
|
1067
|
+
}
|
|
1068
|
+
body.appendChild(rowElement);
|
|
1069
|
+
}
|
|
1070
|
+
table.appendChild(body);
|
|
1071
|
+
host.appendChild(table);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
// An unknown block that carries children or text is still readable as that.
|
|
1075
|
+
if (Array.isArray(node.c)) { blockNodesInto(host, node.c, depth + 1); continue; }
|
|
1076
|
+
if (typeof node.v === 'string' && node.v !== '') host.appendChild(make('p', null, node.v));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function renderDescription() {
|
|
1081
|
+
var snap = snapshot();
|
|
1082
|
+
var host = el.prDescriptionBody;
|
|
1083
|
+
host.textContent = '';
|
|
1084
|
+
var blocks = snap && Array.isArray(snap.bodyBlocks) ? snap.bodyBlocks : null;
|
|
1085
|
+
var plain = snap && typeof snap.body === 'string' ? snap.body.trim() : '';
|
|
1086
|
+
host.className = 'pr-description-body';
|
|
1087
|
+
if (blocks && blocks.length > 0) { blockNodesInto(host, blocks, 0); return; }
|
|
1088
|
+
// No parsed form in this response, or one this server would not hold in full:
|
|
1089
|
+
// the author's own bytes, unwrapped, rather than a cut-off rendering.
|
|
1090
|
+
if (plain !== '') {
|
|
1091
|
+
host.appendChild(make('div', 'pr-plain', plain));
|
|
1092
|
+
if (snap.bodyTooLarge === true) {
|
|
1093
|
+
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.'));
|
|
1094
|
+
}
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
setText(host, 'The author has not provided a PR description. No expected outcome was stated.');
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* The goal the reviewing agent recorded for this revision, at the top of the
|
|
1102
|
+
* page: the reading the review was done against, in the agent's own words. It
|
|
1103
|
+
* belongs to the revision on screen — an analysis of another revision shows
|
|
1104
|
+
* nothing rather than a stale goal — and when none was recorded the block
|
|
1105
|
+
* says so instead of describing the pull request itself.
|
|
1106
|
+
*/
|
|
1107
|
+
function renderGoal() {
|
|
1108
|
+
var snap = snapshot();
|
|
1109
|
+
// A revision that cannot be reviewed gets no goal block: there is nothing to aim the reading at.
|
|
1110
|
+
var reviewable = Boolean(snap) && !(typeof snap.unavailableReason === 'string' && snap.unavailableReason !== '');
|
|
1111
|
+
show(el.prGoal, reviewable);
|
|
1112
|
+
if (!reviewable) return;
|
|
1113
|
+
var current = currentAnalysis();
|
|
1114
|
+
var source = current && current.summary && typeof current.summary === 'object' ? current.summary : null;
|
|
1115
|
+
var text = source && typeof source.text === 'string' ? source.text.trim() : '';
|
|
1116
|
+
setText(el.prGoalText, '');
|
|
1117
|
+
setText(el.prGoalBy, '');
|
|
1118
|
+
show(el.prGoalBy, false);
|
|
1119
|
+
if (text !== '') {
|
|
1120
|
+
el.prGoal.className = 'pr-goal';
|
|
1121
|
+
el.prGoalText.className = 'pr-goal-text';
|
|
1122
|
+
setText(el.prGoalText, text);
|
|
1123
|
+
var by = typeof source.summarizedBy === 'string' ? source.summarizedBy.trim() : '';
|
|
1124
|
+
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.');
|
|
1125
|
+
show(el.prGoalBy, true);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
el.prGoal.className = 'pr-goal is-empty';
|
|
1129
|
+
el.prGoalText.className = 'pr-goal-note';
|
|
1130
|
+
setText(el.prGoalText, goalAbsentReason(snap));
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* The agent's business view under the goal: the processes and rules this
|
|
1135
|
+
* change touches, drawn by the server into a same-origin frame that reports
|
|
1136
|
+
* its own height. Shown only once the agent's explanation was accepted for the
|
|
1137
|
+
* revision on screen; the frame reloads only when that revision changes.
|
|
1138
|
+
*/
|
|
1139
|
+
function renderHow() {
|
|
1140
|
+
var snap = snapshot();
|
|
1141
|
+
var current = currentAnalysis();
|
|
1142
|
+
var explanation = current && current.explanation && typeof current.explanation === 'object' ? current.explanation : null;
|
|
1143
|
+
var ready = Boolean(snap) && explanation !== null && current.snapshotId === snap.id;
|
|
1144
|
+
show(el.prHow, ready);
|
|
1145
|
+
if (!ready) { if (el.prHowFrame.getAttribute('src')) el.prHowFrame.removeAttribute('src'); return; }
|
|
1146
|
+
var by = typeof explanation.explainedBy === 'string' && explanation.explainedBy.trim() !== '' ? explanation.explainedBy.trim() : 'your review agent';
|
|
1147
|
+
var processes = Array.isArray(explanation.processes) ? explanation.processes.length : 0;
|
|
1148
|
+
setText(el.prHowBy, 'Explained by ' + by + ' from the code: ' + processes + (processes === 1 ? ' process' : ' processes') + ' and ' + (explanation.rules || 0) + ((explanation.rules || 0) === 1 ? ' business rule' : ' business rules') + '. Its reading, not a verdict; the diff below is the ground truth.');
|
|
1149
|
+
var src = '/flow?snapshot=' + encodeURIComponent(current.snapshotId) + '&view=business';
|
|
1150
|
+
if (el.prHowFrame.getAttribute('src') !== src) el.prHowFrame.setAttribute('src', src);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** Why no goal is on screen: the analysis is still coming, is of another revision, or recorded none. */
|
|
1154
|
+
function goalAbsentReason(snap) {
|
|
1155
|
+
if (!analysis || analysisFor !== snap.id) return 'Waiting for the local analysis of this revision\u2026';
|
|
1156
|
+
if (analysis.available !== true) {
|
|
1157
|
+
var reason = typeof analysis.reason === 'string' && analysis.reason !== '' ? analysis.reason : 'no local analysis is attached to this session.';
|
|
1158
|
+
return 'No agent-stated goal for this revision: ' + reason;
|
|
1159
|
+
}
|
|
1160
|
+
if (analysis.snapshotId !== snap.id) return 'The pull request changed; waiting for the analysis of this revision before any goal is shown.';
|
|
1161
|
+
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.';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
908
1164
|
/* ------------------------------------------------------------ analysis -- */
|
|
909
1165
|
|
|
910
1166
|
var STATUS_ORDER = ['attention', 'uncertain', 'low', 'passed'];
|
|
1167
|
+
var CHANGE_WORD = { added: 'New', changed: 'Changed', removed: 'Removed' };
|
|
911
1168
|
|
|
912
1169
|
function currentAnalysis() {
|
|
913
1170
|
var snap = snapshot();
|
|
@@ -915,17 +1172,12 @@ function script(csrf) {
|
|
|
915
1172
|
return analysis;
|
|
916
1173
|
}
|
|
917
1174
|
|
|
918
|
-
function
|
|
1175
|
+
function fileHasUncertainty(path) {
|
|
919
1176
|
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] : '';
|
|
1177
|
+
if (!current || !Array.isArray(current.hunks)) return false;
|
|
1178
|
+
return current.hunks.some(function (hunk) {
|
|
1179
|
+
return hunk && hunk.file === path && hunk.status === 'uncertain';
|
|
1180
|
+
});
|
|
929
1181
|
}
|
|
930
1182
|
|
|
931
1183
|
function scheduleAnalysis(delay) {
|
|
@@ -956,7 +1208,9 @@ function script(csrf) {
|
|
|
956
1208
|
var pendingAnswers = analysis && analysis.available === true && analysis.questions && analysis.questions.answered < analysis.questions.total;
|
|
957
1209
|
var pendingOrder = analysis && analysis.available === true && !(analysis.order && analysis.order.source === 'agent');
|
|
958
1210
|
var pendingSuggestions = analysis && analysis.available === true && !analysis.suggestions;
|
|
959
|
-
|
|
1211
|
+
// The goal of the revision is recorded when the agent finishes, which can be after its order arrived.
|
|
1212
|
+
var pendingGoal = analysis && analysis.available === true && !analysis.summary;
|
|
1213
|
+
if (pendingAnswers || pendingOrder || pendingSuggestions || pendingGoal) scheduleAnalysis(10000);
|
|
960
1214
|
});
|
|
961
1215
|
}
|
|
962
1216
|
|
|
@@ -979,12 +1233,8 @@ function script(csrf) {
|
|
|
979
1233
|
return out;
|
|
980
1234
|
}
|
|
981
1235
|
|
|
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]);
|
|
1236
|
+
function uncertaintyTag() {
|
|
1237
|
+
return make('span', 'status-tag status-uncertain', 'Uncertain');
|
|
988
1238
|
}
|
|
989
1239
|
|
|
990
1240
|
/** A path with its directory muted, so the file name reads first. */
|
|
@@ -1013,7 +1263,7 @@ function script(csrf) {
|
|
|
1013
1263
|
return go;
|
|
1014
1264
|
}
|
|
1015
1265
|
|
|
1016
|
-
/** One station of the rail: where the change is
|
|
1266
|
+
/** One station of the rail: where the change is; what to look at lives beside the code. */
|
|
1017
1267
|
function renderHunkEntry(hunk, rank) {
|
|
1018
1268
|
var entry = make('li', 'order-item');
|
|
1019
1269
|
entry.dataset.action = 'goto-stop';
|
|
@@ -1028,8 +1278,7 @@ function script(csrf) {
|
|
|
1028
1278
|
where.appendChild(make('span', 'path-line', ':' + String(hunk.line)));
|
|
1029
1279
|
main.appendChild(where);
|
|
1030
1280
|
var meta = make('div', 'order-meta');
|
|
1031
|
-
|
|
1032
|
-
if (hunk.status === 'attention' || hunk.status === 'uncertain') meta.appendChild(statusTag(hunk.status));
|
|
1281
|
+
if (hunk.status === 'uncertain') meta.appendChild(uncertaintyTag());
|
|
1033
1282
|
meta.appendChild(sizeNode(hunk.added, hunk.removed));
|
|
1034
1283
|
main.appendChild(meta);
|
|
1035
1284
|
var jump = make('a', 'sr-only', 'Go to change ' + rank + ', ' + hunk.file + ' line ' + hunk.line);
|
|
@@ -1049,7 +1298,8 @@ function script(csrf) {
|
|
|
1049
1298
|
var verdicts = answeredVerdicts(hunk);
|
|
1050
1299
|
var facts = Array.isArray(hunk.facts) ? hunk.facts : [];
|
|
1051
1300
|
var note = typeof hunk.note === 'string' ? hunk.note : '';
|
|
1052
|
-
|
|
1301
|
+
var business = Array.isArray(hunk.business) ? hunk.business : [];
|
|
1302
|
+
if (rank === 0 && verdicts.length === 0 && facts.length === 0 && note === '' && business.length === 0) return null;
|
|
1053
1303
|
var why = make('div', 'stop-why' + (rank > 0 ? ' has-rank' : ''));
|
|
1054
1304
|
if (rank > 0) {
|
|
1055
1305
|
var badge = make('span', 'why-rank', String(rank));
|
|
@@ -1057,6 +1307,19 @@ function script(csrf) {
|
|
|
1057
1307
|
why.appendChild(badge);
|
|
1058
1308
|
}
|
|
1059
1309
|
var main = make('div', 'why-main');
|
|
1310
|
+
if (business.length > 0) {
|
|
1311
|
+
var steps = make('div', 'tag-row business-row');
|
|
1312
|
+
steps.appendChild(make('span', 'tag-lead', 'In the business'));
|
|
1313
|
+
for (var b = 0; b < business.length; b += 1) {
|
|
1314
|
+
var link = business[b];
|
|
1315
|
+
var change = typeof link.change === 'string' ? link.change : 'unchanged';
|
|
1316
|
+
var step = make('span', 'business-step change-' + change);
|
|
1317
|
+
if (change !== 'unchanged') step.appendChild(make('span', 'business-change', CHANGE_WORD[change] || change));
|
|
1318
|
+
step.appendChild(make('span', 'business-text', (typeof link.process === 'string' ? link.process + ': ' : 'Rule: ') + String(link.text)));
|
|
1319
|
+
steps.appendChild(step);
|
|
1320
|
+
}
|
|
1321
|
+
main.appendChild(steps);
|
|
1322
|
+
}
|
|
1060
1323
|
if (verdicts.length > 0) {
|
|
1061
1324
|
var said = make('div', 'tag-row');
|
|
1062
1325
|
said.appendChild(make('span', 'sr-only', 'Your agent says: '));
|
|
@@ -1117,6 +1380,9 @@ function script(csrf) {
|
|
|
1117
1380
|
|
|
1118
1381
|
function renderAnalysis() {
|
|
1119
1382
|
var snap = snapshot();
|
|
1383
|
+
// The goal rides on the analysis, so it re-renders wherever the analysis does — every poll included.
|
|
1384
|
+
renderGoal();
|
|
1385
|
+
renderHow();
|
|
1120
1386
|
show(el.analysisSection, Boolean(snap) && !(typeof snap.unavailableReason === 'string' && snap.unavailableReason !== ''));
|
|
1121
1387
|
el.analysisBody.textContent = '';
|
|
1122
1388
|
el.analysisActions.textContent = '';
|
|
@@ -1506,8 +1772,9 @@ function script(csrf) {
|
|
|
1506
1772
|
var src = '/flow?snapshot=' + encodeURIComponent(current.snapshotId) + (path === '' ? '' : '&file=' + encodeURIComponent(path));
|
|
1507
1773
|
if (el.flowFrame.getAttribute('src') !== src) el.flowFrame.setAttribute('src', src);
|
|
1508
1774
|
flowSnapshot = current.snapshotId;
|
|
1509
|
-
|
|
1510
|
-
el.
|
|
1775
|
+
var explained = Boolean(current.explanation);
|
|
1776
|
+
setText(el.flowTitle, path === '' ? (explained ? 'How it works and call flows' : 'Call flows') : 'Call flow: ' + path);
|
|
1777
|
+
el.flowFrame.title = path === '' ? (explained ? 'How it works, and the call flows of every changed file' : 'Call flows of every changed file') : 'Call flow of ' + path;
|
|
1511
1778
|
if (el.flowDrawer.hidden) flowReturn = opener || document.activeElement;
|
|
1512
1779
|
show(el.flowDrawer, true);
|
|
1513
1780
|
document.body.classList.add('flow-open');
|
|
@@ -1531,7 +1798,16 @@ function script(csrf) {
|
|
|
1531
1798
|
|
|
1532
1799
|
/** The diagram in the drawer opened or closed: the drawer takes the whole window while it is open. */
|
|
1533
1800
|
function onFlowMessage(event_) {
|
|
1534
|
-
if (event_.origin !== window.location.origin
|
|
1801
|
+
if (event_.origin !== window.location.origin) return;
|
|
1802
|
+
// The inline business view reports its height; nothing else it sends is read.
|
|
1803
|
+
if (event_.source === el.prHowFrame.contentWindow) {
|
|
1804
|
+
var sized = event_.data;
|
|
1805
|
+
if (sized && sized.type === 'diffninja-business-height' && typeof sized.height === 'number' && isFinite(sized.height)) {
|
|
1806
|
+
el.prHowFrame.style.height = Math.max(120, Math.min(20000, Math.ceil(sized.height))) + 'px';
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (event_.source !== el.flowFrame.contentWindow) return;
|
|
1535
1811
|
var data = event_.data;
|
|
1536
1812
|
if (!data || data.type !== 'diffninja-diagram') return;
|
|
1537
1813
|
document.body.classList.toggle('flow-full', data.open === true);
|
|
@@ -1867,7 +2143,7 @@ function script(csrf) {
|
|
|
1867
2143
|
where.appendChild(make('span', 'path-line', ':' + String(lead.line)));
|
|
1868
2144
|
head.appendChild(where);
|
|
1869
2145
|
head.appendChild(sizeOf([stop.segment]));
|
|
1870
|
-
if (lead.status === '
|
|
2146
|
+
if (lead.status === 'uncertain') head.appendChild(uncertaintyTag());
|
|
1871
2147
|
var tools = make('span', 'file-tools');
|
|
1872
2148
|
if (hasCallFlow(stop.path)) tools.appendChild(flowButton(stop.path, 'Call flow'));
|
|
1873
2149
|
head.appendChild(tools);
|
|
@@ -1891,8 +2167,7 @@ function script(csrf) {
|
|
|
1891
2167
|
var head = make('summary', 'file-head');
|
|
1892
2168
|
head.appendChild(pathNode(group.path, 'file-path'));
|
|
1893
2169
|
head.appendChild(sizeOf(segments));
|
|
1894
|
-
|
|
1895
|
-
if (worst === 'attention' || worst === 'uncertain') head.appendChild(statusTag(worst));
|
|
2170
|
+
if (fileHasUncertainty(group.path)) head.appendChild(uncertaintyTag());
|
|
1896
2171
|
var tools = make('span', 'file-tools');
|
|
1897
2172
|
if (hasCallFlow(group.path)) tools.appendChild(flowButton(group.path, 'Call flow'));
|
|
1898
2173
|
head.appendChild(tools);
|
|
@@ -2280,6 +2555,7 @@ function script(csrf) {
|
|
|
2280
2555
|
placeAnchor = action === 'preview' || action === 'submit' || action === 'refresh' ? node : null;
|
|
2281
2556
|
if (action === 'comment') { event_.preventDefault(); addComment(node); return; }
|
|
2282
2557
|
if (action === 'open-flow') { event_.preventDefault(); openFlow(node.getAttribute('data-path') || '', node); return; }
|
|
2558
|
+
if (action === 'open-how') { event_.preventDefault(); openFlow('', node); return; }
|
|
2283
2559
|
if (action === 'close-flow') { event_.preventDefault(); closeFlow(); return; }
|
|
2284
2560
|
if (action === 'add-suggestion') { event_.preventDefault(); addSuggestion(node); return; }
|
|
2285
2561
|
if (action === 'add-all-suggestions') { event_.preventDefault(); addAllSuggestions(); return; }
|
|
@@ -2334,6 +2610,14 @@ function script(csrf) {
|
|
|
2334
2610
|
el.refreshButton = byId('refresh-button');
|
|
2335
2611
|
el.pageTitle = byId('page-title');
|
|
2336
2612
|
el.pageMeta = byId('page-meta');
|
|
2613
|
+
el.prGoal = byId('pr-goal');
|
|
2614
|
+
el.prGoalText = byId('pr-goal-text');
|
|
2615
|
+
el.prGoalBy = byId('pr-goal-by');
|
|
2616
|
+
el.prHow = byId('pr-how');
|
|
2617
|
+
el.prHowBy = byId('pr-how-by');
|
|
2618
|
+
el.prHowFrame = byId('pr-how-frame');
|
|
2619
|
+
el.prDescription = byId('pr-description');
|
|
2620
|
+
el.prDescriptionBody = byId('pr-description-body');
|
|
2337
2621
|
el.lede = byId('lede');
|
|
2338
2622
|
el.loadSection = byId('load-section');
|
|
2339
2623
|
el.detailsSection = byId('details-section');
|
|
@@ -2451,6 +2735,63 @@ ${BRAND_MARK_STYLES}
|
|
|
2451
2735
|
.brand .brand-mark { width: 20px; height: 20px; }
|
|
2452
2736
|
.page-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 13px; color: var(--ink-soft); }
|
|
2453
2737
|
.page-meta a { font-weight: 600; }
|
|
2738
|
+
/* The agent's stated goal, first thing under the masthead: one sentence, with its attribution. */
|
|
2739
|
+
.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); }
|
|
2740
|
+
.pr-goal.is-empty { border-left-color: var(--line-strong); background: var(--sunken); }
|
|
2741
|
+
.pr-goal-heading { font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
|
|
2742
|
+
.pr-goal-text { margin: 6px 0 0; font-size: 15.5px; font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; max-width: 90ch; }
|
|
2743
|
+
.pr-goal-note { margin: 6px 0 0; font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; max-width: 90ch; }
|
|
2744
|
+
.pr-goal-by { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-faint); overflow-wrap: anywhere; max-width: 90ch; }
|
|
2745
|
+
.pr-how { margin-top: 10px; padding: 14px 16px 10px; border: 1px solid var(--line); border-left: 3px solid var(--ok); border-radius: var(--radius); background: var(--panel); }
|
|
2746
|
+
.pr-how-head { cursor: pointer; width: fit-content; }
|
|
2747
|
+
.pr-how-head h2 { display: inline; }
|
|
2748
|
+
.pr-how-tools { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
|
2749
|
+
.pr-how-tools .pr-goal-by { flex: 1 1 320px; }
|
|
2750
|
+
.pr-how-frame { display: block; width: 100%; height: 480px; margin-top: 10px; border: 0; background: transparent; color-scheme: normal; }
|
|
2751
|
+
.business-row { align-items: flex-start; }
|
|
2752
|
+
.business-step { display: inline-flex; align-items: baseline; gap: 6px; max-width: 100%; padding: 2px 8px; border: 1px solid var(--line); border-radius: 6px; font-size: 12.5px; color: var(--ink); background: var(--panel); overflow-wrap: anywhere; }
|
|
2753
|
+
.business-step.change-added { border-color: var(--ok); background: var(--ok-soft); }
|
|
2754
|
+
.business-step.change-changed { border-color: var(--warn); background: var(--warn-soft); }
|
|
2755
|
+
.business-step.change-removed { border-color: var(--alarm); background: var(--alarm-bg); }
|
|
2756
|
+
.business-change { flex: none; font-size: 10px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); }
|
|
2757
|
+
.change-added .business-change { color: var(--ok); }
|
|
2758
|
+
.change-changed .business-change { color: var(--warn); }
|
|
2759
|
+
.change-removed .business-change { color: var(--alarm); }
|
|
2760
|
+
.pr-description { margin-top: 6px; padding: 12px 16px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
|
|
2761
|
+
.pr-description > summary { cursor: pointer; font-size: 14px; font-weight: 600; }
|
|
2762
|
+
.pr-description > .note { margin-top: 10px; }
|
|
2763
|
+
.pr-description-body { margin-top: 10px; overflow-wrap: anywhere; font: 14px/1.6 var(--sans); max-width: 90ch; }
|
|
2764
|
+
/* No parsed form in the response, or one too large to hold: the author's own bytes, unwrapped. */
|
|
2765
|
+
.pr-description-body .pr-plain { white-space: pre-wrap; }
|
|
2766
|
+
/* The author's Markdown, laid out as the document it is; every size stays below the page's own headings. */
|
|
2767
|
+
.pr-description-body > *:first-child { margin-top: 0; }
|
|
2768
|
+
.pr-description-body > *:last-child { margin-bottom: 0; }
|
|
2769
|
+
.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; }
|
|
2770
|
+
.pr-description-body h3, .pr-description-body h4, .pr-description-body h5, .pr-description-body h6 { margin: 16px 0 6px; line-height: 1.3; }
|
|
2771
|
+
.pr-description-body h3 { font-size: 15px; }
|
|
2772
|
+
.pr-description-body h4 { font-size: 14px; }
|
|
2773
|
+
.pr-description-body h5, .pr-description-body h6 { font-size: 13px; color: var(--ink-soft); }
|
|
2774
|
+
.pr-description-body ul, .pr-description-body ol { padding-left: 22px; }
|
|
2775
|
+
.pr-description-body li { margin: 2px 0; }
|
|
2776
|
+
.pr-description-body li > ul, .pr-description-body li > ol { margin: 2px 0 0; }
|
|
2777
|
+
.pr-description-body li > p { margin: 0 0 6px; }
|
|
2778
|
+
.pr-description-body blockquote { padding: 2px 0 2px 12px; border-left: 3px solid var(--line-strong); color: var(--ink-soft); }
|
|
2779
|
+
.pr-description-body hr { margin: 14px 0; border: 0; border-top: 1px solid var(--line); }
|
|
2780
|
+
.pr-description-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
2781
|
+
.pr-description-body code { font: 12.5px var(--mono); background: var(--sunken); border: 1px solid var(--line-soft); border-radius: 4px; padding: 1px 4px; }
|
|
2782
|
+
.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; }
|
|
2783
|
+
.pr-description-body pre code { padding: 0; border: 0; background: none; font: inherit; }
|
|
2784
|
+
/* Raw HTML in a description is shown, not run: dimmed, so it reads as text the author wrote. */
|
|
2785
|
+
.pr-description-body .pr-raw { color: var(--ink-soft); white-space: pre-wrap; }
|
|
2786
|
+
.pr-description-body .pr-task { list-style: none; }
|
|
2787
|
+
.pr-description-body .pr-task input { margin-right: 6px; vertical-align: baseline; }
|
|
2788
|
+
/* The marker sits beside the item's first line, not above the paragraph it labels. */
|
|
2789
|
+
.pr-description-body .pr-task > p:first-of-type { display: inline; margin: 0; }
|
|
2790
|
+
.pr-description-body table.pr-table { width: 100%; border-collapse: collapse; font-size: 13px; display: block; overflow-x: auto; }
|
|
2791
|
+
.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; }
|
|
2792
|
+
.pr-description-body .pr-table th { background: var(--panel-head, var(--sunken)); font-weight: 650; }
|
|
2793
|
+
.pr-description-body .pr-align-center { text-align: center; }
|
|
2794
|
+
.pr-description-body .pr-align-right { text-align: right; }
|
|
2454
2795
|
.meta-item.sha { font: 12px var(--mono); padding: 2px 7px; border-radius: 6px; background: var(--neutral-soft); color: var(--ink); }
|
|
2455
2796
|
.state-badge {
|
|
2456
2797
|
display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px;
|
|
@@ -2529,7 +2870,6 @@ body.is-busy button { cursor: progress; }
|
|
|
2529
2870
|
font-size: 11.5px; font-weight: 600; white-space: nowrap;
|
|
2530
2871
|
color: var(--ink-soft); background: var(--neutral-soft);
|
|
2531
2872
|
}
|
|
2532
|
-
.status-tag.status-attention { color: var(--alarm); background: var(--alarm-bg); }
|
|
2533
2873
|
.status-tag.status-uncertain { color: var(--warn); background: var(--warn-soft); }
|
|
2534
2874
|
.tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
|
|
2535
2875
|
.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
|
/**
|
|
@@ -14,10 +26,12 @@ export interface ConnectedOptions {
|
|
|
14
26
|
readonly analysis?: () => Promise<ConnectedAnalysisView>;
|
|
15
27
|
/**
|
|
16
28
|
* The call-flow page of the analysis of snapshot `snapshotId`, for one changed
|
|
17
|
-
* file or all of them, for `GET /flow
|
|
18
|
-
*
|
|
29
|
+
* file or all of them, for `GET /flow`; with `view` "business", the business
|
|
30
|
+
* view alone (`GET /flow?snapshot=&view=business`), which the page shows under
|
|
31
|
+
* its goal. Undefined when that snapshot is not the one analyzed or it has
|
|
32
|
+
* nothing to show there.
|
|
19
33
|
*/
|
|
20
|
-
readonly flow?: (snapshotId: string, file: string | undefined) => Promise<string | undefined>;
|
|
34
|
+
readonly flow?: (snapshotId: string, file: string | undefined, view: "flow" | "business") => Promise<string | undefined>;
|
|
21
35
|
}
|
|
22
36
|
/** A single ephemeral session; never exposes a general GitHub API proxy. */
|
|
23
37
|
export declare function serveConnected(review?: ConnectedReview, options?: ConnectedOptions): Promise<ConnectedSession>;
|
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;
|
|
@@ -79,9 +99,10 @@ export async function serveConnected(review = new ConnectedReview(), options = {
|
|
|
79
99
|
const query = new URL(req.url ?? "", origin).searchParams;
|
|
80
100
|
const snapshotId = query.get("snapshot") ?? "";
|
|
81
101
|
const file = query.get("file") ?? undefined;
|
|
102
|
+
const view = query.get("view") === "business" ? "business" : "flow";
|
|
82
103
|
let html;
|
|
83
104
|
try {
|
|
84
|
-
html = options.flow === undefined ? undefined : await options.flow(snapshotId, file);
|
|
105
|
+
html = options.flow === undefined ? undefined : await options.flow(snapshotId, file, view);
|
|
85
106
|
}
|
|
86
107
|
catch {
|
|
87
108
|
html = undefined;
|