@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
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
* saved back to the server.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
CLIENT_BASE_JS,
|
|
24
|
+
CLIENT_RUNTIME_JS,
|
|
25
|
+
dialogScript,
|
|
26
|
+
INLINE_MARKDOWN_JS,
|
|
27
|
+
keysDialogScript,
|
|
28
|
+
THEME_TOGGLE_JS,
|
|
29
|
+
} from "../../lib/web/index.ts";
|
|
23
30
|
import { WEB_CHROME_JS } from "./web-chrome.ts";
|
|
24
31
|
import { WEB_COMMENTS_JS } from "./web-comments.ts";
|
|
25
32
|
import { WEB_COLLAB_JS } from "./web-collab.ts";
|
|
@@ -34,6 +41,7 @@ import { WEB_SETTLE_JS } from "./web-settle.ts";
|
|
|
34
41
|
|
|
35
42
|
export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
36
43
|
"use strict";
|
|
44
|
+
${CLIENT_BASE_JS}
|
|
37
45
|
|
|
38
46
|
var dataNode = document.getElementById("review-data");
|
|
39
47
|
// The caps come from the island, which takes them from the server's own
|
|
@@ -67,12 +75,19 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
67
75
|
var sectionObserver = null;
|
|
68
76
|
var pendingQuote = null;
|
|
69
77
|
var activeIndex = -1;
|
|
78
|
+
/** What the reader asked the plan to show: every block, the changed ones, or the annotated. */
|
|
79
|
+
var view = "all";
|
|
70
80
|
var outlineItems = {};
|
|
71
81
|
/** The island's blocks, or none: one place the client reads them for navigation. */
|
|
72
82
|
function blocks() {
|
|
73
83
|
return Array.isArray(data.blocks) ? data.blocks : [];
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
/** The island's changed blocks, or none: the whole list is scanned, because it is short. */
|
|
87
|
+
function changedBlocks() {
|
|
88
|
+
return Array.isArray(data.changed) ? data.changed : [];
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
/** The island's outline, or none: the one place the client reads it for the outline. */
|
|
77
92
|
function outlineEntries() {
|
|
78
93
|
return Array.isArray(data.outline) ? data.outline : [];
|
|
@@ -111,6 +126,8 @@ ${WEB_COMMENTS_JS}
|
|
|
111
126
|
|
|
112
127
|
${WEB_COLLAB_JS}
|
|
113
128
|
|
|
129
|
+
${INLINE_MARKDOWN_JS}
|
|
130
|
+
|
|
114
131
|
${WEB_MARKDOWN_JS}
|
|
115
132
|
|
|
116
133
|
// Review facts: the plan's step progress, and the comments per section.
|
|
@@ -132,6 +149,7 @@ ${WEB_MARKDOWN_JS}
|
|
|
132
149
|
planEl: planEl,
|
|
133
150
|
statusEl: statusEl,
|
|
134
151
|
refusal: DRAFT_REFUSAL,
|
|
152
|
+
settled: function () { return saved || approved || closed; },
|
|
135
153
|
setStatus: setStatus,
|
|
136
154
|
saveDraft: saveDraft,
|
|
137
155
|
open: openComposer,
|
|
@@ -159,11 +177,13 @@ ${WEB_MARKDOWN_JS}
|
|
|
159
177
|
outlineList: outlineList,
|
|
160
178
|
focused: document.activeElement,
|
|
161
179
|
blocks: blocks,
|
|
180
|
+
changed: function (line) { return changedBlocks().indexOf(line) >= 0; },
|
|
162
181
|
annotations: function () { return annotations; },
|
|
163
182
|
activeIndex: function () { return activeIndex; },
|
|
164
183
|
setActiveIndex: function (index) { activeIndex = index; },
|
|
165
184
|
pendingQuote: function () { return pendingQuote; },
|
|
166
185
|
clearPendingQuote: function () { pendingQuote = null; },
|
|
186
|
+
settled: function () { return saved || approved || closed; },
|
|
167
187
|
composer: composer,
|
|
168
188
|
renderBlock: renderBlockContent,
|
|
169
189
|
renderComments: renderComments,
|
|
@@ -175,8 +195,72 @@ ${WEB_MARKDOWN_JS}
|
|
|
175
195
|
};
|
|
176
196
|
}
|
|
177
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Apply the current view and say what it hides.
|
|
200
|
+
*
|
|
201
|
+
* The count goes on the status line rather than into the document: a filtered plan with
|
|
202
|
+
* no note reads as a shorter plan, and the count is the one fact that undoes that.
|
|
203
|
+
*/
|
|
204
|
+
function syncView() {
|
|
205
|
+
var counts = applyView(planEl, view);
|
|
206
|
+
var buttons = document.querySelectorAll("[data-view]");
|
|
207
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
208
|
+
var button = buttons[i];
|
|
209
|
+
var name = button.getAttribute("data-view");
|
|
210
|
+
button.setAttribute("aria-pressed", name === view ? "true" : "false");
|
|
211
|
+
// Nothing to compare against is not an empty view: the button says why it is off.
|
|
212
|
+
if (name === "changed") {
|
|
213
|
+
var comparable = data.blockMarks === true;
|
|
214
|
+
button.disabled = !comparable;
|
|
215
|
+
button.title = comparable ? "Only what this review changed" : "No earlier review of this plan to compare against";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// The page's own wrapper owns the element: the shared paint takes the node first, so
|
|
219
|
+
// the sentence goes through setStatus rather than being handed over as one. A settled
|
|
220
|
+
// review's status line carries how it ended, not the filter's count.
|
|
221
|
+
if (counts.hidden > 0 && !saved && !approved && !closed)
|
|
222
|
+
setStatus("Showing " + counts.shown + " of " + (counts.shown + counts.hidden) + " blocks — the rest hidden");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Walk from the active block to the next one that carries a comment.
|
|
227
|
+
*
|
|
228
|
+
* The block order is the document's, so "next" means further down the plan, not the next
|
|
229
|
+
* comment by arrival: a reader walking notes is reading the document. A filtered view can
|
|
230
|
+
* hide a commented block, and a hidden one is not a place to send the reader, so the step
|
|
231
|
+
* skips it the way the block walk does. Nothing repaints, so no draft is at risk.
|
|
232
|
+
*/
|
|
233
|
+
function moveComment(delta) {
|
|
234
|
+
var list = blockElements(planEl);
|
|
235
|
+
var start = activeIndex >= 0 ? activeIndex : delta > 0 ? -1 : list.length;
|
|
236
|
+
for (var step = start + delta; step >= 0 && step < list.length; step += delta) {
|
|
237
|
+
var block = list[step];
|
|
238
|
+
if (!block || block.hidden === true) continue;
|
|
239
|
+
if ((block.className || "").split(" ").indexOf("bf-block--commented") < 0) continue;
|
|
240
|
+
var next = setActive(planEl, step);
|
|
241
|
+
if (next !== null) activeIndex = next;
|
|
242
|
+
if (block.scrollIntoView) block.scrollIntoView({ block: "center" });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Cycle the panel's views, skipping the one this page cannot offer.
|
|
249
|
+
*
|
|
250
|
+
* The repaint closes the composer, so a half-written comment refuses first, the way the
|
|
251
|
+
* view buttons do.
|
|
252
|
+
*/
|
|
253
|
+
function cycleView() {
|
|
254
|
+
if (confirmNoDraft()) return;
|
|
255
|
+
var order = data.blockMarks === true ? ["all", "changed", "comments"] : ["all", "comments"];
|
|
256
|
+
var at = order.indexOf(view);
|
|
257
|
+
view = order[(at + 1) % order.length] || "all";
|
|
258
|
+
renderAll();
|
|
259
|
+
}
|
|
260
|
+
|
|
178
261
|
function renderAll() {
|
|
179
262
|
sectionObserver = renderPlanFrame(planFrame(), closeOpenComposer, observeSections, sectionObserver);
|
|
263
|
+
syncView();
|
|
180
264
|
}
|
|
181
265
|
|
|
182
266
|
// Save, approve, and post share the same POST shape; what differs is whether
|
|
@@ -199,6 +283,7 @@ ${WEB_MARKDOWN_JS}
|
|
|
199
283
|
function settlement() {
|
|
200
284
|
return {
|
|
201
285
|
token: function () { return token; },
|
|
286
|
+
base: pageBase,
|
|
202
287
|
role: function () { return role; },
|
|
203
288
|
mine: function () { return annotations.filter(isMine); },
|
|
204
289
|
annotations: function () { return annotations; },
|
|
@@ -319,6 +404,8 @@ ${WEB_MARKDOWN_JS}
|
|
|
319
404
|
var next = moveActive(planEl, activeIndex, delta);
|
|
320
405
|
if (next !== null) activeIndex = next;
|
|
321
406
|
},
|
|
407
|
+
moveComment: moveComment,
|
|
408
|
+
cycleView: cycleView,
|
|
322
409
|
commentOnActive: function () { activeIndex = commentOnActive(composer(), activeIndex, blocks()); },
|
|
323
410
|
askApprove: askApprove,
|
|
324
411
|
save: save,
|
|
@@ -329,6 +416,19 @@ ${WEB_MARKDOWN_JS}
|
|
|
329
416
|
};
|
|
330
417
|
}
|
|
331
418
|
|
|
419
|
+
var viewButtons = document.querySelectorAll("[data-view]");
|
|
420
|
+
for (var v = 0; v < viewButtons.length; v++) {
|
|
421
|
+
viewButtons[v].addEventListener("click", function (event) {
|
|
422
|
+
var next = event.target && event.target.getAttribute ? event.target.getAttribute("data-view") : null;
|
|
423
|
+
if (next !== "all" && next !== "changed" && next !== "comments") return;
|
|
424
|
+
// The filter repaints the document, which closes the composer: a half-written
|
|
425
|
+
// comment would go with it, so the click refuses the way the ways off the page do.
|
|
426
|
+
if (confirmNoDraft()) return;
|
|
427
|
+
view = next;
|
|
428
|
+
renderAll();
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
332
432
|
document.addEventListener("keydown", function (event) {
|
|
333
433
|
handleKey(event, keyActions());
|
|
334
434
|
});
|
|
@@ -344,7 +444,7 @@ ${WEB_MARKDOWN_JS}
|
|
|
344
444
|
if (planEl) planEl.addEventListener("scroll", updateProgress);
|
|
345
445
|
|
|
346
446
|
try {
|
|
347
|
-
stream = new EventSource("/events?token=" + encodeURIComponent(token));
|
|
447
|
+
stream = new EventSource(pageBase() + "/events?token=" + encodeURIComponent(token));
|
|
348
448
|
stream.addEventListener("open", function () { connected = true; everConnected = true; updateState(); });
|
|
349
449
|
stream.addEventListener("message", function (event) { refreshFromServer(event); });
|
|
350
450
|
stream.addEventListener("error", function () {
|
|
@@ -88,19 +88,19 @@ export const WEB_COLLAB_JS = String.raw` /**
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function shareNow() {
|
|
91
|
-
shareAction("/api/share", null, "Could not share: ");
|
|
91
|
+
shareAction(pageBase() + "/api/share", null, "Could not share: ");
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function newLink() {
|
|
95
|
-
shareAction("/api/share/link", null, "Could not mint a link: ");
|
|
95
|
+
shareAction(pageBase() + "/api/share/link", null, "Could not mint a link: ");
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function revokeLink(id) {
|
|
99
|
-
shareAction("/api/share/revoke", { id: id }, "Could not revoke that link: ");
|
|
99
|
+
shareAction(pageBase() + "/api/share/revoke", { id: id }, "Could not revoke that link: ");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function stopSharing() {
|
|
103
|
-
fetch("/api/share/stop?token=" + encodeURIComponent(token), { method: "POST" })
|
|
103
|
+
fetch(pageBase() + "/api/share/stop?token=" + encodeURIComponent(token), { method: "POST" })
|
|
104
104
|
.then(function (response) { return response.json(); })
|
|
105
105
|
.then(function (payload) {
|
|
106
106
|
if (payload && payload.share) showShare(payload.share, null);
|
|
@@ -19,6 +19,8 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
19
19
|
var next = wrap.nextElementSibling;
|
|
20
20
|
if (next && next.classList && next.classList.contains("bf-comments")) return next;
|
|
21
21
|
var box = el("div", "bf-comments");
|
|
22
|
+
// The line it belongs to, so a filtered view can hide the thread with its block.
|
|
23
|
+
box.setAttribute("data-line", wrap.getAttribute("data-line") || "");
|
|
22
24
|
wrap.insertAdjacentElement("afterend", box);
|
|
23
25
|
return box;
|
|
24
26
|
}
|
|
@@ -112,6 +114,10 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
function openComposer(wrap, block, quote, initial) {
|
|
117
|
+
// One composer at a time, and the open one holds text the page has not committed:
|
|
118
|
+
// replacing it would drop that text without a word. A restored draft passes its text
|
|
119
|
+
// as the initial value, because it is the thing being reopened, not a draft to protect.
|
|
120
|
+
if (!initial && guardDraft(planEl, statusEl, DRAFT_REFUSAL, paintStatus)) return;
|
|
115
121
|
closeOpenComposer();
|
|
116
122
|
wrap.classList.add("bf-block--composing");
|
|
117
123
|
var composer = el("div", "bf-composer");
|
|
@@ -135,7 +141,9 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
135
141
|
cancel.addEventListener("click", closeOpenComposer);
|
|
136
142
|
var add = el("button"); add.type = "button"; add.className = "bf-btn bf-btn--primary"; add.textContent = "Add comment";
|
|
137
143
|
var submit = function () {
|
|
138
|
-
|
|
144
|
+
// A settled review takes no new comment: the note could never reach the plan, so
|
|
145
|
+
// adding it would report "Comment added" about something nobody will read.
|
|
146
|
+
if (saved || approved || closed) { setStatus("This review has ended; the comment was not added.", "error"); return; }
|
|
139
147
|
if (role === "reviewer" && !reviewerName) { askName(); return; }
|
|
140
148
|
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
141
149
|
if (!comment) { area.focus(); return; }
|
|
@@ -232,10 +240,12 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
232
240
|
function renderCommentCards(box, list) {
|
|
233
241
|
list.forEach(function (annotation) {
|
|
234
242
|
var index = annotations.indexOf(annotation);
|
|
235
|
-
var card = el("div", "bf-comment");
|
|
243
|
+
var card = el("div", annotation.resolved ? "bf-comment bf-comment--resolved" : "bf-comment");
|
|
236
244
|
var head = el("div", "bf-comment__head");
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
// Every card names a person: "Review" was a category, and on a reviewer's page it
|
|
246
|
+
// could be anybody — including the reader.
|
|
247
|
+
var label = el("span", "bf-comment__label bf-comment__label--author");
|
|
248
|
+
label.textContent = annotation.author ? "@" + annotation.author : role === "owner" ? "You" : "The owner";
|
|
239
249
|
head.appendChild(label);
|
|
240
250
|
if (annotation.moved) {
|
|
241
251
|
// A refinement carried this note off the text it was written on: the placement is
|
|
@@ -247,14 +257,31 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
247
257
|
"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
258
|
head.appendChild(moved);
|
|
249
259
|
}
|
|
260
|
+
if (annotation.resolved) {
|
|
261
|
+
// Settled notes keep their thread and their quote, and lose their body: the record
|
|
262
|
+
// stays, the reader's eye lands on what is still open.
|
|
263
|
+
var settled = el("span", "bf-comment__resolved");
|
|
264
|
+
settled.textContent = "\u2713 settled";
|
|
265
|
+
settled.title = "Settled: kept in the plan as the record of the question";
|
|
266
|
+
head.appendChild(settled);
|
|
267
|
+
}
|
|
250
268
|
head.appendChild(el("span", "bf-comment__spacer"));
|
|
251
269
|
if (isMine(annotation)) {
|
|
252
270
|
var actions = el("div", "bf-comment__actions");
|
|
253
271
|
var edit = el("button"); edit.type = "button"; edit.className = "bf-btn"; edit.textContent = "Edit";
|
|
254
272
|
edit.addEventListener("click", function () { startEdit(card, index); });
|
|
273
|
+
var settle = el("button"); settle.type = "button"; settle.className = "bf-btn";
|
|
274
|
+
settle.textContent = annotation.resolved ? "Reopen" : "Resolve";
|
|
275
|
+
settle.addEventListener("click", function () {
|
|
276
|
+
if (annotation.resolved) delete annotation.resolved;
|
|
277
|
+
else annotation.resolved = true;
|
|
278
|
+
markDirty();
|
|
279
|
+
renderAll();
|
|
280
|
+
setStatus(annotation.resolved ? "Comment resolved." : "Comment reopened.");
|
|
281
|
+
});
|
|
255
282
|
var remove = el("button"); remove.type = "button"; remove.className = "bf-btn"; remove.textContent = "Delete";
|
|
256
283
|
remove.addEventListener("click", function () { removeComment(index); });
|
|
257
|
-
actions.appendChild(edit); actions.appendChild(remove); head.appendChild(actions);
|
|
284
|
+
actions.appendChild(settle); actions.appendChild(edit); actions.appendChild(remove); head.appendChild(actions);
|
|
258
285
|
}
|
|
259
286
|
card.appendChild(head);
|
|
260
287
|
if (annotation.quote) {
|
|
@@ -262,9 +289,11 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
262
289
|
quote.textContent = "\u201c" + annotation.quote + "\u201d";
|
|
263
290
|
card.appendChild(quote);
|
|
264
291
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
292
|
+
if (!annotation.resolved) {
|
|
293
|
+
var body = el("div", "bf-comment__body");
|
|
294
|
+
body.textContent = annotation.comment;
|
|
295
|
+
card.appendChild(body);
|
|
296
|
+
}
|
|
268
297
|
box.appendChild(card);
|
|
269
298
|
});
|
|
270
299
|
}
|
|
@@ -273,6 +302,8 @@ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
|
|
|
273
302
|
var mine = annotations.filter(function (annotation) { return annotation.anchor === anchor; });
|
|
274
303
|
if (mine.length === 0) return;
|
|
275
304
|
var box = el("div", "bf-comments");
|
|
305
|
+
// The block this thread hangs under, as a line: the view filter hides the two together.
|
|
306
|
+
box.setAttribute("data-line", String(anchor));
|
|
276
307
|
renderCommentCards(box, mine);
|
|
277
308
|
container.appendChild(box);
|
|
278
309
|
}
|
|
@@ -21,16 +21,22 @@ export interface ComposerDeps {
|
|
|
21
21
|
statusEl: ClientNode | null;
|
|
22
22
|
/** The sentence that names a half-written comment, so a stale one can be cleared. */
|
|
23
23
|
refusal: string;
|
|
24
|
+
/** Whether the review has ended, so a key opens no composer for it. */
|
|
25
|
+
settled?(): boolean;
|
|
24
26
|
setStatus(text: string, kind?: string): void;
|
|
25
27
|
saveDraft(): void;
|
|
26
28
|
open: OpenComposer;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
* Close the composer, and
|
|
32
|
+
* Close the composer, and drop the text it was holding.
|
|
31
33
|
*
|
|
32
|
-
* A close
|
|
33
|
-
* the
|
|
34
|
+
* A close is the reader's Cancel or Esc, or the commit that turned the text into a
|
|
35
|
+
* comment: either way the composer is not somewhere to resume from, so the draft does not
|
|
36
|
+
* keep it. What protects a half-written comment is the guards on the ways out — a repaint
|
|
37
|
+
* that would close a composer asks first (`guardDraft`), the view control included — not
|
|
38
|
+
* this function. A close with nothing open must not rewrite, and so drop, a draft a reload
|
|
39
|
+
* restored.
|
|
34
40
|
*/
|
|
35
41
|
export function closeComposer(deps: ComposerDeps): void {
|
|
36
42
|
const open = deps.planEl ? deps.planEl.querySelector(".bf-composer") : null;
|
|
@@ -40,6 +46,8 @@ export function closeComposer(deps: ComposerDeps): void {
|
|
|
40
46
|
// The only status a composer leaves behind is the refusal to leave it; once the draft is
|
|
41
47
|
// gone that sentence is stale.
|
|
42
48
|
if (deps.statusEl && deps.statusEl.textContent === deps.refusal) deps.setStatus("");
|
|
49
|
+
// The composer is gone, so recomputing the draft now is what forgets the text it held —
|
|
50
|
+
// a close is not a pause — while the comments this writer added stay in it.
|
|
43
51
|
if (open) deps.saveDraft();
|
|
44
52
|
}
|
|
45
53
|
|
|
@@ -67,6 +75,9 @@ export function reopenComposer(
|
|
|
67
75
|
* navigation, and the block the reader lands on is the block they are commenting on.
|
|
68
76
|
*/
|
|
69
77
|
export function commentOnActive(deps: ComposerDeps, activeIndex: number, blocks: readonly IslandBlock[]): number {
|
|
78
|
+
// A settled review takes no new comment, so the key opens nothing rather than a
|
|
79
|
+
// composer whose note could never be saved.
|
|
80
|
+
if (deps.settled?.() === true) return activeIndex;
|
|
70
81
|
if (blockElements(deps.planEl).length === 0) return activeIndex;
|
|
71
82
|
const index = activeIndex < 0 ? (setActive(deps.planEl, 0) ?? activeIndex) : activeIndex;
|
|
72
83
|
const wrap = blockElements(deps.planEl)[Math.max(0, index)];
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
20
|
+
import type { ReviewRole } from "./types.ts";
|
|
20
21
|
|
|
21
22
|
/** What a key may do: one callback per action the page owns. */
|
|
22
23
|
export interface KeyDeps {
|
|
@@ -30,6 +31,10 @@ export interface KeyDeps {
|
|
|
30
31
|
toggleSheet(): void;
|
|
31
32
|
/** Walk the blocks by one, in the direction the key names. */
|
|
32
33
|
moveActive(delta: number): void;
|
|
34
|
+
/** Walk the blocks that carry a comment, in the direction the key names. */
|
|
35
|
+
moveComment?(delta: number): void;
|
|
36
|
+
/** Cycle what the page shows: everything, the changes, the comments. */
|
|
37
|
+
cycleView?(): void;
|
|
33
38
|
/** Comment on the block the keyboard is on. */
|
|
34
39
|
commentOnActive(): void;
|
|
35
40
|
/** Approve, through its confirmation; and the two writes `s` can stand for. */
|
|
@@ -53,11 +58,142 @@ export interface KeyEvent {
|
|
|
53
58
|
preventDefault(): void;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/** What the page offers, for the rows whose sentence or availability depends on it. */
|
|
62
|
+
export interface KeySurface {
|
|
63
|
+
/** Whose page it is: the owner saves and closes, a reviewer posts. */
|
|
64
|
+
role: ReviewRole;
|
|
65
|
+
/** Whether this review may approve the plan. */
|
|
66
|
+
canApprove: boolean;
|
|
67
|
+
/** Whether the page wired the comment walk (`n`/`N`), so the sheet may name it. */
|
|
68
|
+
hasCommentWalk: boolean;
|
|
69
|
+
/** Whether the page wired the view cycle (`v`). */
|
|
70
|
+
hasViewCycle: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One key the page answers and documents.
|
|
75
|
+
*
|
|
76
|
+
* The bindings, the shortcut sheet and the sidebar hint all read this table, because
|
|
77
|
+
* three hand-kept lists of the same keys is how a sheet comes to name a key the page no
|
|
78
|
+
* longer answers. The two layer keys — `Esc` and `?` — carry no `run`: they are handled
|
|
79
|
+
* before the table, because they work while a composer or the sheet owns the keyboard,
|
|
80
|
+
* and they are listed so the sheet documents them with the rest.
|
|
81
|
+
*/
|
|
82
|
+
export interface WebKey {
|
|
83
|
+
/** Stable id: the sheet's order, and what a test names. */
|
|
84
|
+
id: "approve" | "blocks" | "comments" | "comment" | "add" | "view" | "settle" | "close" | "sheet";
|
|
85
|
+
/** The keys as the sheet prints them; the same strings the handler matches. */
|
|
86
|
+
keys: readonly string[];
|
|
87
|
+
/** The sheet's sentence on this page. */
|
|
88
|
+
label(surface: KeySurface): string;
|
|
89
|
+
/** The words the sidebar hint adds after the key, if it names the key at all. */
|
|
90
|
+
hint?(surface: KeySurface): string;
|
|
91
|
+
/** Whether the page offers it; a key nobody can press is not documented. */
|
|
92
|
+
offered?(surface: KeySurface): boolean;
|
|
93
|
+
/** What the key does, for the keys this handler owns; `key` is which binding fired. */
|
|
94
|
+
run?(deps: KeyDeps, key: string): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The page's keys, in the order the sheet lists them.
|
|
99
|
+
*
|
|
100
|
+
* A function rather than a constant because the bundle carries it the way it carries
|
|
101
|
+
* every other rule here: `String()` of a body. A table spliced from data would lose the
|
|
102
|
+
* closures on `label`, `hint`, `offered`, and `run`, and a table the bundle does not
|
|
103
|
+
* carry is a keymap that throws on the first press.
|
|
104
|
+
*/
|
|
105
|
+
export function webKeys(): readonly WebKey[] {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
id: "approve",
|
|
109
|
+
keys: ["a"],
|
|
110
|
+
offered: (surface) => surface.canApprove,
|
|
111
|
+
label: () => "Approve and run the plan",
|
|
112
|
+
hint: () => "to approve",
|
|
113
|
+
run: (deps) => deps.askApprove(),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "blocks",
|
|
117
|
+
keys: ["j", "k"],
|
|
118
|
+
label: () => "Next and previous block",
|
|
119
|
+
hint: () => "to move",
|
|
120
|
+
// The block keys share a row: `j` walks forward, `k` back.
|
|
121
|
+
run: (deps, key) => deps.moveActive(key === "k" ? -1 : 1),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "comments",
|
|
125
|
+
keys: ["n", "N"],
|
|
126
|
+
// Offered only when the page wired the walk: a documented key that does nothing is
|
|
127
|
+
// worse than a key the sheet leaves out. No `hint`: the sidebar line names what a
|
|
128
|
+
// reader reaches for first and leaves the rest to the sheet, or it grows forever.
|
|
129
|
+
offered: (surface) => surface.hasCommentWalk,
|
|
130
|
+
label: () => "Next and previous block with a comment",
|
|
131
|
+
run: (deps, key) => deps.moveComment?.(key === "N" ? -1 : 1),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "comment",
|
|
135
|
+
keys: ["c"],
|
|
136
|
+
label: () => "Comment on the current block",
|
|
137
|
+
hint: () => "to comment",
|
|
138
|
+
run: (deps) => deps.commentOnActive(),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "add",
|
|
142
|
+
keys: ["Enter"],
|
|
143
|
+
label: () => "Add the comment you are writing",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "view",
|
|
147
|
+
keys: ["v"],
|
|
148
|
+
offered: (surface) => surface.hasViewCycle,
|
|
149
|
+
label: () => "Show everything, what changed, or the comments",
|
|
150
|
+
run: (deps) => deps.cycleView?.(),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "settle",
|
|
154
|
+
keys: ["s"],
|
|
155
|
+
label: (surface) =>
|
|
156
|
+
surface.role === "owner" ? "Save feedback and return to Pi" : "Post your comments to the plan",
|
|
157
|
+
hint: (surface) => (surface.role === "owner" ? "to save" : "to post"),
|
|
158
|
+
run: (deps) => (deps.hasSaveControl() ? deps.save() : deps.postComments()),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "close",
|
|
162
|
+
keys: ["Esc"],
|
|
163
|
+
label: (surface) =>
|
|
164
|
+
surface.role === "owner" ? "Close the composer, the sheet, or the review" : "Close the composer or this sheet",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "sheet",
|
|
168
|
+
keys: ["?"],
|
|
169
|
+
label: () => "Toggle this sheet",
|
|
170
|
+
hint: () => "for all shortcuts",
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The rows this page offers, in sheet order. */
|
|
176
|
+
export function keyRows(surface: KeySurface): WebKey[] {
|
|
177
|
+
return webKeys().filter((row) => row.offered === undefined || row.offered(surface));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The surface a handler sees, read off the callbacks and controls it was given. */
|
|
181
|
+
function surfaceOf(deps: KeyDeps): KeySurface {
|
|
182
|
+
return {
|
|
183
|
+
role: deps.hasSaveControl() ? "owner" : "reviewer",
|
|
184
|
+
canApprove: deps.hasApproveControl(),
|
|
185
|
+
hasCommentWalk: deps.moveComment !== undefined,
|
|
186
|
+
hasViewCycle: deps.cycleView !== undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
56
190
|
/**
|
|
57
191
|
* Handle one keydown.
|
|
58
192
|
*
|
|
59
193
|
* 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.
|
|
194
|
+
* keyboard while it has focus, and so does the shortcut sheet while it is open. The
|
|
195
|
+
* content keys come from {@link webKeys}, so the key that acts and the key the sheet
|
|
196
|
+
* documents cannot part company.
|
|
61
197
|
*/
|
|
62
198
|
export function handleKey(event: KeyEvent, deps: KeyDeps): void {
|
|
63
199
|
if (event.key === "Escape") {
|
|
@@ -78,31 +214,13 @@ export function handleKey(event: KeyEvent, deps: KeyDeps): void {
|
|
|
78
214
|
}
|
|
79
215
|
// While the shortcut sheet is open it owns the keyboard.
|
|
80
216
|
if (deps.sheetOpen()) return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (event.key === "k") {
|
|
217
|
+
const surface = surfaceOf(deps);
|
|
218
|
+
for (const row of keyRows(surface)) {
|
|
219
|
+
if (row.run === undefined || !row.keys.includes(event.key)) continue;
|
|
87
220
|
event.preventDefault();
|
|
88
|
-
deps.
|
|
221
|
+
row.run(deps, event.key);
|
|
89
222
|
return;
|
|
90
223
|
}
|
|
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
224
|
}
|
|
107
225
|
|
|
108
226
|
/** What remembering a selection reads, and where it puts it. */
|
|
@@ -130,5 +248,11 @@ export function rememberSelection(target: unknown, deps: QuoteDeps): void {
|
|
|
130
248
|
deps.setPending(wrap && quote ? { wrap, quote } : null);
|
|
131
249
|
}
|
|
132
250
|
|
|
133
|
-
/**
|
|
134
|
-
|
|
251
|
+
/**
|
|
252
|
+
* The keymap and the quote rule as source, for the client bundle.
|
|
253
|
+
*
|
|
254
|
+
* Every name `handleKey` reads has to travel with it — the table, the rows it offers,
|
|
255
|
+
* and the surface those rows are offered for — or the first press throws instead of
|
|
256
|
+
* matching a row.
|
|
257
|
+
*/
|
|
258
|
+
export const WEB_KEYS_JS = [handleKey, surfaceOf, keyRows, webKeys, rememberSelection].map(String).join("\n\n");
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan review server, loaded when a review first asks for it.
|
|
3
|
+
*
|
|
4
|
+
* The browser review owns `lib/http`, the web kit, and the plan page; none of it
|
|
5
|
+
* is needed to register `/plan` or the plan tools. `index.ts`, `commands.ts`, and
|
|
6
|
+
* `tools.ts` therefore share this proxy over `lib/lazy`: the first review loads
|
|
7
|
+
* `web.ts` once, and `closeAll` on shutdown only closes a review that was already
|
|
8
|
+
* opened, so a session that never reviewed a plan imports none of the server.
|
|
9
|
+
*
|
|
10
|
+
* The synchronous reads (`current`, `shareState`) answer `undefined` until the
|
|
11
|
+
* module is loaded, which is the truth: no review can be open before a load.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { lazy } from "../../lib/lazy.ts";
|
|
15
|
+
import type { PlanStore } from "./plans.ts";
|
|
16
|
+
import type { PlanWebReview, PlanWebReviewDeps } from "./web.ts";
|
|
17
|
+
|
|
18
|
+
/** The review module, as the proxy loads it. */
|
|
19
|
+
export type PlanWebModule = typeof import("./web.ts");
|
|
20
|
+
|
|
21
|
+
/** How the proxy reaches the review module; production uses the dynamic import. */
|
|
22
|
+
export type PlanWebLoader = () => Promise<PlanWebModule>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The deps a caller that injects its own review leaves out.
|
|
26
|
+
*
|
|
27
|
+
* A review needs a place on the session's server, so a caller with no wiring says so
|
|
28
|
+
* rather than binding a socket of its own: the start is refused with a sentence.
|
|
29
|
+
*/
|
|
30
|
+
export const NO_SERVER_DEPS: PlanWebReviewDeps = {
|
|
31
|
+
mount: async () => ({ kind: "refused", reason: "this review has no server wired" }),
|
|
32
|
+
session: () => "",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function createLazyPlanWebReview(
|
|
36
|
+
store: PlanStore,
|
|
37
|
+
deps: PlanWebReviewDeps,
|
|
38
|
+
load: PlanWebLoader = () => import("./web.ts"),
|
|
39
|
+
): PlanWebReview {
|
|
40
|
+
const review = lazy(async () => (await load()).createPlanWebReview(store, deps));
|
|
41
|
+
return {
|
|
42
|
+
start: async (cwd, plan, options) => (await review.get()).start(cwd, plan, options),
|
|
43
|
+
current: () => review.peek()?.current(),
|
|
44
|
+
share: async () => (await review.get()).share(),
|
|
45
|
+
stopSharing: async () => (await review.get()).stopSharing(),
|
|
46
|
+
shareState: () => review.peek()?.shareState(),
|
|
47
|
+
// Shutdown must not start a load: a review that never opened has nothing to close.
|
|
48
|
+
closeAll: async () => {
|
|
49
|
+
const opened = review.peek();
|
|
50
|
+
if (opened !== undefined) await opened.closeAll();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|