@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
package/extensions/plan/tui.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal rendering for the plan review screen.
|
|
3
3
|
*
|
|
4
|
-
* `sanitizePlanText` is exported so the transcript renderer (`tools.ts`) can
|
|
5
|
-
* strip control characters from plan text with the same rules the review screen
|
|
6
|
-
* uses.
|
|
7
|
-
*
|
|
8
4
|
* `PlanViewComponent` is the scrollable review screen `exit_plan_mode` opens
|
|
9
5
|
* for approval. The plan body is rendered by Pi's own `Markdown` component with
|
|
10
6
|
* the active Markdown theme, so headings, lists, code, links, and quotes match
|
|
@@ -30,7 +26,7 @@ import {
|
|
|
30
26
|
type TuiMouseEvent,
|
|
31
27
|
type TuiMouseEventResult,
|
|
32
28
|
} from "@earendil-works/pi-tui";
|
|
33
|
-
import {
|
|
29
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
34
30
|
import {
|
|
35
31
|
editorTheme,
|
|
36
32
|
screenHint,
|
|
@@ -45,8 +41,8 @@ import { titleRule } from "../../lib/tui/rules.ts";
|
|
|
45
41
|
import { emptyState, checkbox } from "../../lib/tui/rows.ts";
|
|
46
42
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
47
43
|
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
48
|
-
import { changedSections,
|
|
49
|
-
import {
|
|
44
|
+
import { changedSections, sanitizePlanText, type SectionDigest } from "./plan-text.ts";
|
|
45
|
+
import { extractPlanSections } from "./steps.ts";
|
|
50
46
|
|
|
51
47
|
/** What the user chose from the read/review screen. */
|
|
52
48
|
export type PlanViewAction = "approve" | "refine" | "keep" | "browser";
|
|
@@ -71,10 +67,10 @@ interface BodyCache {
|
|
|
71
67
|
export interface PlanViewOptions {
|
|
72
68
|
plan: StoredPlan;
|
|
73
69
|
/**
|
|
74
|
-
* The
|
|
75
|
-
* Used only to mark the sections that changed since then.
|
|
70
|
+
* The section digests the previous review of this plan showed, when there was
|
|
71
|
+
* one. Used only to mark the sections that changed since then.
|
|
76
72
|
*/
|
|
77
|
-
|
|
73
|
+
baseline?: SectionDigest[];
|
|
78
74
|
theme: Theme;
|
|
79
75
|
/**
|
|
80
76
|
* Called once when the screen closes. `refinement` carries the typed text
|
|
@@ -110,17 +106,6 @@ const PRIMARY_HINTS: ScreenHint[] = [
|
|
|
110
106
|
const SCROLL_HINTS: ScreenHint[] = ["↑/↓ or k/j scroll", "space/b page", "g/G ends"];
|
|
111
107
|
/** Section-jump hint, shown when the plan has more than one heading section. */
|
|
112
108
|
const SECTION_HINTS: ScreenHint[] = ["n/p section"];
|
|
113
|
-
/** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
|
|
114
|
-
const TAB_WIDTH = 4;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Replace control characters (including ESC and the C1 block) and expand tabs
|
|
118
|
-
* so model-authored plan text cannot drive the terminal. Newlines are kept, so
|
|
119
|
-
* callers can still render or split the text line by line.
|
|
120
|
-
*/
|
|
121
|
-
export function sanitizePlanText(text: string): string {
|
|
122
|
-
return stripControlChars(text.replace(/\t/g, " ".repeat(TAB_WIDTH)));
|
|
123
|
-
}
|
|
124
109
|
|
|
125
110
|
/** A task checkbox in a list marker: `- [x] `, `1. [ ] `. */
|
|
126
111
|
const TASK_MARKER = /^([-*+]|\d+[.)])(\s+)(\[([ xX])\])(\s*)$/;
|
|
@@ -128,8 +113,11 @@ const TASK_MARKER = /^([-*+]|\d+[.)])(\s+)(\[([ xX])\])(\s*)$/;
|
|
|
128
113
|
/**
|
|
129
114
|
* Wrap the Markdown theme so task checkboxes read at a glance with the shared
|
|
130
115
|
* `◼`/`◻` vocabulary, the same box the bytefly screen and the question dialog
|
|
131
|
-
* draw. The
|
|
132
|
-
*
|
|
116
|
+
* draw. The box is one column wide instead of the bracket's three, so the two
|
|
117
|
+
* freed columns are kept as padding: pi-tui derives a continuation line's indent
|
|
118
|
+
* from the original marker's width, and shortening the marker without padding
|
|
119
|
+
* hangs every wrapped task line two columns too far right, where the width
|
|
120
|
+
* truncation clips its tail.
|
|
133
121
|
*/
|
|
134
122
|
function planMarkdownTheme(theme: Theme, base: MarkdownTheme): MarkdownTheme {
|
|
135
123
|
return {
|
|
@@ -138,7 +126,9 @@ function planMarkdownTheme(theme: Theme, base: MarkdownTheme): MarkdownTheme {
|
|
|
138
126
|
const match = TASK_MARKER.exec(text);
|
|
139
127
|
if (!match) return base.listBullet(text);
|
|
140
128
|
const [, bullet, gap, , mark, trail] = match;
|
|
141
|
-
|
|
129
|
+
const themed = `${base.listBullet(`${bullet}${gap}`)}${checkbox(theme, mark.toLowerCase() === "x")}`;
|
|
130
|
+
const pad = Math.max(0, visibleWidth(text) - visibleWidth(themed) - visibleWidth(trail));
|
|
131
|
+
return `${themed}${" ".repeat(pad)}${trail}`;
|
|
142
132
|
},
|
|
143
133
|
};
|
|
144
134
|
}
|
|
@@ -205,10 +195,7 @@ export class PlanViewComponent implements Component {
|
|
|
205
195
|
const base = this.options.markdownTheme ?? getMarkdownTheme();
|
|
206
196
|
const theme = planMarkdownTheme(this.theme, base);
|
|
207
197
|
const current = sanitizePlanText(this.options.plan.content);
|
|
208
|
-
const changed =
|
|
209
|
-
this.options.previous === undefined
|
|
210
|
-
? new Set<string>()
|
|
211
|
-
: changedSections(stripFeedback(this.options.previous), stripFeedback(current));
|
|
198
|
+
const changed = changedSections(this.options.baseline, current);
|
|
212
199
|
const chunks = extractPlanSections(current);
|
|
213
200
|
const lines: string[] = [];
|
|
214
201
|
const sections: SectionOffset[] = [];
|
|
@@ -221,14 +208,14 @@ export class PlanViewComponent implements Component {
|
|
|
221
208
|
sections.push({
|
|
222
209
|
title: chunk.title,
|
|
223
210
|
offset: lines.length,
|
|
224
|
-
...(changed
|
|
211
|
+
...(changed[index] ? { changed: true } : {}),
|
|
225
212
|
});
|
|
226
213
|
lines.push(...rendered);
|
|
227
214
|
// Sections render independently, so restore the blank line a heading-only
|
|
228
215
|
// chunk omits before the next section.
|
|
229
216
|
if (index < chunks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
230
217
|
});
|
|
231
|
-
this.cache = { width, lines, sections, changedCount:
|
|
218
|
+
this.cache = { width, lines, sections, changedCount: changed.filter(Boolean).length };
|
|
232
219
|
return this.cache;
|
|
233
220
|
}
|
|
234
221
|
|
|
@@ -363,7 +350,7 @@ export class PlanViewComponent implements Component {
|
|
|
363
350
|
const theme = this.theme;
|
|
364
351
|
const w = Math.max(1, width);
|
|
365
352
|
const plan = this.options.plan;
|
|
366
|
-
const title = planTitle(basename(plan.path));
|
|
353
|
+
const title = sanitizeLine(planTitle(basename(plan.path)));
|
|
367
354
|
|
|
368
355
|
const previousWidth = this.cache?.width;
|
|
369
356
|
const built = plan.content.trim() ? this.build(w) : { width: w, lines: [], sections: [], changedCount: 0 };
|
package/extensions/plan/types.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Shared types for the plan-mode extension.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { PlanAnnotation } from "./feedback.ts";
|
|
6
|
+
import type { SectionDigest } from "./plan-text.ts";
|
|
5
7
|
import type { StepUpdate } from "./steps.ts";
|
|
6
8
|
|
|
7
9
|
/** Persisted (custom entry) plan-mode state. */
|
|
@@ -14,6 +16,14 @@ export interface PlanModeEntry {
|
|
|
14
16
|
removed?: string[];
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
/** Persisted (custom entry) review baseline for one plan. */
|
|
20
|
+
export interface ReviewBaselineEntry {
|
|
21
|
+
/** Resolved path of the plan file the review showed. */
|
|
22
|
+
path: string;
|
|
23
|
+
/** One digest per heading section, in document order. */
|
|
24
|
+
sections: SectionDigest[];
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
/** Structured result carried in `enter_plan_mode`'s `details`. */
|
|
18
28
|
export interface EnterPlanModeDetails {
|
|
19
29
|
/** Whether the user agreed to enter plan mode. */
|
|
@@ -62,6 +72,8 @@ export interface ExitPlanModeDetails {
|
|
|
62
72
|
refined?: boolean;
|
|
63
73
|
/** The refinement the user typed, when `refined` is true. */
|
|
64
74
|
refinement?: string;
|
|
75
|
+
/** Comments the review carried when it approved or requested a refinement. */
|
|
76
|
+
annotations?: PlanAnnotation[];
|
|
65
77
|
/** True when no interactive UI was available to ask for approval. */
|
|
66
78
|
unavailable?: boolean;
|
|
67
79
|
}
|
|
@@ -17,12 +17,15 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
17
17
|
"use strict";
|
|
18
18
|
|
|
19
19
|
var dataNode = document.getElementById("review-data");
|
|
20
|
-
|
|
20
|
+
// The caps come from the island, which takes them from the server's own
|
|
21
|
+
// validation; the fallback only guards a page whose island failed to parse.
|
|
22
|
+
var fallback = { blocks: [], annotations: [], outline: [], steps: { done: 0, total: 0 }, code: {}, limits: { comments: 200, comment: 2000, quote: 500 }, token: "" };
|
|
21
23
|
var data = fallback;
|
|
22
24
|
if (dataNode && dataNode.textContent) {
|
|
23
25
|
try { data = JSON.parse(dataNode.textContent); } catch (error) { data = fallback; }
|
|
24
26
|
}
|
|
25
27
|
var token = typeof data.token === "string" ? data.token : "";
|
|
28
|
+
var limits = data.limits || fallback.limits;
|
|
26
29
|
var annotations = Array.isArray(data.annotations) ? data.annotations.slice() : [];
|
|
27
30
|
var saved = false;
|
|
28
31
|
var approved = false;
|
|
@@ -34,6 +37,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
34
37
|
var pendingQuote = null;
|
|
35
38
|
var activeIndex = -1;
|
|
36
39
|
var outlineItems = {};
|
|
40
|
+
var stream = null;
|
|
37
41
|
|
|
38
42
|
var planEl = document.getElementById("plan");
|
|
39
43
|
var statusEl = document.getElementById("status");
|
|
@@ -94,12 +98,20 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
94
98
|
stateEl.className = "bf-chip bf-chip--accent review-state" + (tone ? " bf-chip--" + tone : "");
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
// A link is rendered only for an http(s) or mailto target. Resolving against
|
|
102
|
+
// the review origin is what catches a target that only looks relative: the
|
|
103
|
+
// URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
|
|
104
|
+
// it is parsed, and a disguised target must stay text.
|
|
105
|
+
function safeHref(url, base) {
|
|
98
106
|
var trimmed = String(url).trim();
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return trimmed;
|
|
107
|
+
if (!trimmed || /^\/\//.test(trimmed)) return null;
|
|
108
|
+
var resolved;
|
|
109
|
+
try { resolved = new URL(trimmed, base); } catch (error) { return null; }
|
|
110
|
+
if (resolved.protocol === "mailto:") return trimmed;
|
|
111
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
|
|
112
|
+
if (resolved.username || resolved.password) return null;
|
|
113
|
+
if (resolved.origin === new URL(base).origin) return resolved.href;
|
|
114
|
+
return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
// Inline token order matters: code, emphasis, and links are matched before a
|
|
@@ -120,7 +132,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
120
132
|
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
121
133
|
}
|
|
122
134
|
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
123
|
-
var href = safeHref(match[2]);
|
|
135
|
+
var href = safeHref(match[2], location.href);
|
|
124
136
|
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
125
137
|
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
126
138
|
container.appendChild(link); return;
|
|
@@ -133,7 +145,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
133
145
|
}
|
|
134
146
|
var dot = url.indexOf(".");
|
|
135
147
|
var target = /^www\./i.test(url) ? "http://" + url : url;
|
|
136
|
-
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target) : null;
|
|
148
|
+
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
|
|
137
149
|
if (safe) {
|
|
138
150
|
var auto = el("a");
|
|
139
151
|
auto.textContent = url;
|
|
@@ -311,7 +323,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
311
323
|
if (!selection || selection.isCollapsed) return "";
|
|
312
324
|
var range = selection.rangeCount ? selection.getRangeAt(0) : null;
|
|
313
325
|
if (!range || !wrap.contains(range.commonAncestorContainer)) return "";
|
|
314
|
-
return selection.toString().replace(/\s+/g, " ").trim().slice(0,
|
|
326
|
+
return selection.toString().replace(/\s+/g, " ").trim().slice(0, limits.quote);
|
|
315
327
|
}
|
|
316
328
|
|
|
317
329
|
function commentsBoxAfter(wrap) {
|
|
@@ -325,7 +337,9 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
325
337
|
// Enter submits a comment; Shift+Enter is left for the browser.
|
|
326
338
|
function submitOnEnter(area, submit) {
|
|
327
339
|
area.addEventListener("keydown", function (event) {
|
|
328
|
-
|
|
340
|
+
// An IME uses Enter to commit a composition; submitting then would send a
|
|
341
|
+
// half-typed comment.
|
|
342
|
+
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
329
343
|
event.preventDefault();
|
|
330
344
|
submit();
|
|
331
345
|
});
|
|
@@ -343,6 +357,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
343
357
|
var area = el("textarea", "bf-textarea");
|
|
344
358
|
area.placeholder = "Leave feedback on this part of the plan (Enter to add)";
|
|
345
359
|
area.setAttribute("aria-label", "Comment");
|
|
360
|
+
area.maxLength = limits.comment;
|
|
346
361
|
composer.appendChild(area);
|
|
347
362
|
var row = el("div", "bf-composer__row");
|
|
348
363
|
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
@@ -351,7 +366,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
351
366
|
var submit = function () {
|
|
352
367
|
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
353
368
|
if (!comment) { area.focus(); return; }
|
|
354
|
-
if (annotations.length >=
|
|
369
|
+
if (annotations.length >= limits.comments) { setStatus("Comment limit reached (" + limits.comments + ").", "error"); return; }
|
|
355
370
|
annotations.push(quote ? { anchor: block.start, quote: quote, comment: comment } : { anchor: block.start, comment: comment });
|
|
356
371
|
dirty = true;
|
|
357
372
|
renderPlan();
|
|
@@ -369,6 +384,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
369
384
|
var area = el("textarea", "bf-textarea");
|
|
370
385
|
area.value = annotation.comment;
|
|
371
386
|
area.setAttribute("aria-label", "Edit comment");
|
|
387
|
+
area.maxLength = limits.comment;
|
|
372
388
|
card.appendChild(area);
|
|
373
389
|
var row = el("div", "bf-comment__row");
|
|
374
390
|
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
@@ -585,6 +601,10 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
585
601
|
planEl.appendChild(box);
|
|
586
602
|
}
|
|
587
603
|
planEl.scrollTop = scrollTop;
|
|
604
|
+
// The DOM was rebuilt, so the active block's class is gone; restore it in
|
|
605
|
+
// place rather than scrolling, so j/k resume where the reviewer was.
|
|
606
|
+
var active = activeIndex >= 0 ? blockElements()[activeIndex] : null;
|
|
607
|
+
if (active) active.classList.add("bf-block--active");
|
|
588
608
|
renderFacts();
|
|
589
609
|
observeSections();
|
|
590
610
|
updateState();
|
|
@@ -633,6 +653,12 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
633
653
|
controls.appendChild(reload);
|
|
634
654
|
}
|
|
635
655
|
|
|
656
|
+
// The stream exists only to notice a closed tab. Once the review is settled it
|
|
657
|
+
// has no job, and leaving it open retries a dead port until the tab closes.
|
|
658
|
+
function settleStream() {
|
|
659
|
+
if (stream && stream.close) { stream.close(); stream = null; }
|
|
660
|
+
}
|
|
661
|
+
|
|
636
662
|
// Save and approve share the same POST shape; only the route, the settled
|
|
637
663
|
// flag, and the wording differ. Both persist the current annotations.
|
|
638
664
|
function submit(url, okText, failText, mark) {
|
|
@@ -644,6 +670,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
644
670
|
if (response.ok) {
|
|
645
671
|
mark();
|
|
646
672
|
dirty = false;
|
|
673
|
+
settleStream();
|
|
647
674
|
setStatus(okText, "saved");
|
|
648
675
|
lockActions();
|
|
649
676
|
updateState();
|
|
@@ -656,7 +683,12 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
656
683
|
offerReload();
|
|
657
684
|
return;
|
|
658
685
|
}
|
|
659
|
-
|
|
686
|
+
// Surface the server's own reason (a malformed comment, a stale body)
|
|
687
|
+
// rather than a bare status code.
|
|
688
|
+
return response.json().then(
|
|
689
|
+
function (payload) { throw new Error(payload && payload.error ? payload.error : "server returned " + response.status); },
|
|
690
|
+
function () { throw new Error("server returned " + response.status); }
|
|
691
|
+
);
|
|
660
692
|
}).catch(function (error) {
|
|
661
693
|
setStatus(failText + error.message, "error");
|
|
662
694
|
});
|
|
@@ -677,6 +709,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
677
709
|
function cancel() {
|
|
678
710
|
if (saved || approved || closed) return;
|
|
679
711
|
closed = true;
|
|
712
|
+
settleStream();
|
|
680
713
|
lockActions();
|
|
681
714
|
setStatus("Closing\u2026");
|
|
682
715
|
updateState();
|
|
@@ -742,18 +775,18 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
742
775
|
if (event.key === "s") { event.preventDefault(); save(); return; }
|
|
743
776
|
});
|
|
744
777
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
});
|
|
778
|
+
// A closed tab is detected by the stream below: the server sees the last
|
|
779
|
+
// client go away and cancels after a grace period, and a reload reconnects
|
|
780
|
+
// inside it. A beacon here would cancel the review on every reload, including
|
|
781
|
+
// the reload a 409 offers.
|
|
750
782
|
|
|
751
783
|
window.addEventListener("scroll", updateProgress);
|
|
752
784
|
window.addEventListener("resize", updateProgress);
|
|
785
|
+
window.addEventListener("pagehide", settleStream);
|
|
753
786
|
if (planEl) planEl.addEventListener("scroll", updateProgress);
|
|
754
787
|
|
|
755
788
|
try {
|
|
756
|
-
|
|
789
|
+
stream = new EventSource("/events?token=" + encodeURIComponent(token));
|
|
757
790
|
stream.addEventListener("open", function () { connected = true; everConnected = true; updateState(); });
|
|
758
791
|
stream.addEventListener("error", function () {
|
|
759
792
|
// The server ends the stream once the review is settled; keep showing the
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import {
|
|
17
17
|
brandLockup,
|
|
18
18
|
docHead,
|
|
19
|
+
SIDEBAR_COLLAPSE_MAX,
|
|
19
20
|
themeToggle,
|
|
20
21
|
WEB_BASE_CSS,
|
|
21
22
|
WEB_COMPONENTS_CSS,
|
|
@@ -57,6 +58,11 @@ export interface ReviewPageData {
|
|
|
57
58
|
steps: { done: number; total: number };
|
|
58
59
|
/** Highlighted HTML per line, keyed by the code block's 0-based start line. */
|
|
59
60
|
code: Record<string, string[]>;
|
|
61
|
+
/**
|
|
62
|
+
* The caps the server enforces, so the page does not retype them: comment
|
|
63
|
+
* count, comment length, and quote length, from `feedback.ts`.
|
|
64
|
+
*/
|
|
65
|
+
limits: { comments: number; comment: number; quote: number };
|
|
60
66
|
/** Whether the review may approve the plan (only the `exit_plan_mode` flow). */
|
|
61
67
|
canApprove: boolean;
|
|
62
68
|
/** Per-session secret the client echoes on every request. */
|
|
@@ -85,7 +91,6 @@ ${WEB_COMPONENTS_CSS}
|
|
|
85
91
|
.review-progress__fill{display:block;height:100%;width:0;background:var(--accent);transition:width var(--motion-fast) linear}
|
|
86
92
|
/* Review panel: the plan's review facts, the section outline, then the keys. */
|
|
87
93
|
.bf-app__body--single{grid-template-columns:minmax(0,1fr)}
|
|
88
|
-
.bf-app__sidebar .bf-panel{flex:1 1 auto;min-height:0;min-width:0}
|
|
89
94
|
.review-panel{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-4) var(--space-4) var(--space-2)}
|
|
90
95
|
.review-facts{display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-3);border-bottom:1px solid var(--border)}
|
|
91
96
|
.review-fact{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);font-size:var(--fs-sm);color:var(--muted)}
|
|
@@ -167,7 +172,7 @@ ${WEB_COMPONENTS_CSS}
|
|
|
167
172
|
.review-nav .lvl-2 a,.review-nav .lvl-3 a,.review-nav .lvl-4 a,.review-nav .lvl-5 a,.review-nav .lvl-6 a{padding-left:8px}
|
|
168
173
|
.review-hints{display:none}
|
|
169
174
|
}
|
|
170
|
-
@media (max-width
|
|
175
|
+
@media (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
171
176
|
.bf-review{padding:var(--space-4) 18px 80px}
|
|
172
177
|
.review-nav__title{display:none}
|
|
173
178
|
}
|
|
@@ -50,8 +50,13 @@ const HOST_PATTERN = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i;
|
|
|
50
50
|
|
|
51
51
|
const HTML_SECURITY: Record<string, string> = {
|
|
52
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.
|
|
53
56
|
"Content-Security-Policy":
|
|
54
|
-
"default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'self';
|
|
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",
|
|
55
60
|
"X-Content-Type-Options": "nosniff",
|
|
56
61
|
"Referrer-Policy": "no-referrer",
|
|
57
62
|
"Cache-Control": "no-store",
|
|
@@ -62,7 +67,7 @@ const SCRIPT_HEADERS: Record<string, string> = {
|
|
|
62
67
|
"Content-Type": "text/javascript; charset=utf-8",
|
|
63
68
|
};
|
|
64
69
|
|
|
65
|
-
const JSON_HEADERS: Record<string, string> = {
|
|
70
|
+
export const JSON_HEADERS: Record<string, string> = {
|
|
66
71
|
"Content-Type": "application/json; charset=utf-8",
|
|
67
72
|
"X-Content-Type-Options": "nosniff",
|
|
68
73
|
"Referrer-Policy": "no-referrer",
|
|
@@ -112,7 +117,10 @@ function checkEnvelope(
|
|
|
112
117
|
): ServeResponse | undefined {
|
|
113
118
|
const upper = method.toUpperCase();
|
|
114
119
|
if (upper !== "GET" && upper !== "HEAD" && upper !== "POST") {
|
|
115
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
...errorResponse(405, "Only GET, HEAD, and POST are served."),
|
|
122
|
+
headers: { ...JSON_HEADERS, Allow: "GET, HEAD, POST" },
|
|
123
|
+
};
|
|
116
124
|
}
|
|
117
125
|
// Fail closed: a missing or non-string Host is rejected too, not just a
|
|
118
126
|
// foreign one, so the DNS-rebinding guard cannot be skipped.
|