@gavin-hu/my-pi-agent 0.14.0 → 0.15.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 +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* gone; the explicit buttons are the primary save/cancel path.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
14
|
+
import { keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
15
15
|
|
|
16
16
|
export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
17
17
|
"use strict";
|
|
@@ -47,7 +47,6 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
47
47
|
var cancelButton = document.getElementById("cancel");
|
|
48
48
|
var outlineList = document.getElementById("outline-list");
|
|
49
49
|
var progressEl = document.getElementById("progress-fill");
|
|
50
|
-
var keysDialog = document.getElementById("keys-dialog");
|
|
51
50
|
|
|
52
51
|
function el(tag, className) {
|
|
53
52
|
var node = document.createElement(tag);
|
|
@@ -732,23 +731,8 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
732
731
|
pendingQuote = wrap && quote ? { wrap: wrap, quote: quote } : null;
|
|
733
732
|
});
|
|
734
733
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
var open = force !== undefined ? force : keysDialog.hidden;
|
|
738
|
-
keysDialog.hidden = !open;
|
|
739
|
-
if (open) {
|
|
740
|
-
var close = document.getElementById("keys-close");
|
|
741
|
-
if (close) close.focus();
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (keysDialog) {
|
|
746
|
-
keysDialog.addEventListener("click", function (event) { if (event.target === keysDialog) toggleKeys(false); });
|
|
747
|
-
var keysButton = document.getElementById("shortcuts");
|
|
748
|
-
if (keysButton) keysButton.addEventListener("click", function () { toggleKeys(); });
|
|
749
|
-
var keysClose = document.getElementById("keys-close");
|
|
750
|
-
if (keysClose) keysClose.addEventListener("click", function () { toggleKeys(false); });
|
|
751
|
-
}
|
|
734
|
+
// The sheet's own wiring (trigger, close button, backdrop) is shared with browse.
|
|
735
|
+
${keysDialogScript("shortcuts")}
|
|
752
736
|
|
|
753
737
|
function typing(target) {
|
|
754
738
|
var tag = target && target.tagName ? target.tagName : "";
|
|
@@ -759,15 +743,15 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
759
743
|
if (event.key === "Escape") {
|
|
760
744
|
// Esc peels one layer at a time: the composer, then the sheet, then the review.
|
|
761
745
|
if (planEl && planEl.querySelector(".bf-composer")) { closeComposer(); return; }
|
|
762
|
-
if (
|
|
746
|
+
if (window.bfKeys.isOpen()) { window.bfKeys.close(); return; }
|
|
763
747
|
cancel();
|
|
764
748
|
return;
|
|
765
749
|
}
|
|
766
750
|
if (typing(event.target)) return;
|
|
767
751
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
768
|
-
if (event.key === "?") { event.preventDefault();
|
|
752
|
+
if (event.key === "?") { event.preventDefault(); window.bfKeys.toggle(); return; }
|
|
769
753
|
// While the shortcut sheet is open it owns the keyboard.
|
|
770
|
-
if (
|
|
754
|
+
if (window.bfKeys.isOpen()) return;
|
|
771
755
|
if (event.key === "j") { event.preventDefault(); moveActive(1); return; }
|
|
772
756
|
if (event.key === "k") { event.preventDefault(); moveActive(-1); return; }
|
|
773
757
|
if (event.key === "c") { event.preventDefault(); commentOnActive(); return; }
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import {
|
|
17
17
|
brandLockup,
|
|
18
18
|
docHead,
|
|
19
|
+
type KeyHint,
|
|
20
|
+
keysSheet,
|
|
19
21
|
SIDEBAR_COLLAPSE_MAX,
|
|
20
22
|
themeToggle,
|
|
21
23
|
WEB_BASE_CSS,
|
|
@@ -110,7 +112,7 @@ ${WEB_COMPONENTS_CSS}
|
|
|
110
112
|
.review-nav .lvl-3 a{padding-left:30px}
|
|
111
113
|
.review-nav .lvl-4 a,.review-nav .lvl-5 a,.review-nav .lvl-6 a{padding-left:42px}
|
|
112
114
|
.review-hints{margin:auto 0 0;padding-top:var(--space-3);color:var(--muted);font-size:var(--fs-xs);line-height:var(--lh-relaxed)}
|
|
113
|
-
.review-hints .
|
|
115
|
+
.review-hints .bf-kbd{margin:0 1px}
|
|
114
116
|
/* The document: one measure, centred, with a comment rail in its left margin. */
|
|
115
117
|
.bf-review>.bf-block{position:relative;width:100%;max-width:var(--review-measure);margin:0 auto;padding:3px 8px 3px var(--review-gutter);border-radius:var(--radius-md);overflow-wrap:break-word}
|
|
116
118
|
.bf-block[id]{scroll-margin-top:88px}
|
|
@@ -155,13 +157,6 @@ ${WEB_COMPONENTS_CSS}
|
|
|
155
157
|
.bf-comment__body{white-space:pre-wrap;overflow-wrap:anywhere;line-height:var(--lh-snug)}
|
|
156
158
|
.bf-composer{margin:0}
|
|
157
159
|
.bf-block--composing .bf-block__add{opacity:0}
|
|
158
|
-
/* The keyboard sheet, reusing the shared dialog shell. */
|
|
159
|
-
.bf-dialog__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border)}
|
|
160
|
-
.bf-dialog__head h2{margin:0;font-size:var(--fs-lg)}
|
|
161
|
-
.review-keys{display:grid;grid-template-columns:auto 1fr;gap:8px var(--space-4);margin:0;padding:var(--space-4)}
|
|
162
|
-
.review-keys dt{white-space:nowrap}
|
|
163
|
-
.review-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
|
|
164
|
-
.review-kbd{display:inline-block;min-width:20px;padding:1px 6px;border:1px solid var(--border);border-bottom-width:2px;border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:var(--fs-xs) var(--font-mono);text-align:center}
|
|
165
160
|
@media (max-width:1180px){
|
|
166
161
|
.bf-app__body{grid-template-columns:1fr;align-content:start}
|
|
167
162
|
.bf-app__sidebar{position:static;max-height:none;border-right:0;border-bottom:1px solid var(--border)}
|
|
@@ -192,29 +187,23 @@ ${WEB_COMPONENTS_CSS}
|
|
|
192
187
|
* not have to grow every time a binding is added.
|
|
193
188
|
*/
|
|
194
189
|
function reviewHints(canApprove: boolean): string {
|
|
195
|
-
const approve = canApprove ? `, <kbd class="
|
|
196
|
-
return `<p class="review-hints">Press <kbd class="
|
|
190
|
+
const approve = canApprove ? `, <kbd class="bf-kbd">a</kbd> to approve` : "";
|
|
191
|
+
return `<p class="review-hints">Press <kbd class="bf-kbd">j</kbd>/<kbd class="bf-kbd">k</kbd> to move, <kbd class="bf-kbd">c</kbd> to comment${approve}, <kbd class="bf-kbd">s</kbd> to save, <kbd class="bf-kbd">?</kbd> for all shortcuts.</p>`;
|
|
197
192
|
}
|
|
198
193
|
|
|
199
|
-
/** The
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
: ""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<div><dt><kbd class="review-kbd">s</kbd></dt><dd>Save feedback and return to Pi</dd></div>
|
|
213
|
-
<div><dt><kbd class="review-kbd">Esc</kbd></dt><dd>Close the composer, the sheet, or the review</dd></div>
|
|
214
|
-
<div><dt><kbd class="review-kbd">?</kbd></dt><dd>Toggle this sheet</dd></div>
|
|
215
|
-
</dl>
|
|
216
|
-
</div>
|
|
217
|
-
</div>`;
|
|
194
|
+
/** The shortcut sheet's rows: the review's own keys, led by approve when offered. */
|
|
195
|
+
function reviewKeys(canApprove: boolean): KeyHint[] {
|
|
196
|
+
const rows: KeyHint[] = [];
|
|
197
|
+
if (canApprove) rows.push({ keys: "a", label: "Approve and run the plan" });
|
|
198
|
+
rows.push(
|
|
199
|
+
{ keys: ["j", "k"], label: "Next and previous block" },
|
|
200
|
+
{ keys: "c", label: "Comment on the current block" },
|
|
201
|
+
{ keys: "Enter", label: "Add the comment you are writing" },
|
|
202
|
+
{ keys: "s", label: "Save feedback and return to Pi" },
|
|
203
|
+
{ keys: "Esc", label: "Close the composer, the sheet, or the review" },
|
|
204
|
+
{ keys: "?", label: "Toggle this sheet" },
|
|
205
|
+
);
|
|
206
|
+
return rows;
|
|
218
207
|
}
|
|
219
208
|
|
|
220
209
|
/**
|
|
@@ -268,7 +257,7 @@ ${saveButton}
|
|
|
268
257
|
<div class="${bodyClass}">
|
|
269
258
|
${outlinePanel}<main class="bf-app__main bf-review" id="plan" aria-label="Plan"></main>
|
|
270
259
|
</div>
|
|
271
|
-
${
|
|
260
|
+
${keysSheet(reviewKeys(data.canApprove))}
|
|
272
261
|
<script type="application/json" id="review-data">${island}</script>
|
|
273
262
|
<script src="/client.js" defer></script>
|
|
274
263
|
</body>
|
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
* local page from driving the session.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
isLocalHostHeader,
|
|
17
|
+
jsonError,
|
|
18
|
+
jsonHeaders,
|
|
19
|
+
jsonResponse,
|
|
20
|
+
pageHeaders,
|
|
21
|
+
parseRequestUrl,
|
|
22
|
+
scriptHeaders,
|
|
23
|
+
type ServeResponse,
|
|
24
|
+
} from "../../lib/http/index.ts";
|
|
15
25
|
import { THEME_JS } from "../../lib/web/index.ts";
|
|
16
26
|
import { coerceAnnotations, type PlanAnnotation } from "./feedback.ts";
|
|
17
27
|
import { REVIEW_CLIENT_JS } from "./web-client.ts";
|
|
@@ -38,42 +48,10 @@ export interface ReviewContext {
|
|
|
38
48
|
onCancel: () => void;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/** When true, the node server keeps the connection open as an SSE stream. */
|
|
46
|
-
stream?: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const HOST_PATTERN = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i;
|
|
50
|
-
|
|
51
|
-
const HTML_SECURITY: Record<string, string> = {
|
|
52
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
53
|
-
// `frame-ancestors` keeps a local page from framing the review and clickjacking
|
|
54
|
-
// the one control with a side effect (Approve & run); the rest is the same
|
|
55
|
-
// no-script, no-network envelope the page already relied on.
|
|
56
|
-
"Content-Security-Policy":
|
|
57
|
-
"default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'self'; " +
|
|
58
|
-
"connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'none'",
|
|
59
|
-
"X-Frame-Options": "DENY",
|
|
60
|
-
"X-Content-Type-Options": "nosniff",
|
|
61
|
-
"Referrer-Policy": "no-referrer",
|
|
62
|
-
"Cache-Control": "no-store",
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const SCRIPT_HEADERS: Record<string, string> = {
|
|
66
|
-
...HTML_SECURITY,
|
|
67
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const JSON_HEADERS: Record<string, string> = {
|
|
71
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
72
|
-
"X-Content-Type-Options": "nosniff",
|
|
73
|
-
"Referrer-Policy": "no-referrer",
|
|
74
|
-
"Cache-Control": "no-store",
|
|
75
|
-
};
|
|
76
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The event stream's headers: the one response that is not the page policy, because
|
|
53
|
+
* it is a long-lived `text/event-stream` rather than a document.
|
|
54
|
+
*/
|
|
77
55
|
const EVENT_HEADERS: Record<string, string> = {
|
|
78
56
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
79
57
|
"Cache-Control": "no-store",
|
|
@@ -81,14 +59,6 @@ const EVENT_HEADERS: Record<string, string> = {
|
|
|
81
59
|
"X-Accel-Buffering": "no",
|
|
82
60
|
};
|
|
83
61
|
|
|
84
|
-
function jsonResponse(status: number, value: unknown): ServeResponse {
|
|
85
|
-
return { status, headers: { ...JSON_HEADERS }, body: JSON.stringify(value) };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function errorResponse(status: number, message: string): ServeResponse {
|
|
89
|
-
return jsonResponse(status, { error: message });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
62
|
/**
|
|
93
63
|
* Parse and validate an `{ annotations }` request body. Returns the annotations,
|
|
94
64
|
* or an error response to send instead.
|
|
@@ -98,10 +68,10 @@ function parseAnnotations(body: string): PlanAnnotation[] | ServeResponse {
|
|
|
98
68
|
try {
|
|
99
69
|
parsed = JSON.parse(body);
|
|
100
70
|
} catch {
|
|
101
|
-
return
|
|
71
|
+
return jsonError(400, "The request body must be JSON.");
|
|
102
72
|
}
|
|
103
73
|
const annotations = coerceAnnotations((parsed as { annotations?: unknown } | null)?.annotations);
|
|
104
|
-
if (!annotations) return
|
|
74
|
+
if (!annotations) return jsonError(400, "The annotations are malformed.");
|
|
105
75
|
return annotations;
|
|
106
76
|
}
|
|
107
77
|
|
|
@@ -118,15 +88,14 @@ function checkEnvelope(
|
|
|
118
88
|
const upper = method.toUpperCase();
|
|
119
89
|
if (upper !== "GET" && upper !== "HEAD" && upper !== "POST") {
|
|
120
90
|
return {
|
|
121
|
-
...
|
|
122
|
-
headers: { ...
|
|
91
|
+
...jsonError(405, "Only GET, HEAD, and POST are served."),
|
|
92
|
+
headers: { ...jsonHeaders(), Allow: "GET, HEAD, POST" },
|
|
123
93
|
};
|
|
124
94
|
}
|
|
125
95
|
// Fail closed: a missing or non-string Host is rejected too, not just a
|
|
126
96
|
// foreign one, so the DNS-rebinding guard cannot be skipped.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return errorResponse(403, "Unexpected Host header.");
|
|
97
|
+
if (!isLocalHostHeader(headers, { required: true })) {
|
|
98
|
+
return jsonError(403, "Unexpected Host header.");
|
|
130
99
|
}
|
|
131
100
|
return undefined;
|
|
132
101
|
}
|
|
@@ -145,52 +114,46 @@ export async function handleReviewRequest(
|
|
|
145
114
|
const envelope = checkEnvelope(method, headers);
|
|
146
115
|
if (envelope) return envelope;
|
|
147
116
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const url = new URL(rawUrl, "http://127.0.0.1");
|
|
152
|
-
pathname = url.pathname;
|
|
153
|
-
search = url.searchParams;
|
|
154
|
-
} catch {
|
|
155
|
-
return errorResponse(400, "Malformed request URL.");
|
|
156
|
-
}
|
|
117
|
+
const parsed = parseRequestUrl(rawUrl);
|
|
118
|
+
if (!parsed) return jsonError(400, "Malformed request URL.");
|
|
119
|
+
const { pathname, search } = parsed;
|
|
157
120
|
|
|
158
121
|
if (pathname === "/favicon.ico") return { status: 204, headers: { "Cache-Control": "no-store" } };
|
|
159
|
-
if (pathname === "/theme.js") return { status: 200, headers:
|
|
160
|
-
if (pathname === "/client.js") return { status: 200, headers:
|
|
122
|
+
if (pathname === "/theme.js") return { status: 200, headers: scriptHeaders(), body: THEME_JS };
|
|
123
|
+
if (pathname === "/client.js") return { status: 200, headers: scriptHeaders(), body: REVIEW_CLIENT_JS };
|
|
161
124
|
|
|
162
|
-
if (search.get("token") !== ctx.token) return
|
|
125
|
+
if (search.get("token") !== ctx.token) return jsonError(403, "Invalid or missing token.");
|
|
163
126
|
|
|
164
127
|
if (pathname === "/" || pathname === "/index.html") {
|
|
165
|
-
return { status: 200, headers:
|
|
128
|
+
return { status: 200, headers: pageHeaders(), body: renderReviewPage(await ctx.page()) };
|
|
166
129
|
}
|
|
167
130
|
if (pathname === "/events") return { status: 200, headers: { ...EVENT_HEADERS }, stream: true };
|
|
168
131
|
if (pathname === "/api/cancel") {
|
|
169
|
-
if (method.toUpperCase() !== "POST") return
|
|
132
|
+
if (method.toUpperCase() !== "POST") return jsonError(405, "Use POST to cancel.");
|
|
170
133
|
ctx.onCancel();
|
|
171
134
|
return jsonResponse(200, { ok: true });
|
|
172
135
|
}
|
|
173
136
|
if (pathname === "/api/feedback") {
|
|
174
|
-
if (method.toUpperCase() !== "POST") return
|
|
137
|
+
if (method.toUpperCase() !== "POST") return jsonError(405, "Use POST to save feedback.");
|
|
175
138
|
const annotations = parseAnnotations(body);
|
|
176
139
|
if (isResponse(annotations)) return annotations;
|
|
177
140
|
const result = await ctx.save(annotations);
|
|
178
|
-
if (!result.ok) return
|
|
141
|
+
if (!result.ok) return jsonError(result.status, result.message);
|
|
179
142
|
ctx.onSaved(annotations);
|
|
180
143
|
return jsonResponse(200, { ok: true });
|
|
181
144
|
}
|
|
182
145
|
if (pathname === "/api/approve") {
|
|
183
|
-
if (method.toUpperCase() !== "POST") return
|
|
146
|
+
if (method.toUpperCase() !== "POST") return jsonError(405, "Use POST to approve.");
|
|
184
147
|
// Only the exit_plan_mode flow owns the approval side effects, so a comment-only
|
|
185
148
|
// review refuses this even if a client posts it directly.
|
|
186
|
-
if (!ctx.canApprove) return
|
|
149
|
+
if (!ctx.canApprove) return jsonError(403, "This review cannot be approved from the browser.");
|
|
187
150
|
const annotations = parseAnnotations(body);
|
|
188
151
|
if (isResponse(annotations)) return annotations;
|
|
189
152
|
const result = await ctx.save(annotations);
|
|
190
|
-
if (!result.ok) return
|
|
153
|
+
if (!result.ok) return jsonError(result.status, result.message);
|
|
191
154
|
ctx.onApprove(annotations);
|
|
192
155
|
return jsonResponse(200, { ok: true });
|
|
193
156
|
}
|
|
194
157
|
|
|
195
|
-
return
|
|
158
|
+
return jsonError(404, "That page does not exist.");
|
|
196
159
|
}
|
package/extensions/plan/web.ts
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
|
-
import { createServer, type IncomingMessage, type
|
|
14
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
15
15
|
import { basename } from "node:path";
|
|
16
16
|
import { openInBrowser } from "../../lib/browser.ts";
|
|
17
|
+
import { bindLoopback, createCloser, jsonHeaders, respond, type ServeResponse } from "../../lib/http/index.ts";
|
|
17
18
|
import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
|
|
18
19
|
import { codeBody, fenceInfo, planBlocks } from "./blocks.ts";
|
|
19
20
|
import {
|
|
@@ -27,13 +28,7 @@ import {
|
|
|
27
28
|
import { planTitle, type PlanStore, type StoredPlan } from "./plans.ts";
|
|
28
29
|
import { changedSections, type SectionDigest } from "./plan-text.ts";
|
|
29
30
|
import { extractPlanSections, extractPlanSteps } from "./steps.ts";
|
|
30
|
-
import {
|
|
31
|
-
handleReviewRequest,
|
|
32
|
-
JSON_HEADERS,
|
|
33
|
-
type ReviewContext,
|
|
34
|
-
type ServeResponse,
|
|
35
|
-
type SaveResult,
|
|
36
|
-
} from "./web-router.ts";
|
|
31
|
+
import { handleReviewRequest, type ReviewContext, type SaveResult } from "./web-router.ts";
|
|
37
32
|
import type { ReviewPageData } from "./web-page.ts";
|
|
38
33
|
|
|
39
34
|
/** How a browser review ended. */
|
|
@@ -123,19 +118,6 @@ function readBody(request: IncomingMessage, limit: number): Promise<BodyResult>
|
|
|
123
118
|
});
|
|
124
119
|
}
|
|
125
120
|
|
|
126
|
-
async function listen(server: Server, port: number): Promise<number> {
|
|
127
|
-
await new Promise<void>((resolve, reject) => {
|
|
128
|
-
const onError = (error: Error) => reject(error);
|
|
129
|
-
server.once("error", onError);
|
|
130
|
-
server.listen(port, "127.0.0.1", () => {
|
|
131
|
-
server.off("error", onError);
|
|
132
|
-
resolve();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
const address = server.address();
|
|
136
|
-
return typeof address === "object" && address ? address.port : port;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
121
|
export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps = {}): PlanWebReview {
|
|
140
122
|
const sessions = new Set<PlanReviewSession>();
|
|
141
123
|
// Last bound port per plan path, so the same plan keeps one origin across
|
|
@@ -284,7 +266,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
284
266
|
const read =
|
|
285
267
|
method.toUpperCase() === "POST" ? await readBody(request, maxBody) : { ok: true as const, body: "" };
|
|
286
268
|
if (!read.ok) {
|
|
287
|
-
response.writeHead(413, { ...
|
|
269
|
+
response.writeHead(413, { ...jsonHeaders(), Connection: "close" });
|
|
288
270
|
response.end(JSON.stringify({ error: "The request body is too large." }));
|
|
289
271
|
return;
|
|
290
272
|
}
|
|
@@ -295,7 +277,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
295
277
|
} catch {
|
|
296
278
|
result = {
|
|
297
279
|
status: 500,
|
|
298
|
-
headers:
|
|
280
|
+
headers: jsonHeaders(),
|
|
299
281
|
body: JSON.stringify({ error: "Internal error" }),
|
|
300
282
|
};
|
|
301
283
|
}
|
|
@@ -314,11 +296,6 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
314
296
|
});
|
|
315
297
|
return;
|
|
316
298
|
}
|
|
317
|
-
response.writeHead(result.status, result.headers);
|
|
318
|
-
if (method.toUpperCase() === "HEAD") {
|
|
319
|
-
response.end();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
299
|
// A save or cancel only resolves the session after its response is on the
|
|
323
300
|
// wire; otherwise `close()` would force-close the socket mid-reply. The
|
|
324
301
|
// `close` path covers a client that walks away before the flush callback
|
|
@@ -329,25 +306,23 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
329
306
|
pendingResult = undefined;
|
|
330
307
|
finish(terminal);
|
|
331
308
|
};
|
|
332
|
-
|
|
309
|
+
respond(request, response, result, settlePending);
|
|
333
310
|
response.on("close", settlePending);
|
|
334
311
|
})();
|
|
335
312
|
});
|
|
336
313
|
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
server.on("error", () => {});
|
|
341
|
-
|
|
314
|
+
// The accept-level `error` listener lives in `bindLoopback`, so a transient
|
|
315
|
+
// failure cannot take the host down; the review still ends through its own
|
|
316
|
+
// paths: cancel, abort, drop, or shutdown.
|
|
342
317
|
const preferred = deps.port ?? ports.get(plan.path) ?? 0;
|
|
343
318
|
let port: number | undefined;
|
|
344
319
|
try {
|
|
345
|
-
port = await
|
|
320
|
+
port = await bindLoopback(server, preferred);
|
|
346
321
|
} catch {
|
|
347
322
|
// The remembered port may have been taken between turns; fall back once.
|
|
348
323
|
if (preferred !== 0) {
|
|
349
324
|
try {
|
|
350
|
-
port = await
|
|
325
|
+
port = await bindLoopback(server, 0);
|
|
351
326
|
} catch {
|
|
352
327
|
port = undefined;
|
|
353
328
|
}
|
|
@@ -358,6 +333,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
358
333
|
return undefined;
|
|
359
334
|
}
|
|
360
335
|
ports.set(plan.path, port);
|
|
336
|
+
const closeServer = createCloser(server);
|
|
361
337
|
|
|
362
338
|
const url = `http://127.0.0.1:${port}/?token=${token}`;
|
|
363
339
|
let closed = false;
|
|
@@ -370,10 +346,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
370
346
|
for (const client of clients) client.end();
|
|
371
347
|
clients.clear();
|
|
372
348
|
finish({ status: "cancelled" });
|
|
373
|
-
await
|
|
374
|
-
server.close(() => resolve());
|
|
375
|
-
(server as { closeAllConnections?: () => void }).closeAllConnections?.();
|
|
376
|
-
});
|
|
349
|
+
await closeServer();
|
|
377
350
|
};
|
|
378
351
|
|
|
379
352
|
const session: PlanReviewSession = { url, port, done, close };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Answer normalization and model-facing formatting (pure).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
5
|
+
import { lineOf, sanitizeLine } from "../../lib/format.ts";
|
|
6
6
|
import type { Answer, AskResult, Question, Selection } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
export const CANCELLED_TEXT = "User cancelled the question.";
|
|
@@ -93,20 +93,15 @@ export interface CallQuestion {
|
|
|
93
93
|
options?: unknown;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** A partial value read as a single line; anything but a string reads empty. */
|
|
97
|
-
function asLine(value: unknown): string {
|
|
98
|
-
return typeof value === "string" ? sanitizeLine(value) : "";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
96
|
/** Display headers with positional defaults (`Q1`, `Q2`, …). */
|
|
102
97
|
export function questionHeaders(questions: CallQuestion[]): string[] {
|
|
103
|
-
return questions.map((question, i) =>
|
|
98
|
+
return questions.map((question, i) => lineOf(question?.header) || `Q${i + 1}`);
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
/** One `Header: a, b, Other` summary per question that offers options. */
|
|
107
102
|
export function optionSummary(questions: CallQuestion[]): string[] {
|
|
108
103
|
return questions
|
|
109
|
-
.map((question, i) => ({ header:
|
|
104
|
+
.map((question, i) => ({ header: lineOf(question?.header) || `Q${i + 1}`, options: question?.options }))
|
|
110
105
|
.filter(({ options }) => Array.isArray(options) && options.length > 0)
|
|
111
106
|
.map(({ header, options }) => {
|
|
112
107
|
// A partial option has no label yet; list the ones that have arrived.
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
19
19
|
import { flowCells } from "../../lib/tui/columns.ts";
|
|
20
20
|
import { clampScroll, formatRange, keepVisible, selectionMarker, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
21
|
-
import { editorTheme, screenHint } from "../../lib/tui/chrome.ts";
|
|
21
|
+
import { editorTheme, screenHint, type ScreenHint } from "../../lib/tui/chrome.ts";
|
|
22
22
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
23
23
|
import { checkbox } from "../../lib/tui/rows.ts";
|
|
24
24
|
import { plainRule, titleRule } from "../../lib/tui/rules.ts";
|
|
@@ -255,7 +255,7 @@ export async function askViaTui(
|
|
|
255
255
|
}
|
|
256
256
|
lines.push("");
|
|
257
257
|
if (state.message) addWithPrefix(" ", paint(theme, "warn", state.message));
|
|
258
|
-
lines.push(screenHint(theme, renderWidth, ["Enter to submit", "Esc to go back"]));
|
|
258
|
+
lines.push(screenHint(theme, renderWidth, [{ text: "Enter to submit", color: "accent" }, "Esc to go back"]));
|
|
259
259
|
} else if (submitTab) {
|
|
260
260
|
addWithPrefix(" ", paint(theme, "accent", theme.bold("Ready to submit")));
|
|
261
261
|
lines.push("");
|
|
@@ -285,12 +285,12 @@ export async function askViaTui(
|
|
|
285
285
|
// option list can be given exactly the rows that are left.
|
|
286
286
|
const tail: string[] = [""];
|
|
287
287
|
if (state.message) tail.push(...prefixWrapped(" ", paint(theme, "warn", state.message)));
|
|
288
|
-
const hints =
|
|
288
|
+
const hints: ScreenHint[] =
|
|
289
289
|
optionCount(question) === 0
|
|
290
|
-
? ["Enter to type an answer", "Esc cancel"]
|
|
290
|
+
? [{ text: "Enter to type an answer", color: "accent" }, "Esc cancel"]
|
|
291
291
|
: question.multiSelect
|
|
292
|
-
? ["↑/↓ move", "Space toggle", "Enter confirm", "Esc cancel"]
|
|
293
|
-
: ["↑/↓ choose", "Enter select", "Esc cancel"];
|
|
292
|
+
? ["↑/↓ move", "Space toggle", { text: "Enter confirm", color: "accent" }, "Esc cancel"]
|
|
293
|
+
: ["↑/↓ choose", { text: "Enter select", color: "accent" }, "Esc cancel"];
|
|
294
294
|
if (isMulti) hints.unshift(`Tab/${GLYPHS.prev}${GLYPHS.next} navigate`);
|
|
295
295
|
tail.push(screenHint(theme, renderWidth, hints));
|
|
296
296
|
if (question.multiSelect) {
|