@gavin-hu/my-pi-agent 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The request router for browser plan review.
|
|
3
3
|
*
|
|
4
|
-
* A request is validated (method, `Host`, origin, token
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A request is validated (method, `Host`, origin, token, and — on a route that
|
|
5
|
+
* reads a body — that the body is JSON), matched to a route, and answered with a
|
|
6
|
+
* plain {@link ServeResponse}, so every route can be tested without a socket. The
|
|
7
|
+
* `/events` route is a marker: the node server in `web.ts` keeps that response open
|
|
8
|
+
* as a Server-Sent Events stream.
|
|
8
9
|
*
|
|
9
10
|
* The token is a per-session secret carried in the page URL, and on a shared
|
|
10
11
|
* review it is the only credential a reviewer has: it names the caller — the
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
pageHeaders,
|
|
21
22
|
parseRequestUrl,
|
|
22
23
|
postOnly,
|
|
24
|
+
requireJsonBody,
|
|
23
25
|
scriptHeaders,
|
|
24
26
|
type ServeResponse,
|
|
25
27
|
} from "../../lib/http/index.ts";
|
|
@@ -40,7 +42,7 @@ export interface ReviewContext {
|
|
|
40
42
|
caller: (token: string) => ReviewCaller | undefined;
|
|
41
43
|
/** The host names this server answers to now, so a rebound page is refused. */
|
|
42
44
|
hosts: () => readonly string[];
|
|
43
|
-
/** Whether the browser may approve the plan (only the `
|
|
45
|
+
/** Whether the browser may approve the plan (only the `plan` tool's `exit` flow). */
|
|
44
46
|
canApprove: boolean;
|
|
45
47
|
/** Build the page data for the caller (recomputed per request). */
|
|
46
48
|
page: (caller: ReviewCaller) => Promise<ReviewPageData>;
|
|
@@ -157,7 +159,9 @@ export async function handleReviewRequest(
|
|
|
157
159
|
}
|
|
158
160
|
if (pathname === "/api/share/revoke") {
|
|
159
161
|
const refusal =
|
|
160
|
-
postOnly(method, "revoke a link") ??
|
|
162
|
+
postOnly(method, "revoke a link") ??
|
|
163
|
+
requireJsonBody(headers["content-type"]) ??
|
|
164
|
+
roleOnly(caller, ["owner"], "Only the plan owner can revoke a link.");
|
|
161
165
|
if (refusal) return refusal;
|
|
162
166
|
const id = linkIdOf(body);
|
|
163
167
|
if (id === undefined) return jsonError(400, "The request body must name the link's id.");
|
|
@@ -169,7 +173,9 @@ export async function handleReviewRequest(
|
|
|
169
173
|
// A reviewer's whole surface: persist the comments they wrote and end
|
|
170
174
|
// nothing. Only the owner's own save decides the review.
|
|
171
175
|
const refusal =
|
|
172
|
-
postOnly(method, "post comments") ??
|
|
176
|
+
postOnly(method, "post comments") ??
|
|
177
|
+
requireJsonBody(headers["content-type"]) ??
|
|
178
|
+
roleOnly(caller, ["reviewer"], "Only a shared reviewer posts comments.");
|
|
173
179
|
if (refusal) return refusal;
|
|
174
180
|
const annotations = parseAnnotations(body);
|
|
175
181
|
if (isResponse(annotations)) return annotations;
|
|
@@ -182,6 +188,7 @@ export async function handleReviewRequest(
|
|
|
182
188
|
// `/api/comment`, so this route can never settle a shared review.
|
|
183
189
|
const refusal =
|
|
184
190
|
postOnly(method, "save feedback") ??
|
|
191
|
+
requireJsonBody(headers["content-type"]) ??
|
|
185
192
|
roleOnly(caller, ["owner"], "Only the plan owner saves this review; a reviewer posts comments.");
|
|
186
193
|
if (refusal) return refusal;
|
|
187
194
|
const annotations = parseAnnotations(body);
|
|
@@ -192,10 +199,12 @@ export async function handleReviewRequest(
|
|
|
192
199
|
return jsonResponse(200, { ok: true });
|
|
193
200
|
}
|
|
194
201
|
if (pathname === "/api/approve") {
|
|
195
|
-
// Only the owner's own page approves, and only the
|
|
202
|
+
// Only the owner's own page approves, and only the `exit` flow owns
|
|
196
203
|
// the side effects: a reviewer token is refused whatever it posts.
|
|
197
204
|
const refusal =
|
|
198
|
-
postOnly(method, "approve") ??
|
|
205
|
+
postOnly(method, "approve") ??
|
|
206
|
+
requireJsonBody(headers["content-type"]) ??
|
|
207
|
+
roleOnly(caller, ["owner"], "A shared review cannot approve the plan.");
|
|
199
208
|
if (refusal) return refusal;
|
|
200
209
|
if (!ctx.canApprove) return jsonError(403, "This review cannot be approved from the browser.");
|
|
201
210
|
const annotations = parseAnnotations(body);
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
export interface SettleDeps {
|
|
16
16
|
/** The path a write goes to, without its query string. */
|
|
17
17
|
token(): string;
|
|
18
|
+
/** The review's mount on the session's server, e.g. `/plan/3f9c21aa`. */
|
|
19
|
+
base(): string;
|
|
18
20
|
role(): string;
|
|
19
21
|
/** The comments this writer may write: her own when she is a reviewer, all when owner. */
|
|
20
|
-
mine(): readonly
|
|
21
|
-
annotations(): readonly
|
|
22
|
+
mine(): readonly { author?: string }[];
|
|
23
|
+
annotations(): readonly { author?: string }[];
|
|
22
24
|
/** Whether the review has already ended, by any of the three routes. */
|
|
23
25
|
settled(): boolean;
|
|
24
26
|
setStatus(text: string, kind?: string): void;
|
|
@@ -82,9 +84,14 @@ export function submitAnnotations(
|
|
|
82
84
|
if (onOk) onOk();
|
|
83
85
|
deps.updateState();
|
|
84
86
|
// The server binds a reviewer's name to her link on the first post, so a
|
|
85
|
-
// second tab of the same link posts under the same name
|
|
87
|
+
// second tab of the same link posts under the same name — and it may normalise
|
|
88
|
+
// what she typed (`@alice` is stored as `alice`). The comments she just posted
|
|
89
|
+
// are in the file under the bound name, so the page's copy has to say that too:
|
|
90
|
+
// otherwise they stop being hers to edit, and her next post would replace
|
|
91
|
+
// markers it never sent.
|
|
86
92
|
const body = result.body as { author?: unknown } | null;
|
|
87
93
|
if (deps.role() === "reviewer" && body && typeof body.author === "string" && body.author) {
|
|
94
|
+
for (const annotation of payload) if (annotation.author !== undefined) annotation.author = body.author;
|
|
88
95
|
deps.adoptName(body.author);
|
|
89
96
|
}
|
|
90
97
|
return { outcome: "ok" as const };
|
|
@@ -117,7 +124,7 @@ export function saveReview(deps: SettleDeps): Promise<SettleResult> | null {
|
|
|
117
124
|
deps.setStatus("Saving\u2026");
|
|
118
125
|
return submitAnnotations(
|
|
119
126
|
deps,
|
|
120
|
-
"/api/feedback",
|
|
127
|
+
deps.base() + "/api/feedback",
|
|
121
128
|
"Saved \u2014 return to Pi to continue.",
|
|
122
129
|
"Could not save: ",
|
|
123
130
|
function () {
|
|
@@ -140,7 +147,7 @@ export function postReviewComments(deps: SettleDeps): Promise<SettleResult> | nu
|
|
|
140
147
|
deps.setStatus("Posting\u2026");
|
|
141
148
|
return submitAnnotations(
|
|
142
149
|
deps,
|
|
143
|
-
"/api/comment",
|
|
150
|
+
deps.base() + "/api/comment",
|
|
144
151
|
"Comments posted \u2014 the owner sees them now.",
|
|
145
152
|
"Could not post: ",
|
|
146
153
|
null,
|
|
@@ -153,7 +160,7 @@ export function approveReview(deps: SettleDeps): Promise<SettleResult> | null {
|
|
|
153
160
|
deps.setStatus("Approving\u2026");
|
|
154
161
|
return submitAnnotations(
|
|
155
162
|
deps,
|
|
156
|
-
"/api/approve",
|
|
163
|
+
deps.base() + "/api/approve",
|
|
157
164
|
"Approved \u2014 return to Pi to continue.",
|
|
158
165
|
"Could not approve: ",
|
|
159
166
|
function () {
|
|
@@ -180,7 +187,7 @@ export function cancelReview(deps: SettleDeps): Promise<void> | null {
|
|
|
180
187
|
deps.setStatus("Closing\u2026");
|
|
181
188
|
deps.updateState();
|
|
182
189
|
return deps
|
|
183
|
-
.send("/api/cancel?token=" + encodeURIComponent(deps.token()))
|
|
190
|
+
.send(deps.base() + "/api/cancel?token=" + encodeURIComponent(deps.token()))
|
|
184
191
|
.catch(function () {})
|
|
185
192
|
.then(function () {
|
|
186
193
|
deps.setStatus("Closed \u2014 return to Pi.");
|
|
@@ -8,31 +8,25 @@
|
|
|
8
8
|
* hooks, and the frame hooks, so it never reaches into the session's maps.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type HttpMount, LOOPBACK_HOSTS, type SharedListener } from "../../lib/http/index.ts";
|
|
12
12
|
import { qrSvg } from "../../lib/qr.ts";
|
|
13
13
|
import type { ReviewFrame, ReviewRole } from "./types.ts";
|
|
14
14
|
import {
|
|
15
15
|
AT_CAP_REASON,
|
|
16
16
|
createLinkTable,
|
|
17
|
-
lanAddresses,
|
|
18
17
|
NO_SHARE_REASON,
|
|
19
18
|
shareUrl,
|
|
20
19
|
type LinkTable,
|
|
21
|
-
type NetworkInterfaces,
|
|
22
20
|
type ShareResult,
|
|
23
21
|
type ShareState,
|
|
24
22
|
} from "./web-share.ts";
|
|
25
23
|
|
|
26
24
|
/** What the share listener needs from the review session it belongs to. */
|
|
27
25
|
export interface ShareSessionDeps {
|
|
28
|
-
/** The
|
|
29
|
-
|
|
30
|
-
/** The
|
|
31
|
-
|
|
32
|
-
/** The owner's bound port, reused for the link when it is free. */
|
|
33
|
-
ownerPort: () => number;
|
|
34
|
-
/** The machine's interfaces, or a snapshot a test injected. */
|
|
35
|
-
networkInterfaces: () => NetworkInterfaces;
|
|
26
|
+
/** The review's mount on the session's server; sharing borrows its LAN listener. */
|
|
27
|
+
mount: HttpMount;
|
|
28
|
+
/** The mount's prefix, so a reviewer's link names the same page the owner has. */
|
|
29
|
+
base: string;
|
|
36
30
|
/** Add a reviewer token to the session's role map. */
|
|
37
31
|
mintReviewer: () => string;
|
|
38
32
|
/** Remove a reviewer token from the session's role map. */
|
|
@@ -64,16 +58,14 @@ export interface ShareSession {
|
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
67
|
-
let
|
|
68
|
-
let port = 0;
|
|
69
|
-
let server: LoopbackServer | undefined;
|
|
61
|
+
let shared: SharedListener | undefined;
|
|
70
62
|
const links: LinkTable = createLinkTable({
|
|
71
63
|
mint: deps.mintReviewer,
|
|
72
64
|
revoke: deps.revokeReviewer,
|
|
73
65
|
nameOf: deps.nameOf,
|
|
74
66
|
});
|
|
75
67
|
let reason: string | undefined;
|
|
76
|
-
const hosts = (): readonly string[] => (
|
|
68
|
+
const hosts = (): readonly string[] => (shared ? [...LOOPBACK_HOSTS, shared.address] : [...LOOPBACK_HOSTS]);
|
|
77
69
|
|
|
78
70
|
/**
|
|
79
71
|
* The share surface as it is now, read rather than cached: a link's bound name
|
|
@@ -81,42 +73,22 @@ export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
|
81
73
|
* another share action.
|
|
82
74
|
*/
|
|
83
75
|
const snapshot = (): ShareState => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const bind = async (chosen: string): Promise<void> => {
|
|
90
|
-
try {
|
|
91
|
-
const bound = await serveLoopback({
|
|
92
|
-
host: chosen,
|
|
93
|
-
ports: [deps.ownerPort(), 0],
|
|
94
|
-
maxBodyBytes: deps.maxBodyBytes,
|
|
95
|
-
handler: deps.handleRequest,
|
|
96
|
-
});
|
|
97
|
-
server = bound;
|
|
98
|
-
address = chosen;
|
|
99
|
-
port = bound.port;
|
|
100
|
-
} catch {
|
|
101
|
-
// Neither the owner's port nor a free one answered: the caller reports it.
|
|
102
|
-
server = undefined;
|
|
103
|
-
}
|
|
76
|
+
const state: ShareState =
|
|
77
|
+
shared === undefined
|
|
78
|
+
? { sharing: false, origin: "", links: [] }
|
|
79
|
+
: links.state(deps.base, shared.origin, shared.address);
|
|
80
|
+
return reason === undefined ? state : { ...state, reason };
|
|
104
81
|
};
|
|
105
82
|
|
|
106
83
|
/**
|
|
107
|
-
*
|
|
108
|
-
* there is nothing to bind to. Every link answers on this same listener, so
|
|
109
|
-
* runs once per share and the rest is bookkeeping.
|
|
84
|
+
* Ask the session's server for the LAN listener the first time a link needs it, or
|
|
85
|
+
* report why there is nothing to bind to. Every link answers on this same listener, so
|
|
86
|
+
* this runs once per share and the rest is bookkeeping.
|
|
110
87
|
*/
|
|
111
88
|
const ensureListener = async (): Promise<boolean> => {
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
reason = NO_SHARE_REASON;
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
await bind(chosen);
|
|
119
|
-
if (server === undefined) {
|
|
89
|
+
if (shared !== undefined) return true;
|
|
90
|
+
shared = await deps.mount.share();
|
|
91
|
+
if (shared === undefined) {
|
|
120
92
|
reason = NO_SHARE_REASON;
|
|
121
93
|
return false;
|
|
122
94
|
}
|
|
@@ -129,7 +101,7 @@ export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
|
129
101
|
deps.broadcast("share", "owner");
|
|
130
102
|
const state = snapshot();
|
|
131
103
|
if (minted === undefined) return { state };
|
|
132
|
-
return { state, svg: await qrSvg(shareUrl(
|
|
104
|
+
return { state, svg: await qrSvg(shareUrl(deps.base, shared?.origin ?? "", minted)) };
|
|
133
105
|
};
|
|
134
106
|
|
|
135
107
|
const share = async (): Promise<ShareResult> => {
|
|
@@ -172,19 +144,16 @@ export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
|
172
144
|
// The reviewers are told before their listener goes away; the review itself
|
|
173
145
|
// stays open for its owner.
|
|
174
146
|
deps.endStreams("revoked", { role: "reviewer" });
|
|
175
|
-
|
|
176
|
-
server = undefined;
|
|
177
|
-
address = "";
|
|
178
|
-
port = 0;
|
|
147
|
+
shared = undefined;
|
|
179
148
|
reason = undefined;
|
|
180
|
-
|
|
149
|
+
await deps.mount.unshare();
|
|
181
150
|
return settle();
|
|
182
151
|
};
|
|
183
152
|
|
|
184
153
|
const close = async (): Promise<void> => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
154
|
+
if (shared === undefined) return;
|
|
155
|
+
shared = undefined;
|
|
156
|
+
await deps.mount.unshare();
|
|
188
157
|
};
|
|
189
158
|
|
|
190
159
|
return { share, invite, revoke, stop, state: snapshot, hosts, close };
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* failed. Everything else would print a link that cannot work.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type
|
|
15
|
+
import { lanAddresses, type NetworkInterfaces } from "../../lib/http/index.ts";
|
|
16
16
|
|
|
17
|
-
/** The interfaces a review may be shared to, by name. */
|
|
18
|
-
export type NetworkInterfaces
|
|
17
|
+
/** The interfaces a review may be shared to, by name; `lib/http` owns the rule. */
|
|
18
|
+
export type { NetworkInterfaces };
|
|
19
|
+
|
|
20
|
+
export { lanAddresses };
|
|
19
21
|
|
|
20
22
|
/** One address a reviewer can open the review at. */
|
|
21
23
|
export interface ShareLink {
|
|
@@ -33,8 +35,12 @@ export interface ShareLink {
|
|
|
33
35
|
export interface ShareState {
|
|
34
36
|
/** True while reviewer tokens exist and an address is bound. */
|
|
35
37
|
sharing: boolean;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The origin reviewers connect to, `http://192.168.1.24:4780`, or `""` when nothing is
|
|
40
|
+
* shared. The port is the listener's own, which is why it is reported with its address
|
|
41
|
+
* rather than as a number the review could have guessed.
|
|
42
|
+
*/
|
|
43
|
+
origin: string;
|
|
38
44
|
/** Every shared link, oldest first. */
|
|
39
45
|
links: ShareLink[];
|
|
40
46
|
/** Why sharing is unavailable, when it is. */
|
|
@@ -53,35 +59,11 @@ export const NO_SHARE_REASON =
|
|
|
53
59
|
"This machine has no network address to share on. Reviewers can still use a loopback link on this computer.";
|
|
54
60
|
|
|
55
61
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
62
|
+
* The tokenized link for one reviewer: the origin the page answers on, its mount inside
|
|
63
|
+
* that origin, and the token the link carries.
|
|
58
64
|
*/
|
|
59
|
-
export function
|
|
60
|
-
|
|
61
|
-
for (const entries of Object.values(interfaces)) {
|
|
62
|
-
for (const entry of entries ?? []) {
|
|
63
|
-
if (entry.family !== "IPv4" || entry.internal) continue;
|
|
64
|
-
if (entry.address === "0.0.0.0" || entry.address.startsWith("169.254.")) continue;
|
|
65
|
-
addresses.add(entry.address);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return [...addresses].sort(byAddress);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Numeric IPv4 comparison, so `…1.9` sorts before `…1.24`. */
|
|
72
|
-
function byAddress(a: string, b: string): number {
|
|
73
|
-
const left = a.split(".").map(Number);
|
|
74
|
-
const right = b.split(".").map(Number);
|
|
75
|
-
for (let index = 0; index < 4; index++) {
|
|
76
|
-
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
77
|
-
if (difference !== 0) return difference;
|
|
78
|
-
}
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** The tokenized link for one address. */
|
|
83
|
-
export function shareUrl(address: string, port: number, token: string): string {
|
|
84
|
-
return `http://${address}:${port}/?token=${token}`;
|
|
65
|
+
export function shareUrl(base: string, origin: string, token: string): string {
|
|
66
|
+
return `${origin}${base}/?token=${token}`;
|
|
85
67
|
}
|
|
86
68
|
|
|
87
69
|
/**
|
|
@@ -129,7 +111,7 @@ export interface LinkTable {
|
|
|
129
111
|
/** The links currently shared, oldest first. */
|
|
130
112
|
list(): readonly SharedLink[];
|
|
131
113
|
/** The state the owner's page and the terminal read. */
|
|
132
|
-
state(
|
|
114
|
+
state(base: string, origin: string, address: string): ShareState;
|
|
133
115
|
}
|
|
134
116
|
|
|
135
117
|
export function createLinkTable(deps: LinkTableDeps): LinkTable {
|
|
@@ -152,15 +134,15 @@ export function createLinkTable(deps: LinkTableDeps): LinkTable {
|
|
|
152
134
|
return true;
|
|
153
135
|
},
|
|
154
136
|
list: () => [...links],
|
|
155
|
-
state: (
|
|
137
|
+
state: (base, origin, address) => ({
|
|
156
138
|
sharing: links.length > 0,
|
|
157
|
-
|
|
139
|
+
origin,
|
|
158
140
|
links: links.map((link) => {
|
|
159
141
|
const name = deps.nameOf(link.token);
|
|
160
142
|
return {
|
|
161
143
|
id: link.id,
|
|
162
144
|
address,
|
|
163
|
-
url: shareUrl(
|
|
145
|
+
url: shareUrl(base, origin, link.token),
|
|
164
146
|
...(name === undefined ? {} : { name }),
|
|
165
147
|
};
|
|
166
148
|
}),
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review's two write paths, over the plan store.
|
|
3
|
+
*
|
|
4
|
+
* An owner's save decides the review and returns to Pi; a reviewer's post only writes,
|
|
5
|
+
* under the name its link has bound. Both write markers through the one codec
|
|
6
|
+
* (`feedback.ts`), both refuse a file that changed under the page, and neither touches
|
|
7
|
+
* another writer's markers — the store replaces exactly the writer it is handed.
|
|
8
|
+
*
|
|
9
|
+
* Splitting them out of `web.ts` is what leaves that module the session and its sockets:
|
|
10
|
+
* every request that writes the plan comes through here, so a change to the marker
|
|
11
|
+
* grammar or to the refusal wording has one file to land in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { claimAuthor, normalizeAuthor, stripFeedback, type PlanAnnotation } from "./feedback.ts";
|
|
15
|
+
import type { PlanStore, StoredPlan } from "./plans.ts";
|
|
16
|
+
import type { ReviewCaller } from "./types.ts";
|
|
17
|
+
import type { SaveResult } from "./web-router.ts";
|
|
18
|
+
|
|
19
|
+
/** What a write path reads: the store, and the anchor a save is validated against. */
|
|
20
|
+
export interface WriteDeps {
|
|
21
|
+
/** The plan store the markers are written through. */
|
|
22
|
+
store: PlanStore;
|
|
23
|
+
/** The directory the store resolves its paths against. */
|
|
24
|
+
cwd: string;
|
|
25
|
+
/** The plan's path, as the store knows it. */
|
|
26
|
+
path: string;
|
|
27
|
+
/**
|
|
28
|
+
* The stripped text the page rendered. A file whose body no longer matches is a
|
|
29
|
+
* `409`: the comments would otherwise land beside text nobody read.
|
|
30
|
+
*/
|
|
31
|
+
pageText(): string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** An owner's save: the plan as it was written, or why it was refused. */
|
|
35
|
+
export type OwnerWrite = { ok: true; plan: StoredPlan } | { ok: false; status: number; message: string };
|
|
36
|
+
|
|
37
|
+
/** Every refusal both paths share, before anyone's markers are touched. */
|
|
38
|
+
const GONE = { ok: false, status: 404, message: "The plan file is gone." } as const;
|
|
39
|
+
const MOVED = {
|
|
40
|
+
ok: false,
|
|
41
|
+
status: 409,
|
|
42
|
+
message: "The plan changed on disk; reload the page and review again.",
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
/** The file as it is now, when it still matches what the page rendered. */
|
|
46
|
+
async function verified(
|
|
47
|
+
deps: WriteDeps,
|
|
48
|
+
): Promise<{ ok: true; plan: StoredPlan } | { ok: false; status: number; message: string }> {
|
|
49
|
+
const current = await deps.store.read(deps.cwd, deps.path);
|
|
50
|
+
if (!current) return GONE;
|
|
51
|
+
if (stripFeedback(current.content) !== deps.pageText()) return MOVED;
|
|
52
|
+
return { ok: true, plan: current };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save the owner's comments and report the plan they were written into.
|
|
57
|
+
*
|
|
58
|
+
* The caller decides what a save means (it ends the review); this path only writes, so
|
|
59
|
+
* the decision and the write cannot disagree about which file was saved.
|
|
60
|
+
*/
|
|
61
|
+
export async function saveOwnerAnnotations(
|
|
62
|
+
deps: WriteDeps,
|
|
63
|
+
annotations: readonly PlanAnnotation[],
|
|
64
|
+
): Promise<OwnerWrite> {
|
|
65
|
+
const check = await verified(deps);
|
|
66
|
+
if (!check.ok) return check;
|
|
67
|
+
try {
|
|
68
|
+
const plan = await deps.store.saveFeedback(deps.cwd, deps.path, [...annotations]);
|
|
69
|
+
return { ok: true, plan };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { ok: false, status: 500, message: error instanceof Error ? error.message : String(error) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What a reviewer's post additionally needs: who posted, and which names are taken. */
|
|
76
|
+
export interface ReviewerWriteDeps extends WriteDeps {
|
|
77
|
+
/** The caller the link names; its `name` is the one the link has bound, if any. */
|
|
78
|
+
caller: ReviewCaller;
|
|
79
|
+
/** Names other live links hold, so a first post cannot claim one of them. */
|
|
80
|
+
taken: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A reviewer's post: the name it stored under, so a page can adopt it. */
|
|
84
|
+
export type ReviewerWrite = { save: SaveResult; count: number };
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Write one reviewer's comments, signed and scoped to that link.
|
|
88
|
+
*
|
|
89
|
+
* The link, not the payload, decides whose markers this replaces: a page that signs
|
|
90
|
+
* another name is a stale or foreign tab, and re-signing it would move markers that
|
|
91
|
+
* belong to whoever picked that name. Only that writer's annotations are written, so a
|
|
92
|
+
* post cannot disturb the owner's notes or a second reviewer's.
|
|
93
|
+
*/
|
|
94
|
+
export async function postReviewerAnnotations(
|
|
95
|
+
deps: ReviewerWriteDeps,
|
|
96
|
+
annotations: readonly PlanAnnotation[],
|
|
97
|
+
): Promise<ReviewerWrite> {
|
|
98
|
+
const check = await verified(deps);
|
|
99
|
+
if (!check.ok) return { save: check, count: 0 };
|
|
100
|
+
const claim = claimAuthor({ bound: deps.caller.name, taken: deps.taken }, annotations);
|
|
101
|
+
if (!("name" in claim)) return { save: { ok: false, status: claim.status, message: claim.message }, count: 0 };
|
|
102
|
+
const own = annotations.filter((annotation) => normalizeAuthor(annotation.author) === claim.name);
|
|
103
|
+
try {
|
|
104
|
+
await deps.store.saveFeedback(deps.cwd, deps.path, own, { author: claim.name });
|
|
105
|
+
return { save: { ok: true, author: claim.name }, count: own.length };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
save: { ok: false, status: 500, message: error instanceof Error ? error.message : String(error) },
|
|
109
|
+
count: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|