@gavin-hu/my-pi-agent 0.9.0 → 0.11.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 +28 -2
- package/README.zh-CN.md +27 -2
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.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 +261 -36
- 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/tools.ts +5 -2
- package/extensions/bytefly/header/component.ts +21 -10
- package/extensions/bytefly/header/config.ts +0 -3
- 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/separator/component.ts +2 -1
- package/extensions/bytefly/separator/format.ts +17 -10
- package/extensions/bytefly/separator/register.ts +3 -2
- 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 +11 -4
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +7 -7
- package/extensions/file-browse/README.md +3 -1
- package/extensions/file-browse/client.ts +9 -2
- package/extensions/file-browse/git.ts +4 -7
- package/extensions/file-browse/html.ts +27 -17
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +7 -20
- package/extensions/goal/tools.ts +5 -17
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +113 -35
- package/extensions/job/commands.ts +1 -0
- package/extensions/job/config.ts +4 -0
- package/extensions/job/format.ts +19 -7
- package/extensions/job/index.ts +25 -20
- package/extensions/job/process.ts +24 -2
- package/extensions/job/registry.ts +3 -0
- package/extensions/job/runtime.ts +253 -18
- package/extensions/job/schema.ts +34 -0
- package/extensions/job/store.ts +13 -1
- package/extensions/job/tools.ts +55 -15
- package/extensions/job/tui.ts +41 -15
- package/extensions/job/types.ts +9 -1
- package/extensions/memory/README.md +6 -0
- package/extensions/memory/format.ts +5 -3
- package/extensions/plan/README.md +32 -12
- package/extensions/plan/tools.ts +20 -5
- package/extensions/plan/web-client.ts +58 -17
- package/extensions/plan/web-page.ts +34 -9
- package/extensions/plan/web-router.ts +40 -9
- package/extensions/plan/web.ts +22 -2
- package/extensions/question/README.md +13 -1
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +11 -6
- package/extensions/rewind/README.md +2 -0
- package/extensions/rewind/format.ts +6 -1
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +56 -29
- package/extensions/todo/README.md +19 -12
- package/extensions/todo/format.ts +17 -25
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/tui.ts +3 -2
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/format.ts +2 -2
- package/extensions/watch/index.ts +28 -23
- package/extensions/watch/tools.ts +5 -3
- package/extensions/watch/tui.ts +12 -7
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +1 -1
- package/extensions/web-access/search/tool.ts +3 -3
- package/extensions/wechat/README.md +6 -0
- package/extensions/worktree/README.md +6 -0
- package/lib/README.md +2 -1
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/tui/chrome.ts +8 -4
- package/lib/tui/cursor.ts +2 -1
- package/lib/tui/glyphs.ts +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +3 -1
- package/lib/tui/rows.ts +39 -5
- package/lib/tui/rules.ts +9 -3
- package/lib/tui/tokens.ts +14 -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
package/extensions/job/tui.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { clip, fitCells } from "../../lib/tui/columns.ts";
|
|
|
21
21
|
import { fitRows, formatRange, IdCursor, isCloseKey, navIntent, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
22
22
|
import { expandHint, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
23
23
|
import { LinePane } from "../../lib/tui/line-pane.ts";
|
|
24
|
-
import { emptyState,
|
|
24
|
+
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
25
25
|
import { titleRule } from "../../lib/tui/rules.ts";
|
|
26
26
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
27
27
|
import {
|
|
@@ -51,21 +51,28 @@ const LOG_CHROME_ROWS = 6;
|
|
|
51
51
|
const jobId = (job: JobRecord): string => job.id;
|
|
52
52
|
|
|
53
53
|
/** One job row: selection marker, status glyph, id, label, and a right-aligned
|
|
54
|
-
* elapsed value
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
* elapsed value, tagged `peer` when another session owns it. `statusRow` owns
|
|
55
|
+
* the clipping and the alignment. */
|
|
56
|
+
function jobRow(
|
|
57
|
+
job: JobRecord,
|
|
58
|
+
theme: Theme,
|
|
59
|
+
width: number,
|
|
60
|
+
options: Pick<StatusRow, "selected" | "marker"> & { foreign?: boolean } = {},
|
|
61
|
+
): string {
|
|
62
|
+
const elapsed = formatDuration(elapsedMs(job, Date.now()));
|
|
63
|
+
return statusRow(theme, width, {
|
|
64
|
+
...options,
|
|
58
65
|
glyph: statusGlyphChar(job.status),
|
|
59
66
|
glyphRole: statusRole(job.status),
|
|
60
67
|
label: `${job.id} ${shortLabel(job)}`,
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
// The tag rides the right-aligned cell, which survives a narrow terminal.
|
|
69
|
+
trailing: options.foreign ? `${elapsed}${SEPARATORS.item}peer` : elapsed,
|
|
63
70
|
});
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
/** Input for the transcript result: a themed job list or a log tail. */
|
|
67
74
|
export type JobResultInput =
|
|
68
|
-
| { kind: "list"; jobs: JobRecord[]; more: number }
|
|
75
|
+
| { kind: "list"; jobs: JobRecord[]; more: number; sessionId?: string }
|
|
69
76
|
| { kind: "logs"; lines: string[]; earlier: number };
|
|
70
77
|
|
|
71
78
|
/**
|
|
@@ -95,7 +102,10 @@ export class JobResult implements Component {
|
|
|
95
102
|
// A leading blank line separates the result from the call header.
|
|
96
103
|
const lines: string[] = [""];
|
|
97
104
|
if (this.input.kind === "list") {
|
|
98
|
-
for (const job of this.input.jobs)
|
|
105
|
+
for (const job of this.input.jobs) {
|
|
106
|
+
const foreign = this.input.sessionId !== undefined && job.sessionId !== this.input.sessionId;
|
|
107
|
+
lines.push(jobRow(job, this.theme, w, { marker: false, foreign }));
|
|
108
|
+
}
|
|
99
109
|
if (this.input.more > 0) {
|
|
100
110
|
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
101
111
|
lines.push(clip(` ${note}`, w));
|
|
@@ -114,6 +124,8 @@ export class JobResult implements Component {
|
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
export interface JobListCallbacks {
|
|
127
|
+
/** This session's id: a record with another id belongs to a peer session. */
|
|
128
|
+
sessionId(): string;
|
|
117
129
|
/** Current sanitized log lines for a job, or undefined when unknown. */
|
|
118
130
|
logs(id: string): { lines: string[]; more?: boolean } | undefined;
|
|
119
131
|
kill(id: string): void;
|
|
@@ -171,6 +183,11 @@ export class JobListComponent implements Component {
|
|
|
171
183
|
return [...this.jobs()].sort(compareJobs);
|
|
172
184
|
}
|
|
173
185
|
|
|
186
|
+
/** Whether another session owns this job, so the screen leaves it alone. */
|
|
187
|
+
private isForeign(job: JobRecord): boolean {
|
|
188
|
+
return job.sessionId !== this.callbacks.sessionId();
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
/** Focus job `next` (clamped) and keep it visible. */
|
|
175
192
|
private select(next: number): void {
|
|
176
193
|
const jobs = this.ordered;
|
|
@@ -252,7 +269,10 @@ export class JobListComponent implements Component {
|
|
|
252
269
|
else if (data === "d" || data === "K") {
|
|
253
270
|
const job = ordered[index];
|
|
254
271
|
if (!job) return;
|
|
255
|
-
if (job
|
|
272
|
+
if (this.isForeign(job)) {
|
|
273
|
+
// Another session's job is shown here, not managed from here.
|
|
274
|
+
this.notice = `${job.id} is another session's job; not killed.`;
|
|
275
|
+
} else if (job.status === "running") {
|
|
256
276
|
this.confirm = { kind: "kill", id: job.id, label: shortLabel(job) };
|
|
257
277
|
} else {
|
|
258
278
|
// A finished job has nothing to kill; say so instead of a silent no-op.
|
|
@@ -260,7 +280,9 @@ export class JobListComponent implements Component {
|
|
|
260
280
|
}
|
|
261
281
|
this.requestRender();
|
|
262
282
|
} else if (data === "x") {
|
|
263
|
-
|
|
283
|
+
// Only this session's finished jobs are ours to clear, so the prompt and the
|
|
284
|
+
// result agree.
|
|
285
|
+
const finished = ordered.filter((job) => !this.isForeign(job) && job.status !== "running");
|
|
264
286
|
if (finished.length > 0) {
|
|
265
287
|
this.confirm = {
|
|
266
288
|
kind: "clear",
|
|
@@ -316,10 +338,11 @@ export class JobListComponent implements Component {
|
|
|
316
338
|
if (this.pane.handleKey(data)) this.requestRender();
|
|
317
339
|
}
|
|
318
340
|
|
|
319
|
-
private summary(counts: JobCounts, width: number): string {
|
|
341
|
+
private summary(counts: JobCounts, width: number, foreign: number): string {
|
|
320
342
|
const cells = [paint(this.theme, "meta", `${counts.total} job${counts.total === 1 ? "" : "s"}`)];
|
|
321
343
|
if (counts.running > 0) cells.push(paint(this.theme, "meta", `${counts.running} running`));
|
|
322
344
|
if (counts.failed > 0) cells.push(paint(this.theme, "bad", `${counts.failed} failed`));
|
|
345
|
+
if (foreign > 0) cells.push(paint(this.theme, "faint", `${foreign} peer`));
|
|
323
346
|
const separator = paint(this.theme, "meta", SEPARATORS.item);
|
|
324
347
|
return clip(` ${fitCells(cells, Math.max(1, width - 2), separator)}`, width);
|
|
325
348
|
}
|
|
@@ -334,7 +357,7 @@ export class JobListComponent implements Component {
|
|
|
334
357
|
this.cursor.clamp(jobs, jobId);
|
|
335
358
|
const index = this.cursor.indexOf(jobs, jobId);
|
|
336
359
|
const focused = jobs[index];
|
|
337
|
-
const detail = focused ? formatJobDetail(focused, Date.now()) : [];
|
|
360
|
+
const detail = focused ? formatJobDetail(focused, Date.now(), this.callbacks.sessionId()) : [];
|
|
338
361
|
this.cursor.visible = fitRows(
|
|
339
362
|
this.viewportRowsSource,
|
|
340
363
|
jobs.length,
|
|
@@ -350,7 +373,10 @@ export class JobListComponent implements Component {
|
|
|
350
373
|
body.push(...emptyState(this.theme, w, { label: "No background jobs." }));
|
|
351
374
|
} else {
|
|
352
375
|
const end = Math.min(jobs.length, this.cursor.scroll + this.cursor.visible);
|
|
353
|
-
for (let i = this.cursor.scroll; i < end; i++)
|
|
376
|
+
for (let i = this.cursor.scroll; i < end; i++) {
|
|
377
|
+
const row = jobs[i];
|
|
378
|
+
body.push(jobRow(row, this.theme, w, { selected: i === index, foreign: this.isForeign(row) }));
|
|
379
|
+
}
|
|
354
380
|
if (this.cursor.scroll > 0 || end < jobs.length) {
|
|
355
381
|
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: jobs.length });
|
|
356
382
|
}
|
|
@@ -371,7 +397,7 @@ export class JobListComponent implements Component {
|
|
|
371
397
|
|
|
372
398
|
return dockListFrame({
|
|
373
399
|
header: titleRule(this.theme, w, "Jobs"),
|
|
374
|
-
summary: this.summary(jobCounts(jobs), w),
|
|
400
|
+
summary: this.summary(jobCounts(jobs), w, jobs.filter((job) => this.isForeign(job)).length),
|
|
375
401
|
body,
|
|
376
402
|
range,
|
|
377
403
|
// The detail pane is separated from the list by a blank line.
|
package/extensions/job/types.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const JOB_STATUSES = ["running", "exited", "killed", "failed", "unknown"]
|
|
|
10
10
|
|
|
11
11
|
export type JobStatus = (typeof JOB_STATUSES)[number];
|
|
12
12
|
|
|
13
|
-
export const JOB_ACTIONS = ["start", "list", "status", "logs", "kill", "wait", "clear"] as const;
|
|
13
|
+
export const JOB_ACTIONS = ["start", "list", "status", "logs", "send", "kill", "wait", "clear"] as const;
|
|
14
14
|
|
|
15
15
|
export type JobAction = (typeof JOB_ACTIONS)[number];
|
|
16
16
|
|
|
@@ -40,6 +40,8 @@ export interface JobRecord {
|
|
|
40
40
|
detached: boolean;
|
|
41
41
|
/** Wake the agent with one triggered turn when the job finishes. */
|
|
42
42
|
wake: boolean;
|
|
43
|
+
/** Started with a stdin pipe, so `send` can write to it while this session holds the child. */
|
|
44
|
+
stdin: boolean;
|
|
43
45
|
/** Session that started the job. */
|
|
44
46
|
sessionId: string;
|
|
45
47
|
/** Whether the completion has already been reported to the model. */
|
|
@@ -83,10 +85,16 @@ export interface JobDetails {
|
|
|
83
85
|
signalled?: boolean;
|
|
84
86
|
/** Signal sent by `kill`; defaults to SIGTERM. */
|
|
85
87
|
signal?: KillSignal;
|
|
88
|
+
/** Characters of `input` written by `send`, excluding the added newline. */
|
|
89
|
+
sent?: number;
|
|
90
|
+
/** True when `send` ended the job's stdin (EOF). */
|
|
91
|
+
stdinClosed?: boolean;
|
|
86
92
|
/** True when `wait` hit its timeout. */
|
|
87
93
|
timedOut?: boolean;
|
|
88
94
|
/** True when `wait` was cancelled by the tool's abort signal. */
|
|
89
95
|
cancelled?: boolean;
|
|
96
|
+
/** Session that made the call, so a transcript can mark another session's rows. */
|
|
97
|
+
sessionId?: string;
|
|
90
98
|
/** Model-readable validation message when the call was rejected. */
|
|
91
99
|
error?: string;
|
|
92
100
|
}
|
|
@@ -13,6 +13,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pac
|
|
|
13
13
|
pi install ./ # install the package
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Tool rendering
|
|
17
|
+
|
|
18
|
+
How the `memory` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
19
|
+
|
|
20
|
+
<img src="../../assets/tools/memory.png" alt="The memory tool's result: the call line and an indented body confirming the write and the new project and global counts" width="100%">
|
|
21
|
+
|
|
16
22
|
## What it does
|
|
17
23
|
|
|
18
24
|
- Registers one model-callable tool, `memory`, with four actions: `add` a note,
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -142,7 +150,9 @@ the user.
|
|
|
142
150
|
|
|
143
151
|
Browser review is a local-turn feature: the server binds `127.0.0.1`, so a remote
|
|
144
152
|
(WeChat) turn cannot reach it and `/plan review` there refuses with a notice
|
|
145
|
-
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.
|
|
146
156
|
|
|
147
157
|
### Transcript
|
|
148
158
|
|
|
@@ -254,15 +264,15 @@ agent file that overrides it.
|
|
|
254
264
|
|
|
255
265
|
## Browser review
|
|
256
266
|
|
|
257
|
-
<img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title and Save
|
|
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%">
|
|
258
268
|
|
|
259
269
|
Press `w` on the review screen — or run `/plan review [path]` — to open the plan
|
|
260
270
|
in the default browser on `127.0.0.1`. The page is a reading-first review
|
|
261
271
|
surface: a sticky header carrying the plan's H1 (falling back to the file-name
|
|
262
272
|
slug), a live status chip (connection, comment count, and unsaved changes), the
|
|
263
|
-
theme toggle, and
|
|
264
|
-
side panel shows the plan's step progress and comment count above a
|
|
265
|
-
outline**, and the document is GitHub's Markdown content column — one centred
|
|
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
|
|
266
276
|
measure of at most 1012px with a comment rail in its left margin. Every block
|
|
267
277
|
reveals a **+ Comment** action on hover; select
|
|
268
278
|
text first to quote it. **Save feedback** writes the comments into the plan
|
|
@@ -284,12 +294,22 @@ file and returns them to the model.
|
|
|
284
294
|
labelled and indented as a thread, so it reads in place and the plan keeps one
|
|
285
295
|
measure at every width. There is no separate column to reflow.
|
|
286
296
|
- **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
|
|
287
|
-
on the current block, `
|
|
288
|
-
|
|
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.
|
|
289
308
|
- **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
|
|
290
|
-
code, GFM tables, `---` rules, and bare-URL autolinks render;
|
|
291
|
-
|
|
292
|
-
|
|
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.
|
|
293
313
|
- **Highlighted code.** Fenced code is tokenized by the shared
|
|
294
314
|
`lib/highlight.ts`; the client copies only text and `tok-*` spans through
|
|
295
315
|
`DOMParser`, so a code line is never parsed as markup.
|
|
@@ -439,10 +459,10 @@ file and returns them to the model.
|
|
|
439
459
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
440
460
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
441
461
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
442
|
-
| `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. |
|
|
443
463
|
| `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes. |
|
|
444
464
|
| `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/
|
|
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). |
|
|
446
466
|
| `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
|
|
447
467
|
| [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
|
|
448
468
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -26,12 +26,12 @@ import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui/chrom
|
|
|
26
26
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
27
27
|
import { expandHint } from "../../lib/tui/glyphs.ts";
|
|
28
28
|
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
29
|
-
import { feedbackSummary } from "./feedback.ts";
|
|
29
|
+
import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
|
|
30
30
|
import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
|
|
31
31
|
import { PLAN_OUTLINE, extractPlanSteps, type PlanStep, type StepUpdate } from "./steps.ts";
|
|
32
32
|
import { PlanViewComponent, sanitizePlanText, type PlanViewAction } from "./tui.ts";
|
|
33
33
|
import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
|
|
34
|
-
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
34
|
+
import { createPlanWebReview, type PlanReviewResult, type PlanReviewSession, type PlanWebReview } from "./web.ts";
|
|
35
35
|
|
|
36
36
|
const ExitPlanModeParams = Type.Object({
|
|
37
37
|
plan_path: Type.String({
|
|
@@ -394,6 +394,8 @@ export function registerTools(
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
let outcome = await reviewPlan(ctx, file);
|
|
397
|
+
// Comments carried by a browser approval, reported alongside the approval.
|
|
398
|
+
let browserComments: PlanAnnotation[] = [];
|
|
397
399
|
// Review, refine, and browser review all loop back here until the user
|
|
398
400
|
// approves, keeps planning, or a refinement is handed to the model. The
|
|
399
401
|
// dialog-based refine editor carries no text from the review screen, so an
|
|
@@ -401,15 +403,25 @@ export function registerTools(
|
|
|
401
403
|
// as "not approved".
|
|
402
404
|
for (;;) {
|
|
403
405
|
if (outcome.action === "browser") {
|
|
404
|
-
const session = await web
|
|
406
|
+
const session: PlanReviewSession | undefined = await web
|
|
407
|
+
.start(ctx.cwd, file, { canApprove: true })
|
|
408
|
+
.catch(() => undefined);
|
|
405
409
|
if (!session) {
|
|
406
410
|
ctx.ui.notify("Plan review: could not start the browser review server.", "error");
|
|
407
411
|
outcome = await reviewPlan(ctx, file);
|
|
408
412
|
continue;
|
|
409
413
|
}
|
|
410
414
|
ctx.ui.notify(`Plan review: opened ${session.url}`, "info");
|
|
411
|
-
const result = await session.done;
|
|
415
|
+
const result: PlanReviewResult = await session.done;
|
|
412
416
|
await session.close();
|
|
417
|
+
if (result.status === "approved") {
|
|
418
|
+
// Approve from the browser is the TUI's approve: keep the comments it
|
|
419
|
+
// carried and fall through to the shared approval path below.
|
|
420
|
+
file = result.plan;
|
|
421
|
+
browserComments = result.annotations;
|
|
422
|
+
outcome = { action: "approve" };
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
413
425
|
if (result.status === "saved") {
|
|
414
426
|
// The comments are already in the file; hand them back as a refinement so
|
|
415
427
|
// the model revises the same plan and calls exit_plan_mode again.
|
|
@@ -491,11 +503,14 @@ export function registerTools(
|
|
|
491
503
|
: steps.length > 0
|
|
492
504
|
? ` Could not record the steps in the todo list${error ? `: ${error}` : ""}. Keep these steps in mind and tick them off with ${SET_PLAN_STEPS_TOOL} as you go.`
|
|
493
505
|
: " If the todo tool is available, record the steps with it before you start.";
|
|
506
|
+
const browserNote = browserComments.length
|
|
507
|
+
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(browserComments)}`
|
|
508
|
+
: "";
|
|
494
509
|
return {
|
|
495
510
|
content: [
|
|
496
511
|
{
|
|
497
512
|
type: "text",
|
|
498
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${file.relativePath} is the source of truth; follow its steps.${tail}${listing}`,
|
|
513
|
+
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${file.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${listing}`,
|
|
499
514
|
},
|
|
500
515
|
],
|
|
501
516
|
details: {
|
|
@@ -25,6 +25,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
25
25
|
var token = typeof data.token === "string" ? data.token : "";
|
|
26
26
|
var annotations = Array.isArray(data.annotations) ? data.annotations.slice() : [];
|
|
27
27
|
var saved = false;
|
|
28
|
+
var approved = false;
|
|
28
29
|
var closed = false;
|
|
29
30
|
var connected = null;
|
|
30
31
|
var everConnected = false;
|
|
@@ -37,6 +38,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
37
38
|
var planEl = document.getElementById("plan");
|
|
38
39
|
var statusEl = document.getElementById("status");
|
|
39
40
|
var stateEl = document.getElementById("review-state");
|
|
41
|
+
var approveButton = document.getElementById("approve");
|
|
40
42
|
var saveButton = document.getElementById("save");
|
|
41
43
|
var cancelButton = document.getElementById("cancel");
|
|
42
44
|
var outlineList = document.getElementById("outline-list");
|
|
@@ -62,9 +64,15 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
62
64
|
if (!stateEl) return;
|
|
63
65
|
var label;
|
|
64
66
|
var tone = "";
|
|
65
|
-
if (
|
|
67
|
+
if (approved) {
|
|
68
|
+
label = "Approved";
|
|
69
|
+
tone = "success";
|
|
70
|
+
} else if (saved) {
|
|
66
71
|
label = "Saved";
|
|
67
72
|
tone = "success";
|
|
73
|
+
} else if (closed) {
|
|
74
|
+
// Settled by the user, not by a dropped stream: never flash Offline.
|
|
75
|
+
label = "Closed";
|
|
68
76
|
} else if (connected === true) {
|
|
69
77
|
label = "Connected";
|
|
70
78
|
tone = "success";
|
|
@@ -78,7 +86,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
78
86
|
}
|
|
79
87
|
var count = annotations.length;
|
|
80
88
|
label += " \u00b7 " + count + (count === 1 ? " comment" : " comments");
|
|
81
|
-
if (dirty && !saved) {
|
|
89
|
+
if (dirty && !saved && !approved && !closed) {
|
|
82
90
|
label += " \u00b7 unsaved";
|
|
83
91
|
if (!tone) tone = "warn";
|
|
84
92
|
}
|
|
@@ -254,9 +262,14 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
254
262
|
return;
|
|
255
263
|
}
|
|
256
264
|
if (block.kind === "list") {
|
|
265
|
+
var items = [];
|
|
257
266
|
lines.forEach(function (line) {
|
|
258
267
|
var match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line);
|
|
259
268
|
if (!match) {
|
|
269
|
+
// A following unmarked line is a CommonMark lazy continuation of the
|
|
270
|
+
// item above; join it so the paragraph hangs under the item's text.
|
|
271
|
+
var last = items[items.length - 1];
|
|
272
|
+
if (last) { if (line.trim()) last.rest += " " + line.trim(); return; }
|
|
260
273
|
var plain = el("p"); renderInline(plain, line); wrap.appendChild(plain); return;
|
|
261
274
|
}
|
|
262
275
|
var row = el("div", "bf-block__li");
|
|
@@ -272,9 +285,12 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
272
285
|
rest = box[2];
|
|
273
286
|
}
|
|
274
287
|
marker.textContent = markerText;
|
|
275
|
-
var body = el("span");
|
|
288
|
+
var body = el("span");
|
|
276
289
|
row.appendChild(marker); row.appendChild(body); wrap.appendChild(row);
|
|
290
|
+
items.push({ body: body, rest: rest });
|
|
277
291
|
});
|
|
292
|
+
// Render each body once, after its continuation lines are collected.
|
|
293
|
+
items.forEach(function (item) { renderInline(item.body, item.rest); });
|
|
278
294
|
return;
|
|
279
295
|
}
|
|
280
296
|
var paragraph = el("p");
|
|
@@ -600,6 +616,7 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
600
616
|
}
|
|
601
617
|
|
|
602
618
|
function lockActions() {
|
|
619
|
+
if (approveButton) approveButton.disabled = true;
|
|
603
620
|
if (saveButton) saveButton.disabled = true;
|
|
604
621
|
if (cancelButton) cancelButton.disabled = true;
|
|
605
622
|
}
|
|
@@ -618,51 +635,67 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
618
635
|
controls.appendChild(reload);
|
|
619
636
|
}
|
|
620
637
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
fetch("
|
|
638
|
+
// Save and approve share the same POST shape; only the route, the settled
|
|
639
|
+
// flag, and the wording differ. Both persist the current annotations.
|
|
640
|
+
function submit(url, okText, failText, mark) {
|
|
641
|
+
fetch(url + "?token=" + encodeURIComponent(token), {
|
|
625
642
|
method: "POST",
|
|
626
643
|
headers: { "Content-Type": "application/json" },
|
|
627
644
|
body: JSON.stringify({ annotations: annotations })
|
|
628
645
|
}).then(function (response) {
|
|
629
646
|
if (response.ok) {
|
|
630
|
-
|
|
647
|
+
mark();
|
|
631
648
|
dirty = false;
|
|
632
|
-
setStatus(
|
|
649
|
+
setStatus(okText, "saved");
|
|
633
650
|
lockActions();
|
|
634
651
|
updateState();
|
|
635
652
|
return;
|
|
636
653
|
}
|
|
637
654
|
if (response.status === 409) {
|
|
638
655
|
setStatus("The plan changed on disk \u2014 reload to review again.", "error");
|
|
656
|
+
if (approveButton) approveButton.disabled = true;
|
|
639
657
|
if (saveButton) saveButton.disabled = true;
|
|
640
658
|
offerReload();
|
|
641
659
|
return;
|
|
642
660
|
}
|
|
643
661
|
throw new Error("server returned " + response.status);
|
|
644
662
|
}).catch(function (error) {
|
|
645
|
-
setStatus(
|
|
663
|
+
setStatus(failText + error.message, "error");
|
|
646
664
|
});
|
|
647
665
|
}
|
|
648
666
|
|
|
667
|
+
function save() {
|
|
668
|
+
if (saved || approved || closed) return;
|
|
669
|
+
setStatus("Saving\u2026");
|
|
670
|
+
submit("/api/feedback", "Saved \u2014 return to Pi to continue.", "Could not save: ", function () { saved = true; });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function approve() {
|
|
674
|
+
if (saved || approved || closed) return;
|
|
675
|
+
setStatus("Approving\u2026");
|
|
676
|
+
submit("/api/approve", "Approved \u2014 return to Pi to continue.", "Could not approve: ", function () { approved = true; });
|
|
677
|
+
}
|
|
678
|
+
|
|
649
679
|
function cancel() {
|
|
650
|
-
if (saved || closed) return;
|
|
680
|
+
if (saved || approved || closed) return;
|
|
651
681
|
closed = true;
|
|
652
682
|
lockActions();
|
|
653
683
|
setStatus("Closing\u2026");
|
|
684
|
+
updateState();
|
|
654
685
|
fetch("/api/cancel?token=" + encodeURIComponent(token), { method: "POST" }).catch(function () {}).then(function () {
|
|
655
|
-
setStatus("Closed
|
|
686
|
+
setStatus("Closed \u2014 return to Pi.");
|
|
687
|
+
updateState();
|
|
656
688
|
});
|
|
657
689
|
}
|
|
658
690
|
|
|
691
|
+
if (approveButton) approveButton.addEventListener("click", approve);
|
|
659
692
|
if (saveButton) saveButton.addEventListener("click", save);
|
|
660
693
|
if (cancelButton) cancelButton.addEventListener("click", cancel);
|
|
661
694
|
|
|
662
695
|
// Remember a selection made inside a block so clicking the rail button can
|
|
663
696
|
// still quote it after the button press has collapsed the selection.
|
|
664
697
|
document.addEventListener("mouseup", function (event) {
|
|
665
|
-
if (!planEl || saved) return;
|
|
698
|
+
if (!planEl || saved || approved || closed) return;
|
|
666
699
|
var wrap = event.target && event.target.closest ? event.target.closest(".bf-block") : null;
|
|
667
700
|
var quote = wrap && !wrap.querySelector(".bf-composer") ? selectionIn(wrap) : "";
|
|
668
701
|
pendingQuote = wrap && quote ? { wrap: wrap, quote: quote } : null;
|
|
@@ -692,7 +725,13 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
692
725
|
}
|
|
693
726
|
|
|
694
727
|
document.addEventListener("keydown", function (event) {
|
|
695
|
-
if (event.key === "Escape") {
|
|
728
|
+
if (event.key === "Escape") {
|
|
729
|
+
// Esc peels one layer at a time: the composer, then the sheet, then the review.
|
|
730
|
+
if (planEl && planEl.querySelector(".bf-composer")) { closeComposer(); return; }
|
|
731
|
+
if (keysDialog && !keysDialog.hidden) { toggleKeys(false); return; }
|
|
732
|
+
cancel();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
696
735
|
if (typing(event.target)) return;
|
|
697
736
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
698
737
|
if (event.key === "?") { event.preventDefault(); toggleKeys(); return; }
|
|
@@ -701,11 +740,12 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
701
740
|
if (event.key === "j") { event.preventDefault(); moveActive(1); return; }
|
|
702
741
|
if (event.key === "k") { event.preventDefault(); moveActive(-1); return; }
|
|
703
742
|
if (event.key === "c") { event.preventDefault(); commentOnActive(); return; }
|
|
743
|
+
if (event.key === "a" && approveButton) { event.preventDefault(); approve(); return; }
|
|
704
744
|
if (event.key === "s") { event.preventDefault(); save(); return; }
|
|
705
745
|
});
|
|
706
746
|
|
|
707
747
|
window.addEventListener("beforeunload", function () {
|
|
708
|
-
if (saved || closed) return;
|
|
748
|
+
if (saved || approved || closed) return;
|
|
709
749
|
var url = "/api/cancel?token=" + encodeURIComponent(token);
|
|
710
750
|
if (navigator.sendBeacon) navigator.sendBeacon(url);
|
|
711
751
|
});
|
|
@@ -718,8 +758,9 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
|
718
758
|
var stream = new EventSource("/events?token=" + encodeURIComponent(token));
|
|
719
759
|
stream.addEventListener("open", function () { connected = true; everConnected = true; updateState(); });
|
|
720
760
|
stream.addEventListener("error", function () {
|
|
721
|
-
// The server ends the stream
|
|
722
|
-
|
|
761
|
+
// The server ends the stream once the review is settled; keep showing the
|
|
762
|
+
// terminal state rather than Offline.
|
|
763
|
+
if (!saved && !approved && !closed) { connected = false; updateState(); }
|
|
723
764
|
});
|
|
724
765
|
} catch (error) {
|
|
725
766
|
// The server still sees the POST; the stream is only for close detection.
|