@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -1,76 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The block half of the plan review's Markdown: tables, fenced code, headings, quotes,
|
|
3
|
+
* and lists.
|
|
3
4
|
*
|
|
4
5
|
* It is a fragment of the same IIFE `web-client.ts` ships (`WEB_MARKDOWN_JS` is
|
|
5
6
|
* 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.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* event wiring and its document rendering are read one at a time. The text inside a
|
|
8
|
+
* block is rendered by `renderInline`, which crosses from `lib/web/markdown.ts` with
|
|
9
|
+
* the board's card notes — one inline grammar and one link policy for both pages.
|
|
10
|
+
*
|
|
11
|
+
* Every value it writes is a DOM text node: plan and comment text never becomes markup,
|
|
12
|
+
* and the only HTML it copies is the server's own `tok-*` spans.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
|
-
export const WEB_MARKDOWN_JS = String.raw` //
|
|
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
|
|
15
|
+
export const WEB_MARKDOWN_JS = String.raw` // Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
74
16
|
// line can never introduce markup even though it arrives as a string.
|
|
75
17
|
function copyTokens(from, to) {
|
|
76
18
|
Array.prototype.forEach.call(from.childNodes, function (node) {
|
|
@@ -199,7 +141,7 @@ export const WEB_MARKDOWN_JS = String.raw` // A link is rendered only for an ht
|
|
|
199
141
|
var markerText = match[2];
|
|
200
142
|
var rest = match[3];
|
|
201
143
|
var box = /^\[([ xX])\]\s*(.*)$/.exec(rest);
|
|
202
|
-
var marker = el("span", "
|
|
144
|
+
var marker = el("span", "bf-block__marker");
|
|
203
145
|
if (box) {
|
|
204
146
|
var done = box[1].toLowerCase() === "x";
|
|
205
147
|
markerText = done ? "\u2713" : "\u2610";
|
|
@@ -23,6 +23,8 @@ export interface OutlineEntry {
|
|
|
23
23
|
line: number;
|
|
24
24
|
text: string;
|
|
25
25
|
changed?: boolean;
|
|
26
|
+
/** Content blocks this section no longer holds, when the baseline can say so. */
|
|
27
|
+
removed?: number;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/** What the browser's `IntersectionObserver` answers for a section watcher. */
|
|
@@ -73,6 +75,14 @@ export function renderOutline(list: ClientNode | null, entries: readonly Outline
|
|
|
73
75
|
dot.title = "Changed since your last review";
|
|
74
76
|
link.appendChild(dot);
|
|
75
77
|
}
|
|
78
|
+
if (item.removed !== undefined && item.removed > 0) {
|
|
79
|
+
// A block that left cannot be drawn where it was, so the count is the fact the
|
|
80
|
+
// page can still give: how much shorter this section has become.
|
|
81
|
+
link.title =
|
|
82
|
+
item.removed === 1
|
|
83
|
+
? "1 block removed since your last review"
|
|
84
|
+
: item.removed + " blocks removed since your last review";
|
|
85
|
+
}
|
|
76
86
|
const count = el("span", "review-nav__count");
|
|
77
87
|
count.hidden = true;
|
|
78
88
|
link.appendChild(count);
|
|
@@ -137,6 +147,8 @@ export function observeSections(
|
|
|
137
147
|
/** One review comment, as the facts need it: the line it is anchored to. */
|
|
138
148
|
export interface AnchoredComment {
|
|
139
149
|
anchor: number;
|
|
150
|
+
/** A settled note counts in the section's thread but not among the open ones. */
|
|
151
|
+
resolved?: true;
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/** What a plan's step list says: the numbers the server counted. */
|
|
@@ -170,9 +182,17 @@ export function renderFacts(
|
|
|
170
182
|
const fill = doc.getElementById("step-fill");
|
|
171
183
|
if (fill) fill.style.width = (steps.total > 0 ? Math.round((steps.done / steps.total) * 100) : 0) + "%";
|
|
172
184
|
const count = doc.getElementById("step-count");
|
|
173
|
-
|
|
185
|
+
// Named for what it counts: before approval these ticks are the plan's ambition, not
|
|
186
|
+
// work done, and after it completion belongs to `todo`.
|
|
187
|
+
if (count) count.textContent = steps.total === 0 ? "" : steps.done + "/" + steps.total + " ticked";
|
|
174
188
|
const total = doc.getElementById("comment-total");
|
|
175
|
-
if (total)
|
|
189
|
+
if (total) {
|
|
190
|
+
// Open first, because a settled note is history: "3 open · 2 resolved" says what is
|
|
191
|
+
// left to answer, and the resolved half stays because the file keeps it.
|
|
192
|
+
const settled = comments.filter((comment) => comment.resolved === true).length;
|
|
193
|
+
const open = comments.length - settled;
|
|
194
|
+
total.textContent = settled === 0 ? String(open) : open + " open · " + settled + " resolved";
|
|
195
|
+
}
|
|
176
196
|
for (const line of Object.keys(counters)) {
|
|
177
197
|
const node = counters[Number(line)];
|
|
178
198
|
if (node) node.hidden = true;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
+
baseAttribute,
|
|
17
18
|
brandLockup,
|
|
18
19
|
docHead,
|
|
19
20
|
type KeyHint,
|
|
@@ -26,69 +27,14 @@ import {
|
|
|
26
27
|
WEB_LAYOUT_CSS,
|
|
27
28
|
WEB_THEME_CSS,
|
|
28
29
|
} from "../../lib/web/index.ts";
|
|
29
|
-
import type
|
|
30
|
-
import type { PlanAnnotation } from "./feedback.ts";
|
|
30
|
+
import { keyRows, type KeySurface } from "./web-keys.ts";
|
|
31
31
|
import type { ReviewRole } from "./types.ts";
|
|
32
|
-
import type {
|
|
32
|
+
import type { ReviewPageData } from "./web-payload.ts";
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/** 0-based source line. */
|
|
39
|
-
line: number;
|
|
40
|
-
/** Heading level, 1-6. */
|
|
41
|
-
level: number;
|
|
42
|
-
/** Heading text. */
|
|
43
|
-
text: string;
|
|
44
|
-
/** True when this section changed since the previous review of the plan. */
|
|
45
|
-
changed?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Everything the browser needs to render one plan review. */
|
|
49
|
-
export interface ReviewPageData {
|
|
50
|
-
/** Plan title: the document's H1, or the file-name slug when it has none. */
|
|
51
|
-
title: string;
|
|
52
|
-
/** Source-line blocks of the feedback-stripped plan. */
|
|
53
|
-
blocks: PlanBlock[];
|
|
54
|
-
/** Feedback already stored in the file. */
|
|
55
|
-
annotations: PlanAnnotation[];
|
|
56
|
-
/** Section outline, in document order. */
|
|
57
|
-
outline: ReviewOutlineItem[];
|
|
58
|
-
/**
|
|
59
|
-
* The plan's step progress, from the same `extractPlanSteps` the approval and
|
|
60
|
-
* `set_plan_steps` use: the `Steps` section when there is one, so the page
|
|
61
|
-
* agrees with what approval seeds rather than counting every checkbox.
|
|
62
|
-
*/
|
|
63
|
-
steps: { done: number; total: number };
|
|
64
|
-
/** Highlighted HTML per line, keyed by the code block's 0-based start line. */
|
|
65
|
-
code: Record<string, string[]>;
|
|
66
|
-
/**
|
|
67
|
-
* The caps the server enforces, so the page does not retype them: comment
|
|
68
|
-
* count, comment length, and quote length, from `feedback.ts`.
|
|
69
|
-
*/
|
|
70
|
-
limits: { comments: number; comment: number; quote: number };
|
|
71
|
-
/** Whether the review may approve the plan (only the `exit_plan_mode` flow). */
|
|
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
|
-
/** A running share, so a reload re-renders the link the owner already minted. */
|
|
82
|
-
share?: ShareState;
|
|
83
|
-
/** Per-session secret the client echoes on every request. */
|
|
84
|
-
token: string;
|
|
85
|
-
/**
|
|
86
|
-
* A short digest of the plan text the page rendered. It keys the unsaved draft a
|
|
87
|
-
* tab keeps for itself, so a reload restores the comments this writer has not
|
|
88
|
-
* saved yet, and a plan that changed underneath drops them instead.
|
|
89
|
-
*/
|
|
90
|
-
rev: string;
|
|
91
|
-
}
|
|
34
|
+
// The island's two types live with the code that builds it (`web-payload.ts`); they are
|
|
35
|
+
// re-exported here because a page's data and the page's markup are read from the same
|
|
36
|
+
// imports in every caller, and two declarations of one island is how its halves drift.
|
|
37
|
+
export type { ReviewOutlineItem, ReviewPageData } from "./web-payload.ts";
|
|
92
38
|
|
|
93
39
|
/** Page styles: the shared base + theme + shell + components, then the review. */
|
|
94
40
|
export const REVIEW_CSS = `${WEB_BASE_CSS}
|
|
@@ -117,16 +63,24 @@ ${WEB_COMPONENTS_CSS}
|
|
|
117
63
|
.review-status{flex:0 0 auto;max-width:42ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
118
64
|
.review-status:empty{display:none}
|
|
119
65
|
.review-controls{display:inline-flex;align-items:center;gap:var(--space-2);flex:0 0 auto}
|
|
66
|
+
.review-caption{margin:0;color:var(--muted);font-size:var(--fs-xs);line-height:var(--lh-snug)}
|
|
120
67
|
.review-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden}
|
|
121
68
|
.review-progress__fill{display:block;height:100%;width:0;background:var(--accent);transition:width var(--motion-fast) linear}
|
|
122
69
|
/* Review panel: the plan's review facts, the section outline, then the keys. */
|
|
123
70
|
.bf-app__body--single{grid-template-columns:minmax(0,1fr)}
|
|
124
71
|
.review-panel{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-4) var(--space-4) var(--space-2)}
|
|
125
72
|
.review-facts{display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-3);border-bottom:1px solid var(--border)}
|
|
126
|
-
.review-fact{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);font-size:var(--fs-sm);color:var(--muted)}
|
|
73
|
+
.review-fact{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--space-3);font-size:var(--fs-sm);color:var(--muted)}
|
|
74
|
+
/* The label is one word or two, never a column: squeezed by the value beside it, it broke
|
|
75
|
+
"Plan steps" over two lines. It holds its line, and the value wraps below instead. */
|
|
76
|
+
.review-fact>span:first-child{white-space:nowrap}
|
|
127
77
|
.review-fact .bf-progress{color:var(--text)}
|
|
128
78
|
.review-fact .bf-progress__track{width:96px}
|
|
129
79
|
.review-fact__value{color:var(--text);font-family:var(--font-mono);font-variant-numeric:tabular-nums}
|
|
80
|
+
.review-view{display:flex;gap:2px;padding:2px;border:1px solid var(--border);border-radius:var(--radius-md)}
|
|
81
|
+
.review-view .bf-btn{flex:1 1 auto;justify-content:center;padding:3px 8px;font-size:var(--fs-xs);border:0;background:none}
|
|
82
|
+
.review-view .bf-btn[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent);font-weight:var(--fw-semibold)}
|
|
83
|
+
.review-view .bf-btn:disabled{opacity:.5;cursor:default}
|
|
130
84
|
.review-nav{min-width:0}
|
|
131
85
|
.review-nav__title{margin:0;font:var(--fw-semibold) var(--fs-xs) var(--font-mono);color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
|
|
132
86
|
.review-nav ul{list-style:none;margin:0;padding:0}
|
|
@@ -149,6 +103,10 @@ ${WEB_COMPONENTS_CSS}
|
|
|
149
103
|
.bf-block__add:hover{border-color:var(--accent);color:var(--accent)}
|
|
150
104
|
.bf-block:hover .bf-block__add,.bf-block:focus-within .bf-block__add,.bf-block--active .bf-block__add,.bf-block--commented .bf-block__add{opacity:1}
|
|
151
105
|
.bf-block__count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border-radius:var(--radius-pill);background:var(--accent-soft);color:var(--accent);font:var(--fw-semibold) var(--fs-xs)/1 var(--font-mono)}
|
|
106
|
+
/* A block the last review did not hold: the one mark that says "read this line again". The
|
|
107
|
+
rule sits at the text edge, inside the gutter the rail already reserves, so it cannot
|
|
108
|
+
reflow the measure or collide with a comment badge. */
|
|
109
|
+
.bf-block[data-changed]{box-shadow:inset 2px 0 0 var(--accent)}
|
|
152
110
|
.bf-block:hover,.bf-block--active{background:var(--surface)}
|
|
153
111
|
.bf-block--active{box-shadow:inset 0 0 0 1px var(--border)}
|
|
154
112
|
.bf-block h1,.bf-block h2,.bf-block h3,.bf-block h4,.bf-block h5,.bf-block h6{margin:24px 0 8px;line-height:var(--lh-tight)}
|
|
@@ -183,6 +141,9 @@ ${WEB_COMPONENTS_CSS}
|
|
|
183
141
|
/* A note a refinement carried off its own block: muted, because the complaint is the guess, not the text. */
|
|
184
142
|
.bf-comment__moved{font:var(--fw-medium) var(--fs-xs) var(--font-mono);color:var(--muted);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 6px}
|
|
185
143
|
.bf-comment__spacer{flex:1 1 auto}
|
|
144
|
+
/* A settled note keeps its quote and loses its body: the record stays, the eye moves on. */
|
|
145
|
+
.bf-comment--resolved{border-left-color:var(--success);opacity:.72}
|
|
146
|
+
.bf-comment__resolved{font:var(--fw-medium) var(--fs-xs) var(--font-mono);color:var(--success);text-transform:uppercase;letter-spacing:.06em}
|
|
186
147
|
.bf-comment__actions{display:flex;gap:4px}
|
|
187
148
|
.bf-comment__actions .bf-btn{padding:1px 8px;font-size:var(--fs-xs)}
|
|
188
149
|
.bf-comment__quote{margin-bottom:6px;padding-left:8px;border-left:2px solid var(--border);color:var(--muted);font-size:var(--fs-sm);overflow-wrap:anywhere}
|
|
@@ -225,7 +186,12 @@ ${WEB_COMPONENTS_CSS}
|
|
|
225
186
|
@media print{
|
|
226
187
|
.bf-review{padding:0}
|
|
227
188
|
.bf-review>.bf-block{max-width:none;padding:0 0 6px}
|
|
228
|
-
.bf-block__rail,.bf-composer,.bf-block__add,.review-progress{display:none}
|
|
189
|
+
.bf-block__rail,.bf-composer,.bf-block__add,.review-progress,.review-view{display:none}
|
|
190
|
+
/* A printed review is the whole plan, never the view the reader had on screen: the
|
|
191
|
+
base layer's hidden rule is !important, so the un-hide has to be too, and a thread
|
|
192
|
+
the filter hid on screen goes back under its block. */
|
|
193
|
+
.bf-block[hidden]{display:block!important}
|
|
194
|
+
.bf-comments[hidden]{display:flex!important}
|
|
229
195
|
.bf-block:hover,.bf-block--active{background:none;box-shadow:none}
|
|
230
196
|
.bf-comments{padding-left:0}
|
|
231
197
|
.bf-comment{break-inside:avoid}
|
|
@@ -237,28 +203,19 @@ ${WEB_COMPONENTS_CSS}
|
|
|
237
203
|
* not have to grow every time a binding is added.
|
|
238
204
|
*/
|
|
239
205
|
function reviewHints(role: ReviewRole, canApprove: boolean): string {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
206
|
+
const surface: KeySurface = { role, canApprove, hasCommentWalk: true, hasViewCycle: true };
|
|
207
|
+
const parts = keyRows(surface)
|
|
208
|
+
.filter((row) => row.hint !== undefined)
|
|
209
|
+
.map(
|
|
210
|
+
(row) => `${row.keys.map((key) => `<kbd class="bf-kbd">${key}</kbd>`).join("/")} ${row.hint?.(surface) ?? ""}`,
|
|
211
|
+
);
|
|
212
|
+
return `<p class="review-hints">Press ${parts.join(", ")}.</p>`;
|
|
243
213
|
}
|
|
244
214
|
|
|
245
|
-
/** The shortcut sheet's rows: the
|
|
215
|
+
/** The shortcut sheet's rows: the same table the handler dispatches through. */
|
|
246
216
|
function reviewKeys(role: ReviewRole, canApprove: boolean): KeyHint[] {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
rows.push(
|
|
250
|
-
{ keys: ["j", "k"], label: "Next and previous block" },
|
|
251
|
-
{ keys: "c", label: "Comment on the current block" },
|
|
252
|
-
{ keys: "Enter", label: "Add the comment you are writing" },
|
|
253
|
-
role === "owner"
|
|
254
|
-
? { keys: "s", label: "Save feedback and return to Pi" }
|
|
255
|
-
: { keys: "s", label: "Post your comments to the plan" },
|
|
256
|
-
role === "owner"
|
|
257
|
-
? { keys: "Esc", label: "Close the composer, the sheet, or the review" }
|
|
258
|
-
: { keys: "Esc", label: "Close the composer or this sheet" },
|
|
259
|
-
{ keys: "?", label: "Toggle this sheet" },
|
|
260
|
-
);
|
|
261
|
-
return rows;
|
|
217
|
+
const surface: KeySurface = { role, canApprove, hasCommentWalk: true, hasViewCycle: true };
|
|
218
|
+
return keyRows(surface).map((row) => ({ keys: [...row.keys], label: row.label(surface) }));
|
|
262
219
|
}
|
|
263
220
|
|
|
264
221
|
/**
|
|
@@ -311,13 +268,26 @@ function nameDialog(): string {
|
|
|
311
268
|
*/
|
|
312
269
|
export function renderReviewPage(data: ReviewPageData): string {
|
|
313
270
|
const island = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
271
|
+
// One sentence saying what the two decisions do, because the buttons are the same weight
|
|
272
|
+
// and the difference was only ever in the docs. It sits in the panel, not the sticky
|
|
273
|
+
// header: a taller header would hide the section an outline link just jumped to.
|
|
274
|
+
const caption =
|
|
275
|
+
data.role === "owner"
|
|
276
|
+
? "Approving runs the plan and returns these comments; saving keeps planning."
|
|
277
|
+
: "Your comments are signed with your name and returned to the plan.";
|
|
314
278
|
const hasOutline = data.outline.length > 0;
|
|
315
279
|
const bodyClass = hasOutline ? "bf-app__body" : "bf-app__body bf-app__body--single";
|
|
316
280
|
const outlinePanel = hasOutline
|
|
317
281
|
? `<details class="bf-app__sidebar review-outline" open>
|
|
318
282
|
<summary class="bf-panel__summary">Sections</summary>
|
|
319
283
|
<div class="bf-panel review-panel">
|
|
284
|
+
<p class="review-caption">${caption}</p>
|
|
320
285
|
${reviewFacts()}
|
|
286
|
+
<div class="review-view" role="group" aria-label="What to show" id="review-view">
|
|
287
|
+
<button type="button" class="bf-btn" data-view="all" aria-pressed="true">All</button>
|
|
288
|
+
<button type="button" class="bf-btn" data-view="changed" aria-pressed="false">Changed</button>
|
|
289
|
+
<button type="button" class="bf-btn" data-view="comments" aria-pressed="false">Comments</button>
|
|
290
|
+
</div>
|
|
321
291
|
<nav class="review-nav" aria-labelledby="outline-title"><p class="review-nav__title" id="outline-title">Sections</p><ul id="outline-list"></ul></nav>
|
|
322
292
|
${reviewHints(data.role, data.canApprove)}
|
|
323
293
|
</div>
|
|
@@ -342,9 +312,9 @@ ${reviewHints(data.role, data.canApprove)}
|
|
|
342
312
|
? `<button type="button" class="bf-btn" id="share">Share</button>`
|
|
343
313
|
: `<button type="button" class="bf-btn" id="reviewer-name" hidden></button>`;
|
|
344
314
|
return `<!doctype html>
|
|
345
|
-
<html lang="en">
|
|
315
|
+
<html lang="en"${baseAttribute(data.base)}>
|
|
346
316
|
<head>
|
|
347
|
-
${docHead({ title: tabTitle("plan review", data.title), css: REVIEW_CSS })}
|
|
317
|
+
${docHead({ title: tabTitle("plan review", data.title), css: REVIEW_CSS, base: data.base })}
|
|
348
318
|
</head>
|
|
349
319
|
<body class="bf-app">
|
|
350
320
|
<a class="skip" href="#plan">Skip to the plan</a>
|
|
@@ -367,7 +337,7 @@ ${keysSheet(reviewKeys(data.role, data.canApprove))}
|
|
|
367
337
|
${data.canApprove ? approveDialog() : ""}
|
|
368
338
|
${data.role === "owner" ? shareDialog() : nameDialog()}
|
|
369
339
|
<script type="application/json" id="review-data">${island}</script>
|
|
370
|
-
<script src="/client.js" defer></script>
|
|
340
|
+
<script src="${data.base}/client.js" defer></script>
|
|
371
341
|
</body>
|
|
372
342
|
</html>
|
|
373
343
|
`;
|
|
@@ -379,7 +349,7 @@ ${data.role === "owner" ? shareDialog() : nameDialog()}
|
|
|
379
349
|
*/
|
|
380
350
|
function reviewFacts(): string {
|
|
381
351
|
return `<div class="review-facts" id="review-facts">
|
|
382
|
-
<div class="review-fact" id="step-fact"><span>
|
|
352
|
+
<div class="review-fact" id="step-fact"><span>Plan 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>
|
|
383
353
|
<div class="review-fact"><span>Comments</span><span class="review-fact__value" id="comment-total">0</span></div>
|
|
384
354
|
</div>`;
|
|
385
355
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's island: every fact the browser renders, built from one read of the
|
|
3
|
+
* plan.
|
|
4
|
+
*
|
|
5
|
+
* The page's markup is `web-page.ts` and its lifetime is `web.ts`; this module is the
|
|
6
|
+
* data between them, so a fact the page grows — a per-block change mark, a count, a
|
|
7
|
+
* flag — is added here and in one client module rather than inside the server's start
|
|
8
|
+
* closure. Everything below is pure: the plan text, the baseline, and who is asking come
|
|
9
|
+
* in, the island goes out.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { basename } from "node:path";
|
|
13
|
+
import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
|
|
14
|
+
import { codeBody, fenceInfo, type PlanBlock } from "./blocks.ts";
|
|
15
|
+
import {
|
|
16
|
+
MAX_ANNOTATIONS,
|
|
17
|
+
MAX_COMMENT_LENGTH,
|
|
18
|
+
MAX_QUOTE_LENGTH,
|
|
19
|
+
readFeedback,
|
|
20
|
+
type PlanAnnotation,
|
|
21
|
+
} from "./feedback.ts";
|
|
22
|
+
import { sanitizePlanText } from "./plan-text.ts";
|
|
23
|
+
import { blockChangeMarks, changedSections, planDigest, type SectionDigest } from "./plan-text.ts";
|
|
24
|
+
import { extractPlanSteps } from "./steps.ts";
|
|
25
|
+
import { planTitle } from "./plans.ts";
|
|
26
|
+
import type { ReviewCaller, ReviewRole } from "./types.ts";
|
|
27
|
+
import type { ShareState } from "./web-share.ts";
|
|
28
|
+
|
|
29
|
+
/** A heading the outline can jump to. */
|
|
30
|
+
export interface ReviewOutlineItem {
|
|
31
|
+
/** DOM id the client gives the heading. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** 0-based source line. */
|
|
34
|
+
line: number;
|
|
35
|
+
/** Heading level, 1-6. */
|
|
36
|
+
level: number;
|
|
37
|
+
/** Heading text. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** True when this section changed since the previous review of the plan. */
|
|
40
|
+
changed?: boolean;
|
|
41
|
+
/** Content blocks this section no longer holds, when the baseline can say so. */
|
|
42
|
+
removed?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Everything the browser needs to render one plan review. */
|
|
46
|
+
export interface ReviewPageData {
|
|
47
|
+
/** The mount prefix the review answers under, so its own script is named from there. */
|
|
48
|
+
base: string;
|
|
49
|
+
/** Plan title: the document's H1, or the file-name slug when it has none. */
|
|
50
|
+
title: string;
|
|
51
|
+
/** Source-line blocks of the feedback-stripped plan. */
|
|
52
|
+
blocks: PlanBlock[];
|
|
53
|
+
/** Absolute start lines of the blocks that are new or edited since the previous review. */
|
|
54
|
+
changed?: number[];
|
|
55
|
+
/** Whether the previous review's baseline carried block digests, so `changed` means something. */
|
|
56
|
+
blockMarks?: boolean;
|
|
57
|
+
/** Feedback already stored in the file. */
|
|
58
|
+
annotations: PlanAnnotation[];
|
|
59
|
+
/** Section outline, in document order. */
|
|
60
|
+
outline: ReviewOutlineItem[];
|
|
61
|
+
/**
|
|
62
|
+
* The plan's step progress, from the same `extractPlanSteps` the approval and
|
|
63
|
+
* the `steps` action use: the `Steps` section when there is one, so the page
|
|
64
|
+
* agrees with what approval seeds rather than counting every checkbox.
|
|
65
|
+
*/
|
|
66
|
+
steps: { done: number; total: number };
|
|
67
|
+
/** Highlighted HTML per line, keyed by the code block's 0-based start line. */
|
|
68
|
+
code: Record<string, string[]>;
|
|
69
|
+
/**
|
|
70
|
+
* The caps the server enforces, so the page does not retype them: comment
|
|
71
|
+
* count, comment length, and quote length, from `feedback.ts`.
|
|
72
|
+
*/
|
|
73
|
+
limits: { comments: number; comment: number; quote: number };
|
|
74
|
+
/** Whether the review may approve the plan (only the `plan` tool's `exit` flow). */
|
|
75
|
+
canApprove: boolean;
|
|
76
|
+
/** Who is looking: `owner` may approve, `reviewer` may only comment. */
|
|
77
|
+
role: ReviewRole;
|
|
78
|
+
/**
|
|
79
|
+
* The name a reviewer's link posts under, once it has been bound. Absent for the
|
|
80
|
+
* owner and for a reviewer that has not written yet, which is what tells the page
|
|
81
|
+
* whether its name control is still a choice.
|
|
82
|
+
*/
|
|
83
|
+
author?: string;
|
|
84
|
+
/** A running share, so a reload re-renders the link the owner already minted. */
|
|
85
|
+
share?: ShareState;
|
|
86
|
+
/** Per-session secret the client echoes on every request. */
|
|
87
|
+
token: string;
|
|
88
|
+
/**
|
|
89
|
+
* A short digest of the plan text the page rendered. It keys the unsaved draft a
|
|
90
|
+
* tab keeps for itself, so a reload restores the comments this writer has not
|
|
91
|
+
* saved yet, and a plan that changed underneath drops them instead.
|
|
92
|
+
*/
|
|
93
|
+
rev: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What one island build reads. */
|
|
97
|
+
export interface ReviewPayloadInput {
|
|
98
|
+
/** The mount prefix the review answers under, e.g. `/plan/3f9c21aa`. */
|
|
99
|
+
base: string;
|
|
100
|
+
/** The plan's path, for the title's file-name fallback. */
|
|
101
|
+
path: string;
|
|
102
|
+
/** The plan as stored, feedback markers included. */
|
|
103
|
+
content: string;
|
|
104
|
+
/** The section digests the previous review showed, so this page can mark what moved. */
|
|
105
|
+
baseline?: readonly SectionDigest[];
|
|
106
|
+
/** Who is asking; the role decides what the page offers. */
|
|
107
|
+
caller: ReviewCaller;
|
|
108
|
+
/** The session secret the page echoes on every request. */
|
|
109
|
+
token: string;
|
|
110
|
+
/** Whether this review may approve the plan. */
|
|
111
|
+
canApprove: boolean;
|
|
112
|
+
/** The running share, read lazily: sharing can start after the page is first served. */
|
|
113
|
+
share: () => ShareState | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the island for one page request.
|
|
118
|
+
*
|
|
119
|
+
* The plan is read once, and the blocks, the comments, and the outline all come out of
|
|
120
|
+
* that same parse: an annotation names a block from this read, so the island cannot mark
|
|
121
|
+
* a comment onto a line the page never rendered.
|
|
122
|
+
*/
|
|
123
|
+
export function reviewPageData(input: ReviewPayloadInput): ReviewPageData {
|
|
124
|
+
const { caller } = input;
|
|
125
|
+
const role = caller.role;
|
|
126
|
+
// Normalized the way the baseline is, so a block's digest here is the one the baseline
|
|
127
|
+
// wrote: the page and the baseline must read the plan the same way, or every tabbed plan
|
|
128
|
+
// would come back marked as changed. Not stripped, because the comments have to survive
|
|
129
|
+
// for `readFeedback` to parse them.
|
|
130
|
+
const read = readFeedback(sanitizePlanText(input.content));
|
|
131
|
+
const current = read.body;
|
|
132
|
+
const blocks = read.blocks;
|
|
133
|
+
// Per-section change marks, each carrying its own section: the same shape the
|
|
134
|
+
// terminal review renders, so a heading and its mark cannot be paired wrongly.
|
|
135
|
+
const marks = changedSections(input.baseline, current);
|
|
136
|
+
// Which blocks moved, in the document's own numbering: the page draws these, and a
|
|
137
|
+
// section's shrinkage is the half of a diff a document cannot place in the flow.
|
|
138
|
+
const blockMarks = blockChangeMarks(input.baseline, current);
|
|
139
|
+
const outline = marks.flatMap(({ section, changed }) =>
|
|
140
|
+
section.title === ""
|
|
141
|
+
? []
|
|
142
|
+
: [
|
|
143
|
+
{
|
|
144
|
+
id: `s-${section.line}`,
|
|
145
|
+
line: section.line,
|
|
146
|
+
level: section.level,
|
|
147
|
+
text: section.title,
|
|
148
|
+
...(changed ? { changed: true } : {}),
|
|
149
|
+
...(blockMarks.removed[section.line] === undefined ? {} : { removed: blockMarks.removed[section.line] }),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
);
|
|
153
|
+
// The same step list approval seeds and the `steps` action indexes, so the page's
|
|
154
|
+
// progress cannot disagree with the todo list.
|
|
155
|
+
const stepList = extractPlanSteps(current);
|
|
156
|
+
// Highlight code blocks once, at the boundary: the client copies only the tok-*
|
|
157
|
+
// spans out of the server-escaped HTML.
|
|
158
|
+
const code: Record<string, string[]> = {};
|
|
159
|
+
for (const block of blocks) {
|
|
160
|
+
if (block.kind !== "code") continue;
|
|
161
|
+
const language = fenceLanguage(fenceInfo(block.lines[0] ?? ""));
|
|
162
|
+
if (language) code[String(block.start)] = highlightLines(codeBody(block), language);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
base: input.base,
|
|
166
|
+
// The document's own H1 reads better than the file-name slug, and the slug is
|
|
167
|
+
// still the fallback for a plan that starts with something else.
|
|
168
|
+
title: marks.find((mark) => mark.section.level === 1)?.section.title || planTitle(basename(input.path)),
|
|
169
|
+
blocks,
|
|
170
|
+
...(blockMarks.starts.length === 0 ? {} : { changed: blockMarks.starts }),
|
|
171
|
+
...(blockMarks.compared ? { blockMarks: true } : {}),
|
|
172
|
+
annotations: read.annotations,
|
|
173
|
+
outline,
|
|
174
|
+
steps: {
|
|
175
|
+
done: stepList.filter((step) => step.status === "completed").length,
|
|
176
|
+
total: stepList.length,
|
|
177
|
+
},
|
|
178
|
+
code,
|
|
179
|
+
limits: { comments: MAX_ANNOTATIONS, comment: MAX_COMMENT_LENGTH, quote: MAX_QUOTE_LENGTH },
|
|
180
|
+
token: input.token,
|
|
181
|
+
role,
|
|
182
|
+
// What the page keys this tab's unsaved draft on: same plan text, same draft.
|
|
183
|
+
rev: planDigest(current),
|
|
184
|
+
...(caller.name === undefined ? {} : { author: caller.name }),
|
|
185
|
+
canApprove: role === "owner" && input.canApprove,
|
|
186
|
+
share: role === "owner" ? input.share() : undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -37,11 +37,15 @@ export interface PlanRenderDeps {
|
|
|
37
37
|
/** The element that had focus before this frame, or null when nothing did. */
|
|
38
38
|
focused: ClientNode | null;
|
|
39
39
|
blocks(): readonly IslandBlock[];
|
|
40
|
+
/** Whether this block is new or edited since the previous review; absent means no marks. */
|
|
41
|
+
changed?(line: number): boolean;
|
|
40
42
|
annotations(): readonly { anchor: number }[];
|
|
41
43
|
activeIndex(): number;
|
|
42
44
|
setActiveIndex(index: number): void;
|
|
43
45
|
pendingQuote(): PendingQuote | null;
|
|
44
46
|
clearPendingQuote(): void;
|
|
47
|
+
/** Whether the review has ended, so the rail offers no new comment. */
|
|
48
|
+
settled?(): boolean;
|
|
45
49
|
/** The composer lifecycle, for the close a repaint begins with. */
|
|
46
50
|
composer(): ComposerDeps;
|
|
47
51
|
/** The block's content, from the Markdown half. */
|
|
@@ -87,24 +91,30 @@ export function renderPlanFrame(
|
|
|
87
91
|
const wrap = el("div", "bf-block");
|
|
88
92
|
wrap.setAttribute("data-line", String(block.start));
|
|
89
93
|
wrap.setAttribute("data-kind", String(block.kind));
|
|
94
|
+
// The one mark that says "read this line again": a block the last review did not have.
|
|
95
|
+
if (deps.changed?.(block.start) === true) wrap.setAttribute("data-changed", "1");
|
|
90
96
|
if (block.kind === "heading") wrap.id = "s-" + block.start;
|
|
91
97
|
deps.renderBlock(wrap, block);
|
|
92
98
|
const mine = deps.annotations().filter((annotation) => annotation.anchor === block.start);
|
|
93
99
|
if (mine.length > 0) wrap.classList.add("bf-block--commented");
|
|
94
100
|
const rail = el("div", "bf-block__rail");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
// A settled review takes no new comment: this control would open a composer whose
|
|
102
|
+
// note could never be saved, so it is not drawn rather than drawn and refused.
|
|
103
|
+
if (deps.settled?.() !== true) {
|
|
104
|
+
const add = el("button", "bf-block__add");
|
|
105
|
+
add.type = "button";
|
|
106
|
+
add.textContent = "+";
|
|
107
|
+
add.setAttribute("aria-label", "Comment on this block");
|
|
108
|
+
add.addEventListener("click", function () {
|
|
109
|
+
// Read at click time, not at render time: a selection made after this frame is
|
|
110
|
+
// the quote the reader means.
|
|
111
|
+
const waiting = deps.pendingQuote();
|
|
112
|
+
const quoted = waiting && waiting.wrap === wrap ? waiting.quote : deps.selectionIn(wrap);
|
|
113
|
+
deps.clearPendingQuote();
|
|
114
|
+
deps.open(wrap, block, quoted);
|
|
115
|
+
});
|
|
116
|
+
rail.appendChild(add);
|
|
117
|
+
}
|
|
108
118
|
if (mine.length > 0) {
|
|
109
119
|
const badge = el("span", "bf-block__count");
|
|
110
120
|
badge.textContent = String(mine.length);
|