@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* The browser review client, served as `/client.js`.
|
|
3
3
|
*
|
|
4
4
|
* Kept as one self-contained script (like `file-browse/client.ts`) with no
|
|
5
|
-
* build step.
|
|
6
|
-
*
|
|
5
|
+
* build step. Its document half — the inline and block Markdown rendering — is
|
|
6
|
+
* `web-markdown.ts`, a fragment of the same IIFE interpolated below. It renders
|
|
7
|
+
* the plan from the JSON data island using DOM text nodes only — plan and
|
|
8
|
+
* comment text never becomes markup — renders each block
|
|
7
9
|
* with its comment threads indented directly beneath it, highlights code by
|
|
8
10
|
* copying only `tok-*` spans through `DOMParser`, builds the section outline
|
|
9
11
|
* and review facts, and saves the annotation list back to the server. An
|
|
@@ -11,7 +13,11 @@
|
|
|
11
13
|
* gone; the explicit buttons are the primary save/cancel path.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import { keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
16
|
+
import { dialogScript, keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
17
|
+
import { WEB_CHROME_JS } from "./web-chrome.ts";
|
|
18
|
+
import { WEB_COMMENTS_JS } from "./web-comments.ts";
|
|
19
|
+
import { WEB_COLLAB_JS } from "./web-collab.ts";
|
|
20
|
+
import { WEB_MARKDOWN_JS } from "./web-markdown.ts";
|
|
15
21
|
|
|
16
22
|
export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
17
23
|
"use strict";
|
|
@@ -26,6 +32,18 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
26
32
|
}
|
|
27
33
|
var token = typeof data.token === "string" ? data.token : "";
|
|
28
34
|
var limits = data.limits || fallback.limits;
|
|
35
|
+
// Who the server says is looking: the owner approves, a reviewer comments.
|
|
36
|
+
var role = data.role === "reviewer" ? "reviewer" : "owner";
|
|
37
|
+
var reviewerName = typeof data.author === "string" ? data.author : "";
|
|
38
|
+
if (!reviewerName) {
|
|
39
|
+
try { reviewerName = window.localStorage.getItem("bf-review-name") || ""; } catch (error) { reviewerName = ""; }
|
|
40
|
+
}
|
|
41
|
+
// Set once the server has bound a name to this link, which makes it final.
|
|
42
|
+
var reviewerBound = typeof data.author === "string" && !!data.author;
|
|
43
|
+
var DRAFT_REFUSAL = "Add or discard the comment you are writing.";
|
|
44
|
+
// What a body-less conflict says. A 409 the server explained is reported in its
|
|
45
|
+
// own words: a plan that moved and a link bound to another name both land here.
|
|
46
|
+
var PLAN_MOVED = "The plan changed on disk \u2014 reload to review again.";
|
|
29
47
|
var annotations = Array.isArray(data.annotations) ? data.annotations.slice() : [];
|
|
30
48
|
var saved = false;
|
|
31
49
|
var approved = false;
|
|
@@ -44,6 +62,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
44
62
|
var stateEl = document.getElementById("review-state");
|
|
45
63
|
var approveButton = document.getElementById("approve");
|
|
46
64
|
var saveButton = document.getElementById("save");
|
|
65
|
+
var postButton = document.getElementById("post");
|
|
47
66
|
var cancelButton = document.getElementById("cancel");
|
|
48
67
|
var outlineList = document.getElementById("outline-list");
|
|
49
68
|
var progressEl = document.getElementById("progress-fill");
|
|
@@ -54,386 +73,25 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
54
73
|
return node;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
|
|
58
|
-
if (!statusEl) return;
|
|
59
|
-
statusEl.textContent = text || "";
|
|
60
|
-
statusEl.title = text || "";
|
|
61
|
-
statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
|
|
62
|
-
}
|
|
76
|
+
${WEB_CHROME_JS}
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
// there are edits the file does not have yet.
|
|
66
|
-
function updateState() {
|
|
67
|
-
if (!stateEl) return;
|
|
68
|
-
var label;
|
|
69
|
-
var tone = "";
|
|
70
|
-
if (approved) {
|
|
71
|
-
label = "Approved";
|
|
72
|
-
tone = "success";
|
|
73
|
-
} else if (saved) {
|
|
74
|
-
label = "Saved";
|
|
75
|
-
tone = "success";
|
|
76
|
-
} else if (closed) {
|
|
77
|
-
// Settled by the user, not by a dropped stream: never flash Offline.
|
|
78
|
-
label = "Closed";
|
|
79
|
-
} else if (connected === true) {
|
|
80
|
-
label = "Connected";
|
|
81
|
-
tone = "success";
|
|
82
|
-
} else if (everConnected) {
|
|
83
|
-
// Only after the stream has opened once: an error before that is a
|
|
84
|
-
// transient initial connect, so keep showing Connecting rather than Offline.
|
|
85
|
-
label = "Offline";
|
|
86
|
-
tone = "danger";
|
|
87
|
-
} else {
|
|
88
|
-
label = "Connecting\u2026";
|
|
89
|
-
}
|
|
90
|
-
var count = annotations.length;
|
|
91
|
-
label += " \u00b7 " + count + (count === 1 ? " comment" : " comments");
|
|
92
|
-
if (dirty && !saved && !approved && !closed) {
|
|
93
|
-
label += " \u00b7 unsaved";
|
|
94
|
-
if (!tone) tone = "warn";
|
|
95
|
-
}
|
|
96
|
-
stateEl.textContent = label;
|
|
97
|
-
stateEl.className = "bf-chip bf-chip--accent review-state" + (tone ? " bf-chip--" + tone : "");
|
|
98
|
-
}
|
|
78
|
+
${WEB_COMMENTS_JS}
|
|
99
79
|
|
|
100
|
-
|
|
101
|
-
// the review origin is what catches a target that only looks relative: the
|
|
102
|
-
// URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
|
|
103
|
-
// it is parsed, and a disguised target must stay text.
|
|
104
|
-
function safeHref(url, base) {
|
|
105
|
-
var trimmed = String(url).trim();
|
|
106
|
-
if (!trimmed || /^\/\//.test(trimmed)) return null;
|
|
107
|
-
var resolved;
|
|
108
|
-
try { resolved = new URL(trimmed, base); } catch (error) { return null; }
|
|
109
|
-
if (resolved.protocol === "mailto:") return trimmed;
|
|
110
|
-
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
|
|
111
|
-
if (resolved.username || resolved.password) return null;
|
|
112
|
-
if (resolved.origin === new URL(base).origin) return resolved.href;
|
|
113
|
-
return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Inline token order matters: code, emphasis, and links are matched before a
|
|
117
|
-
// bare URL, so a URL inside a link target or a code span is never re-split.
|
|
118
|
-
var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
|
|
119
|
-
|
|
120
|
-
function renderInline(container, text) {
|
|
121
|
-
String(text).split(INLINE).forEach(function (part) {
|
|
122
|
-
if (!part) return;
|
|
123
|
-
var match;
|
|
124
|
-
if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
|
|
125
|
-
var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
|
|
126
|
-
}
|
|
127
|
-
if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
|
|
128
|
-
var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
|
|
129
|
-
}
|
|
130
|
-
if ((match = /^\*([^*]+)\*$/.exec(part))) {
|
|
131
|
-
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
132
|
-
}
|
|
133
|
-
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
134
|
-
var href = safeHref(match[2], location.href);
|
|
135
|
-
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
136
|
-
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
137
|
-
container.appendChild(link); return;
|
|
138
|
-
}
|
|
139
|
-
// GFM extended autolink: a bare http(s) or www. URL in running text.
|
|
140
|
-
if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
|
|
141
|
-
var url = match[0].replace(/[.,;:!?*_~]+$/, "");
|
|
142
|
-
while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
|
|
143
|
-
url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
|
|
144
|
-
}
|
|
145
|
-
var dot = url.indexOf(".");
|
|
146
|
-
var target = /^www\./i.test(url) ? "http://" + url : url;
|
|
147
|
-
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
|
|
148
|
-
if (safe) {
|
|
149
|
-
var auto = el("a");
|
|
150
|
-
auto.textContent = url;
|
|
151
|
-
auto.href = safe;
|
|
152
|
-
auto.target = "_blank";
|
|
153
|
-
auto.rel = "noreferrer noopener";
|
|
154
|
-
container.appendChild(auto);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
container.appendChild(document.createTextNode(part));
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
163
|
-
// line can never introduce markup even though it arrives as a string.
|
|
164
|
-
function copyTokens(from, to) {
|
|
165
|
-
Array.prototype.forEach.call(from.childNodes, function (node) {
|
|
166
|
-
if (node.nodeType === 3) { to.appendChild(document.createTextNode(node.nodeValue)); return; }
|
|
167
|
-
if (node.nodeType === 1 && node.tagName === "SPAN" && /^tok-/.test(node.className || "")) {
|
|
168
|
-
var span = el("span", node.className);
|
|
169
|
-
copyTokens(node, span);
|
|
170
|
-
to.appendChild(span);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
to.appendChild(document.createTextNode(node.textContent || ""));
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function codeBody(lines) {
|
|
178
|
-
var body = Array.isArray(lines) ? lines.slice() : [];
|
|
179
|
-
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[0])) body.shift();
|
|
180
|
-
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[body.length - 1])) body.pop();
|
|
181
|
-
return body;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function renderCode(wrap, block) {
|
|
185
|
-
var body = codeBody(block.lines);
|
|
186
|
-
var highlighted = data.code && data.code[String(block.start)];
|
|
187
|
-
var pre = el("pre", "bf-code");
|
|
188
|
-
var code = el("code");
|
|
189
|
-
if (Array.isArray(highlighted)) {
|
|
190
|
-
body.forEach(function (line, index) {
|
|
191
|
-
var span = el("span", "bf-code__line");
|
|
192
|
-
var html = highlighted[index];
|
|
193
|
-
if (html) {
|
|
194
|
-
try { copyTokens(new DOMParser().parseFromString(html, "text/html").body, span); }
|
|
195
|
-
catch (error) { span.textContent = line; }
|
|
196
|
-
} else {
|
|
197
|
-
span.textContent = line;
|
|
198
|
-
}
|
|
199
|
-
code.appendChild(span);
|
|
200
|
-
if (index < body.length - 1) code.appendChild(document.createTextNode("\n"));
|
|
201
|
-
});
|
|
202
|
-
} else {
|
|
203
|
-
code.textContent = body.join("\n");
|
|
204
|
-
}
|
|
205
|
-
pre.appendChild(code);
|
|
206
|
-
wrap.appendChild(pre);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// A GFM table: split each pipe row into cells and honour the alignment row.
|
|
210
|
-
function splitRow(line) {
|
|
211
|
-
var text = String(line).trim();
|
|
212
|
-
if (text.charAt(0) === "|") text = text.slice(1);
|
|
213
|
-
if (text.charAt(text.length - 1) === "|") text = text.slice(0, -1);
|
|
214
|
-
return text.split("|").map(function (cell) { return cell.trim(); });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function columnAlign(delim) {
|
|
218
|
-
return splitRow(delim).map(function (cell) {
|
|
219
|
-
var left = cell.charAt(0) === ":";
|
|
220
|
-
var right = cell.charAt(cell.length - 1) === ":";
|
|
221
|
-
return left && right ? "center" : right ? "right" : left ? "left" : "";
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function renderTable(wrap, block) {
|
|
226
|
-
var rows = block.lines.map(splitRow);
|
|
227
|
-
var align = columnAlign(block.lines[1] || "");
|
|
228
|
-
var table = el("table", "review-table");
|
|
229
|
-
var thead = el("thead");
|
|
230
|
-
var headRow = el("tr");
|
|
231
|
-
rows[0].forEach(function (cell, index) {
|
|
232
|
-
var th = el("th");
|
|
233
|
-
if (align[index]) th.style.textAlign = align[index];
|
|
234
|
-
renderInline(th, cell);
|
|
235
|
-
headRow.appendChild(th);
|
|
236
|
-
});
|
|
237
|
-
thead.appendChild(headRow);
|
|
238
|
-
table.appendChild(thead);
|
|
239
|
-
var tbody = el("tbody");
|
|
240
|
-
rows.slice(2).forEach(function (cells) {
|
|
241
|
-
var tr = el("tr");
|
|
242
|
-
rows[0].forEach(function (unused, index) {
|
|
243
|
-
var td = el("td");
|
|
244
|
-
if (align[index]) td.style.textAlign = align[index];
|
|
245
|
-
renderInline(td, cells[index] || "");
|
|
246
|
-
tr.appendChild(td);
|
|
247
|
-
});
|
|
248
|
-
tbody.appendChild(tr);
|
|
249
|
-
});
|
|
250
|
-
table.appendChild(tbody);
|
|
251
|
-
var scroll = el("div", "review-table-wrap");
|
|
252
|
-
scroll.appendChild(table);
|
|
253
|
-
wrap.appendChild(scroll);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function renderBlockContent(wrap, block) {
|
|
257
|
-
var lines = Array.isArray(block.lines) ? block.lines : [];
|
|
258
|
-
if (block.kind === "heading") {
|
|
259
|
-
var text = lines.join(" ").replace(/^\s*#{1,6}\s*/, "");
|
|
260
|
-
var level = Math.min(6, Math.max(1, (lines[0].match(/^ {0,3}(#{1,6})/) || ["", "#"])[1].length));
|
|
261
|
-
var heading = el("h" + level);
|
|
262
|
-
renderInline(heading, text);
|
|
263
|
-
wrap.appendChild(heading);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (block.kind === "code") { renderCode(wrap, block); return; }
|
|
267
|
-
if (block.kind === "table") { renderTable(wrap, block); return; }
|
|
268
|
-
if (block.kind === "hr") { wrap.appendChild(el("hr")); return; }
|
|
269
|
-
if (block.kind === "quote") {
|
|
270
|
-
var quote = el("blockquote");
|
|
271
|
-
renderInline(quote, lines.map(function (line) { return line.replace(/^\s*>\s?/, ""); }).join("\n"));
|
|
272
|
-
wrap.appendChild(quote);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (block.kind === "list") {
|
|
276
|
-
var items = [];
|
|
277
|
-
lines.forEach(function (line) {
|
|
278
|
-
var match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line);
|
|
279
|
-
if (!match) {
|
|
280
|
-
// A following unmarked line is a CommonMark lazy continuation of the
|
|
281
|
-
// item above; join it so the paragraph hangs under the item's text.
|
|
282
|
-
var last = items[items.length - 1];
|
|
283
|
-
if (last) { if (line.trim()) last.rest += " " + line.trim(); return; }
|
|
284
|
-
var plain = el("p"); renderInline(plain, line); wrap.appendChild(plain); return;
|
|
285
|
-
}
|
|
286
|
-
var row = el("div", "bf-block__li");
|
|
287
|
-
row.style.paddingLeft = String(Math.floor(match[1].replace(/\t/g, " ").length / 2) * 16) + "px";
|
|
288
|
-
var markerText = match[2];
|
|
289
|
-
var rest = match[3];
|
|
290
|
-
var box = /^\[([ xX])\]\s*(.*)$/.exec(rest);
|
|
291
|
-
var marker = el("span", "marker");
|
|
292
|
-
if (box) {
|
|
293
|
-
var done = box[1].toLowerCase() === "x";
|
|
294
|
-
markerText = done ? "\u2713" : "\u2610";
|
|
295
|
-
marker.className = done ? "bf-block__marker bf-block__marker--done" : "bf-block__marker";
|
|
296
|
-
rest = box[2];
|
|
297
|
-
}
|
|
298
|
-
marker.textContent = markerText;
|
|
299
|
-
var body = el("span");
|
|
300
|
-
row.appendChild(marker); row.appendChild(body); wrap.appendChild(row);
|
|
301
|
-
items.push({ body: body, rest: rest });
|
|
302
|
-
});
|
|
303
|
-
// Render each body once, after its continuation lines are collected.
|
|
304
|
-
items.forEach(function (item) { renderInline(item.body, item.rest); });
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
var paragraph = el("p");
|
|
308
|
-
renderInline(paragraph, lines.join(" "));
|
|
309
|
-
wrap.appendChild(paragraph);
|
|
310
|
-
}
|
|
80
|
+
${WEB_COLLAB_JS}
|
|
311
81
|
|
|
82
|
+
${WEB_MARKDOWN_JS}
|
|
312
83
|
function closeComposer() {
|
|
313
84
|
var open = planEl ? planEl.querySelector(".bf-composer") : null;
|
|
314
85
|
if (open && open.parentNode) open.parentNode.removeChild(open);
|
|
315
86
|
Array.prototype.forEach.call(planEl ? planEl.querySelectorAll(".bf-block--composing") : [], function (block) {
|
|
316
87
|
block.classList.remove("bf-block--composing");
|
|
317
88
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!range || !wrap.contains(range.commonAncestorContainer)) return "";
|
|
325
|
-
return selection.toString().replace(/\s+/g, " ").trim().slice(0, limits.quote);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function commentsBoxAfter(wrap) {
|
|
329
|
-
var next = wrap.nextElementSibling;
|
|
330
|
-
if (next && next.classList && next.classList.contains("bf-comments")) return next;
|
|
331
|
-
var box = el("div", "bf-comments");
|
|
332
|
-
wrap.insertAdjacentElement("afterend", box);
|
|
333
|
-
return box;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Enter submits a comment; Shift+Enter is left for the browser.
|
|
337
|
-
function submitOnEnter(area, submit) {
|
|
338
|
-
area.addEventListener("keydown", function (event) {
|
|
339
|
-
// An IME uses Enter to commit a composition; submitting then would send a
|
|
340
|
-
// half-typed comment.
|
|
341
|
-
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
342
|
-
event.preventDefault();
|
|
343
|
-
submit();
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function openComposer(wrap, block, quote) {
|
|
348
|
-
closeComposer();
|
|
349
|
-
wrap.classList.add("bf-block--composing");
|
|
350
|
-
var composer = el("div", "bf-composer");
|
|
351
|
-
if (quote) {
|
|
352
|
-
var chip = el("div", "bf-quote-chip");
|
|
353
|
-
chip.textContent = "\u201c" + quote + "\u201d";
|
|
354
|
-
composer.appendChild(chip);
|
|
355
|
-
}
|
|
356
|
-
var area = el("textarea", "bf-textarea");
|
|
357
|
-
area.placeholder = "Leave feedback on this part of the plan (Enter to add)";
|
|
358
|
-
area.setAttribute("aria-label", "Comment");
|
|
359
|
-
area.maxLength = limits.comment;
|
|
360
|
-
composer.appendChild(area);
|
|
361
|
-
var row = el("div", "bf-composer__row");
|
|
362
|
-
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
363
|
-
cancel.addEventListener("click", closeComposer);
|
|
364
|
-
var add = el("button"); add.type = "button"; add.className = "bf-btn bf-btn--primary"; add.textContent = "Add comment";
|
|
365
|
-
var submit = function () {
|
|
366
|
-
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
367
|
-
if (!comment) { area.focus(); return; }
|
|
368
|
-
if (annotations.length >= limits.comments) { setStatus("Comment limit reached (" + limits.comments + ").", "error"); return; }
|
|
369
|
-
annotations.push(quote ? { anchor: block.start, quote: quote, comment: comment } : { anchor: block.start, comment: comment });
|
|
370
|
-
dirty = true;
|
|
371
|
-
renderPlan();
|
|
372
|
-
};
|
|
373
|
-
add.addEventListener("click", submit);
|
|
374
|
-
submitOnEnter(area, submit);
|
|
375
|
-
row.appendChild(cancel); row.appendChild(add); composer.appendChild(row);
|
|
376
|
-
commentsBoxAfter(wrap).appendChild(composer);
|
|
377
|
-
area.focus();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function startEdit(card, index) {
|
|
381
|
-
var annotation = annotations[index];
|
|
382
|
-
card.textContent = "";
|
|
383
|
-
var area = el("textarea", "bf-textarea");
|
|
384
|
-
area.value = annotation.comment;
|
|
385
|
-
area.setAttribute("aria-label", "Edit comment");
|
|
386
|
-
area.maxLength = limits.comment;
|
|
387
|
-
card.appendChild(area);
|
|
388
|
-
var row = el("div", "bf-comment__row");
|
|
389
|
-
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
390
|
-
cancel.addEventListener("click", renderPlan);
|
|
391
|
-
var apply = el("button"); apply.type = "button"; apply.className = "bf-btn bf-btn--primary"; apply.textContent = "Save";
|
|
392
|
-
apply.addEventListener("click", function () {
|
|
393
|
-
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
394
|
-
if (!comment) { area.focus(); return; }
|
|
395
|
-
annotation.comment = comment;
|
|
396
|
-
dirty = true;
|
|
397
|
-
renderPlan();
|
|
398
|
-
});
|
|
399
|
-
submitOnEnter(area, function () { apply.click(); });
|
|
400
|
-
row.appendChild(cancel); row.appendChild(apply); card.appendChild(row);
|
|
401
|
-
area.focus();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function renderCommentCards(box, list) {
|
|
405
|
-
list.forEach(function (annotation) {
|
|
406
|
-
var index = annotations.indexOf(annotation);
|
|
407
|
-
var card = el("div", "bf-comment");
|
|
408
|
-
var head = el("div", "bf-comment__head");
|
|
409
|
-
var label = el("span", "bf-comment__label"); label.textContent = "Review";
|
|
410
|
-
head.appendChild(label);
|
|
411
|
-
head.appendChild(el("span", "bf-comment__spacer"));
|
|
412
|
-
var actions = el("div", "bf-comment__actions");
|
|
413
|
-
var edit = el("button"); edit.type = "button"; edit.className = "bf-btn"; edit.textContent = "Edit";
|
|
414
|
-
edit.addEventListener("click", function () { startEdit(card, index); });
|
|
415
|
-
var remove = el("button"); remove.type = "button"; remove.className = "bf-btn"; remove.textContent = "Delete";
|
|
416
|
-
remove.addEventListener("click", function () { annotations.splice(index, 1); dirty = true; renderPlan(); });
|
|
417
|
-
actions.appendChild(edit); actions.appendChild(remove); head.appendChild(actions);
|
|
418
|
-
card.appendChild(head);
|
|
419
|
-
if (annotation.quote) {
|
|
420
|
-
var quote = el("div", "bf-comment__quote");
|
|
421
|
-
quote.textContent = "\u201c" + annotation.quote + "\u201d";
|
|
422
|
-
card.appendChild(quote);
|
|
423
|
-
}
|
|
424
|
-
var body = el("div", "bf-comment__body");
|
|
425
|
-
body.textContent = annotation.comment;
|
|
426
|
-
card.appendChild(body);
|
|
427
|
-
box.appendChild(card);
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function renderComments(container, anchor) {
|
|
432
|
-
var mine = annotations.filter(function (annotation) { return annotation.anchor === anchor; });
|
|
433
|
-
if (mine.length === 0) return;
|
|
434
|
-
var box = el("div", "bf-comments");
|
|
435
|
-
renderCommentCards(box, mine);
|
|
436
|
-
container.appendChild(box);
|
|
89
|
+
// The only status a composer leaves behind is the refusal to leave it; once
|
|
90
|
+
// the draft is gone that sentence is stale.
|
|
91
|
+
if (statusEl && statusEl.textContent === DRAFT_REFUSAL) setStatus("");
|
|
92
|
+
// Closing is how a half-typed comment is discarded, so the stored draft follows
|
|
93
|
+
// it. A close with nothing open must not rewrite — and so drop — a restored one.
|
|
94
|
+
if (open) saveDraft();
|
|
437
95
|
}
|
|
438
96
|
|
|
439
97
|
function observeSections() {
|
|
@@ -501,6 +159,9 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
501
159
|
// The server counts the same step list approval seeds, so this never
|
|
502
160
|
// disagrees with the todo list over a checkbox outside the Steps section.
|
|
503
161
|
var steps = data.steps || { done: 0, total: 0 };
|
|
162
|
+
// A plan with no Steps section has no progress to show; 0/0 is noise.
|
|
163
|
+
var stepFact = document.getElementById("step-fact");
|
|
164
|
+
if (stepFact) stepFact.hidden = !steps.total;
|
|
504
165
|
var stepFill = document.getElementById("step-fill");
|
|
505
166
|
if (stepFill) stepFill.style.width = (steps.total > 0 ? Math.round((steps.done / steps.total) * 100) : 0) + "%";
|
|
506
167
|
var stepCount = document.getElementById("step-count");
|
|
@@ -540,19 +201,36 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
540
201
|
setActive(activeIndex + delta);
|
|
541
202
|
}
|
|
542
203
|
|
|
204
|
+
/** The rendered block and its island block for a source line, when both exist. */
|
|
205
|
+
function blockAtLine(line) {
|
|
206
|
+
var wrap = planEl ? planEl.querySelector('.bf-block[data-line="' + line + '"]') : null;
|
|
207
|
+
var block = (data.blocks || []).filter(function (item) { return item.start === line; })[0];
|
|
208
|
+
return wrap && block ? { wrap: wrap, block: block } : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Reopen the composer a reload interrupted, once its block has rendered. */
|
|
212
|
+
function reopenComposer(typing) {
|
|
213
|
+
if (!typing || saved || approved || closed) return;
|
|
214
|
+
var target = blockAtLine(typing.anchor);
|
|
215
|
+
if (target) openComposer(target.wrap, target.block, typing.quote || "", typing.text);
|
|
216
|
+
}
|
|
217
|
+
|
|
543
218
|
function commentOnActive() {
|
|
544
|
-
|
|
545
|
-
if (blocks.length === 0) return;
|
|
219
|
+
if (blockElements().length === 0) return;
|
|
546
220
|
if (activeIndex < 0) setActive(0);
|
|
547
221
|
var wrap = blockElements()[Math.max(0, activeIndex)];
|
|
548
222
|
if (!wrap) return;
|
|
549
|
-
var
|
|
550
|
-
|
|
551
|
-
if (block) openComposer(wrap, block, "");
|
|
223
|
+
var target = blockAtLine(Number(wrap.getAttribute("data-line")));
|
|
224
|
+
if (target) openComposer(target.wrap, target.block, "");
|
|
552
225
|
}
|
|
553
226
|
|
|
554
227
|
function renderPlan() {
|
|
555
228
|
if (!planEl) return;
|
|
229
|
+
// The DOM below is thrown away and rebuilt. Focus is only put back when it was
|
|
230
|
+
// inside this document to begin with: a repaint the stream caused must never
|
|
231
|
+
// pull the keyboard away from the page the reviewer is working in.
|
|
232
|
+
var focused = document.activeElement;
|
|
233
|
+
var keepFocus = !!(focused && focused !== planEl && planEl.contains(focused));
|
|
556
234
|
closeComposer();
|
|
557
235
|
pendingQuote = null;
|
|
558
236
|
var scrollTop = planEl.scrollTop;
|
|
@@ -604,83 +282,52 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
604
282
|
// place rather than scrolling, so j/k resume where the reviewer was.
|
|
605
283
|
var active = activeIndex >= 0 ? blockElements()[activeIndex] : null;
|
|
606
284
|
if (active) active.classList.add("bf-block--active");
|
|
285
|
+
// The focused element went with the old DOM, so put the tab stop back on the
|
|
286
|
+
// block's comment control: that is where adding, editing, or deleting left it.
|
|
287
|
+
if (keepFocus && active) {
|
|
288
|
+
var stop = active.querySelector(".bf-block__add");
|
|
289
|
+
if (stop) stop.focus();
|
|
290
|
+
}
|
|
607
291
|
renderFacts();
|
|
608
292
|
observeSections();
|
|
609
293
|
updateState();
|
|
610
294
|
}
|
|
611
295
|
|
|
612
|
-
function renderHeader() {
|
|
613
|
-
var titleEl = document.getElementById("plan-title");
|
|
614
|
-
if (titleEl && data.title) {
|
|
615
|
-
titleEl.textContent = data.title;
|
|
616
|
-
titleEl.title = data.title;
|
|
617
|
-
}
|
|
618
|
-
if (data.title) document.title = "Plan review \u00b7 " + data.title;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function updateProgress() {
|
|
622
|
-
if (!progressEl) return;
|
|
623
|
-
var doc = document.scrollingElement || document.documentElement;
|
|
624
|
-
var top = doc ? doc.scrollTop : 0;
|
|
625
|
-
var height = doc ? doc.scrollHeight : 0;
|
|
626
|
-
var view = doc ? doc.clientHeight : 0;
|
|
627
|
-
if (planEl && planEl.scrollHeight > planEl.clientHeight + 4) {
|
|
628
|
-
top = planEl.scrollTop; height = planEl.scrollHeight; view = planEl.clientHeight;
|
|
629
|
-
}
|
|
630
|
-
var max = height - view;
|
|
631
|
-
var percent = max > 0 ? Math.min(100, Math.max(0, (top / max) * 100)) : 0;
|
|
632
|
-
progressEl.style.width = percent + "%";
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function lockActions() {
|
|
636
|
-
if (approveButton) approveButton.disabled = true;
|
|
637
|
-
if (saveButton) saveButton.disabled = true;
|
|
638
|
-
if (cancelButton) cancelButton.disabled = true;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// A conflict means the file moved under the page; only a reload can re-anchor.
|
|
642
|
-
function offerReload() {
|
|
643
|
-
if (document.getElementById("reload")) return;
|
|
644
|
-
var controls = document.querySelector(".review-controls");
|
|
645
|
-
if (!controls) return;
|
|
646
|
-
var reload = el("button");
|
|
647
|
-
reload.type = "button";
|
|
648
|
-
reload.id = "reload";
|
|
649
|
-
reload.className = "bf-btn bf-btn--primary";
|
|
650
|
-
reload.textContent = "Reload";
|
|
651
|
-
reload.addEventListener("click", function () { window.location.reload(); });
|
|
652
|
-
controls.appendChild(reload);
|
|
653
|
-
}
|
|
654
296
|
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
// Save and approve share the same POST shape; only the route, the settled
|
|
662
|
-
// flag, and the wording differ. Both persist the current annotations.
|
|
663
|
-
function submit(url, okText, failText, mark) {
|
|
297
|
+
// Save, approve, and post share the same POST shape; what differs is whether
|
|
298
|
+
// the review settles. Only the owner's save and approve resolve the session,
|
|
299
|
+
// so only their callback marks the page terminal. A reviewer posts only their
|
|
300
|
+
// own comments, so a page showing everyone's cannot re-post another writer's.
|
|
301
|
+
function submit(url, okText, failText, onOk) {
|
|
302
|
+
var payload = role === "reviewer" ? annotations.filter(isMine) : annotations;
|
|
664
303
|
fetch(url + "?token=" + encodeURIComponent(token), {
|
|
665
304
|
method: "POST",
|
|
666
305
|
headers: { "Content-Type": "application/json" },
|
|
667
|
-
body: JSON.stringify({ annotations:
|
|
306
|
+
body: JSON.stringify({ annotations: payload })
|
|
668
307
|
}).then(function (response) {
|
|
669
308
|
if (response.ok) {
|
|
670
|
-
mark();
|
|
671
309
|
dirty = false;
|
|
672
|
-
|
|
310
|
+
clearDraft();
|
|
673
311
|
setStatus(okText, "saved");
|
|
674
|
-
|
|
312
|
+
if (onOk) onOk();
|
|
675
313
|
updateState();
|
|
676
|
-
|
|
314
|
+
// The server binds a reviewer's name to her link on the first post; adopt it
|
|
315
|
+
// so a second tab of the same link posts under the same name.
|
|
316
|
+
return response.json().then(function (payload) {
|
|
317
|
+
if (role === "reviewer" && payload && typeof payload.author === "string" && payload.author) adoptName(payload.author);
|
|
318
|
+
}, function () {});
|
|
677
319
|
}
|
|
678
320
|
if (response.status === 409) {
|
|
679
|
-
|
|
321
|
+
// A conflict is either a plan that moved under the page or a link already
|
|
322
|
+
// bound to another name; a reload fixes both, and the server names which.
|
|
680
323
|
if (approveButton) approveButton.disabled = true;
|
|
681
324
|
if (saveButton) saveButton.disabled = true;
|
|
325
|
+
if (postButton) postButton.disabled = true;
|
|
682
326
|
offerReload();
|
|
683
|
-
return
|
|
327
|
+
return response.json().then(
|
|
328
|
+
function (payload) { setStatus(payload && payload.error ? payload.error : PLAN_MOVED, "error"); },
|
|
329
|
+
function () { setStatus(PLAN_MOVED, "error"); }
|
|
330
|
+
);
|
|
684
331
|
}
|
|
685
332
|
// Surface the server's own reason (a malformed comment, a stale body)
|
|
686
333
|
// rather than a bare status code.
|
|
@@ -693,20 +340,55 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
693
340
|
});
|
|
694
341
|
}
|
|
695
342
|
|
|
343
|
+
// A composer holds text the page has not committed, so the ways off the page
|
|
344
|
+
// refuse while it does: leaving would drop what the reviewer typed.
|
|
345
|
+
function draftTextarea() {
|
|
346
|
+
var open = planEl ? planEl.querySelector(".bf-composer") : null;
|
|
347
|
+
var area = open ? open.querySelector("textarea") : null;
|
|
348
|
+
return area && area.value.trim() ? area : null;
|
|
349
|
+
}
|
|
350
|
+
function guardDraft() {
|
|
351
|
+
var area = draftTextarea();
|
|
352
|
+
if (!area) return false;
|
|
353
|
+
setStatus(DRAFT_REFUSAL, "error");
|
|
354
|
+
area.focus();
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
696
358
|
function save() {
|
|
697
359
|
if (saved || approved || closed) return;
|
|
360
|
+
if (guardDraft()) return;
|
|
698
361
|
setStatus("Saving\u2026");
|
|
699
|
-
submit("/api/feedback", "Saved \u2014 return to Pi to continue.", "Could not save: ", function () {
|
|
362
|
+
submit("/api/feedback", "Saved \u2014 return to Pi to continue.", "Could not save: ", function () {
|
|
363
|
+
saved = true;
|
|
364
|
+
settleStream();
|
|
365
|
+
lockActions();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// A reviewer's write never settles the page: the server keeps the review open,
|
|
370
|
+
// so the page keeps following it and she can keep editing her own comments.
|
|
371
|
+
function postComments() {
|
|
372
|
+
if (closed) return;
|
|
373
|
+
if (guardDraft()) return;
|
|
374
|
+
setStatus("Posting\u2026");
|
|
375
|
+
submit("/api/comment", "Comments posted \u2014 the owner sees them now.", "Could not post: ", null);
|
|
700
376
|
}
|
|
701
377
|
|
|
702
378
|
function approve() {
|
|
703
379
|
if (saved || approved || closed) return;
|
|
704
380
|
setStatus("Approving\u2026");
|
|
705
|
-
submit("/api/approve", "Approved \u2014 return to Pi to continue.", "Could not approve: ", function () {
|
|
381
|
+
submit("/api/approve", "Approved \u2014 return to Pi to continue.", "Could not approve: ", function () {
|
|
382
|
+
approved = true;
|
|
383
|
+
settleStream();
|
|
384
|
+
lockActions();
|
|
385
|
+
});
|
|
706
386
|
}
|
|
707
387
|
|
|
708
388
|
function cancel() {
|
|
709
|
-
|
|
389
|
+
// Closing the review is the owner's decision; a reviewer closes their tab.
|
|
390
|
+
if (role !== "owner" || saved || approved || closed) return;
|
|
391
|
+
if (guardDraft()) return;
|
|
710
392
|
closed = true;
|
|
711
393
|
settleStream();
|
|
712
394
|
lockActions();
|
|
@@ -718,10 +400,53 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
718
400
|
});
|
|
719
401
|
}
|
|
720
402
|
|
|
721
|
-
if (
|
|
403
|
+
if (postButton) postButton.addEventListener("click", postComments);
|
|
722
404
|
if (saveButton) saveButton.addEventListener("click", save);
|
|
723
405
|
if (cancelButton) cancelButton.addEventListener("click", cancel);
|
|
724
406
|
|
|
407
|
+
// Approving runs the plan and cannot be undone from the page, so the header
|
|
408
|
+
// button and the a key open the confirmation instead of posting. The dialog
|
|
409
|
+
// script is the same one the shortcut sheet uses: Tab trapped, Esc, focus back
|
|
410
|
+
// on whatever opened it.
|
|
411
|
+
${dialogScript({ dialogId: "approve-dialog", closeId: "approve-cancel", globalName: "bfApprove" })}
|
|
412
|
+
var approveDialog = document.getElementById("approve-dialog");
|
|
413
|
+
|
|
414
|
+
function askApprove() {
|
|
415
|
+
if (saved || approved || closed) return;
|
|
416
|
+
if (guardDraft()) return;
|
|
417
|
+
if (!approveDialog || !window.bfApprove) { approve(); return; }
|
|
418
|
+
window.bfApprove.open();
|
|
419
|
+
}
|
|
420
|
+
function confirmApprove() {
|
|
421
|
+
if (window.bfApprove) window.bfApprove.close();
|
|
422
|
+
approve();
|
|
423
|
+
}
|
|
424
|
+
if (approveButton) approveButton.addEventListener("click", askApprove);
|
|
425
|
+
var approveConfirm = document.getElementById("approve-confirm");
|
|
426
|
+
if (approveConfirm) approveConfirm.addEventListener("click", confirmApprove);
|
|
427
|
+
|
|
428
|
+
// The share and name dialogs. The fragment above holds their behaviour; this
|
|
429
|
+
// is the wiring, which needs the page's own element handles.
|
|
430
|
+
var shareButton = document.getElementById("share");
|
|
431
|
+
var shareDialogNode = document.getElementById("share-dialog");
|
|
432
|
+
var shareLinksEl = document.getElementById("share-links");
|
|
433
|
+
var shareQrEl = document.getElementById("share-qr");
|
|
434
|
+
var shareStop = document.getElementById("share-stop");
|
|
435
|
+
var shareNew = document.getElementById("share-new");
|
|
436
|
+
var shareNote = document.getElementById("share-note");
|
|
437
|
+
var nameButton = document.getElementById("reviewer-name");
|
|
438
|
+
var nameInput = document.getElementById("name-input");
|
|
439
|
+
|
|
440
|
+
if (shareButton) shareButton.addEventListener("click", shareNow);
|
|
441
|
+
if (shareStop) shareStop.addEventListener("click", stopSharing);
|
|
442
|
+
if (shareNew) shareNew.addEventListener("click", newLink);
|
|
443
|
+
${dialogScript({ dialogId: "share-dialog", closeId: "share-close", globalName: "bfShare" })}
|
|
444
|
+
|
|
445
|
+
if (nameButton) nameButton.addEventListener("click", askName);
|
|
446
|
+
var nameSave = document.getElementById("name-save");
|
|
447
|
+
if (nameSave) nameSave.addEventListener("click", saveName);
|
|
448
|
+
${dialogScript({ dialogId: "name-dialog", closeId: "name-cancel", globalName: "bfName" })}
|
|
449
|
+
|
|
725
450
|
// Remember a selection made inside a block so clicking the rail button can
|
|
726
451
|
// still quote it after the button press has collapsed the selection.
|
|
727
452
|
document.addEventListener("mouseup", function (event) {
|
|
@@ -731,7 +456,8 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
731
456
|
pendingQuote = wrap && quote ? { wrap: wrap, quote: quote } : null;
|
|
732
457
|
});
|
|
733
458
|
|
|
734
|
-
// The sheet's own wiring
|
|
459
|
+
// The sheet's own wiring — trigger, close button, backdrop, Tab trap, and Esc —
|
|
460
|
+
// is shared with browse; the page keeps only its own keymap.
|
|
735
461
|
${keysDialogScript("shortcuts")}
|
|
736
462
|
|
|
737
463
|
function typing(target) {
|
|
@@ -741,9 +467,8 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
741
467
|
|
|
742
468
|
document.addEventListener("keydown", function (event) {
|
|
743
469
|
if (event.key === "Escape") {
|
|
744
|
-
//
|
|
470
|
+
// The sheet owns its own Esc; this peels the composer, then the review.
|
|
745
471
|
if (planEl && planEl.querySelector(".bf-composer")) { closeComposer(); return; }
|
|
746
|
-
if (window.bfKeys.isOpen()) { window.bfKeys.close(); return; }
|
|
747
472
|
cancel();
|
|
748
473
|
return;
|
|
749
474
|
}
|
|
@@ -755,8 +480,8 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
755
480
|
if (event.key === "j") { event.preventDefault(); moveActive(1); return; }
|
|
756
481
|
if (event.key === "k") { event.preventDefault(); moveActive(-1); return; }
|
|
757
482
|
if (event.key === "c") { event.preventDefault(); commentOnActive(); return; }
|
|
758
|
-
if (event.key === "a" && approveButton) { event.preventDefault();
|
|
759
|
-
if (event.key === "s") { event.preventDefault(); save(); return; }
|
|
483
|
+
if (event.key === "a" && approveButton) { event.preventDefault(); askApprove(); return; }
|
|
484
|
+
if (event.key === "s") { event.preventDefault(); if (saveButton) save(); else postComments(); return; }
|
|
760
485
|
});
|
|
761
486
|
|
|
762
487
|
// A closed tab is detected by the stream below: the server sees the last
|
|
@@ -772,6 +497,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
772
497
|
try {
|
|
773
498
|
stream = new EventSource("/events?token=" + encodeURIComponent(token));
|
|
774
499
|
stream.addEventListener("open", function () { connected = true; everConnected = true; updateState(); });
|
|
500
|
+
stream.addEventListener("message", function (event) { refreshFromServer(event); });
|
|
775
501
|
stream.addEventListener("error", function () {
|
|
776
502
|
// The server ends the stream once the review is settled; keep showing the
|
|
777
503
|
// terminal state rather than Offline.
|
|
@@ -786,9 +512,14 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
786
512
|
|
|
787
513
|
${THEME_TOGGLE_JS}
|
|
788
514
|
|
|
515
|
+
// A reload during a review keeps this tab's unsaved work: the comments it added
|
|
516
|
+
// and the composer it was typing into, which needs the blocks rendered first.
|
|
517
|
+
var restoredTyping = restoreDraft();
|
|
789
518
|
renderHeader();
|
|
519
|
+
renderName();
|
|
790
520
|
renderOutline();
|
|
791
521
|
renderPlan();
|
|
522
|
+
reopenComposer(restoredTyping);
|
|
792
523
|
updateProgress();
|
|
793
524
|
})();
|
|
794
525
|
`;
|