@gavin-hu/my-pi-agent 0.8.1 → 0.10.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 +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- 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.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.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.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.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/bytefly/README.md +306 -68
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
|
@@ -78,7 +78,9 @@ export function formatResultText(details: MemoryDetails, expanded = false): stri
|
|
|
78
78
|
].join("\n");
|
|
79
79
|
case "add":
|
|
80
80
|
if (details.changed) {
|
|
81
|
-
|
|
81
|
+
// The call line already names the note, so the result confirms the write
|
|
82
|
+
// and the new counts rather than repeating it.
|
|
83
|
+
return `Added to ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`;
|
|
82
84
|
}
|
|
83
85
|
if (details.existing !== undefined && details.existing !== details.entry) {
|
|
84
86
|
return `Already in ${scopeLabel(details.scope)} memory (near-duplicate): ${details.existing}`;
|
|
@@ -86,10 +88,10 @@ export function formatResultText(details: MemoryDetails, expanded = false): stri
|
|
|
86
88
|
return `Already in ${scopeLabel(details.scope)} memory: ${details.existing ?? details.entry ?? ""}`;
|
|
87
89
|
case "update":
|
|
88
90
|
return details.changed
|
|
89
|
-
? `Updated ${scopeLabel(details.scope)} memory
|
|
91
|
+
? `Updated ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`
|
|
90
92
|
: `Already set in ${scopeLabel(details.scope)} memory: ${details.entry ?? ""}`;
|
|
91
93
|
case "forget":
|
|
92
|
-
return `Removed from ${scopeLabel(details.scope)} memory
|
|
94
|
+
return `Removed from ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
14
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
15
16
|
import { messageOf } from "./errors.ts";
|
|
16
17
|
import { formatCallText, formatResultText, scopeLabel } from "./format.ts";
|
|
17
18
|
import type { MemoryRuntime } from "./runtime.ts";
|
|
@@ -142,7 +143,7 @@ export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
|
|
|
142
143
|
args as { action?: unknown; text?: unknown; to?: unknown; scope?: unknown },
|
|
143
144
|
context.argsComplete,
|
|
144
145
|
);
|
|
145
|
-
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
146
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "meta", body));
|
|
146
147
|
return text;
|
|
147
148
|
},
|
|
148
149
|
|
|
@@ -155,7 +156,7 @@ export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
|
|
|
155
156
|
return text;
|
|
156
157
|
}
|
|
157
158
|
if (details.error) {
|
|
158
|
-
text.setText(theme
|
|
159
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
159
160
|
return text;
|
|
160
161
|
}
|
|
161
162
|
text.setText(formatResultText(details, expanded));
|
package/extensions/memory/tui.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* every render (`entries()` refreshes from disk first) and asks for a repaint
|
|
6
6
|
* after an action settles, so an external edit or a mutation is reflected
|
|
7
7
|
* without the component owning any authoritative state. Navigation and scroll
|
|
8
|
-
* math come from `lib/
|
|
8
|
+
* math come from `lib/tui/cursor.ts` and the screen chrome from `lib/tui/chrome.ts`;
|
|
9
9
|
* the inline note editor is the shared `Editor` from `@earendil-works/pi-tui`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -14,23 +14,18 @@ import {
|
|
|
14
14
|
Editor,
|
|
15
15
|
Key,
|
|
16
16
|
matchesKey,
|
|
17
|
-
truncateToWidth,
|
|
18
17
|
type Component,
|
|
19
18
|
type TUI,
|
|
20
19
|
type TuiMouseEvent,
|
|
21
20
|
type TuiMouseEventResult,
|
|
22
21
|
} from "@earendil-works/pi-tui";
|
|
23
22
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
scrollList,
|
|
31
|
-
selectionMarker,
|
|
32
|
-
} from "../../lib/list-cursor.ts";
|
|
33
|
-
import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui.ts";
|
|
23
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
24
|
+
import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
25
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
26
|
+
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
27
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
28
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
34
29
|
import { messageOf } from "./errors.ts";
|
|
35
30
|
import { scopeLabel, type MemoryEntries } from "./format.ts";
|
|
36
31
|
import type { MemoryScope } from "./types.ts";
|
|
@@ -301,6 +296,13 @@ export class MemoryPickerComponent implements Component {
|
|
|
301
296
|
this.requestRender();
|
|
302
297
|
return;
|
|
303
298
|
}
|
|
299
|
+
// The hint advertises `g`/`G`, so bind them to the ends.
|
|
300
|
+
if (data === "g" || data === "G") {
|
|
301
|
+
this.cursor.set(data === "g" ? 0 : notes.length - 1, notes.length);
|
|
302
|
+
this.selectedKey = notes[this.cursor.selected] ? noteKey(notes[this.cursor.selected]) : undefined;
|
|
303
|
+
this.requestRender();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
304
306
|
|
|
305
307
|
const focused = notes[this.cursor.selected];
|
|
306
308
|
if (matchesKey(data, Key.tab) || data === "s") this.cycleFilter();
|
|
@@ -344,22 +346,21 @@ export class MemoryPickerComponent implements Component {
|
|
|
344
346
|
private summary(width: number): string {
|
|
345
347
|
const entries = this.callbacks.entries();
|
|
346
348
|
const filter = this.effectiveFilter();
|
|
347
|
-
let text = this.theme
|
|
348
|
-
text += this.theme
|
|
349
|
-
if (!this.callbacks.trusted()) text += this.theme
|
|
350
|
-
return
|
|
349
|
+
let text = paint(this.theme, "accent", filter === "all" ? "All" : scopeLabel(filter));
|
|
350
|
+
text += paint(this.theme, "meta", ` · ${entries.project.length} project, ${entries.global.length} global`);
|
|
351
|
+
if (!this.callbacks.trusted()) text += paint(this.theme, "warn", " · project untrusted");
|
|
352
|
+
return clip(` ${text}`, width);
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
/** One note row: selection marker, scope tag, and the sanitized note text. */
|
|
354
356
|
private noteRow(note: Note, selected: boolean, width: number): string {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
);
|
|
357
|
+
const tag = `${paint(this.theme, "meta", scopeLabel(note.scope))} ${paint(this.theme, "faint", "·")}`;
|
|
358
|
+
return listRow(this.theme, width, {
|
|
359
|
+
selected,
|
|
360
|
+
leading: tag,
|
|
361
|
+
label: sanitizeLine(note.text),
|
|
362
|
+
labelRole: selected ? "accent" : "label",
|
|
363
|
+
});
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
private confirmPrompt(): string {
|
|
@@ -377,13 +378,10 @@ export class MemoryPickerComponent implements Component {
|
|
|
377
378
|
const body: string[] = [];
|
|
378
379
|
for (const line of this.editor.render(Math.max(1, width - 2))) body.push(`${pad}${line}`);
|
|
379
380
|
return dockListFrame({
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
summary: truncateToWidth(
|
|
384
|
-
` ${this.theme.fg("muted", this.mode === "edit" ? "Editing the focused note" : `New ${scope} note`)}`,
|
|
381
|
+
header: titleRule(this.theme, width, `Memory · ${verb} ${scope}`),
|
|
382
|
+
summary: clip(
|
|
383
|
+
` ${paint(this.theme, "meta", this.mode === "edit" ? "Editing the focused note" : `New ${scope} note`)}`,
|
|
385
384
|
width,
|
|
386
|
-
"…",
|
|
387
385
|
),
|
|
388
386
|
body,
|
|
389
387
|
hint: screenHint(this.theme, width, ["Enter save", "Esc cancel"]),
|
|
@@ -401,7 +399,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
401
399
|
|
|
402
400
|
const body: string[] = [];
|
|
403
401
|
if (notes.length === 0) {
|
|
404
|
-
body.push(
|
|
402
|
+
body.push(...emptyState(this.theme, w, { label: "No notes yet. Press a to add one." }));
|
|
405
403
|
} else {
|
|
406
404
|
const { start, end } = this.cursor.window(notes.length);
|
|
407
405
|
for (let index = start; index < end; index++) {
|
|
@@ -410,15 +408,13 @@ export class MemoryPickerComponent implements Component {
|
|
|
410
408
|
}
|
|
411
409
|
|
|
412
410
|
const hint = this.confirm
|
|
413
|
-
?
|
|
411
|
+
? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
|
|
414
412
|
: this.notice !== undefined
|
|
415
|
-
?
|
|
413
|
+
? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
|
|
416
414
|
: screenHint(this.theme, w, ["Esc close", "Enter/e edit", "a add", "d forget", "s/Tab scope", "↑/↓", "g/G"]);
|
|
417
415
|
|
|
418
416
|
return dockListFrame({
|
|
419
|
-
|
|
420
|
-
width: w,
|
|
421
|
-
title: "Memory",
|
|
417
|
+
header: titleRule(this.theme, w, "Memory"),
|
|
422
418
|
summary: this.summary(w),
|
|
423
419
|
body,
|
|
424
420
|
range: this.cursor.clipped(notes.length)
|
|
@@ -12,6 +12,14 @@ pi -e . # load the whole @gavin-hu/my-pi-agent packag
|
|
|
12
12
|
pi --plan # start in plan mode
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Tool rendering
|
|
16
|
+
|
|
17
|
+
How the `write_plan` and `exit_plan_mode` calls and their results look in the transcript — the call line as a headline and the body indented under it:
|
|
18
|
+
|
|
19
|
+
<img src="../../assets/tools/write_plan.png" alt="The write_plan tool's call and result: the call line and an indented body with the saved plan path and byte count" width="100%">
|
|
20
|
+
|
|
21
|
+
<img src="../../assets/tools/exit_plan_mode.png" alt="The exit_plan_mode tool's call and result: the call line and an indented body with the approval line and the approved plan preview" width="100%">
|
|
22
|
+
|
|
15
23
|
## What it does
|
|
16
24
|
|
|
17
25
|
- **Read-only gating, with one exception.** Tools that are not read-only are
|
|
@@ -48,8 +56,11 @@ pi --plan # start in plan mode
|
|
|
48
56
|
- **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
|
|
49
57
|
it for approval. The user approves, keeps planning, or asks for a refinement.
|
|
50
58
|
- **Browser review on demand.** Press `w` on the review screen (or run
|
|
51
|
-
`/plan review [path]`) to open the plan in a local browser
|
|
52
|
-
|
|
59
|
+
`/plan review [path]`) to open the plan in a local browser: a reading-first
|
|
60
|
+
page with the plan's H1 as its title, GitHub's Markdown content column, a
|
|
61
|
+
comment rail in
|
|
62
|
+
the left margin, and live section and step progress. Select text to leave an
|
|
63
|
+
inline comment, and save them back into the plan file itself as
|
|
53
64
|
`> [!REVIEW]` lines. Saved comments return to the model as a refinement
|
|
54
65
|
request.
|
|
55
66
|
- **The file is the source of truth.** On approval the model follows the plan
|
|
@@ -107,9 +118,9 @@ The review screen is TUI-only. `exit_plan_mode` opens `PlanViewComponent`: a
|
|
|
107
118
|
scrollable, width-safe view of the plan with approve/refine/keep in the footer.
|
|
108
119
|
The body is rendered with Pi's own `Markdown` component and `getMarkdownTheme()`,
|
|
109
120
|
so headings, lists, inline code, code blocks, links, and quotes carry the active
|
|
110
|
-
theme's colors; task items get
|
|
111
|
-
|
|
112
|
-
`r refine` in `accent`).
|
|
121
|
+
theme's colors; task items get the shared `◼` (done) or `◻` (pending) marker,
|
|
122
|
+
like the bytefly screen and the question dialog. The footer colors the primary
|
|
123
|
+
actions (`a approve & run` in `success`, `r refine` in `accent`).
|
|
113
124
|
`a` ends plan mode, restores write access, and seeds `todo`; `r` keeps the plan
|
|
114
125
|
mounted and opens an inline editor beneath it (`Enter` submits, an empty buffer
|
|
115
126
|
does nothing, `Esc` returns to the plan); `Esc` keeps planning and tells the
|
|
@@ -118,7 +129,8 @@ model the plan was not approved. Scroll with `↑`/`↓` or `j`/`k`, `space`/`b`
|
|
|
118
129
|
the next or previous heading; the scroll row names the current section when the
|
|
119
130
|
plan has more than one. When the plan overflows,
|
|
120
131
|
a gauge row above the hints shows the scroll position, the percent, and the
|
|
121
|
-
visible line range (the range drops first on a narrow terminal); the gauge
|
|
132
|
+
visible line range (the range drops first on a narrow terminal); the gauge comes
|
|
133
|
+
from the design system's `gaugeBar` and stays
|
|
122
134
|
visible while the refine editor is open, since the body above is still clipped.
|
|
123
135
|
Hints are
|
|
124
136
|
dropped whole as the terminal narrows, and each footer leads with its safe exit
|
|
@@ -138,7 +150,9 @@ the user.
|
|
|
138
150
|
|
|
139
151
|
Browser review is a local-turn feature: the server binds `127.0.0.1`, so a remote
|
|
140
152
|
(WeChat) turn cannot reach it and `/plan review` there refuses with a notice
|
|
141
|
-
instead of opening a server nobody can use.
|
|
153
|
+
instead of opening a server nobody can use. The browser's **Approve & run** is
|
|
154
|
+
offered only for the review `exit_plan_mode` opens, since only that flow owns
|
|
155
|
+
leaving plan mode; `/plan review` stays comment-only.
|
|
142
156
|
|
|
143
157
|
### Transcript
|
|
144
158
|
|
|
@@ -189,8 +203,8 @@ command. The read-only guarantee is layered:
|
|
|
189
203
|
strips control characters from the plan body before rendering it in the
|
|
190
204
|
transcript and in non-TUI notices; a plan file cannot drive the terminal.
|
|
191
205
|
- **Loopback browser review.** The review server binds `127.0.0.1` and requires a
|
|
192
|
-
random per-session token in the URL,
|
|
193
|
-
`GET`/`POST`, and caps the request body; the page renders plan and comment text
|
|
206
|
+
random per-session token in the URL, rejects a missing or foreign `Host`
|
|
207
|
+
header, serves only `GET`/`POST`, and caps the request body; the page renders plan and comment text
|
|
194
208
|
with DOM text nodes, never `innerHTML`, so a plan cannot script the reviewer.
|
|
195
209
|
|
|
196
210
|
## Read-only git shell
|
|
@@ -250,26 +264,52 @@ agent file that overrides it.
|
|
|
250
264
|
|
|
251
265
|
## Browser review
|
|
252
266
|
|
|
253
|
-
<img src="../../assets/plan-review.png" alt="The browser-based plan review
|
|
267
|
+
<img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail and an inline review thread" width="100%">
|
|
254
268
|
|
|
255
269
|
Press `w` on the review screen — or run `/plan review [path]` — to open the plan
|
|
256
|
-
in the default browser on `127.0.0.1`. The page is a review
|
|
257
|
-
header
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
in the default browser on `127.0.0.1`. The page is a reading-first review
|
|
271
|
+
surface: a sticky header carrying the plan's H1 (falling back to the file-name
|
|
272
|
+
slug), a live status chip (connection, comment count, and unsaved changes), the
|
|
273
|
+
theme toggle, and the review actions, over a hairline reading-progress bar. A
|
|
274
|
+
sticky side panel shows the plan's step progress and comment count above a
|
|
275
|
+
**section outline**, and the document is GitHub's Markdown content column — one centred
|
|
276
|
+
measure of at most 1012px with a comment rail in its left margin. Every block
|
|
277
|
+
reveals a **+ Comment** action on hover; select
|
|
278
|
+
text first to quote it. **Save feedback** writes the comments into the plan
|
|
279
|
+
file and returns them to the model.
|
|
262
280
|
|
|
263
281
|
- **Shared theme.** The page uses the same Bytefly tokens, base rules, and
|
|
264
282
|
syntax-token colors as `file-browse` (`lib/web/`), follows the system light/dark
|
|
265
283
|
preference until the header toggle sets an explicit choice, and remembers that
|
|
266
284
|
choice under the shared `pi-web-theme` key.
|
|
267
|
-
- **
|
|
268
|
-
|
|
269
|
-
|
|
285
|
+
- **One content column, one rail.** The document is GitHub's Markdown content
|
|
286
|
+
column (a centred measure of at most 1012px), so a plan reads the way the same
|
|
287
|
+
file does on GitHub; each block carries its `+ Comment` button and
|
|
288
|
+
comment-count badge
|
|
289
|
+
in a rail in the left margin, so annotating never reflows the text.
|
|
290
|
+
- **Section outline and facts.** Headings become a sticky outline with jump
|
|
291
|
+
links, an active-section highlight, and a per-section comment count; the panel
|
|
292
|
+
above it reports the plan's checkbox steps (`n/m`) and total comments.
|
|
270
293
|
- **Inline threads.** A comment renders directly under the block it anchors to,
|
|
271
|
-
indented as a thread, so it reads in place and the plan keeps one
|
|
272
|
-
every width. There is no separate
|
|
294
|
+
labelled and indented as a thread, so it reads in place and the plan keeps one
|
|
295
|
+
measure at every width. There is no separate column to reflow.
|
|
296
|
+
- **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
|
|
297
|
+
on the current block, `a` approves (the approval flow only), `s` saves, `Esc`
|
|
298
|
+
closes the composer or the sheet and then the review, and `?` opens a shortcut
|
|
299
|
+
sheet; the header's `?` button opens the same sheet.
|
|
300
|
+
- **Decision in place.** A review opened for approval (`w` on the review screen)
|
|
301
|
+
shows **Approve & run** beside Save feedback: it persists the current comments,
|
|
302
|
+
ends plan mode, restores write access, seeds `todo`, and hands control back to
|
|
303
|
+
the model. **Save feedback** keeps plan mode and returns the comments as a
|
|
304
|
+
refinement; **Back to Pi** closes the review with no decision. Each action
|
|
305
|
+
settles the page on a terminal state — `Approved`, `Saved`, or `Closed` — so
|
|
306
|
+
the tab never falls to Offline. `/plan review` is comment-only: it renders no
|
|
307
|
+
approve action and the server refuses one.
|
|
308
|
+
- **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
|
|
309
|
+
code, GFM tables, `---` rules, and bare-URL autolinks render; a list item's
|
|
310
|
+
continuation lines hang under the item's text, inline code is a soft fill
|
|
311
|
+
rather than a bordered chip, tables honour the alignment row and scroll
|
|
312
|
+
horizontally when narrow.
|
|
273
313
|
- **Highlighted code.** Fenced code is tokenized by the shared
|
|
274
314
|
`lib/highlight.ts`; the client copies only text and `tok-*` spans through
|
|
275
315
|
`DOMParser`, so a code line is never parsed as markup.
|
|
@@ -277,18 +317,24 @@ returns them to the model.
|
|
|
277
317
|
- **The file is the store.** Each comment is written as
|
|
278
318
|
`> [!REVIEW] <comment>` on the line after the block it anchors to. Saving is
|
|
279
319
|
idempotent: the previous markers are stripped first, so a second save replaces
|
|
280
|
-
rather than appends.
|
|
320
|
+
rather than appends. Stripping is fence-aware: a `> [!REVIEW]` line inside a
|
|
321
|
+
code block is documentation, not feedback. The browser keeps no copy of its own.
|
|
281
322
|
- **Anchored to source.** `blocks.ts` splits the plan (fence-aware) into
|
|
282
323
|
source-line ranges; a comment is placed after the whole anchored block, so a
|
|
283
324
|
fenced code block is never split. A save re-checks the file against what the
|
|
284
|
-
page rendered and answers `409
|
|
325
|
+
page rendered and answers `409`; the page then offers a **Reload** action,
|
|
326
|
+
because only a reload can re-anchor the comments to the new text.
|
|
285
327
|
- **Back to the model.** A save from the review screen returns `refined: true`
|
|
286
328
|
with the comments, so the model stays in plan mode and revises the same file.
|
|
287
329
|
`/plan review` sends them as a user message (a plan-mode revision or a plain
|
|
288
330
|
update, depending on the mode).
|
|
289
331
|
- **Loopback and tokenized.** The server binds `127.0.0.1`, carries a random
|
|
290
|
-
per-session token in the URL,
|
|
291
|
-
`POST`, and caps the request body.
|
|
332
|
+
per-session token in the URL, rejects a missing or foreign `Host` header,
|
|
333
|
+
serves only `GET` and `POST`, and caps the request body. A save or cancel
|
|
334
|
+
resolves the session only after its HTTP response is delivered, so the browser
|
|
335
|
+
never sees a false "Failed to fetch". The last bound port is remembered per
|
|
336
|
+
plan, so reviewing the same plan again reuses the origin (and the browser's
|
|
337
|
+
saved theme with it). It is closed on save, cancel, tab close, or
|
|
292
338
|
`session_shutdown`.
|
|
293
339
|
|
|
294
340
|
| Route | Result |
|
|
@@ -312,6 +358,9 @@ returns them to the model.
|
|
|
312
358
|
turn cannot drive it.
|
|
313
359
|
- A browser that never connects leaves the review waiting until the user
|
|
314
360
|
cancels, closes the tab, or aborts the turn.
|
|
361
|
+
- Feedback stripping follows fenced code blocks; an unterminated fence makes the
|
|
362
|
+
rest of the file code, so a marker-looking line after it is treated as content
|
|
363
|
+
rather than stored feedback.
|
|
315
364
|
|
|
316
365
|
## Non-goals
|
|
317
366
|
|
|
@@ -386,12 +435,13 @@ returns them to the model.
|
|
|
386
435
|
existing heading-only split cannot, and the page renders text with DOM nodes
|
|
387
436
|
so review never widens the plan's trust surface. The browser opener is shared
|
|
388
437
|
through `lib/browser.ts` rather than imported from `file-browse`.
|
|
389
|
-
- **Comments render in the document flow, not a
|
|
390
|
-
source line, so the browser needs no paired
|
|
391
|
-
after its block and the plan keeps one measure at every width.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
438
|
+
- **Comments render in the document flow, not a second column.** The anchor is
|
|
439
|
+
already a source line, so the browser needs no paired column: a thread is
|
|
440
|
+
appended after its block and the plan keeps one measure at every width. The
|
|
441
|
+
per-block affordance lives in a narrow rail inside the measure's left margin,
|
|
442
|
+
so annotating never moves the text. That removes the two-column grid, the
|
|
443
|
+
`bf-review--no-rail` collapse, and the breakpoint ladder, and matches the TUI,
|
|
444
|
+
where neither review header carries a progress counter.
|
|
395
445
|
|
|
396
446
|
## Files
|
|
397
447
|
|
|
@@ -407,15 +457,15 @@ returns them to the model.
|
|
|
407
457
|
| `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`. |
|
|
408
458
|
| `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
|
|
409
459
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
410
|
-
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, blanks). |
|
|
460
|
+
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
411
461
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
412
|
-
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/cancel/abort session. |
|
|
462
|
+
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/approve/cancel/abort session. |
|
|
413
463
|
| `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes. |
|
|
414
|
-
| `web-page.ts` | The review page shell (header, section outline), its data island, and inline CSS. |
|
|
415
|
-
| `web-client.ts` | The `/client.js` source (block/thread rendering, outline, inline comments, selection quotes, highlighted code, save/
|
|
464
|
+
| `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
|
|
465
|
+
| `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/approve/back). |
|
|
416
466
|
| `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
|
|
417
467
|
| [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
|
|
418
468
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
|
|
419
469
|
| [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, and shared markup. |
|
|
420
470
|
| `../../lib/policy.ts` | Shared read-only capability policy (default-deny + `readOnlyHint`). |
|
|
421
|
-
| `../../lib/tui.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
|
|
471
|
+
| `../../lib/tui/chrome.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/** How a block is rendered and grouped. */
|
|
18
|
-
export type PlanBlockKind = "heading" | "paragraph" | "list" | "code" | "quote" | "blank";
|
|
18
|
+
export type PlanBlockKind = "heading" | "paragraph" | "list" | "code" | "quote" | "table" | "hr" | "blank";
|
|
19
19
|
|
|
20
20
|
/** A contiguous, source-line-anchored chunk of a plan. */
|
|
21
21
|
export interface PlanBlock {
|
|
@@ -36,6 +36,25 @@ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
|
36
36
|
const LIST_ITEM = /^( *)([-*+]|\d+[.)])(\s+)/;
|
|
37
37
|
/** A blockquote line. */
|
|
38
38
|
const QUOTE = /^ {0,3}>/;
|
|
39
|
+
/** A thematic break (`---`, `***`, `___`, spaced or not). */
|
|
40
|
+
const THEMATIC_BREAK = /^ {0,3}(?:(?:\* *){3,}|(?:- *){3,}|(?:_ *){3,})$/;
|
|
41
|
+
/** A line that can be a table row: it carries at least one pipe. */
|
|
42
|
+
const TABLE_ROW = /\|/;
|
|
43
|
+
/** A GFM alignment row (`| --- | :--: |`), with or without edge pipes. */
|
|
44
|
+
const TABLE_DELIM = /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/;
|
|
45
|
+
|
|
46
|
+
/** Whether a table starts at `index`: a pipe row followed by an alignment row. */
|
|
47
|
+
function startsTable(lines: string[], index: number): boolean {
|
|
48
|
+
const header = lines[index];
|
|
49
|
+
const delim = lines[index + 1];
|
|
50
|
+
return (
|
|
51
|
+
header !== undefined &&
|
|
52
|
+
delim !== undefined &&
|
|
53
|
+
TABLE_ROW.test(header) &&
|
|
54
|
+
delim.includes("-") &&
|
|
55
|
+
TABLE_DELIM.test(delim)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
39
58
|
|
|
40
59
|
/** Leading spaces and tabs of a line, in columns (tab = 4). */
|
|
41
60
|
function indentOf(line: string): number {
|
|
@@ -102,6 +121,21 @@ export function planBlocks(plan: string): PlanBlock[] {
|
|
|
102
121
|
continue;
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
if (startsTable(lines, i)) {
|
|
125
|
+
let end = i + 1;
|
|
126
|
+
// The header and alignment rows, then every contiguous pipe row.
|
|
127
|
+
while (end + 1 < lines.length && lines[end + 1].trim() !== "" && TABLE_ROW.test(lines[end + 1])) end += 1;
|
|
128
|
+
push(i, end, "table");
|
|
129
|
+
i = end + 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (THEMATIC_BREAK.test(line)) {
|
|
134
|
+
push(i, i, "hr");
|
|
135
|
+
i += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
const item = line.match(LIST_ITEM);
|
|
106
140
|
if (item) {
|
|
107
141
|
const indent = item[1].length;
|
|
@@ -123,7 +157,15 @@ export function planBlocks(plan: string): PlanBlock[] {
|
|
|
123
157
|
let end = i;
|
|
124
158
|
while (end + 1 < lines.length) {
|
|
125
159
|
const next = lines[end + 1];
|
|
126
|
-
if (
|
|
160
|
+
if (
|
|
161
|
+
next.trim() === "" ||
|
|
162
|
+
FENCE.test(next) ||
|
|
163
|
+
HEADING.test(next) ||
|
|
164
|
+
QUOTE.test(next) ||
|
|
165
|
+
LIST_ITEM.test(next) ||
|
|
166
|
+
THEMATIC_BREAK.test(next) ||
|
|
167
|
+
startsTable(lines, end + 1)
|
|
168
|
+
) {
|
|
127
169
|
break;
|
|
128
170
|
}
|
|
129
171
|
end += 1;
|
|
@@ -40,11 +40,25 @@ export const MAX_QUOTE_LENGTH = 500;
|
|
|
40
40
|
/** A stored feedback line, capturing its comment. */
|
|
41
41
|
const MARKER_LINE = /^\s*>\s*\[!REVIEW\]\s?(.*)$/;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Source lines inside a fenced code block. A marker-looking line there is
|
|
45
|
+
* documentation, not stored feedback, so it is never stripped or parsed.
|
|
46
|
+
*/
|
|
47
|
+
function fencedLines(plan: string): Set<number> {
|
|
48
|
+
const fenced = new Set<number>();
|
|
49
|
+
for (const block of planBlocks(plan)) {
|
|
50
|
+
if (block.kind !== "code") continue;
|
|
51
|
+
for (let line = block.start; line <= block.end; line++) fenced.add(line);
|
|
52
|
+
}
|
|
53
|
+
return fenced;
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
/** Remove every stored feedback line, leaving the plan body as authored. */
|
|
44
57
|
export function stripFeedback(plan: string): string {
|
|
58
|
+
const fenced = fencedLines(plan);
|
|
45
59
|
return plan
|
|
46
60
|
.split("\n")
|
|
47
|
-
.filter((line) => !MARKER_LINE.test(line))
|
|
61
|
+
.filter((line, index) => fenced.has(index) || !MARKER_LINE.test(line))
|
|
48
62
|
.join("\n");
|
|
49
63
|
}
|
|
50
64
|
|
|
@@ -53,17 +67,18 @@ export function stripFeedback(plan: string): string {
|
|
|
53
67
|
* marker in the stripped plan, so a comment keeps its place across reloads.
|
|
54
68
|
*/
|
|
55
69
|
export function parseFeedback(plan: string): PlanAnnotation[] {
|
|
70
|
+
const fenced = fencedLines(plan);
|
|
56
71
|
const clean: string[] = [];
|
|
57
72
|
const markers: Array<{ after: number; comment: string }> = [];
|
|
58
|
-
|
|
59
|
-
const match = MARKER_LINE.exec(line);
|
|
73
|
+
plan.split("\n").forEach((line, index) => {
|
|
74
|
+
const match = fenced.has(index) ? null : MARKER_LINE.exec(line);
|
|
60
75
|
if (match) {
|
|
61
76
|
const comment = match[1].trim();
|
|
62
77
|
if (comment) markers.push({ after: clean.length, comment });
|
|
63
|
-
|
|
78
|
+
return;
|
|
64
79
|
}
|
|
65
80
|
clean.push(line);
|
|
66
|
-
}
|
|
81
|
+
});
|
|
67
82
|
if (markers.length === 0) return [];
|
|
68
83
|
|
|
69
84
|
const blocks = planBlocks(clean.join("\n"));
|
package/extensions/plan/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
import { hasPathInput } from "./path-guard.ts";
|
|
23
23
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
24
24
|
import { ASK_USER_QUESTION_TOOL, SUBAGENT_READ_ONLY_PARAM, SUBAGENT_TOOL } from "../../lib/tool-names.ts";
|
|
25
|
-
import { STATUS_KEYS } from "../../lib/
|
|
25
|
+
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
26
26
|
import { planModeNotice, registerCommands } from "./commands.ts";
|
|
27
27
|
import { checkReadOnlyGit } from "./git-bash.ts";
|
|
28
28
|
import {
|
|
@@ -20,7 +20,8 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
20
20
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
22
22
|
import type { ReadOnlyPolicy } from "../../lib/policy.ts";
|
|
23
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/
|
|
23
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
24
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
24
25
|
import { createPlanStore, type PlanStore } from "./plans.ts";
|
|
25
26
|
import type { PlanModeEntry } from "./types.ts";
|
|
26
27
|
|
|
@@ -143,7 +144,7 @@ export function createPlanRuntime(
|
|
|
143
144
|
lastPlanPath ? `${GLYPHS.plan} plan · ${statusName(lastPlanPath)}` : `${GLYPHS.plan} plan`;
|
|
144
145
|
|
|
145
146
|
const setStatus = (ctx: ExtensionContext): void => {
|
|
146
|
-
ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? ctx.ui.theme
|
|
147
|
+
ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? paint(ctx.ui.theme, "warn", statusText()) : undefined);
|
|
147
148
|
};
|
|
148
149
|
|
|
149
150
|
const setEnabled = (next: boolean, ctx: ExtensionContext | undefined, persist: boolean): void => {
|