@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown half of the review client script.
|
|
3
|
+
*
|
|
4
|
+
* It is a fragment of the same IIFE `web-client.ts` ships (`WEB_MARKDOWN_JS` is
|
|
5
|
+
* interpolated into `REVIEW_CLIENT_JS`), kept in its own module so the client's
|
|
6
|
+
* event wiring and its document rendering are read one at a time. Every value it
|
|
7
|
+
* writes is a DOM text node: plan and comment text never becomes markup, and the
|
|
8
|
+
* only HTML it copies is the server's own `tok-*` spans.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const WEB_MARKDOWN_JS = String.raw` // A link is rendered only for an http(s) or mailto target. Resolving against
|
|
12
|
+
// the review origin is what catches a target that only looks relative: the
|
|
13
|
+
// URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
|
|
14
|
+
// it is parsed, and a disguised target must stay text.
|
|
15
|
+
function safeHref(url, base) {
|
|
16
|
+
var trimmed = String(url).trim();
|
|
17
|
+
if (!trimmed || /^\/\//.test(trimmed)) return null;
|
|
18
|
+
var resolved;
|
|
19
|
+
try { resolved = new URL(trimmed, base); } catch (error) { return null; }
|
|
20
|
+
if (resolved.protocol === "mailto:") return trimmed;
|
|
21
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
|
|
22
|
+
if (resolved.username || resolved.password) return null;
|
|
23
|
+
if (resolved.origin === new URL(base).origin) return resolved.href;
|
|
24
|
+
return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Inline token order matters: code, emphasis, and links are matched before a
|
|
28
|
+
// bare URL, so a URL inside a link target or a code span is never re-split.
|
|
29
|
+
var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
|
|
30
|
+
|
|
31
|
+
function renderInline(container, text) {
|
|
32
|
+
String(text).split(INLINE).forEach(function (part) {
|
|
33
|
+
if (!part) return;
|
|
34
|
+
var match;
|
|
35
|
+
if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
|
|
36
|
+
var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
|
|
37
|
+
}
|
|
38
|
+
if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
|
|
39
|
+
var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
|
|
40
|
+
}
|
|
41
|
+
if ((match = /^\*([^*]+)\*$/.exec(part))) {
|
|
42
|
+
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
43
|
+
}
|
|
44
|
+
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
45
|
+
var href = safeHref(match[2], location.href);
|
|
46
|
+
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
47
|
+
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
48
|
+
container.appendChild(link); return;
|
|
49
|
+
}
|
|
50
|
+
// GFM extended autolink: a bare http(s) or www. URL in running text.
|
|
51
|
+
if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
|
|
52
|
+
var url = match[0].replace(/[.,;:!?*_~]+$/, "");
|
|
53
|
+
while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
|
|
54
|
+
url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
|
|
55
|
+
}
|
|
56
|
+
var dot = url.indexOf(".");
|
|
57
|
+
var target = /^www\./i.test(url) ? "http://" + url : url;
|
|
58
|
+
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
|
|
59
|
+
if (safe) {
|
|
60
|
+
var auto = el("a");
|
|
61
|
+
auto.textContent = url;
|
|
62
|
+
auto.href = safe;
|
|
63
|
+
auto.target = "_blank";
|
|
64
|
+
auto.rel = "noreferrer noopener";
|
|
65
|
+
container.appendChild(auto);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
container.appendChild(document.createTextNode(part));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
74
|
+
// line can never introduce markup even though it arrives as a string.
|
|
75
|
+
function copyTokens(from, to) {
|
|
76
|
+
Array.prototype.forEach.call(from.childNodes, function (node) {
|
|
77
|
+
if (node.nodeType === 3) { to.appendChild(document.createTextNode(node.nodeValue)); return; }
|
|
78
|
+
if (node.nodeType === 1 && node.tagName === "SPAN" && /^tok-/.test(node.className || "")) {
|
|
79
|
+
var span = el("span", node.className);
|
|
80
|
+
copyTokens(node, span);
|
|
81
|
+
to.appendChild(span);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
to.appendChild(document.createTextNode(node.textContent || ""));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function codeBody(lines) {
|
|
89
|
+
var body = Array.isArray(lines) ? lines.slice() : [];
|
|
90
|
+
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[0])) body.shift();
|
|
91
|
+
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[body.length - 1])) body.pop();
|
|
92
|
+
return body;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderCode(wrap, block) {
|
|
96
|
+
var body = codeBody(block.lines);
|
|
97
|
+
var highlighted = data.code && data.code[String(block.start)];
|
|
98
|
+
var pre = el("pre", "bf-code");
|
|
99
|
+
var code = el("code");
|
|
100
|
+
if (Array.isArray(highlighted)) {
|
|
101
|
+
body.forEach(function (line, index) {
|
|
102
|
+
var span = el("span", "bf-code__line");
|
|
103
|
+
var html = highlighted[index];
|
|
104
|
+
if (html) {
|
|
105
|
+
try { copyTokens(new DOMParser().parseFromString(html, "text/html").body, span); }
|
|
106
|
+
catch (error) { span.textContent = line; }
|
|
107
|
+
} else {
|
|
108
|
+
span.textContent = line;
|
|
109
|
+
}
|
|
110
|
+
code.appendChild(span);
|
|
111
|
+
if (index < body.length - 1) code.appendChild(document.createTextNode("\n"));
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
code.textContent = body.join("\n");
|
|
115
|
+
}
|
|
116
|
+
pre.appendChild(code);
|
|
117
|
+
wrap.appendChild(pre);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A GFM table: split each pipe row into cells and honour the alignment row.
|
|
121
|
+
function splitRow(line) {
|
|
122
|
+
var text = String(line).trim();
|
|
123
|
+
if (text.charAt(0) === "|") text = text.slice(1);
|
|
124
|
+
if (text.charAt(text.length - 1) === "|") text = text.slice(0, -1);
|
|
125
|
+
return text.split("|").map(function (cell) { return cell.trim(); });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function columnAlign(delim) {
|
|
129
|
+
return splitRow(delim).map(function (cell) {
|
|
130
|
+
var left = cell.charAt(0) === ":";
|
|
131
|
+
var right = cell.charAt(cell.length - 1) === ":";
|
|
132
|
+
return left && right ? "center" : right ? "right" : left ? "left" : "";
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderTable(wrap, block) {
|
|
137
|
+
var rows = block.lines.map(splitRow);
|
|
138
|
+
var align = columnAlign(block.lines[1] || "");
|
|
139
|
+
var table = el("table", "review-table");
|
|
140
|
+
var thead = el("thead");
|
|
141
|
+
var headRow = el("tr");
|
|
142
|
+
rows[0].forEach(function (cell, index) {
|
|
143
|
+
var th = el("th");
|
|
144
|
+
if (align[index]) th.style.textAlign = align[index];
|
|
145
|
+
renderInline(th, cell);
|
|
146
|
+
headRow.appendChild(th);
|
|
147
|
+
});
|
|
148
|
+
thead.appendChild(headRow);
|
|
149
|
+
table.appendChild(thead);
|
|
150
|
+
var tbody = el("tbody");
|
|
151
|
+
rows.slice(2).forEach(function (cells) {
|
|
152
|
+
var tr = el("tr");
|
|
153
|
+
rows[0].forEach(function (unused, index) {
|
|
154
|
+
var td = el("td");
|
|
155
|
+
if (align[index]) td.style.textAlign = align[index];
|
|
156
|
+
renderInline(td, cells[index] || "");
|
|
157
|
+
tr.appendChild(td);
|
|
158
|
+
});
|
|
159
|
+
tbody.appendChild(tr);
|
|
160
|
+
});
|
|
161
|
+
table.appendChild(tbody);
|
|
162
|
+
var scroll = el("div", "review-table-wrap");
|
|
163
|
+
scroll.appendChild(table);
|
|
164
|
+
wrap.appendChild(scroll);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderBlockContent(wrap, block) {
|
|
168
|
+
var lines = Array.isArray(block.lines) ? block.lines : [];
|
|
169
|
+
if (block.kind === "heading") {
|
|
170
|
+
var text = lines.join(" ").replace(/^\s*#{1,6}\s*/, "");
|
|
171
|
+
var level = Math.min(6, Math.max(1, (lines[0].match(/^ {0,3}(#{1,6})/) || ["", "#"])[1].length));
|
|
172
|
+
var heading = el("h" + level);
|
|
173
|
+
renderInline(heading, text);
|
|
174
|
+
wrap.appendChild(heading);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (block.kind === "code") { renderCode(wrap, block); return; }
|
|
178
|
+
if (block.kind === "table") { renderTable(wrap, block); return; }
|
|
179
|
+
if (block.kind === "hr") { wrap.appendChild(el("hr")); return; }
|
|
180
|
+
if (block.kind === "quote") {
|
|
181
|
+
var quote = el("blockquote");
|
|
182
|
+
renderInline(quote, lines.map(function (line) { return line.replace(/^\s*>\s?/, ""); }).join("\n"));
|
|
183
|
+
wrap.appendChild(quote);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (block.kind === "list") {
|
|
187
|
+
var items = [];
|
|
188
|
+
lines.forEach(function (line) {
|
|
189
|
+
var match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line);
|
|
190
|
+
if (!match) {
|
|
191
|
+
// A following unmarked line is a CommonMark lazy continuation of the
|
|
192
|
+
// item above; join it so the paragraph hangs under the item's text.
|
|
193
|
+
var last = items[items.length - 1];
|
|
194
|
+
if (last) { if (line.trim()) last.rest += " " + line.trim(); return; }
|
|
195
|
+
var plain = el("p"); renderInline(plain, line); wrap.appendChild(plain); return;
|
|
196
|
+
}
|
|
197
|
+
var row = el("div", "bf-block__li");
|
|
198
|
+
row.style.paddingLeft = String(Math.floor(match[1].replace(/\t/g, " ").length / 2) * 16) + "px";
|
|
199
|
+
var markerText = match[2];
|
|
200
|
+
var rest = match[3];
|
|
201
|
+
var box = /^\[([ xX])\]\s*(.*)$/.exec(rest);
|
|
202
|
+
var marker = el("span", "marker");
|
|
203
|
+
if (box) {
|
|
204
|
+
var done = box[1].toLowerCase() === "x";
|
|
205
|
+
markerText = done ? "\u2713" : "\u2610";
|
|
206
|
+
marker.className = done ? "bf-block__marker bf-block__marker--done" : "bf-block__marker";
|
|
207
|
+
rest = box[2];
|
|
208
|
+
}
|
|
209
|
+
marker.textContent = markerText;
|
|
210
|
+
var body = el("span");
|
|
211
|
+
row.appendChild(marker); row.appendChild(body); wrap.appendChild(row);
|
|
212
|
+
items.push({ body: body, rest: rest });
|
|
213
|
+
});
|
|
214
|
+
// Render each body once, after its continuation lines are collected.
|
|
215
|
+
items.forEach(function (item) { renderInline(item.body, item.rest); });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
var paragraph = el("p");
|
|
219
|
+
renderInline(paragraph, lines.join(" "));
|
|
220
|
+
wrap.appendChild(paragraph);
|
|
221
|
+
}
|
|
222
|
+
`;
|
|
@@ -25,8 +25,11 @@ import {
|
|
|
25
25
|
WEB_LAYOUT_CSS,
|
|
26
26
|
WEB_THEME_CSS,
|
|
27
27
|
} from "../../lib/web/index.ts";
|
|
28
|
+
import { escapeHtml } from "../../lib/escape.ts";
|
|
28
29
|
import type { PlanBlock } from "./blocks.ts";
|
|
29
30
|
import type { PlanAnnotation } from "./feedback.ts";
|
|
31
|
+
import type { ReviewRole } from "./types.ts";
|
|
32
|
+
import type { ShareState } from "./web-share.ts";
|
|
30
33
|
|
|
31
34
|
/** A heading the outline can jump to. */
|
|
32
35
|
export interface ReviewOutlineItem {
|
|
@@ -67,8 +70,29 @@ export interface ReviewPageData {
|
|
|
67
70
|
limits: { comments: number; comment: number; quote: number };
|
|
68
71
|
/** Whether the review may approve the plan (only the `exit_plan_mode` flow). */
|
|
69
72
|
canApprove: boolean;
|
|
73
|
+
/** Who is looking: `owner` may approve, `reviewer` may only comment. */
|
|
74
|
+
role: ReviewRole;
|
|
75
|
+
/**
|
|
76
|
+
* The name a reviewer's link posts under, once it has been bound. Absent for the
|
|
77
|
+
* owner and for a reviewer that has not written yet, which is what tells the page
|
|
78
|
+
* whether its name control is still a choice.
|
|
79
|
+
*/
|
|
80
|
+
author?: string;
|
|
81
|
+
/**
|
|
82
|
+
* One line the owner needs before acting, such as why approval happens in the
|
|
83
|
+
* terminal; absent when the page needs no caveat.
|
|
84
|
+
*/
|
|
85
|
+
note?: string;
|
|
86
|
+
/** A running share, so a reload re-renders the link the owner already minted. */
|
|
87
|
+
share?: ShareState;
|
|
70
88
|
/** Per-session secret the client echoes on every request. */
|
|
71
89
|
token: string;
|
|
90
|
+
/**
|
|
91
|
+
* A short digest of the plan text the page rendered. It keys the unsaved draft a
|
|
92
|
+
* tab keeps for itself, so a reload restores the comments this writer has not
|
|
93
|
+
* saved yet, and a plan that changed underneath drops them instead.
|
|
94
|
+
*/
|
|
95
|
+
rev: string;
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
/** Page styles: the shared base + theme + shell + components, then the review. */
|
|
@@ -76,6 +100,15 @@ export const REVIEW_CSS = `${WEB_BASE_CSS}
|
|
|
76
100
|
${WEB_THEME_CSS}
|
|
77
101
|
${WEB_LAYOUT_CSS}
|
|
78
102
|
${WEB_COMPONENTS_CSS}
|
|
103
|
+
.review-share__list{list-style:none;margin:0 0 var(--space-3);padding:0;display:flex;flex-direction:column;gap:var(--space-2)}
|
|
104
|
+
.review-share__row{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--radius-md)}
|
|
105
|
+
.review-share__url{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:var(--fs-sm) var(--font-mono);color:var(--text)}
|
|
106
|
+
.review-share__who{flex:0 0 auto;font:var(--fs-xs) var(--font-mono);color:var(--muted)}
|
|
107
|
+
.review-share__who--bound{color:var(--accent)}
|
|
108
|
+
.review-share__actions{display:flex;flex:0 0 auto;gap:4px}
|
|
109
|
+
.review-share__actions .bf-btn{padding:1px 8px;font-size:var(--fs-xs)}
|
|
110
|
+
.review-share__qr{display:flex;justify-content:center;margin:0 0 var(--space-3)}
|
|
111
|
+
.review-share__qr svg{width:168px;height:168px;background:#fff;border-radius:var(--radius-md);padding:var(--space-1)}
|
|
79
112
|
.bf-review{--review-measure:1012px;--review-rail:26px;--review-gutter:34px;flex:1 1 auto;padding:var(--space-5) var(--space-6) 120px}
|
|
80
113
|
/* The document is one content column at GitHub's Markdown body width, with the
|
|
81
114
|
comment rail inside it, so a plan reads the way the same file does on GitHub. */
|
|
@@ -83,9 +116,9 @@ ${WEB_COMPONENTS_CSS}
|
|
|
83
116
|
hairline reading-progress bar pinned to the header's bottom edge. */
|
|
84
117
|
.review-bar{align-items:center;gap:var(--space-2) var(--space-4)}
|
|
85
118
|
.review-bar .bf-brand{flex:0 0 auto}
|
|
86
|
-
.review-heading{display:flex;align-items:center;flex:1 1
|
|
87
|
-
.review-title{display:block;flex:1 1 auto;min-width:
|
|
88
|
-
.review-state{flex:0 0 auto;white-space:nowrap}
|
|
119
|
+
.review-heading{display:flex;align-items:center;flex:1 1 240px;min-width:0;overflow:hidden;border-left:1px solid var(--border);padding-left:var(--space-4)}
|
|
120
|
+
.review-title{display:block;flex:1 1 auto;min-width:8ch;font-size:var(--fs-lg);font-weight:var(--fw-semibold);line-height:var(--lh-tight);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
121
|
+
.review-state{flex:0 0 auto;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
89
122
|
.review-status{flex:0 0 auto;max-width:42ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
90
123
|
.review-status:empty{display:none}
|
|
91
124
|
.review-controls{display:inline-flex;align-items:center;gap:var(--space-2);flex:0 0 auto}
|
|
@@ -150,6 +183,8 @@ ${WEB_COMPONENTS_CSS}
|
|
|
150
183
|
.bf-comment{padding:var(--space-3) var(--space-4)}
|
|
151
184
|
.bf-comment__head{display:flex;align-items:center;gap:var(--space-2);margin-bottom:6px}
|
|
152
185
|
.bf-comment__label{font:var(--fw-semibold) var(--fs-xs) var(--font-mono);color:var(--warn);text-transform:uppercase;letter-spacing:.06em}
|
|
186
|
+
/* A reviewer's name is a name, not a label: keep its own capitalisation. */
|
|
187
|
+
.bf-comment__label--author{text-transform:none;letter-spacing:0}
|
|
153
188
|
.bf-comment__spacer{flex:1 1 auto}
|
|
154
189
|
.bf-comment__actions{display:flex;gap:4px}
|
|
155
190
|
.bf-comment__actions .bf-btn{padding:1px 8px;font-size:var(--fs-xs)}
|
|
@@ -171,41 +206,107 @@ ${WEB_COMPONENTS_CSS}
|
|
|
171
206
|
.bf-review{padding:var(--space-4) 18px 80px}
|
|
172
207
|
.review-nav__title{display:none}
|
|
173
208
|
}
|
|
174
|
-
|
|
209
|
+
/* The controls are the widest thing in the bar — seven of them once a review can
|
|
210
|
+
be shared — so the title moves to its own row while they still need a row of
|
|
211
|
+
their own, rather than being squeezed to nothing beside it. */
|
|
212
|
+
@media (max-width:1120px){
|
|
175
213
|
.review-bar{align-items:flex-start}
|
|
176
214
|
.review-heading{flex:1 1 100%;order:3;border-left:0;padding-left:0;margin-top:var(--space-1)}
|
|
177
215
|
.review-controls{order:2;margin-left:auto}
|
|
178
216
|
.review-status{order:4;flex:1 1 100%;max-width:none;white-space:normal}
|
|
179
217
|
}
|
|
218
|
+
/* Below a phone's width the controls wrap instead of running off the page. */
|
|
219
|
+
@media (max-width:640px){
|
|
220
|
+
.review-controls{flex-wrap:wrap;justify-content:flex-end;max-width:100%}
|
|
221
|
+
.review-heading{gap:var(--space-2)}
|
|
222
|
+
.review-title{min-width:0}
|
|
223
|
+
}
|
|
180
224
|
@media (prefers-reduced-motion:no-preference){.bf-block{transition:background var(--motion-fast) var(--motion-ease)}}
|
|
181
225
|
@media (hover:none){.bf-block__add{opacity:1}}
|
|
182
|
-
|
|
226
|
+
/* On paper the document is the whole page: no comment rail to hover, no composer,
|
|
227
|
+
no reading progress, and the comments stay with the blocks they belong to. */
|
|
228
|
+
@media print{
|
|
229
|
+
.bf-review{padding:0}
|
|
230
|
+
.bf-review>.bf-block{max-width:none;padding:0 0 6px}
|
|
231
|
+
.bf-block__rail,.bf-composer,.bf-block__add,.review-progress{display:none}
|
|
232
|
+
.bf-block:hover,.bf-block--active{background:none;box-shadow:none}
|
|
233
|
+
.bf-comments{padding-left:0}
|
|
234
|
+
.bf-comment{break-inside:avoid}
|
|
235
|
+
}`.trim();
|
|
183
236
|
|
|
184
237
|
/**
|
|
185
238
|
* The sidebar's one-line keyboard reminder. It names the keys a reviewer reaches
|
|
186
239
|
* for first and defers to the shortcut sheet (`?`) for the rest, so the line does
|
|
187
240
|
* not have to grow every time a binding is added.
|
|
188
241
|
*/
|
|
189
|
-
function reviewHints(canApprove: boolean): string {
|
|
242
|
+
function reviewHints(role: ReviewRole, canApprove: boolean): string {
|
|
190
243
|
const approve = canApprove ? `, <kbd class="bf-kbd">a</kbd> to approve` : "";
|
|
191
|
-
|
|
244
|
+
const save = role === "owner" ? "save" : "post";
|
|
245
|
+
return `<p class="review-hints">Press <kbd class="bf-kbd">j</kbd>/<kbd class="bf-kbd">k</kbd> to move, <kbd class="bf-kbd">c</kbd> to comment${approve}, <kbd class="bf-kbd">s</kbd> to ${save}, <kbd class="bf-kbd">?</kbd> for all shortcuts.</p>`;
|
|
192
246
|
}
|
|
193
247
|
|
|
194
248
|
/** The shortcut sheet's rows: the review's own keys, led by approve when offered. */
|
|
195
|
-
function reviewKeys(canApprove: boolean): KeyHint[] {
|
|
249
|
+
function reviewKeys(role: ReviewRole, canApprove: boolean): KeyHint[] {
|
|
196
250
|
const rows: KeyHint[] = [];
|
|
197
251
|
if (canApprove) rows.push({ keys: "a", label: "Approve and run the plan" });
|
|
198
252
|
rows.push(
|
|
199
253
|
{ keys: ["j", "k"], label: "Next and previous block" },
|
|
200
254
|
{ keys: "c", label: "Comment on the current block" },
|
|
201
255
|
{ keys: "Enter", label: "Add the comment you are writing" },
|
|
202
|
-
|
|
203
|
-
|
|
256
|
+
role === "owner"
|
|
257
|
+
? { keys: "s", label: "Save feedback and return to Pi" }
|
|
258
|
+
: { keys: "s", label: "Post your comments to the plan" },
|
|
259
|
+
role === "owner"
|
|
260
|
+
? { keys: "Esc", label: "Close the composer, the sheet, or the review" }
|
|
261
|
+
: { keys: "Esc", label: "Close the composer or this sheet" },
|
|
204
262
|
{ keys: "?", label: "Toggle this sheet" },
|
|
205
263
|
);
|
|
206
264
|
return rows;
|
|
207
265
|
}
|
|
208
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The approval confirmation. Approving leaves plan mode and runs the plan, which
|
|
269
|
+
* cannot be undone from the page, so it is one deliberate dialog rather than the
|
|
270
|
+
* header button's single click. The shared `bf-dialog` styles and the shared
|
|
271
|
+
* dialog runtime supply the backdrop, the Tab trap, and `Esc`.
|
|
272
|
+
*/
|
|
273
|
+
function approveDialog(): string {
|
|
274
|
+
return `<div class="bf-dialog" id="approve-dialog" hidden>
|
|
275
|
+
<div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="approve-title">
|
|
276
|
+
<div class="bf-dialog__head"><h2 id="approve-title">Approve and run the plan?</h2></div>
|
|
277
|
+
<div class="bf-dialog__body"><p>Pi leaves plan mode and starts the work. The comments you added are saved with the plan.</p><p class="bf-dialog__hint">Choose <strong>Approve & run</strong> to go ahead, or <strong>Cancel</strong> to keep reviewing.</p></div>
|
|
278
|
+
<div class="bf-dialog__actions"><button type="button" class="bf-btn" id="approve-cancel">Cancel</button><button type="button" class="bf-btn bf-btn--primary" id="approve-confirm">Approve & run</button></div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The owner's Share dialog: one row per reviewer link, the QR of the link just
|
|
285
|
+
* minted, and the two actions that change the set. The list is empty on purpose —
|
|
286
|
+
* the client fills it after the server mints a token, so a link never travels in
|
|
287
|
+
* the page source.
|
|
288
|
+
*/
|
|
289
|
+
function shareDialog(): string {
|
|
290
|
+
return `<div class="bf-dialog" id="share-dialog" hidden>
|
|
291
|
+
<div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="share-title">
|
|
292
|
+
<div class="bf-dialog__head"><h2 id="share-title">Share this review</h2><span class="bf-spacer"></span><button type="button" class="bf-btn" id="share-close">Close</button></div>
|
|
293
|
+
<div class="bf-dialog__body"><ul class="review-share__list" id="share-links"></ul><div class="review-share__qr" id="share-qr"></div><p class="bf-dialog__hint" id="share-note"></p></div>
|
|
294
|
+
<div class="bf-dialog__actions"><button type="button" class="bf-btn" id="share-stop">Stop sharing</button><button type="button" class="bf-btn bf-btn--primary" id="share-new">New link</button></div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The reviewer's name dialog: one line, which the link keeps after the first post. */
|
|
300
|
+
function nameDialog(): string {
|
|
301
|
+
return `<div class="bf-dialog" id="name-dialog" hidden>
|
|
302
|
+
<div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="name-title">
|
|
303
|
+
<div class="bf-dialog__head"><h2 id="name-title">Your name</h2></div>
|
|
304
|
+
<div class="bf-dialog__body"><p>Your comments are signed with this name, and this link keeps it after your first post.</p><p class="bf-field"><input class="bf-input" id="name-input" maxlength="40" placeholder="alice" aria-labelledby="name-title"></p></div>
|
|
305
|
+
<div class="bf-dialog__actions"><button type="button" class="bf-btn" id="name-cancel">Cancel</button><button type="button" class="bf-btn bf-btn--primary" id="name-save">Save name</button></div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>`;
|
|
308
|
+
}
|
|
309
|
+
|
|
209
310
|
/**
|
|
210
311
|
* Render the review page for `data`. The markup is a fixed shell; the client
|
|
211
312
|
* fills every dynamic field, so this function is the only place that needs the
|
|
@@ -221,18 +322,29 @@ export function renderReviewPage(data: ReviewPageData): string {
|
|
|
221
322
|
<div class="bf-panel review-panel">
|
|
222
323
|
${reviewFacts()}
|
|
223
324
|
<nav class="review-nav" aria-labelledby="outline-title"><p class="review-nav__title" id="outline-title">Sections</p><ul id="outline-list"></ul></nav>
|
|
224
|
-
${
|
|
325
|
+
${data.note ? `<p class="review-hint">${escapeHtml(data.note)}</p>` : ""}
|
|
326
|
+
${reviewHints(data.role, data.canApprove)}
|
|
225
327
|
</div>
|
|
226
328
|
</details>
|
|
227
329
|
`
|
|
228
330
|
: "";
|
|
229
|
-
// Approve is the decisive action, so it takes the primary button and
|
|
230
|
-
// demoted; a comment-only review has no approve button and
|
|
331
|
+
// Approve is the decisive action, so it takes the primary button and the
|
|
332
|
+
// owner's Save is demoted; a comment-only review has no approve button and
|
|
333
|
+
// keeps Save primary. A reviewer's one write action is Post, and nothing on
|
|
334
|
+
// their page closes the review or mints another link.
|
|
335
|
+
const isOwner = data.role === "owner";
|
|
231
336
|
const approveButton = data.canApprove
|
|
232
337
|
? `
|
|
233
338
|
<button type="button" class="bf-btn bf-btn--primary" id="approve">Approve & run</button>`
|
|
234
339
|
: "";
|
|
235
|
-
const
|
|
340
|
+
const saveControl = isOwner
|
|
341
|
+
? `<button type="button" class="${data.canApprove ? "bf-btn" : "bf-btn bf-btn--primary"}" id="save">Save feedback</button>`
|
|
342
|
+
: `<button type="button" class="bf-btn bf-btn--primary" id="post">Post comments</button>`;
|
|
343
|
+
const closeControl = isOwner ? `<button type="button" class="bf-btn" id="cancel">Back to Pi</button>` : "";
|
|
344
|
+
// Sharing belongs to the owner; a reviewer gets the name they comment under.
|
|
345
|
+
const shareControl = isOwner
|
|
346
|
+
? `<button type="button" class="bf-btn" id="share">Share</button>`
|
|
347
|
+
: `<button type="button" class="bf-btn" id="reviewer-name" hidden></button>`;
|
|
236
348
|
return `<!doctype html>
|
|
237
349
|
<html lang="en">
|
|
238
350
|
<head>
|
|
@@ -249,15 +361,15 @@ ${brandLockup("plan review")}
|
|
|
249
361
|
<span class="bf-status review-status" id="status" role="status" aria-live="polite"></span>
|
|
250
362
|
<div class="review-controls">
|
|
251
363
|
${themeToggle()}
|
|
252
|
-
<button type="button" class="bf-btn bf-btn--icon" id="shortcuts" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>${approveButton}
|
|
253
|
-
${saveButton}
|
|
254
|
-
<button type="button" class="bf-btn" id="cancel">Back to Pi</button>
|
|
364
|
+
<button type="button" class="bf-btn bf-btn--icon" id="shortcuts" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>${shareControl}${approveButton}${saveControl}${closeControl}
|
|
255
365
|
</div>
|
|
256
366
|
</div><div class="review-progress" aria-hidden="true"><span class="review-progress__fill" id="progress-fill"></span></div></header>
|
|
257
367
|
<div class="${bodyClass}">
|
|
258
368
|
${outlinePanel}<main class="bf-app__main bf-review" id="plan" aria-label="Plan"></main>
|
|
259
369
|
</div>
|
|
260
|
-
${keysSheet(reviewKeys(data.canApprove))}
|
|
370
|
+
${keysSheet(reviewKeys(data.role, data.canApprove))}
|
|
371
|
+
${data.canApprove ? approveDialog() : ""}
|
|
372
|
+
${data.role === "owner" ? shareDialog() : nameDialog()}
|
|
261
373
|
<script type="application/json" id="review-data">${island}</script>
|
|
262
374
|
<script src="/client.js" defer></script>
|
|
263
375
|
</body>
|
|
@@ -271,7 +383,7 @@ ${keysSheet(reviewKeys(data.canApprove))}
|
|
|
271
383
|
*/
|
|
272
384
|
function reviewFacts(): string {
|
|
273
385
|
return `<div class="review-facts" id="review-facts">
|
|
274
|
-
<div class="review-fact"><span>Steps</span><span class="bf-progress"><span class="bf-progress__track"><span class="bf-progress__fill" id="step-fill"></span></span><span class="review-fact__value" id="step-count">0/0</span></span></div>
|
|
386
|
+
<div class="review-fact" id="step-fact"><span>Steps</span><span class="bf-progress"><span class="bf-progress__track"><span class="bf-progress__fill" id="step-fill"></span></span><span class="review-fact__value" id="step-count">0/0</span></span></div>
|
|
275
387
|
<div class="review-fact"><span>Comments</span><span class="review-fact__value" id="comment-total">0</span></div>
|
|
276
388
|
</div>`;
|
|
277
389
|
}
|