@gavin-hu/my-pi-agent 0.8.0 → 0.9.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 +9 -8
- package/README.zh-CN.md +8 -8
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +49 -36
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +6 -5
- 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 +33 -16
- package/extensions/bytefly/header/config.ts +28 -18
- package/extensions/bytefly/index.ts +1 -1
- 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/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +13 -40
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/document/README.md +1 -1
- package/extensions/document/render.ts +11 -10
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +62 -14
- package/extensions/file-browse/client.ts +103 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +176 -1
- package/extensions/file-browse/html.ts +412 -23
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +98 -5
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/goal/format.ts +11 -10
- package/extensions/goal/tools.ts +6 -5
- package/extensions/goal/tui.ts +1 -1
- package/extensions/job/README.md +2 -2
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +1 -1
- package/extensions/job/tools.ts +11 -10
- package/extensions/job/tui.ts +141 -220
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +64 -34
- 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 +23 -22
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +335 -30
- package/extensions/plan/web-page.ts +110 -30
- package/extensions/plan/web-router.ts +6 -4
- package/extensions/plan/web.ts +41 -10
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +12 -11
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +1 -1
- package/extensions/rewind/format.ts +29 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/render.ts +62 -47
- package/extensions/todo/README.md +2 -2
- package/extensions/todo/format.ts +31 -10
- package/extensions/todo/tools.ts +5 -4
- package/extensions/todo/tui.ts +45 -26
- package/extensions/watch/README.md +2 -2
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +1 -1
- package/extensions/watch/tools.ts +12 -7
- package/extensions/watch/tui.ts +135 -194
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +2 -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/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +10 -7
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +9 -30
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +56 -3
- package/lib/tui/gauge.ts +78 -0
- package/lib/tui/index.ts +25 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +109 -0
- package/lib/tui/rules.ts +88 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +58 -0
- package/package.json +2 -2
- /package/lib/{ui.ts → tui/glyphs.ts} +0 -0
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)
|
|
@@ -48,8 +48,11 @@ pi --plan # start in plan mode
|
|
|
48
48
|
- **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
|
|
49
49
|
it for approval. The user approves, keeps planning, or asks for a refinement.
|
|
50
50
|
- **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
|
-
|
|
51
|
+
`/plan review [path]`) to open the plan in a local browser: a reading-first
|
|
52
|
+
page with the plan's H1 as its title, GitHub's Markdown content column, a
|
|
53
|
+
comment rail in
|
|
54
|
+
the left margin, and live section and step progress. Select text to leave an
|
|
55
|
+
inline comment, and save them back into the plan file itself as
|
|
53
56
|
`> [!REVIEW]` lines. Saved comments return to the model as a refinement
|
|
54
57
|
request.
|
|
55
58
|
- **The file is the source of truth.** On approval the model follows the plan
|
|
@@ -107,9 +110,9 @@ The review screen is TUI-only. `exit_plan_mode` opens `PlanViewComponent`: a
|
|
|
107
110
|
scrollable, width-safe view of the plan with approve/refine/keep in the footer.
|
|
108
111
|
The body is rendered with Pi's own `Markdown` component and `getMarkdownTheme()`,
|
|
109
112
|
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`).
|
|
113
|
+
theme's colors; task items get the shared `◼` (done) or `◻` (pending) marker,
|
|
114
|
+
like the bytefly screen and the question dialog. The footer colors the primary
|
|
115
|
+
actions (`a approve & run` in `success`, `r refine` in `accent`).
|
|
113
116
|
`a` ends plan mode, restores write access, and seeds `todo`; `r` keeps the plan
|
|
114
117
|
mounted and opens an inline editor beneath it (`Enter` submits, an empty buffer
|
|
115
118
|
does nothing, `Esc` returns to the plan); `Esc` keeps planning and tells the
|
|
@@ -118,7 +121,8 @@ model the plan was not approved. Scroll with `↑`/`↓` or `j`/`k`, `space`/`b`
|
|
|
118
121
|
the next or previous heading; the scroll row names the current section when the
|
|
119
122
|
plan has more than one. When the plan overflows,
|
|
120
123
|
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
|
|
124
|
+
visible line range (the range drops first on a narrow terminal); the gauge comes
|
|
125
|
+
from the design system's `gaugeBar` and stays
|
|
122
126
|
visible while the refine editor is open, since the body above is still clipped.
|
|
123
127
|
Hints are
|
|
124
128
|
dropped whole as the terminal narrows, and each footer leads with its safe exit
|
|
@@ -189,8 +193,8 @@ command. The read-only guarantee is layered:
|
|
|
189
193
|
strips control characters from the plan body before rendering it in the
|
|
190
194
|
transcript and in non-TUI notices; a plan file cannot drive the terminal.
|
|
191
195
|
- **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
|
|
196
|
+
random per-session token in the URL, rejects a missing or foreign `Host`
|
|
197
|
+
header, serves only `GET`/`POST`, and caps the request body; the page renders plan and comment text
|
|
194
198
|
with DOM text nodes, never `innerHTML`, so a plan cannot script the reviewer.
|
|
195
199
|
|
|
196
200
|
## Read-only git shell
|
|
@@ -250,26 +254,42 @@ agent file that overrides it.
|
|
|
250
254
|
|
|
251
255
|
## Browser review
|
|
252
256
|
|
|
253
|
-
<img src="../../assets/plan-review.png" alt="The browser-based plan review
|
|
257
|
+
<img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title and Save/Close, 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
258
|
|
|
255
259
|
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
|
-
|
|
260
|
+
in the default browser on `127.0.0.1`. The page is a reading-first review
|
|
261
|
+
surface: a sticky header carrying the plan's H1 (falling back to the file-name
|
|
262
|
+
slug), a live status chip (connection, comment count, and unsaved changes), the
|
|
263
|
+
theme toggle, and Save/Close, over a hairline reading-progress bar. A sticky
|
|
264
|
+
side panel shows the plan's step progress and comment count above a **section
|
|
265
|
+
outline**, and the document is GitHub's Markdown content column — one centred
|
|
266
|
+
measure of at most 1012px with a comment rail in its left margin. Every block
|
|
267
|
+
reveals a **+ Comment** action on hover; select
|
|
268
|
+
text first to quote it. **Save feedback** writes the comments into the plan
|
|
269
|
+
file and returns them to the model.
|
|
262
270
|
|
|
263
271
|
- **Shared theme.** The page uses the same Bytefly tokens, base rules, and
|
|
264
272
|
syntax-token colors as `file-browse` (`lib/web/`), follows the system light/dark
|
|
265
273
|
preference until the header toggle sets an explicit choice, and remembers that
|
|
266
274
|
choice under the shared `pi-web-theme` key.
|
|
267
|
-
- **
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
- **One content column, one rail.** The document is GitHub's Markdown content
|
|
276
|
+
column (a centred measure of at most 1012px), so a plan reads the way the same
|
|
277
|
+
file does on GitHub; each block carries its `+ Comment` button and
|
|
278
|
+
comment-count badge
|
|
279
|
+
in a rail in the left margin, so annotating never reflows the text.
|
|
280
|
+
- **Section outline and facts.** Headings become a sticky outline with jump
|
|
281
|
+
links, an active-section highlight, and a per-section comment count; the panel
|
|
282
|
+
above it reports the plan's checkbox steps (`n/m`) and total comments.
|
|
270
283
|
- **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
|
|
284
|
+
labelled and indented as a thread, so it reads in place and the plan keeps one
|
|
285
|
+
measure at every width. There is no separate column to reflow.
|
|
286
|
+
- **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
|
|
287
|
+
on the current block, `s` saves, `Esc` closes the composer, and `?` opens a
|
|
288
|
+
shortcut sheet; the header's `?` button opens the same sheet.
|
|
289
|
+
- **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
|
|
290
|
+
code, GFM tables, `---` rules, and bare-URL autolinks render; tables honour the
|
|
291
|
+
alignment row and
|
|
292
|
+
scroll horizontally when narrow.
|
|
273
293
|
- **Highlighted code.** Fenced code is tokenized by the shared
|
|
274
294
|
`lib/highlight.ts`; the client copies only text and `tok-*` spans through
|
|
275
295
|
`DOMParser`, so a code line is never parsed as markup.
|
|
@@ -277,18 +297,24 @@ returns them to the model.
|
|
|
277
297
|
- **The file is the store.** Each comment is written as
|
|
278
298
|
`> [!REVIEW] <comment>` on the line after the block it anchors to. Saving is
|
|
279
299
|
idempotent: the previous markers are stripped first, so a second save replaces
|
|
280
|
-
rather than appends.
|
|
300
|
+
rather than appends. Stripping is fence-aware: a `> [!REVIEW]` line inside a
|
|
301
|
+
code block is documentation, not feedback. The browser keeps no copy of its own.
|
|
281
302
|
- **Anchored to source.** `blocks.ts` splits the plan (fence-aware) into
|
|
282
303
|
source-line ranges; a comment is placed after the whole anchored block, so a
|
|
283
304
|
fenced code block is never split. A save re-checks the file against what the
|
|
284
|
-
page rendered and answers `409
|
|
305
|
+
page rendered and answers `409`; the page then offers a **Reload** action,
|
|
306
|
+
because only a reload can re-anchor the comments to the new text.
|
|
285
307
|
- **Back to the model.** A save from the review screen returns `refined: true`
|
|
286
308
|
with the comments, so the model stays in plan mode and revises the same file.
|
|
287
309
|
`/plan review` sends them as a user message (a plan-mode revision or a plain
|
|
288
310
|
update, depending on the mode).
|
|
289
311
|
- **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.
|
|
312
|
+
per-session token in the URL, rejects a missing or foreign `Host` header,
|
|
313
|
+
serves only `GET` and `POST`, and caps the request body. A save or cancel
|
|
314
|
+
resolves the session only after its HTTP response is delivered, so the browser
|
|
315
|
+
never sees a false "Failed to fetch". The last bound port is remembered per
|
|
316
|
+
plan, so reviewing the same plan again reuses the origin (and the browser's
|
|
317
|
+
saved theme with it). It is closed on save, cancel, tab close, or
|
|
292
318
|
`session_shutdown`.
|
|
293
319
|
|
|
294
320
|
| Route | Result |
|
|
@@ -312,6 +338,9 @@ returns them to the model.
|
|
|
312
338
|
turn cannot drive it.
|
|
313
339
|
- A browser that never connects leaves the review waiting until the user
|
|
314
340
|
cancels, closes the tab, or aborts the turn.
|
|
341
|
+
- Feedback stripping follows fenced code blocks; an unterminated fence makes the
|
|
342
|
+
rest of the file code, so a marker-looking line after it is treated as content
|
|
343
|
+
rather than stored feedback.
|
|
315
344
|
|
|
316
345
|
## Non-goals
|
|
317
346
|
|
|
@@ -386,12 +415,13 @@ returns them to the model.
|
|
|
386
415
|
existing heading-only split cannot, and the page renders text with DOM nodes
|
|
387
416
|
so review never widens the plan's trust surface. The browser opener is shared
|
|
388
417
|
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
|
-
|
|
418
|
+
- **Comments render in the document flow, not a second column.** The anchor is
|
|
419
|
+
already a source line, so the browser needs no paired column: a thread is
|
|
420
|
+
appended after its block and the plan keeps one measure at every width. The
|
|
421
|
+
per-block affordance lives in a narrow rail inside the measure's left margin,
|
|
422
|
+
so annotating never moves the text. That removes the two-column grid, the
|
|
423
|
+
`bf-review--no-rail` collapse, and the breakpoint ladder, and matches the TUI,
|
|
424
|
+
where neither review header carries a progress counter.
|
|
395
425
|
|
|
396
426
|
## Files
|
|
397
427
|
|
|
@@ -407,15 +437,15 @@ returns them to the model.
|
|
|
407
437
|
| `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`. |
|
|
408
438
|
| `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
|
|
409
439
|
| `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). |
|
|
440
|
+
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
411
441
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
412
442
|
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/cancel/abort session. |
|
|
413
443
|
| `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/cancel). |
|
|
444
|
+
| `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
|
|
445
|
+
| `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/cancel). |
|
|
416
446
|
| `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
|
|
417
447
|
| [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
|
|
418
448
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
|
|
419
449
|
| [`../../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
450
|
| `../../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. |
|
|
451
|
+
| `../../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 => {
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -22,8 +22,9 @@ import { Type, type Static } from "typebox";
|
|
|
22
22
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
23
23
|
import { askHuman } from "../../lib/interaction.ts";
|
|
24
24
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
25
|
-
import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui.ts";
|
|
26
|
-
import {
|
|
25
|
+
import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
26
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
27
|
+
import { expandHint } from "../../lib/tui/glyphs.ts";
|
|
27
28
|
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
28
29
|
import { feedbackSummary } from "./feedback.ts";
|
|
29
30
|
import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
|
|
@@ -238,7 +239,7 @@ export function registerTools(
|
|
|
238
239
|
|
|
239
240
|
renderCall(_args, theme, context) {
|
|
240
241
|
const text = reuseText(context?.lastComponent);
|
|
241
|
-
text.setText(theme.fg("toolTitle", theme.bold(`${ENTER_TOOL} `)) + theme
|
|
242
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${ENTER_TOOL} `)) + paint(theme, "meta", "requested plan mode"));
|
|
242
243
|
return text;
|
|
243
244
|
},
|
|
244
245
|
|
|
@@ -250,10 +251,10 @@ export function registerTools(
|
|
|
250
251
|
text.setText(first?.type === "text" ? first.text : "");
|
|
251
252
|
return text;
|
|
252
253
|
}
|
|
253
|
-
if (details.unavailable) text.setText(theme
|
|
254
|
-
else if (details.already) text.setText(theme
|
|
255
|
-
else if (details.entered) text.setText(theme
|
|
256
|
-
else text.setText(theme
|
|
254
|
+
if (details.unavailable) text.setText(paint(theme, "warn", "No UI to confirm plan mode"));
|
|
255
|
+
else if (details.already) text.setText(paint(theme, "meta", "Already in plan mode"));
|
|
256
|
+
else if (details.entered) text.setText(paint(theme, "ok", "✓ Plan mode enabled"));
|
|
257
|
+
else text.setText(paint(theme, "meta", "Stayed in normal mode"));
|
|
257
258
|
return text;
|
|
258
259
|
},
|
|
259
260
|
});
|
|
@@ -315,7 +316,7 @@ export function registerTools(
|
|
|
315
316
|
renderCall(args, theme, context) {
|
|
316
317
|
const text = reuseText(context?.lastComponent);
|
|
317
318
|
const title = (args as Partial<WritePlanArgs>).title;
|
|
318
|
-
const suffix = title ? theme
|
|
319
|
+
const suffix = title ? paint(theme, "meta", sanitizeLine(title)) : paint(theme, "meta", "plan");
|
|
319
320
|
text.setText(theme.fg("toolTitle", theme.bold(`${WRITE_PLAN_TOOL} `)) + suffix);
|
|
320
321
|
return text;
|
|
321
322
|
},
|
|
@@ -328,7 +329,7 @@ export function registerTools(
|
|
|
328
329
|
text.setText(first?.type === "text" ? first.text : "");
|
|
329
330
|
return text;
|
|
330
331
|
}
|
|
331
|
-
text.setText(theme
|
|
332
|
+
text.setText(paint(theme, "ok", "✓ Saved plan ") + paint(theme, "meta", details.relativePath));
|
|
332
333
|
return text;
|
|
333
334
|
},
|
|
334
335
|
});
|
|
@@ -508,7 +509,7 @@ export function registerTools(
|
|
|
508
509
|
|
|
509
510
|
renderCall(_args, theme, context) {
|
|
510
511
|
const text = reuseText(context?.lastComponent);
|
|
511
|
-
text.setText(theme.fg("toolTitle", theme.bold(`${EXIT_TOOL} `)) + theme
|
|
512
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${EXIT_TOOL} `)) + paint(theme, "meta", "submitted a plan"));
|
|
512
513
|
return text;
|
|
513
514
|
},
|
|
514
515
|
|
|
@@ -521,25 +522,25 @@ export function registerTools(
|
|
|
521
522
|
return text;
|
|
522
523
|
}
|
|
523
524
|
const heading = details.unavailable
|
|
524
|
-
? theme
|
|
525
|
+
? paint(theme, "warn", "No UI to approve the plan")
|
|
525
526
|
: details.approved
|
|
526
|
-
? theme
|
|
527
|
+
? paint(theme, "ok", "✓ Plan approved")
|
|
527
528
|
: details.refined
|
|
528
|
-
? theme
|
|
529
|
-
: theme
|
|
529
|
+
? paint(theme, "accent", "Refining the plan")
|
|
530
|
+
: paint(theme, "warn", "Plan not approved");
|
|
530
531
|
const extra =
|
|
531
532
|
details.refined && details.refinement
|
|
532
|
-
? `\n${theme
|
|
533
|
+
? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
|
|
533
534
|
: "";
|
|
534
535
|
const shown = details.relativePath ?? details.planPath;
|
|
535
|
-
const path = shown ? `\n${theme
|
|
536
|
+
const path = shown ? `\n${paint(theme, "meta", shown)}` : "";
|
|
536
537
|
const planText = sanitizePlanText(details.plan);
|
|
537
538
|
let out = `${heading}${extra}${path}`;
|
|
538
539
|
if (planText) {
|
|
539
540
|
const { body, more } = expanded ? { body: planText, more: 0 } : preview(planText);
|
|
540
541
|
// A blank line separates the header block from the plan body.
|
|
541
542
|
out += `\n\n${theme.fg("toolOutput", body)}`;
|
|
542
|
-
if (more > 0) out += `\n${theme
|
|
543
|
+
if (more > 0) out += `\n${paint(theme, "faint", `… ${more} more lines`)} ${expandHint(theme)}`;
|
|
543
544
|
}
|
|
544
545
|
text.setText(out);
|
|
545
546
|
return text;
|
|
@@ -609,7 +610,7 @@ export function registerTools(
|
|
|
609
610
|
: updates && updates.length > 1
|
|
610
611
|
? `${updates.length} steps`
|
|
611
612
|
: "steps";
|
|
612
|
-
text.setText(theme.fg("toolTitle", theme.bold(`${SET_PLAN_STEPS_TOOL} `)) + theme
|
|
613
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${SET_PLAN_STEPS_TOOL} `)) + paint(theme, "meta", label));
|
|
613
614
|
return text;
|
|
614
615
|
},
|
|
615
616
|
|
|
@@ -625,10 +626,10 @@ export function registerTools(
|
|
|
625
626
|
const pending = details.updates.filter((update) => update.status === "pending").map((u) => u.step);
|
|
626
627
|
const heading =
|
|
627
628
|
completed.length > 0
|
|
628
|
-
? theme
|
|
629
|
-
(pending.length > 0 ? theme
|
|
630
|
-
: theme
|
|
631
|
-
text.setText(heading + theme
|
|
629
|
+
? paint(theme, "ok", `✓ ${stepLabel(completed)} done`) +
|
|
630
|
+
(pending.length > 0 ? paint(theme, "meta", ` · ${stepLabel(pending)} reopened`) : "")
|
|
631
|
+
: paint(theme, "meta", `${stepLabel(pending)} reopened`);
|
|
632
|
+
text.setText(heading + paint(theme, "faint", ` · ${details.done}/${details.total}`));
|
|
632
633
|
return text;
|
|
633
634
|
},
|
|
634
635
|
});
|