@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -112,7 +112,7 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function openComposer(wrap, block, quote, initial) {
|
|
115
|
-
|
|
115
|
+
closeOpenComposer();
|
|
116
116
|
wrap.classList.add("bf-block--composing");
|
|
117
117
|
var composer = el("div", "bf-composer");
|
|
118
118
|
if (quote) {
|
|
@@ -132,7 +132,7 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
132
132
|
composer.appendChild(area);
|
|
133
133
|
var row = el("div", "bf-composer__row");
|
|
134
134
|
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
135
|
-
cancel.addEventListener("click",
|
|
135
|
+
cancel.addEventListener("click", closeOpenComposer);
|
|
136
136
|
var add = el("button"); add.type = "button"; add.className = "bf-btn bf-btn--primary"; add.textContent = "Add comment";
|
|
137
137
|
var submit = function () {
|
|
138
138
|
if (closed) { setStatus("The review is closed; this comment was not added.", "error"); return; }
|
|
@@ -144,10 +144,10 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
144
144
|
if (role === "reviewer") entry.author = reviewerName;
|
|
145
145
|
annotations.push(entry);
|
|
146
146
|
// Close first: the text is now a comment, so the draft keeps one or the other.
|
|
147
|
-
|
|
147
|
+
closeOpenComposer();
|
|
148
148
|
markDirty();
|
|
149
149
|
setStatus("Comment added.");
|
|
150
|
-
|
|
150
|
+
renderAll();
|
|
151
151
|
};
|
|
152
152
|
add.addEventListener("click", submit);
|
|
153
153
|
submitOnEnter(area, submit);
|
|
@@ -168,7 +168,7 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
168
168
|
card.appendChild(area);
|
|
169
169
|
var row = el("div", "bf-comment__row");
|
|
170
170
|
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
171
|
-
cancel.addEventListener("click",
|
|
171
|
+
cancel.addEventListener("click", renderAll);
|
|
172
172
|
var apply = el("button"); apply.type = "button"; apply.className = "bf-btn bf-btn--primary"; apply.textContent = "Save";
|
|
173
173
|
apply.addEventListener("click", function () {
|
|
174
174
|
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
@@ -176,7 +176,7 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
176
176
|
annotation.comment = comment;
|
|
177
177
|
markDirty();
|
|
178
178
|
setStatus("Comment updated.");
|
|
179
|
-
|
|
179
|
+
renderAll();
|
|
180
180
|
});
|
|
181
181
|
submitOnEnter(area, function () { apply.click(); });
|
|
182
182
|
row.appendChild(cancel); row.appendChild(apply); card.appendChild(row);
|
|
@@ -205,7 +205,7 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
205
205
|
annotations.splice(Math.min(index, annotations.length), 0, annotation);
|
|
206
206
|
markDirty();
|
|
207
207
|
clearUndo();
|
|
208
|
-
|
|
208
|
+
renderAll();
|
|
209
209
|
setStatus("Comment restored.", "saved");
|
|
210
210
|
});
|
|
211
211
|
controls.appendChild(button);
|
|
@@ -217,7 +217,7 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
217
217
|
if (!annotation) return;
|
|
218
218
|
annotations.splice(index, 1);
|
|
219
219
|
markDirty();
|
|
220
|
-
|
|
220
|
+
renderAll();
|
|
221
221
|
offerUndo(index, annotation);
|
|
222
222
|
setStatus("Comment deleted.");
|
|
223
223
|
}
|
|
@@ -237,6 +237,16 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
237
237
|
var label = el("span", annotation.author ? "bf-comment__label bf-comment__label--author" : "bf-comment__label");
|
|
238
238
|
label.textContent = annotation.author ? "@" + annotation.author : "Review";
|
|
239
239
|
head.appendChild(label);
|
|
240
|
+
if (annotation.moved) {
|
|
241
|
+
// A refinement carried this note off the text it was written on: the placement is
|
|
242
|
+
// this build's best guess, and saying so is the difference between the reviewer's
|
|
243
|
+
// anchor and ours.
|
|
244
|
+
var moved = el("span", "bf-comment__moved");
|
|
245
|
+
moved.textContent = "moved";
|
|
246
|
+
moved.title =
|
|
247
|
+
"Written on an earlier revision. The block it was on changed, so it sits here now: check it still refers to the text below.";
|
|
248
|
+
head.appendChild(moved);
|
|
249
|
+
}
|
|
240
250
|
head.appendChild(el("span", "bf-comment__spacer"));
|
|
241
251
|
if (isMine(annotation)) {
|
|
242
252
|
var actions = el("div", "bf-comment__actions");
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's composer lifecycle: closing it, reopening the one a reload interrupted,
|
|
3
|
+
* and opening it on the block the keyboard is on.
|
|
4
|
+
*
|
|
5
|
+
* The sixth capability out of the review client's template literal, and the first that
|
|
6
|
+
* needs a bundle rather than a list of arguments: closing touches the page, the status
|
|
7
|
+
* line, the sentence that names a half-written comment, and the draft store — so those
|
|
8
|
+
* arrive as one `ComposerDeps` the client builds where the state already exists. What
|
|
9
|
+
* `openComposer` does with the block stays in `web-comments.ts`, which owns the form.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
13
|
+
import { blockAtLine, blockElements, type IslandBlock, setActive } from "./web-blocks.ts";
|
|
14
|
+
|
|
15
|
+
/** What opening a composer takes: the block, its island entry, a quote, and any draft. */
|
|
16
|
+
export type OpenComposer = (wrap: ClientNode, block: IslandBlock, quote: string, initial?: string) => void;
|
|
17
|
+
|
|
18
|
+
/** What the lifecycle reads of the page. Built where the state lives, once per call. */
|
|
19
|
+
export interface ComposerDeps {
|
|
20
|
+
planEl: ClientNode | null;
|
|
21
|
+
statusEl: ClientNode | null;
|
|
22
|
+
/** The sentence that names a half-written comment, so a stale one can be cleared. */
|
|
23
|
+
refusal: string;
|
|
24
|
+
setStatus(text: string, kind?: string): void;
|
|
25
|
+
saveDraft(): void;
|
|
26
|
+
open: OpenComposer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Close the composer, and let the stored draft follow it.
|
|
31
|
+
*
|
|
32
|
+
* A close with nothing open must not rewrite — and so drop — a draft a reload restored, so
|
|
33
|
+
* the draft is saved only when a composer was actually there.
|
|
34
|
+
*/
|
|
35
|
+
export function closeComposer(deps: ComposerDeps): void {
|
|
36
|
+
const open = deps.planEl ? deps.planEl.querySelector(".bf-composer") : null;
|
|
37
|
+
if (open) open.remove();
|
|
38
|
+
const composing = deps.planEl ? deps.planEl.querySelectorAll(".bf-block--composing") : [];
|
|
39
|
+
for (let i = 0; i < composing.length; i++) composing[i]?.classList.remove("bf-block--composing");
|
|
40
|
+
// The only status a composer leaves behind is the refusal to leave it; once the draft is
|
|
41
|
+
// gone that sentence is stale.
|
|
42
|
+
if (deps.statusEl && deps.statusEl.textContent === deps.refusal) deps.setStatus("");
|
|
43
|
+
if (open) deps.saveDraft();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reopen the composer a reload interrupted, once its block has rendered.
|
|
48
|
+
*
|
|
49
|
+
* A review that has settled, been approved, or been closed has no draft to restore: the
|
|
50
|
+
* text belonged to a page that is no longer asking.
|
|
51
|
+
*/
|
|
52
|
+
export function reopenComposer(
|
|
53
|
+
deps: ComposerDeps,
|
|
54
|
+
typing: { anchor: number; quote?: string; text?: string } | undefined,
|
|
55
|
+
settled: { saved: boolean; approved: boolean; closed: boolean },
|
|
56
|
+
blocks: readonly IslandBlock[],
|
|
57
|
+
): void {
|
|
58
|
+
if (!typing || settled.saved || settled.approved || settled.closed) return;
|
|
59
|
+
const target = blockAtLine(deps.planEl, blocks, typing.anchor);
|
|
60
|
+
if (target) deps.open(target.wrap, target.block, typing.quote || "", typing.text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Open the composer on the block the keyboard is on, starting one if none is active.
|
|
65
|
+
*
|
|
66
|
+
* The index is returned so the client keeps its one `activeIndex`: opening a composer is a
|
|
67
|
+
* navigation, and the block the reader lands on is the block they are commenting on.
|
|
68
|
+
*/
|
|
69
|
+
export function commentOnActive(deps: ComposerDeps, activeIndex: number, blocks: readonly IslandBlock[]): number {
|
|
70
|
+
if (blockElements(deps.planEl).length === 0) return activeIndex;
|
|
71
|
+
const index = activeIndex < 0 ? (setActive(deps.planEl, 0) ?? activeIndex) : activeIndex;
|
|
72
|
+
const wrap = blockElements(deps.planEl)[Math.max(0, index)];
|
|
73
|
+
if (!wrap) return index;
|
|
74
|
+
const target = blockAtLine(deps.planEl, blocks, Number(wrap.getAttribute("data-line")));
|
|
75
|
+
if (target) deps.open(target.wrap, target.block, "");
|
|
76
|
+
return index;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The composer lifecycle as source, for the client bundle. */
|
|
80
|
+
export const WEB_COMPOSER_JS = [closeComposer, reopenComposer, commentOnActive].map(String).join("\n\n");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's draft guard: a composer holding text the page has not committed.
|
|
3
|
+
*
|
|
4
|
+
* The fifth capability out of the review client's template literal. `draftTextarea` is
|
|
5
|
+
* pure — hand it the plan element and it answers the open composer's textarea, or nothing.
|
|
6
|
+
* `guardDraft` is the rule built on it: the ways off the page refuse while a draft exists,
|
|
7
|
+
* say why in the status line, and leave the cursor where the text is, so the reader is not
|
|
8
|
+
* told about a draft they then have to hunt for.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
12
|
+
|
|
13
|
+
/** The open composer's textarea, when it holds anything a reader would lose. */
|
|
14
|
+
export function draftTextarea(planEl: ClientNode | null): { value: string; focus(): void } | null {
|
|
15
|
+
const open = planEl ? planEl.querySelector(".bf-composer") : null;
|
|
16
|
+
const area = open ? (open.querySelector("textarea") as { value: string; focus(): void } | null) : null;
|
|
17
|
+
return area && area.value.trim() ? area : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether an action must stop because a comment is half-written, and say so if it must.
|
|
22
|
+
*
|
|
23
|
+
* The refusal is the status line's own sentence rather than an alert: every way off the
|
|
24
|
+
* page runs this, and a modal on each of them would be four dialogs for one fact.
|
|
25
|
+
*/
|
|
26
|
+
export function guardDraft(
|
|
27
|
+
planEl: ClientNode | null,
|
|
28
|
+
statusEl: ClientNode | null,
|
|
29
|
+
refusal: string,
|
|
30
|
+
paint: (node: ClientNode, text: string, kind?: string) => void,
|
|
31
|
+
): boolean {
|
|
32
|
+
const area = draftTextarea(planEl);
|
|
33
|
+
if (!area) return false;
|
|
34
|
+
if (statusEl) paint(statusEl, refusal, "error");
|
|
35
|
+
area.focus();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The draft rules as source, for the client bundle. */
|
|
40
|
+
export const WEB_DRAFT_JS = [draftTextarea, guardDraft].map(String).join("\n\n");
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's keyboard, and the one piece of pointer state a key acts on.
|
|
3
|
+
*
|
|
4
|
+
* The ninth capability out of the review client's template literal — and the last one, by
|
|
5
|
+
* decision: what is left in the bundle after this is wiring (which listener to attach, the
|
|
6
|
+
* frame stream, the boot order), and a rule about which key does what does not belong in a
|
|
7
|
+
* list of `addEventListener` calls.
|
|
8
|
+
*
|
|
9
|
+
* Two rules live here:
|
|
10
|
+
*
|
|
11
|
+
* - **Esc peels one layer.** The shortcut sheet owns its own Esc (the shared dialog runtime
|
|
12
|
+
* captures it), so this peels the composer first and closes the review second, which is
|
|
13
|
+
* the order a reader pressing it twice expects.
|
|
14
|
+
* - **The selection a reader makes is the quote the next comment carries.** The rail button
|
|
15
|
+
* collapses the selection when it is pressed, so the text is remembered on `mouseup` and
|
|
16
|
+
* read again at click time; a block with a composer open is not a selection to quote.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
20
|
+
|
|
21
|
+
/** What a key may do: one callback per action the page owns. */
|
|
22
|
+
export interface KeyDeps {
|
|
23
|
+
/** Whether a composer is open, and how to close it. */
|
|
24
|
+
composerOpen(): boolean;
|
|
25
|
+
closeComposer(): void;
|
|
26
|
+
/** Close the review without saving: the owner's, and a no-op for a reviewer. */
|
|
27
|
+
cancel(): void;
|
|
28
|
+
/** The shortcut sheet: whether it is open (it then owns the keyboard) and its toggle. */
|
|
29
|
+
sheetOpen(): boolean;
|
|
30
|
+
toggleSheet(): void;
|
|
31
|
+
/** Walk the blocks by one, in the direction the key names. */
|
|
32
|
+
moveActive(delta: number): void;
|
|
33
|
+
/** Comment on the block the keyboard is on. */
|
|
34
|
+
commentOnActive(): void;
|
|
35
|
+
/** Approve, through its confirmation; and the two writes `s` can stand for. */
|
|
36
|
+
askApprove(): void;
|
|
37
|
+
save(): void;
|
|
38
|
+
postComments(): void;
|
|
39
|
+
/** Whether a control exists for the key to stand for. */
|
|
40
|
+
hasSaveControl(): boolean;
|
|
41
|
+
hasApproveControl(): boolean;
|
|
42
|
+
/** Whether the key landed in a field, where every key is text. */
|
|
43
|
+
typing(target: unknown): boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The keys a reader can press, as far as this page is concerned. */
|
|
47
|
+
export interface KeyEvent {
|
|
48
|
+
key: string;
|
|
49
|
+
metaKey?: boolean;
|
|
50
|
+
ctrlKey?: boolean;
|
|
51
|
+
altKey?: boolean;
|
|
52
|
+
target?: unknown;
|
|
53
|
+
preventDefault(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Handle one keydown.
|
|
58
|
+
*
|
|
59
|
+
* A modified key is never a shortcut: `ctrl+s` belongs to the browser. A field owns the
|
|
60
|
+
* keyboard while it has focus, and so does the shortcut sheet while it is open.
|
|
61
|
+
*/
|
|
62
|
+
export function handleKey(event: KeyEvent, deps: KeyDeps): void {
|
|
63
|
+
if (event.key === "Escape") {
|
|
64
|
+
// The sheet owns its own Esc; this peels the composer, then the review.
|
|
65
|
+
if (deps.composerOpen()) {
|
|
66
|
+
deps.closeComposer();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
deps.cancel();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (deps.typing(event.target)) return;
|
|
73
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
74
|
+
if (event.key === "?") {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
deps.toggleSheet();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// While the shortcut sheet is open it owns the keyboard.
|
|
80
|
+
if (deps.sheetOpen()) return;
|
|
81
|
+
if (event.key === "j") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
deps.moveActive(1);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event.key === "k") {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
deps.moveActive(-1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (event.key === "c") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
deps.commentOnActive();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (event.key === "a" && deps.hasApproveControl()) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
deps.askApprove();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (event.key === "s") {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
if (deps.hasSaveControl()) deps.save();
|
|
104
|
+
else deps.postComments();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What remembering a selection reads, and where it puts it. */
|
|
109
|
+
export interface QuoteDeps {
|
|
110
|
+
planEl: ClientNode | null;
|
|
111
|
+
/** A settled review takes no more comments, so it remembers no selection. */
|
|
112
|
+
settled(): boolean;
|
|
113
|
+
/** The text a reader selected inside one block. */
|
|
114
|
+
selectionIn(wrap: ClientNode): string;
|
|
115
|
+
/** Remember it, or forget the last one when the release selected nothing. */
|
|
116
|
+
setPending(quote: { wrap: ClientNode; quote: string } | null): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remember the text a reader selected, so the rail button can still quote it.
|
|
121
|
+
*
|
|
122
|
+
* A release outside a block, or inside a block whose composer is open, clears what was
|
|
123
|
+
* remembered: the next click must not quote text from somewhere the reader has left.
|
|
124
|
+
*/
|
|
125
|
+
export function rememberSelection(target: unknown, deps: QuoteDeps): void {
|
|
126
|
+
if (!deps.planEl || deps.settled()) return;
|
|
127
|
+
const node = target as { closest?: (selector: string) => ClientNode | null } | null;
|
|
128
|
+
const wrap = node && typeof node.closest === "function" ? node.closest(".bf-block") : null;
|
|
129
|
+
const quote = wrap && !wrap.querySelector(".bf-composer") ? deps.selectionIn(wrap) : "";
|
|
130
|
+
deps.setPending(wrap && quote ? { wrap, quote } : null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The keymap and the quote rule as source, for the client bundle. */
|
|
134
|
+
export const WEB_KEYS_JS = [handleKey, rememberSelection].map(String).join("\n\n");
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's outline: the section list in the sidebar, and the one rule that says
|
|
3
|
+
* which section a source line belongs to.
|
|
4
|
+
*
|
|
5
|
+
* Written as ordinary TypeScript and exported as source — `WEB_OUTLINE_JS` — because the
|
|
6
|
+
* review client is a template literal no compiler sees. The two functions are given what
|
|
7
|
+
* they read rather than closing over the bundle's variables, which is what makes them
|
|
8
|
+
* testable here: `renderOutline` returns the per-line counters it built instead of
|
|
9
|
+
* assigning the client's `outlineItems`, and `sectionFor` takes the outline it searches.
|
|
10
|
+
*
|
|
11
|
+
* `el` is the client runtime's builder, declared here because this source is spliced into
|
|
12
|
+
* the same bundle it lives in: the declaration emits nothing, so the body calls the one
|
|
13
|
+
* builder. The nodes are reached through a structural type because this project compiles
|
|
14
|
+
* without a DOM lib; the annotations are erased before the source is spliced.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
18
|
+
|
|
19
|
+
/** One entry of the page's `outline` island: a heading, its line, and whether it moved. */
|
|
20
|
+
export interface OutlineEntry {
|
|
21
|
+
id: string;
|
|
22
|
+
level: number;
|
|
23
|
+
line: number;
|
|
24
|
+
text: string;
|
|
25
|
+
changed?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What the browser's `IntersectionObserver` answers for a section watcher. */
|
|
29
|
+
export interface SectionWatcher {
|
|
30
|
+
observe(target: ClientNode): void;
|
|
31
|
+
disconnect(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What `el` has to answer for an outline. Spliced beside it; declared for the checker. */
|
|
35
|
+
type Build = (tag: string, className?: string, text?: string | null) => ClientNode;
|
|
36
|
+
declare const el: Build;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The section a source line belongs to: the last heading at or before it.
|
|
40
|
+
*
|
|
41
|
+
* `null` when the line sits above the first heading, which is how a preamble renders
|
|
42
|
+
* without a section of its own.
|
|
43
|
+
*/
|
|
44
|
+
export function sectionFor(entries: readonly OutlineEntry[], line: number): OutlineEntry | null {
|
|
45
|
+
let found: OutlineEntry | null = null;
|
|
46
|
+
for (const item of entries) if (item.line <= line) found = item;
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Draw the section list and return the counter each line owns.
|
|
52
|
+
*
|
|
53
|
+
* The caller keeps the returned map because the counters are updated later, when the
|
|
54
|
+
* comments are counted per section; returning it is what lets this function be a pure
|
|
55
|
+
* build with no handle on the client's state.
|
|
56
|
+
*/
|
|
57
|
+
export function renderOutline(list: ClientNode | null, entries: readonly OutlineEntry[]): Record<number, ClientNode> {
|
|
58
|
+
const counters: Record<number, ClientNode> = {};
|
|
59
|
+
if (!list) return counters;
|
|
60
|
+
list.replaceChildren();
|
|
61
|
+
for (const item of entries) {
|
|
62
|
+
const li = el("li", "lvl-" + item.level);
|
|
63
|
+
const link = el("a");
|
|
64
|
+
link.href = "#" + item.id;
|
|
65
|
+
link.setAttribute("data-id", item.id);
|
|
66
|
+
const label = el("span", "review-nav__link");
|
|
67
|
+
label.textContent = item.text;
|
|
68
|
+
link.appendChild(label);
|
|
69
|
+
if (item.changed) {
|
|
70
|
+
const dot = el("span", "review-changed");
|
|
71
|
+
dot.setAttribute("role", "img");
|
|
72
|
+
dot.setAttribute("aria-label", "Changed since your last review");
|
|
73
|
+
dot.title = "Changed since your last review";
|
|
74
|
+
link.appendChild(dot);
|
|
75
|
+
}
|
|
76
|
+
const count = el("span", "review-nav__count");
|
|
77
|
+
count.hidden = true;
|
|
78
|
+
link.appendChild(count);
|
|
79
|
+
li.appendChild(link);
|
|
80
|
+
list.appendChild(li);
|
|
81
|
+
counters[item.line] = count;
|
|
82
|
+
}
|
|
83
|
+
return counters;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Watch the blocks and mark the section the reader is in, replacing any previous watcher.
|
|
88
|
+
*
|
|
89
|
+
* The observer is returned rather than stored, so the client owns the one handle it has to
|
|
90
|
+
* disconnect and this function owns the rule: the last block still in the document the
|
|
91
|
+
* observer was built over wins, and a page with no headings needs no watcher at all.
|
|
92
|
+
*/
|
|
93
|
+
export function observeSections(
|
|
94
|
+
planEl: ClientNode | null,
|
|
95
|
+
list: ClientNode | null,
|
|
96
|
+
previous: SectionWatcher | null,
|
|
97
|
+
): SectionWatcher | null {
|
|
98
|
+
if (previous) previous.disconnect();
|
|
99
|
+
if (!planEl || !list) return null;
|
|
100
|
+
const Observer = (
|
|
101
|
+
globalThis as unknown as {
|
|
102
|
+
IntersectionObserver?: new (
|
|
103
|
+
callback: (entries: ArrayLike<{ isIntersecting: boolean; target: { id: string } }>) => void,
|
|
104
|
+
options: Record<string, unknown>,
|
|
105
|
+
) => SectionWatcher;
|
|
106
|
+
}
|
|
107
|
+
).IntersectionObserver;
|
|
108
|
+
if (!Observer) return null;
|
|
109
|
+
const links: Record<string, ClientNode> = {};
|
|
110
|
+
const anchors = list.querySelectorAll("a");
|
|
111
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
112
|
+
const link = anchors[i];
|
|
113
|
+
if (link) links[link.getAttribute("data-id") ?? ""] = link;
|
|
114
|
+
}
|
|
115
|
+
const headings = planEl.querySelectorAll(".bf-block[id]");
|
|
116
|
+
if (headings.length === 0) return null;
|
|
117
|
+
const watcher = new Observer(
|
|
118
|
+
(entries) => {
|
|
119
|
+
for (let i = 0; i < entries.length; i++) {
|
|
120
|
+
const entry = entries[i];
|
|
121
|
+
if (!entry || !entry.isIntersecting) continue;
|
|
122
|
+
const all = list.querySelectorAll("a");
|
|
123
|
+
for (let j = 0; j < all.length; j++) all[j]?.removeAttribute("aria-current");
|
|
124
|
+
const link = links[entry.target.id];
|
|
125
|
+
if (link) link.setAttribute("aria-current", "true");
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ rootMargin: "-88px 0px -68% 0px", threshold: 0 },
|
|
129
|
+
);
|
|
130
|
+
for (let i = 0; i < headings.length; i++) {
|
|
131
|
+
const heading = headings[i];
|
|
132
|
+
if (heading) watcher.observe(heading);
|
|
133
|
+
}
|
|
134
|
+
return watcher;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One review comment, as the facts need it: the line it is anchored to. */
|
|
138
|
+
export interface AnchoredComment {
|
|
139
|
+
anchor: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** What a plan's step list says: the numbers the server counted. */
|
|
143
|
+
export interface StepProgress {
|
|
144
|
+
done: number;
|
|
145
|
+
total: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Paint the review facts: the plan's step progress, the comment total, and one count per
|
|
150
|
+
* section in the outline.
|
|
151
|
+
*
|
|
152
|
+
* The counters are the ones `renderOutline` returned, so the two functions agree by
|
|
153
|
+
* construction; a section a comment cannot be placed in is skipped rather than counted
|
|
154
|
+
* against the first heading, which is what "not attached to a current block" means.
|
|
155
|
+
*/
|
|
156
|
+
export function renderFacts(
|
|
157
|
+
steps: StepProgress,
|
|
158
|
+
comments: readonly AnchoredComment[],
|
|
159
|
+
counters: Record<number, ClientNode>,
|
|
160
|
+
entries: readonly OutlineEntry[],
|
|
161
|
+
): void {
|
|
162
|
+
const doc = (
|
|
163
|
+
globalThis as unknown as {
|
|
164
|
+
document: { getElementById(id: string): ClientNode | null };
|
|
165
|
+
}
|
|
166
|
+
).document;
|
|
167
|
+
// A plan with no Steps section has no progress to show; 0/0 is noise.
|
|
168
|
+
const fact = doc.getElementById("step-fact");
|
|
169
|
+
if (fact) fact.hidden = steps.total === 0;
|
|
170
|
+
const fill = doc.getElementById("step-fill");
|
|
171
|
+
if (fill) fill.style.width = (steps.total > 0 ? Math.round((steps.done / steps.total) * 100) : 0) + "%";
|
|
172
|
+
const count = doc.getElementById("step-count");
|
|
173
|
+
if (count) count.textContent = steps.done + "/" + steps.total;
|
|
174
|
+
const total = doc.getElementById("comment-total");
|
|
175
|
+
if (total) total.textContent = String(comments.length);
|
|
176
|
+
for (const line of Object.keys(counters)) {
|
|
177
|
+
const node = counters[Number(line)];
|
|
178
|
+
if (node) node.hidden = true;
|
|
179
|
+
}
|
|
180
|
+
for (const comment of comments) {
|
|
181
|
+
const section = sectionFor(entries, comment.anchor);
|
|
182
|
+
if (!section) continue;
|
|
183
|
+
const node = counters[section.line];
|
|
184
|
+
if (!node) continue;
|
|
185
|
+
const value = node.hidden ? 0 : Number(node.textContent);
|
|
186
|
+
node.textContent = String(value + 1);
|
|
187
|
+
node.hidden = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The section list, the facts beside it, and the watcher that marks where the reader is. */
|
|
192
|
+
export const WEB_OUTLINE_JS = [sectionFor, renderOutline, observeSections, renderFacts].map(String).join("\n\n");
|
|
@@ -19,13 +19,13 @@ import {
|
|
|
19
19
|
type KeyHint,
|
|
20
20
|
keysSheet,
|
|
21
21
|
SIDEBAR_COLLAPSE_MAX,
|
|
22
|
+
tabTitle,
|
|
22
23
|
themeToggle,
|
|
23
24
|
WEB_BASE_CSS,
|
|
24
25
|
WEB_COMPONENTS_CSS,
|
|
25
26
|
WEB_LAYOUT_CSS,
|
|
26
27
|
WEB_THEME_CSS,
|
|
27
28
|
} from "../../lib/web/index.ts";
|
|
28
|
-
import { escapeHtml } from "../../lib/escape.ts";
|
|
29
29
|
import type { PlanBlock } from "./blocks.ts";
|
|
30
30
|
import type { PlanAnnotation } from "./feedback.ts";
|
|
31
31
|
import type { ReviewRole } from "./types.ts";
|
|
@@ -78,11 +78,6 @@ export interface ReviewPageData {
|
|
|
78
78
|
* whether its name control is still a choice.
|
|
79
79
|
*/
|
|
80
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
81
|
/** A running share, so a reload re-renders the link the owner already minted. */
|
|
87
82
|
share?: ShareState;
|
|
88
83
|
/** Per-session secret the client echoes on every request. */
|
|
@@ -185,6 +180,8 @@ ${WEB_COMPONENTS_CSS}
|
|
|
185
180
|
.bf-comment__label{font:var(--fw-semibold) var(--fs-xs) var(--font-mono);color:var(--warn);text-transform:uppercase;letter-spacing:.06em}
|
|
186
181
|
/* A reviewer's name is a name, not a label: keep its own capitalisation. */
|
|
187
182
|
.bf-comment__label--author{text-transform:none;letter-spacing:0}
|
|
183
|
+
/* A note a refinement carried off its own block: muted, because the complaint is the guess, not the text. */
|
|
184
|
+
.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}
|
|
188
185
|
.bf-comment__spacer{flex:1 1 auto}
|
|
189
186
|
.bf-comment__actions{display:flex;gap:4px}
|
|
190
187
|
.bf-comment__actions .bf-btn{padding:1px 8px;font-size:var(--fs-xs)}
|
|
@@ -322,7 +319,6 @@ export function renderReviewPage(data: ReviewPageData): string {
|
|
|
322
319
|
<div class="bf-panel review-panel">
|
|
323
320
|
${reviewFacts()}
|
|
324
321
|
<nav class="review-nav" aria-labelledby="outline-title"><p class="review-nav__title" id="outline-title">Sections</p><ul id="outline-list"></ul></nav>
|
|
325
|
-
${data.note ? `<p class="review-hint">${escapeHtml(data.note)}</p>` : ""}
|
|
326
322
|
${reviewHints(data.role, data.canApprove)}
|
|
327
323
|
</div>
|
|
328
324
|
</details>
|
|
@@ -348,7 +344,7 @@ ${reviewHints(data.role, data.canApprove)}
|
|
|
348
344
|
return `<!doctype html>
|
|
349
345
|
<html lang="en">
|
|
350
346
|
<head>
|
|
351
|
-
${docHead({ title: "
|
|
347
|
+
${docHead({ title: tabTitle("plan review", data.title), css: REVIEW_CSS })}
|
|
352
348
|
</head>
|
|
353
349
|
<body class="bf-app">
|
|
354
350
|
<a class="skip" href="#plan">Skip to the plan</a>
|