@gavin-hu/my-pi-agent 0.18.0 → 0.19.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 +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- 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 +554 -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} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- 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 +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +226 -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} +258 -162
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -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 +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- 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/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- 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 -15
- 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/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- 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/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- 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} +13 -13
- 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 +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- 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 +76 -92
- 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/{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/guard.ts +3 -5
- package/lib/README.md +8 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -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/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- 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
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
WEB_LAYOUT_CSS,
|
|
26
26
|
WEB_THEME_CSS,
|
|
27
27
|
} from "../../lib/web/index.ts";
|
|
28
|
-
import { escapeHtml } from "../../lib/escape.ts";
|
|
29
28
|
import type { PlanBlock } from "./blocks.ts";
|
|
30
29
|
import type { PlanAnnotation } from "./feedback.ts";
|
|
31
30
|
import type { ReviewRole } from "./types.ts";
|
|
@@ -78,11 +77,6 @@ export interface ReviewPageData {
|
|
|
78
77
|
* whether its name control is still a choice.
|
|
79
78
|
*/
|
|
80
79
|
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
80
|
/** A running share, so a reload re-renders the link the owner already minted. */
|
|
87
81
|
share?: ShareState;
|
|
88
82
|
/** Per-session secret the client echoes on every request. */
|
|
@@ -322,7 +316,6 @@ export function renderReviewPage(data: ReviewPageData): string {
|
|
|
322
316
|
<div class="bf-panel review-panel">
|
|
323
317
|
${reviewFacts()}
|
|
324
318
|
<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
319
|
${reviewHints(data.role, data.canApprove)}
|
|
327
320
|
</div>
|
|
328
321
|
</details>
|
|
@@ -13,14 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
+
envelopeRefusal,
|
|
16
17
|
eventHeaders,
|
|
17
|
-
hostHeaderName,
|
|
18
|
-
isAllowedHostHeader,
|
|
19
18
|
jsonError,
|
|
20
|
-
jsonHeaders,
|
|
21
19
|
jsonResponse,
|
|
22
20
|
pageHeaders,
|
|
23
21
|
parseRequestUrl,
|
|
22
|
+
postOnly,
|
|
24
23
|
scriptHeaders,
|
|
25
24
|
type ServeResponse,
|
|
26
25
|
} from "../../lib/http/index.ts";
|
|
@@ -84,12 +83,6 @@ function isResponse(value: PlanAnnotation[] | ServeResponse): value is ServeResp
|
|
|
84
83
|
return !Array.isArray(value);
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
/** The 405 for a route that only accepts POST, or nothing when the method is right. */
|
|
88
|
-
function postOnly(method: string, what: string): ServeResponse | undefined {
|
|
89
|
-
if (method.toUpperCase() === "POST") return undefined;
|
|
90
|
-
return jsonError(405, `Use POST to ${what}.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
86
|
/**
|
|
94
87
|
* The 403 for a route the caller's role may not use, or nothing when it may. Each
|
|
95
88
|
* route keeps its own sentence, because the reason differs: a reviewer posts
|
|
@@ -111,53 +104,6 @@ function linkIdOf(body: string): string | undefined {
|
|
|
111
104
|
return typeof id === "string" && id !== "" ? id : undefined;
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
/** Validate method and `Host`; returns an error response to send instead. */
|
|
115
|
-
function checkEnvelope(
|
|
116
|
-
ctx: ReviewContext,
|
|
117
|
-
method: string,
|
|
118
|
-
headers: Record<string, string | string[] | undefined>,
|
|
119
|
-
): ServeResponse | undefined {
|
|
120
|
-
const upper = method.toUpperCase();
|
|
121
|
-
if (upper !== "GET" && upper !== "HEAD" && upper !== "POST") {
|
|
122
|
-
return {
|
|
123
|
-
...jsonError(405, "Only GET, HEAD, and POST are served."),
|
|
124
|
-
headers: { ...jsonHeaders(), Allow: "GET, HEAD, POST" },
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
// Fail closed: a missing or non-string Host is rejected too, not just a
|
|
128
|
-
// foreign one, so the DNS-rebinding guard cannot be skipped.
|
|
129
|
-
if (!isAllowedHostHeader(headers, { hosts: ctx.hosts(), required: true })) {
|
|
130
|
-
return jsonError(403, "Unexpected Host header.");
|
|
131
|
-
}
|
|
132
|
-
return upper === "POST" ? checkOrigin(headers) : undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Refuse a state-changing request another site made. A browser sends `Origin`
|
|
137
|
-
* on every POST and `Sec-Fetch-Site` with its own classification; a request
|
|
138
|
-
* with neither is a script or a test, which has no ambient credential to abuse.
|
|
139
|
-
*/
|
|
140
|
-
function checkOrigin(headers: Record<string, string | string[] | undefined>): ServeResponse | undefined {
|
|
141
|
-
const site = headers["sec-fetch-site"];
|
|
142
|
-
if (typeof site === "string" && site !== "" && site !== "same-origin" && site !== "none") {
|
|
143
|
-
return jsonError(403, "Cross-origin request refused.");
|
|
144
|
-
}
|
|
145
|
-
const origin = headers.origin;
|
|
146
|
-
if (origin === undefined) return undefined;
|
|
147
|
-
if (typeof origin !== "string" || origin === "") return jsonError(403, "Unexpected Origin header.");
|
|
148
|
-
let parsed: URL;
|
|
149
|
-
try {
|
|
150
|
-
parsed = new URL(origin);
|
|
151
|
-
} catch {
|
|
152
|
-
return jsonError(403, "Unexpected Origin header.");
|
|
153
|
-
}
|
|
154
|
-
const host = headers.host;
|
|
155
|
-
if (typeof host !== "string" || hostHeaderName(parsed.host) !== hostHeaderName(host)) {
|
|
156
|
-
return jsonError(403, "Cross-origin request refused.");
|
|
157
|
-
}
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
107
|
/**
|
|
162
108
|
* Handle one request. `method`, `rawUrl`, and `headers` are the raw HTTP
|
|
163
109
|
* values; `body` is the request body for a POST.
|
|
@@ -169,7 +115,7 @@ export async function handleReviewRequest(
|
|
|
169
115
|
headers: Record<string, string | string[] | undefined> = {},
|
|
170
116
|
body = "",
|
|
171
117
|
): Promise<ServeResponse> {
|
|
172
|
-
const envelope =
|
|
118
|
+
const envelope = envelopeRefusal({ method, headers, hosts: ctx.hosts() });
|
|
173
119
|
if (envelope) return envelope;
|
|
174
120
|
|
|
175
121
|
const parsed = parseRequestUrl(rawUrl);
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* network, and the state the owner's page and `/plan share` read.
|
|
4
4
|
*
|
|
5
5
|
* Separate from `web.ts` because the review session's job is its own lifecycle —
|
|
6
|
-
* open, settle, close — while this module owns the
|
|
7
|
-
*
|
|
6
|
+
* open, settle, close — while this module owns the address, the links, and the
|
|
7
|
+
* listener's lifetime. It is handed the one request handler, the token
|
|
8
8
|
* hooks, and the frame hooks, so it never reaches into the session's maps.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { bindHost, createCloser, LOOPBACK_HOSTS } from "../../lib/http/index.ts";
|
|
11
|
+
import { LOOPBACK_HOSTS, type LoopbackHandler, type LoopbackServer, serveLoopback } from "../../lib/http/index.ts";
|
|
13
12
|
import { qrSvg } from "../../lib/qr.ts";
|
|
14
13
|
import type { ReviewFrame, ReviewRole } from "./types.ts";
|
|
15
14
|
import {
|
|
@@ -27,7 +26,9 @@ import {
|
|
|
27
26
|
/** What the share listener needs from the review session it belongs to. */
|
|
28
27
|
export interface ShareSessionDeps {
|
|
29
28
|
/** The one request handler: a loopback page and a shared page are the same page. */
|
|
30
|
-
handleRequest:
|
|
29
|
+
handleRequest: LoopbackHandler;
|
|
30
|
+
/** The body cap both listeners read under, so a shared page is not read differently. */
|
|
31
|
+
maxBodyBytes: number;
|
|
31
32
|
/** The owner's bound port, reused for the link when it is free. */
|
|
32
33
|
ownerPort: () => number;
|
|
33
34
|
/** The machine's interfaces, or a snapshot a test injected. */
|
|
@@ -65,8 +66,7 @@ export interface ShareSession {
|
|
|
65
66
|
export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
66
67
|
let address = "";
|
|
67
68
|
let port = 0;
|
|
68
|
-
let server:
|
|
69
|
-
let closeServer: (() => Promise<void>) | undefined;
|
|
69
|
+
let server: LoopbackServer | undefined;
|
|
70
70
|
const links: LinkTable = createLinkTable({
|
|
71
71
|
mint: deps.mintReviewer,
|
|
72
72
|
revoke: deps.revokeReviewer,
|
|
@@ -87,24 +87,20 @@ export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
|
87
87
|
|
|
88
88
|
/** Bind the shared listener on `chosen`, reusing the owner's port when it is free. */
|
|
89
89
|
const bind = async (chosen: string): Promise<void> => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
99
103
|
}
|
|
100
|
-
if (bound === undefined) {
|
|
101
|
-
candidate.close();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
server = candidate;
|
|
105
|
-
closeServer = createCloser(candidate);
|
|
106
|
-
address = chosen;
|
|
107
|
-
port = bound;
|
|
108
104
|
};
|
|
109
105
|
|
|
110
106
|
/**
|
|
@@ -176,21 +172,19 @@ export function createShareSession(deps: ShareSessionDeps): ShareSession {
|
|
|
176
172
|
// The reviewers are told before their listener goes away; the review itself
|
|
177
173
|
// stays open for its owner.
|
|
178
174
|
deps.endStreams("revoked", { role: "reviewer" });
|
|
179
|
-
const closer =
|
|
175
|
+
const closer = server;
|
|
180
176
|
server = undefined;
|
|
181
|
-
closeServer = undefined;
|
|
182
177
|
address = "";
|
|
183
178
|
port = 0;
|
|
184
179
|
reason = undefined;
|
|
185
|
-
if (closer) await closer();
|
|
180
|
+
if (closer) await closer.close();
|
|
186
181
|
return settle();
|
|
187
182
|
};
|
|
188
183
|
|
|
189
184
|
const close = async (): Promise<void> => {
|
|
190
|
-
const closer =
|
|
185
|
+
const closer = server;
|
|
191
186
|
server = undefined;
|
|
192
|
-
|
|
193
|
-
if (closer) await closer();
|
|
187
|
+
if (closer) await closer.close();
|
|
194
188
|
};
|
|
195
189
|
|
|
196
190
|
return { share, invite, revoke, stop, state: snapshot, hosts, close };
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* when a closed tab ends the review.
|
|
4
4
|
*
|
|
5
5
|
* A frame carries only a kind, so the registry is the only thing that knows who
|
|
6
|
-
* is connected. It is keyed by
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* is connected. It is keyed by sink — a page may hold more than one stream — and
|
|
7
|
+
* records the caller it belongs to, so an owner-only frame, or one revoked link,
|
|
8
|
+
* can be addressed without the caller reaching into the map. Liveness is the
|
|
9
|
+
* sink's own (a write after the end is a no-op), so the registry never asks the
|
|
10
|
+
* socket whether it is still writable.
|
|
9
11
|
*
|
|
10
12
|
* The closed-tab rule lives here because it is a fact about streams: a reviewer's
|
|
11
13
|
* tab opening or closing changes nothing, while the owner's own page going away
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
* against that reload, so it takes the timer as a seam a test can drive by hand.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
import type {
|
|
18
|
+
import type { EventSink } from "../../lib/http/index.ts";
|
|
17
19
|
import type { ArmTimer } from "../../lib/timer.ts";
|
|
18
20
|
import type { ReviewFrame, ReviewRole } from "./types.ts";
|
|
19
21
|
|
|
@@ -44,9 +46,9 @@ export interface StreamRegistryDeps {
|
|
|
44
46
|
/** The stream set one running review holds. */
|
|
45
47
|
export interface StreamRegistry {
|
|
46
48
|
/** Register a stream, and stand down a pending closed-tab grace. */
|
|
47
|
-
add(
|
|
49
|
+
add(sink: EventSink, record: StreamRecord): void;
|
|
48
50
|
/** Forget a stream, arming the grace when it was the owner's last one. */
|
|
49
|
-
remove(
|
|
51
|
+
remove(sink: EventSink): void;
|
|
50
52
|
/** Tell the open pages something changed. */
|
|
51
53
|
broadcast(kind: ReviewFrame, only?: StreamFilter): void;
|
|
52
54
|
/** Send one last frame and end the stream with it. */
|
|
@@ -60,7 +62,7 @@ export interface StreamRegistry {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function createStreams(deps: StreamRegistryDeps): StreamRegistry {
|
|
63
|
-
const clients = new Map<
|
|
65
|
+
const clients = new Map<EventSink, StreamRecord>();
|
|
64
66
|
let sawOwner = false;
|
|
65
67
|
let cancelDrop: (() => void) | undefined;
|
|
66
68
|
|
|
@@ -91,29 +93,27 @@ export function createStreams(deps: StreamRegistryDeps): StreamRegistry {
|
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
return {
|
|
94
|
-
add(
|
|
95
|
-
clients.set(
|
|
96
|
+
add(sink, record) {
|
|
97
|
+
clients.set(sink, record);
|
|
96
98
|
if (record.role === "owner") {
|
|
97
99
|
sawOwner = true;
|
|
98
100
|
cancel();
|
|
99
101
|
}
|
|
100
102
|
},
|
|
101
|
-
remove(
|
|
102
|
-
const record = clients.get(
|
|
103
|
-
clients.delete(
|
|
103
|
+
remove(sink) {
|
|
104
|
+
const record = clients.get(sink);
|
|
105
|
+
clients.delete(sink);
|
|
104
106
|
if (record?.role === "owner") scheduleDrop();
|
|
105
107
|
},
|
|
106
108
|
broadcast(kind, only) {
|
|
107
109
|
for (const [client, record] of clients) {
|
|
108
110
|
if (!matches(record, only)) continue;
|
|
109
|
-
if (client.writableEnded || client.destroyed) continue;
|
|
110
111
|
client.write(`data: ${kind}\n\n`);
|
|
111
112
|
}
|
|
112
113
|
},
|
|
113
114
|
end(kind, only) {
|
|
114
115
|
for (const [client, record] of clients) {
|
|
115
116
|
if (!matches(record, only)) continue;
|
|
116
|
-
if (client.writableEnded || client.destroyed) continue;
|
|
117
117
|
client.end(`data: ${kind}\n\n`);
|
|
118
118
|
}
|
|
119
119
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* carries only the name, so a shared name is a shared marker.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { mintToken } from "../../lib/http/index.ts";
|
|
19
19
|
import type { ReviewCaller, ReviewRole } from "./types.ts";
|
|
20
20
|
|
|
21
21
|
/** The token → caller table one running review holds. */
|
|
@@ -38,7 +38,7 @@ export interface TokenTable {
|
|
|
38
38
|
const COLLISION_ATTEMPTS = 8;
|
|
39
39
|
|
|
40
40
|
/** A 128-bit token; the one credential a reviewer holds. */
|
|
41
|
-
const defaultToken = (): string =>
|
|
41
|
+
const defaultToken = (): string => mintToken();
|
|
42
42
|
|
|
43
43
|
/** Build the table for one review. `mint` is the generator a test replaces. */
|
|
44
44
|
export function createTokens(mint: () => string = defaultToken): TokenTable {
|
package/extensions/plan/web.ts
CHANGED
|
@@ -6,39 +6,38 @@
|
|
|
6
6
|
* for one plan, opens the default browser at a tokenized URL, and resolves
|
|
7
7
|
* `done` when the user saves feedback, cancels, closes the tab (detected by the
|
|
8
8
|
* event stream dropping), or aborts the turn. The route logic lives in the pure
|
|
9
|
-
* `web-router.ts`; this module
|
|
10
|
-
* and it takes injected seams so tests can run one on `127.0.0.1:0`.
|
|
9
|
+
* `web-router.ts`; this module owns the routes and the timers, `lib/http` owns
|
|
10
|
+
* the socket, and it takes injected seams so tests can run one on `127.0.0.1:0`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
14
13
|
import { networkInterfaces } from "node:os";
|
|
15
14
|
import { basename } from "node:path";
|
|
16
15
|
import { openInBrowser } from "../../lib/browser.ts";
|
|
17
16
|
import {
|
|
18
|
-
bindLoopback,
|
|
19
|
-
createCloser,
|
|
20
17
|
jsonHeaders,
|
|
21
|
-
|
|
18
|
+
type LoopbackRequest,
|
|
19
|
+
type LoopbackReply,
|
|
20
|
+
type LoopbackServer,
|
|
22
21
|
parseRequestUrl,
|
|
23
|
-
|
|
22
|
+
serveLoopback,
|
|
24
23
|
type ServeResponse,
|
|
25
24
|
} from "../../lib/http/index.ts";
|
|
26
25
|
import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
|
|
27
26
|
import { type ArmTimer, defaultInterval, defaultTimer, type RepeatTimer } from "../../lib/timer.ts";
|
|
28
|
-
import { codeBody, fenceInfo
|
|
27
|
+
import { codeBody, fenceInfo } from "./blocks.ts";
|
|
29
28
|
import {
|
|
30
29
|
claimAuthor,
|
|
31
30
|
MAX_ANNOTATIONS,
|
|
32
31
|
MAX_COMMENT_LENGTH,
|
|
33
32
|
MAX_QUOTE_LENGTH,
|
|
34
33
|
normalizeAuthor,
|
|
35
|
-
|
|
34
|
+
readFeedback,
|
|
36
35
|
stripFeedback,
|
|
37
36
|
type PlanAnnotation,
|
|
38
37
|
} from "./feedback.ts";
|
|
39
38
|
import { planTitle, type PlanStore, type StoredPlan } from "./plans.ts";
|
|
40
39
|
import { changedSections, planDigest, type SectionDigest } from "./plan-text.ts";
|
|
41
|
-
import {
|
|
40
|
+
import { extractPlanSteps } from "./steps.ts";
|
|
42
41
|
import type { ReviewCaller } from "./types.ts";
|
|
43
42
|
import { handleReviewRequest, type ReviewContext, type SaveResult } from "./web-router.ts";
|
|
44
43
|
import type { ReviewPageData } from "./web-page.ts";
|
|
@@ -71,8 +70,6 @@ export interface PlanReviewOptions {
|
|
|
71
70
|
signal?: AbortSignal;
|
|
72
71
|
/** Whether the browser may approve the plan (only the `exit_plan_mode` flow). */
|
|
73
72
|
canApprove?: boolean;
|
|
74
|
-
/** A line the page shows the owner, such as why approval belongs in the terminal. */
|
|
75
|
-
note?: string;
|
|
76
73
|
/** The section digests the previous review showed, so this page can mark changed sections. */
|
|
77
74
|
baseline?: SectionDigest[];
|
|
78
75
|
/**
|
|
@@ -143,40 +140,6 @@ const DROP_GRACE_MS = 1500;
|
|
|
143
140
|
*/
|
|
144
141
|
const PLAN_POLL_MS = 1000;
|
|
145
142
|
|
|
146
|
-
/** A request body, or the fact that it was too large to read. */
|
|
147
|
-
type BodyResult = { ok: true; body: string } | { ok: false };
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Read a request body, stopping at {@link MAX_BODY_BYTES}. An oversize body is
|
|
151
|
-
* reported rather than destroyed here, so the handler can still answer 413 over
|
|
152
|
-
* the open socket instead of the client seeing a dropped connection.
|
|
153
|
-
*/
|
|
154
|
-
function readBody(request: IncomingMessage, limit: number): Promise<BodyResult> {
|
|
155
|
-
return new Promise((resolve) => {
|
|
156
|
-
const chunks: Buffer[] = [];
|
|
157
|
-
let size = 0;
|
|
158
|
-
let done = false;
|
|
159
|
-
const settle = (result: BodyResult): void => {
|
|
160
|
-
if (done) return;
|
|
161
|
-
done = true;
|
|
162
|
-
resolve(result);
|
|
163
|
-
};
|
|
164
|
-
request.on("data", (chunk: Buffer) => {
|
|
165
|
-
if (done) return;
|
|
166
|
-
size += chunk.length;
|
|
167
|
-
if (size > limit) {
|
|
168
|
-
chunks.length = 0;
|
|
169
|
-
request.pause();
|
|
170
|
-
settle({ ok: false });
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
chunks.push(chunk);
|
|
174
|
-
});
|
|
175
|
-
request.on("end", () => settle({ ok: true, body: Buffer.concat(chunks).toString("utf-8") }));
|
|
176
|
-
request.on("error", () => settle({ ok: false }));
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
143
|
export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps = {}): PlanWebReview {
|
|
181
144
|
const sessions = new Set<PlanReviewSession>();
|
|
182
145
|
// What each running review can do about sharing; the session itself stays the
|
|
@@ -193,7 +156,9 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
193
156
|
): Promise<PlanReviewSession | undefined> => {
|
|
194
157
|
const { signal } = options;
|
|
195
158
|
const canApprove = options.canApprove === true;
|
|
196
|
-
|
|
159
|
+
// One cap for both listeners: a reviewer's page posts the same bodies as the
|
|
160
|
+
// owner's, so the shared one cannot be the stricter or the looser of the two.
|
|
161
|
+
const bodyLimit = deps.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
197
162
|
// One token per holder: the owner's is minted here and never shared, and a
|
|
198
163
|
// reviewer token is minted by `share()`. The table owns the collision rule and
|
|
199
164
|
// the name each link has bound.
|
|
@@ -279,13 +244,16 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
279
244
|
const page = async (caller: ReviewCaller): Promise<ReviewPageData> => {
|
|
280
245
|
const { role } = caller;
|
|
281
246
|
const stored = await refreshPageText();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
247
|
+
// The body, its blocks, and the comments out of one read of the file: an
|
|
248
|
+
// anchor names a block from this same parse, so the island cannot mark a
|
|
249
|
+
// comment onto a line the page never rendered.
|
|
250
|
+
const read = readFeedback(stored.content);
|
|
251
|
+
const current = read.body;
|
|
252
|
+
const blocks = read.blocks;
|
|
253
|
+
// Per-section change marks, each carrying its own section: the same shape the
|
|
254
|
+
// terminal review renders, so a heading and its mark cannot be paired wrongly.
|
|
255
|
+
const marks = changedSections(options.baseline, current);
|
|
256
|
+
const outline = marks.flatMap(({ section, changed }) =>
|
|
289
257
|
section.title === ""
|
|
290
258
|
? []
|
|
291
259
|
: [
|
|
@@ -294,7 +262,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
294
262
|
line: section.line,
|
|
295
263
|
level: section.level,
|
|
296
264
|
text: section.title,
|
|
297
|
-
...(changed
|
|
265
|
+
...(changed ? { changed: true } : {}),
|
|
298
266
|
},
|
|
299
267
|
],
|
|
300
268
|
);
|
|
@@ -312,9 +280,9 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
312
280
|
return {
|
|
313
281
|
// The document's own H1 reads better than the file-name slug, and the
|
|
314
282
|
// slug is still the fallback for a plan that starts with something else.
|
|
315
|
-
title:
|
|
283
|
+
title: marks.find((mark) => mark.section.level === 1)?.section.title || planTitle(basename(plan.path)),
|
|
316
284
|
blocks,
|
|
317
|
-
annotations:
|
|
285
|
+
annotations: read.annotations,
|
|
318
286
|
outline,
|
|
319
287
|
steps: {
|
|
320
288
|
done: stepList.filter((step) => step.status === "completed").length,
|
|
@@ -328,7 +296,6 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
328
296
|
rev: planDigest(current),
|
|
329
297
|
...(caller.name === undefined ? {} : { author: caller.name }),
|
|
330
298
|
canApprove: role === "owner" && canApprove,
|
|
331
|
-
...(role === "owner" && options.note !== undefined ? { note: options.note } : {}),
|
|
332
299
|
share: role === "owner" ? shareSession.state() : undefined,
|
|
333
300
|
};
|
|
334
301
|
};
|
|
@@ -376,6 +343,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
376
343
|
// frame hooks, so sharing stays one concern instead of five fields here.
|
|
377
344
|
const shareSession = createShareSession({
|
|
378
345
|
handleRequest,
|
|
346
|
+
maxBodyBytes: bodyLimit,
|
|
379
347
|
ownerPort: () => currentPort,
|
|
380
348
|
networkInterfaces: deps.networkInterfaces ?? networkInterfaces,
|
|
381
349
|
mintReviewer: () => tokens.mint("reviewer"),
|
|
@@ -410,78 +378,56 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
410
378
|
// Live event streams, used only to notice a closed owner tab: a reviewer's
|
|
411
379
|
// tab is not the review's lifetime. The explicit Cancel button and the abort
|
|
412
380
|
// signal are the deterministic paths.
|
|
413
|
-
const server = createServer(handleRequest);
|
|
414
381
|
/** One request, on either listener: the loopback page or a shared one. */
|
|
415
|
-
function handleRequest(request:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
const body = read.body;
|
|
426
|
-
let result: ServeResponse;
|
|
427
|
-
try {
|
|
428
|
-
result = await handleReviewRequest(context, method, request.url ?? "/", request.headers, body);
|
|
429
|
-
} catch {
|
|
430
|
-
result = {
|
|
431
|
-
status: 500,
|
|
432
|
-
headers: jsonHeaders(),
|
|
433
|
-
body: JSON.stringify({ error: "Internal error" }),
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
if (result.stream) {
|
|
437
|
-
openEventStream(response, result);
|
|
438
|
-
// A page that walks away mid-frame must not take the session down.
|
|
439
|
-
response.on("error", () => {});
|
|
440
|
-
const token = parseRequestUrl(request.url ?? "/")?.search.get("token") ?? "";
|
|
441
|
-
const streamCaller = context.caller(token);
|
|
442
|
-
if (streamCaller !== undefined) streams.add(response, { role: streamCaller.role, token });
|
|
443
|
-
request.on("close", () => streams.remove(response));
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
// A save or cancel only resolves the session after its response is on the
|
|
447
|
-
// wire; otherwise `close()` would force-close the socket mid-reply. The
|
|
448
|
-
// `close` path covers a client that walks away before the flush callback
|
|
449
|
-
// runs, so a settle cannot be lost with the socket.
|
|
450
|
-
const settlePending = (): void => {
|
|
451
|
-
const terminal = pendingResult;
|
|
452
|
-
if (!terminal) return;
|
|
453
|
-
pendingResult = undefined;
|
|
454
|
-
finish(terminal);
|
|
382
|
+
async function handleRequest(request: LoopbackRequest): Promise<LoopbackReply> {
|
|
383
|
+
let result: ServeResponse;
|
|
384
|
+
try {
|
|
385
|
+
result = await handleReviewRequest(context, request.method, request.url, request.headers, request.body);
|
|
386
|
+
} catch {
|
|
387
|
+
result = {
|
|
388
|
+
status: 500,
|
|
389
|
+
headers: jsonHeaders(),
|
|
390
|
+
body: JSON.stringify({ error: "Internal error" }),
|
|
455
391
|
};
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
392
|
+
}
|
|
393
|
+
// A save or cancel only resolves the session after its response is on the
|
|
394
|
+
// wire; otherwise `close()` would force-close the socket mid-reply. The same
|
|
395
|
+
// hook covers a client that walks away before the flush, so a settle cannot be
|
|
396
|
+
// lost with the socket.
|
|
397
|
+
const settlePending = (): void => {
|
|
398
|
+
const terminal = pendingResult;
|
|
399
|
+
if (!terminal) return;
|
|
400
|
+
pendingResult = undefined;
|
|
401
|
+
finish(terminal);
|
|
402
|
+
};
|
|
403
|
+
if (!result.stream) return { result, afterResponse: settlePending };
|
|
404
|
+
const token = parseRequestUrl(request.url)?.search.get("token") ?? "";
|
|
405
|
+
const streamCaller = context.caller(token);
|
|
406
|
+
return {
|
|
407
|
+
result,
|
|
408
|
+
openSink: (sink) => {
|
|
409
|
+
if (streamCaller !== undefined) streams.add(sink, { role: streamCaller.role, token });
|
|
410
|
+
return () => streams.remove(sink);
|
|
411
|
+
},
|
|
412
|
+
};
|
|
459
413
|
}
|
|
460
414
|
|
|
461
|
-
// The accept-level `error` listener lives in `bindHost`, so a transient
|
|
462
|
-
// failure cannot take the host down; the review still ends through its own
|
|
463
|
-
// paths: cancel, abort, drop, or shutdown.
|
|
464
415
|
const preferred = deps.port ?? ports.get(plan.path) ?? 0;
|
|
465
|
-
let
|
|
416
|
+
let server: LoopbackServer;
|
|
466
417
|
try {
|
|
467
|
-
|
|
418
|
+
server = await serveLoopback({
|
|
419
|
+
ports: preferred === 0 ? [0] : [preferred, 0],
|
|
420
|
+
maxBodyBytes: bodyLimit,
|
|
421
|
+
handler: handleRequest,
|
|
422
|
+
});
|
|
468
423
|
} catch {
|
|
469
|
-
// The remembered port may have been taken between turns;
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
port = await bindLoopback(server, 0);
|
|
473
|
-
} catch {
|
|
474
|
-
port = undefined;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (port === undefined) {
|
|
479
|
-
server.close();
|
|
424
|
+
// The remembered port may have been taken between turns; the fallback above
|
|
425
|
+
// covers that, so reaching here means neither port answered.
|
|
480
426
|
return undefined;
|
|
481
427
|
}
|
|
428
|
+
const port = server.port;
|
|
482
429
|
ports.set(plan.path, port);
|
|
483
430
|
currentPort = port;
|
|
484
|
-
const closeServer = createCloser(server);
|
|
485
431
|
|
|
486
432
|
const url = `http://127.0.0.1:${port}/?token=${token}`;
|
|
487
433
|
let closed = false;
|
|
@@ -498,7 +444,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
498
444
|
stopTicker();
|
|
499
445
|
streams.clear();
|
|
500
446
|
await shareSession.close();
|
|
501
|
-
await
|
|
447
|
+
await server.close();
|
|
502
448
|
};
|
|
503
449
|
|
|
504
450
|
const session: PlanReviewSession = { url, port, done, close };
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { withRailsSuppressed } from "
|
|
12
|
-
import { formatRewindListText, formatRewindRow } from "
|
|
11
|
+
import { withRailsSuppressed } from "../../../lib/rails.ts";
|
|
12
|
+
import { formatRewindListText, formatRewindRow } from "../text/format.ts";
|
|
13
13
|
import { openReview } from "./review.ts";
|
|
14
14
|
import { rewindTo } from "./rewind.ts";
|
|
15
|
-
import type { RewindRuntime } from "
|
|
16
|
-
import { RewindListComponent } from "
|
|
17
|
-
import { buildRewindPoints, type RewindPoint } from "
|
|
15
|
+
import type { RewindRuntime } from "../state/runtime.ts";
|
|
16
|
+
import { RewindListComponent } from "../view/rewind-tui.ts";
|
|
17
|
+
import { buildRewindPoints, type RewindPoint } from "../state/timeline.ts";
|
|
18
18
|
|
|
19
19
|
/** Pick a point from the timeline, or undefined when the user cancels. */
|
|
20
20
|
async function choosePoint(ctx: ExtensionCommandContext, points: RewindPoint[]): Promise<RewindPoint | undefined> {
|