@gavin-hu/my-pi-agent 0.7.0 → 0.8.1
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 +10 -10
- package/README.zh-CN.md +153 -0
- package/extensions/bytefly/README.md +129 -122
- package/extensions/bytefly/doctor/format.ts +3 -22
- package/extensions/bytefly/doctor/register.ts +10 -8
- package/extensions/bytefly/footer/register.ts +17 -91
- package/extensions/bytefly/header/component.ts +8 -4
- package/extensions/bytefly/header/config.ts +11 -6
- package/extensions/bytefly/header/register.ts +7 -27
- package/extensions/bytefly/index.ts +15 -14
- package/extensions/bytefly/panel/commands.ts +147 -0
- package/extensions/bytefly/panel/format.ts +135 -0
- package/extensions/bytefly/panel/join.ts +103 -0
- package/extensions/bytefly/panel/tui.ts +416 -0
- package/extensions/bytefly/panel/types.ts +44 -0
- package/extensions/bytefly/picker/resources.ts +0 -11
- package/extensions/file-browse/README.md +20 -7
- package/extensions/file-browse/html.ts +29 -6
- package/extensions/file-browse/markdown.ts +288 -46
- package/extensions/plan/README.md +16 -12
- package/extensions/plan/plans.ts +16 -17
- package/extensions/plan/runtime.ts +1 -1
- package/extensions/plan/steps.ts +31 -19
- package/extensions/plan/tools.ts +62 -32
- package/extensions/plan/types.ts +6 -6
- package/lib/README.md +1 -1
- package/lib/requirements.ts +3 -3
- package/lib/tui.ts +7 -8
- package/lib/web/components.ts +11 -0
- package/package.json +2 -2
- package/extensions/bytefly/doctor/commands.ts +0 -68
- package/extensions/bytefly/doctor/tui.ts +0 -140
- package/extensions/bytefly/picker/commands.ts +0 -73
- package/extensions/bytefly/picker/tui.ts +0 -260
|
@@ -42,8 +42,9 @@ pi --plan # start in plan mode
|
|
|
42
42
|
them into `todo`.
|
|
43
43
|
- **A navigable review.** The review screen splits the body at its headings,
|
|
44
44
|
names the section you are in, and jumps between sections with `n`/`p`.
|
|
45
|
-
- **Kept current.** After approval, `
|
|
46
|
-
the file, so the plan mirrors what has actually been
|
|
45
|
+
- **Kept current.** After approval, `set_plan_steps` flips the checkboxes of
|
|
46
|
+
the steps you name in the file, so the plan mirrors what has actually been
|
|
47
|
+
done.
|
|
47
48
|
- **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
|
|
48
49
|
it for approval. The user approves, keeps planning, or asks for a refinement.
|
|
49
50
|
- **Browser review on demand.** Press `w` on the review screen (or run
|
|
@@ -68,7 +69,7 @@ pi --plan # start in plan mode
|
|
|
68
69
|
| `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
|
|
69
70
|
| `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. |
|
|
70
71
|
| `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. |
|
|
71
|
-
| `
|
|
72
|
+
| `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. |
|
|
72
73
|
|
|
73
74
|
### Commands and flags
|
|
74
75
|
|
|
@@ -249,6 +250,8 @@ agent file that overrides it.
|
|
|
249
250
|
|
|
250
251
|
## Browser review
|
|
251
252
|
|
|
253
|
+
<img src="../../assets/plan-review.png" alt="The browser-based plan review, showing the plan's sections, a rendered code block, and an inline comment box" width="100%">
|
|
254
|
+
|
|
252
255
|
Press `w` on the review screen — or run `/plan review [path]` — to open the plan
|
|
253
256
|
in the default browser on `127.0.0.1`. The page is a review surface: a sticky
|
|
254
257
|
header with the plan title, theme toggle, and Save/Close; a sticky **section
|
|
@@ -314,7 +317,7 @@ returns them to the model.
|
|
|
314
317
|
|
|
315
318
|
- **Not a sandbox.** Extensions share Pi's OS permissions.
|
|
316
319
|
- **No progress tracker of record.** `todo` owns completion state; the plan
|
|
317
|
-
file only mirrors it (`
|
|
320
|
+
file only mirrors it (`set_plan_steps`), and plan mode does not read completion
|
|
318
321
|
back out.
|
|
319
322
|
- **No auto-execution.** Approval hands control back to the model.
|
|
320
323
|
|
|
@@ -322,7 +325,7 @@ returns them to the model.
|
|
|
322
325
|
|
|
323
326
|
| Integration point | How |
|
|
324
327
|
|---|---|
|
|
325
|
-
| Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `
|
|
328
|
+
| Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`); no `outputSchema`/`structuredContent`. |
|
|
326
329
|
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
327
330
|
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })` and rebuilt from `ctx.sessionManager.getBranch()`; the last plan path lives in runtime memory for the footer chip. |
|
|
328
331
|
| Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
|
|
@@ -360,10 +363,11 @@ returns them to the model.
|
|
|
360
363
|
source-line map, so `tui.ts` splits the body at its headings and renders each
|
|
361
364
|
section separately. That gives exact offsets for the current-section label and
|
|
362
365
|
`n`/`p`; the single-section case is byte-for-byte the old render.
|
|
363
|
-
- **`
|
|
364
|
-
|
|
365
|
-
(`isContainedPlan`),
|
|
366
|
-
|
|
366
|
+
- **`set_plan_steps` is a second contained write.** It changes only the named
|
|
367
|
+
steps' checkboxes, in an existing markdown plan inside the plans directory
|
|
368
|
+
(`isContainedPlan`), in a single all-or-nothing write — refusing every entry
|
|
369
|
+
if any step is out of range — and refuses while plan mode is on, so the
|
|
370
|
+
read-only guarantee and the path backstop are unchanged.
|
|
367
371
|
- **Direction: enforce capabilities at the tool boundary.** (1) A shared
|
|
368
372
|
default-deny policy classifies every call as read-only or mutating instead of
|
|
369
373
|
maintaining a mutator list. (2) Read-only delegation is enforced in the child
|
|
@@ -394,13 +398,13 @@ returns them to the model.
|
|
|
394
398
|
| File | Responsibility |
|
|
395
399
|
|---|---|
|
|
396
400
|
| `index.ts` | Wiring: tools, command, flag, shortcut, events, context injection. |
|
|
397
|
-
| `types.ts` | `PlanModeEntry`, `EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `
|
|
401
|
+
| `types.ts` | `PlanModeEntry`, `EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`. |
|
|
398
402
|
| `policy.ts` | Plan mode's read-only policy, the shared prompt summary, and the bash-refusal guidance. |
|
|
399
403
|
| `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
|
|
400
|
-
| `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, write/read/`
|
|
404
|
+
| `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, write/read/`setSteps`/`saveFeedback`. |
|
|
401
405
|
| `steps.ts` | Plan-document structure (pure): the outline, step extraction, heading sections, and the checkbox edit. |
|
|
402
406
|
| `runtime.ts` | Enabled state, tool gating, persistence, control-tool identity, footer status. |
|
|
403
|
-
| `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `
|
|
407
|
+
| `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`. |
|
|
404
408
|
| `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
|
|
405
409
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
406
410
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, blanks). |
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works
|
|
|
20
20
|
import { repoRootFor } from "../../lib/git/index.ts";
|
|
21
21
|
import { isInside, isInsideReal } from "../../lib/path.ts";
|
|
22
22
|
import { formatFeedback, type PlanAnnotation } from "./feedback.ts";
|
|
23
|
-
import { extractPlanSteps,
|
|
23
|
+
import { extractPlanSteps, setStepStatuses, type StepUpdate } from "./steps.ts";
|
|
24
24
|
|
|
25
25
|
/** Largest plan accepted, in UTF-8 bytes. */
|
|
26
26
|
export const MAX_PLAN_BYTES = 256 * 1024;
|
|
@@ -57,8 +57,8 @@ export interface PlanStore {
|
|
|
57
57
|
write(cwd: string, input: WritePlanInput): Promise<WrittenPlan>;
|
|
58
58
|
/** Read a plan inside the plans directory, or undefined when missing/outside. */
|
|
59
59
|
read(cwd: string, planPath: string): Promise<StoredPlan | undefined>;
|
|
60
|
-
/** Set
|
|
61
|
-
|
|
60
|
+
/** Set the given top-level steps' checkboxes in an existing plan. Throws when the file or any step is invalid. */
|
|
61
|
+
setSteps(cwd: string, planPath: string, updates: StepUpdate[]): Promise<StoredPlan>;
|
|
62
62
|
/** Replace the plan's stored browser feedback. Throws when the file is missing or outside the plans directory. */
|
|
63
63
|
saveFeedback(cwd: string, planPath: string, annotations: PlanAnnotation[]): Promise<StoredPlan>;
|
|
64
64
|
}
|
|
@@ -222,26 +222,25 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
222
222
|
}
|
|
223
223
|
};
|
|
224
224
|
|
|
225
|
-
const
|
|
226
|
-
cwd: string,
|
|
227
|
-
planPath: string,
|
|
228
|
-
step: number,
|
|
229
|
-
status: PlanStep["status"],
|
|
230
|
-
): Promise<StoredPlan> => {
|
|
225
|
+
const setSteps = async (cwd: string, planPath: string, updates: StepUpdate[]): Promise<StoredPlan> => {
|
|
231
226
|
const plan = await read(cwd, planPath);
|
|
232
227
|
if (!plan) throw new Error("No plan file there; pass a plan_path written by write_plan.");
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
const total = extractPlanSteps(plan.content).length;
|
|
229
|
+
const outOfRange = updates.find(
|
|
230
|
+
(update) => !Number.isInteger(update.step) || update.step < 1 || update.step > total,
|
|
231
|
+
);
|
|
232
|
+
if (outOfRange) {
|
|
236
233
|
const range = total === 0 ? "it has no steps" : `valid steps are 1 to ${total}`;
|
|
237
|
-
throw new Error(`Step ${step} is out of range; ${range}.`);
|
|
234
|
+
throw new Error(`Step ${outOfRange.step} is out of range; ${range}.`);
|
|
238
235
|
}
|
|
239
|
-
|
|
236
|
+
const updated = setStepStatuses(plan.content, updates);
|
|
237
|
+
if (!updated) throw new Error("Could not update the plan steps; pass steps from the plan file.");
|
|
238
|
+
writeFileSync(plan.path, updated, "utf-8");
|
|
240
239
|
return {
|
|
241
240
|
path: plan.path,
|
|
242
241
|
relativePath: plan.relativePath,
|
|
243
|
-
content: updated
|
|
244
|
-
bytes: Buffer.byteLength(updated
|
|
242
|
+
content: updated,
|
|
243
|
+
bytes: Buffer.byteLength(updated, "utf-8"),
|
|
245
244
|
};
|
|
246
245
|
};
|
|
247
246
|
|
|
@@ -263,5 +262,5 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
263
262
|
};
|
|
264
263
|
};
|
|
265
264
|
|
|
266
|
-
return { dirFor, write, read,
|
|
265
|
+
return { dirFor, write, read, setSteps, saveFeedback };
|
|
267
266
|
}
|
|
@@ -31,7 +31,7 @@ export const ENTER_TOOL = "enter_plan_mode";
|
|
|
31
31
|
/** Tool the model calls to save the plan artifact. */
|
|
32
32
|
export const WRITE_PLAN_TOOL = "write_plan";
|
|
33
33
|
/** Tool the model calls to keep an approved plan's steps current. */
|
|
34
|
-
export const
|
|
34
|
+
export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
|
|
35
35
|
/** Custom-entry type used for persistence. */
|
|
36
36
|
export const STATE_TYPE = "plan-mode";
|
|
37
37
|
|
package/extensions/plan/steps.ts
CHANGED
|
@@ -166,28 +166,40 @@ export function extractPlanSteps(plan: string): PlanStep[] {
|
|
|
166
166
|
return topLevelItems(plan).map((item) => ({ content: elide(item.step.content), status: item.step.status }));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/** One checkbox change: a 1-based step number and the state to set. */
|
|
170
|
+
export interface StepUpdate {
|
|
171
|
+
step: number;
|
|
172
|
+
status: PlanStep["status"];
|
|
173
|
+
}
|
|
174
|
+
|
|
169
175
|
/**
|
|
170
|
-
*
|
|
171
|
-
* {@link extractPlanSteps}) and return the rewritten plan
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
176
|
+
* Apply a batch of checkbox changes to the given top-level steps (1-based,
|
|
177
|
+
* matching {@link extractPlanSteps}) and return the rewritten plan, or
|
|
178
|
+
* undefined when any step number is out of range. All-or-nothing: every step is
|
|
179
|
+
* validated before any edit, so one bad entry changes nothing. Only the marker
|
|
180
|
+
* changes: a plain `1.`/`-` item gains a checkbox, an existing one is set, a
|
|
181
|
+
* `[DONE:n]` marker is dropped when the step is reopened, and the step text is
|
|
182
|
+
* otherwise left exactly as written. When a step appears twice, the last entry
|
|
183
|
+
* wins.
|
|
175
184
|
*/
|
|
176
|
-
export function
|
|
177
|
-
plan
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const target = topLevelItems(plan)[step - 1];
|
|
183
|
-
if (!target) return undefined;
|
|
184
|
-
|
|
185
|
-
const existing = target.raw.match(CHECKBOX);
|
|
186
|
-
const text = existing ? existing[2] : target.raw;
|
|
187
|
-
const checkbox = status === "completed" ? "[x]" : "[ ]";
|
|
185
|
+
export function setStepStatuses(plan: string, updates: StepUpdate[]): string | undefined {
|
|
186
|
+
const items = topLevelItems(plan);
|
|
187
|
+
for (const update of updates) {
|
|
188
|
+
if (!Number.isInteger(update.step) || update.step < 1 || !items[update.step - 1]) return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
188
191
|
const lines = plan.split("\n");
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
for (const update of updates) {
|
|
193
|
+
const target = items[update.step - 1];
|
|
194
|
+
const existing = target.raw.match(CHECKBOX);
|
|
195
|
+
let text = existing ? existing[2] : target.raw;
|
|
196
|
+
// A reopened step must lose its marker too: the marker alone reads as
|
|
197
|
+
// completed, so keeping it would contradict the checkbox.
|
|
198
|
+
if (update.status === "pending") text = text.replace(DONE_MARKER, "").trimEnd();
|
|
199
|
+
const checkbox = update.status === "completed" ? "[x]" : "[ ]";
|
|
200
|
+
lines[target.line] = `${" ".repeat(target.indent)}${target.lead}${checkbox} ${text}`;
|
|
201
|
+
}
|
|
202
|
+
return lines.join("\n");
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
/**
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* approval (a scrollable review screen in the TUI, a select dialog elsewhere),
|
|
8
8
|
* and on approval seeds the `todo` tool with the plan's steps (through
|
|
9
9
|
* `ctx.executeTool`, so the todo widget and validation run normally).
|
|
10
|
-
* `
|
|
10
|
+
* `set_plan_steps` then keeps the file current: after approval the model ticks
|
|
11
11
|
* each step off in place as it finishes it. The file is the source of truth:
|
|
12
12
|
* the model executes from it.
|
|
13
13
|
*
|
|
@@ -26,10 +26,10 @@ import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui.ts";
|
|
|
26
26
|
import { expandHint } from "../../lib/ui.ts";
|
|
27
27
|
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
28
28
|
import { feedbackSummary } from "./feedback.ts";
|
|
29
|
-
import { ENTER_TOOL, EXIT_TOOL,
|
|
30
|
-
import { PLAN_OUTLINE, extractPlanSteps, type PlanStep } from "./steps.ts";
|
|
29
|
+
import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
|
|
30
|
+
import { PLAN_OUTLINE, extractPlanSteps, type PlanStep, type StepUpdate } from "./steps.ts";
|
|
31
31
|
import { PlanViewComponent, sanitizePlanText, type PlanViewAction } from "./tui.ts";
|
|
32
|
-
import type { EnterPlanModeDetails, ExitPlanModeDetails,
|
|
32
|
+
import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
|
|
33
33
|
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
34
34
|
|
|
35
35
|
const ExitPlanModeParams = Type.Object({
|
|
@@ -54,10 +54,7 @@ const WritePlanParams = Type.Object({
|
|
|
54
54
|
),
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
plan_path: Type.String({
|
|
59
|
-
description: "Path to the approved plan file, as returned by write_plan or exit_plan_mode.",
|
|
60
|
-
}),
|
|
57
|
+
const StepUpdateParams = Type.Object({
|
|
61
58
|
step: Type.Integer({
|
|
62
59
|
minimum: 1,
|
|
63
60
|
description: "1-based step number, in the order the plan was approved.",
|
|
@@ -67,9 +64,19 @@ const SetPlanStepParams = Type.Object({
|
|
|
67
64
|
}),
|
|
68
65
|
});
|
|
69
66
|
|
|
67
|
+
const SetPlanStepsParams = Type.Object({
|
|
68
|
+
plan_path: Type.String({
|
|
69
|
+
description: "Path to the approved plan file, as returned by write_plan or exit_plan_mode.",
|
|
70
|
+
}),
|
|
71
|
+
updates: Type.Array(StepUpdateParams, {
|
|
72
|
+
minItems: 1,
|
|
73
|
+
description: "The steps to set; each entry sets one step's checkbox, and the last entry wins for a duplicate step.",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
70
77
|
type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
|
|
71
78
|
type WritePlanArgs = Static<typeof WritePlanParams>;
|
|
72
|
-
type
|
|
79
|
+
type SetPlanStepsArgs = Static<typeof SetPlanStepsParams>;
|
|
73
80
|
|
|
74
81
|
/** Lines of plan shown in an unexpanded result. */
|
|
75
82
|
const PREVIEW_LINES = 8;
|
|
@@ -89,6 +96,21 @@ function preview(plan: string): { body: string; more: number } {
|
|
|
89
96
|
return { body: lines.slice(0, PREVIEW_LINES).join("\n"), more: lines.length - PREVIEW_LINES };
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
/** "Step 2" or "Steps 1, 2" for a set of step numbers. */
|
|
100
|
+
function stepLabel(steps: number[]): string {
|
|
101
|
+
return `${steps.length === 1 ? "Step" : "Steps"} ${steps.join(", ")}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** "Steps 1, 2 completed; step 3 reopened" for the tool's text result. */
|
|
105
|
+
function describeUpdates(updates: StepUpdate[]): string {
|
|
106
|
+
const completed = updates.filter((update) => update.status === "completed").map((update) => update.step);
|
|
107
|
+
const pending = updates.filter((update) => update.status === "pending").map((update) => update.step);
|
|
108
|
+
const parts: string[] = [];
|
|
109
|
+
if (completed.length > 0) parts.push(`${stepLabel(completed)} completed`);
|
|
110
|
+
if (pending.length > 0) parts.push(`${stepLabel(pending)} reopened`);
|
|
111
|
+
return parts.join("; ");
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
/** Seed the todo list with the plan's steps; returns what was recorded. */
|
|
93
115
|
async function seedTodos(
|
|
94
116
|
ctx: ExtensionToolContext,
|
|
@@ -464,9 +486,9 @@ export function registerTools(
|
|
|
464
486
|
: "";
|
|
465
487
|
const tail =
|
|
466
488
|
recorded > 0
|
|
467
|
-
? ` Recorded ${recorded} step${recorded === 1 ? "" : "s"} in the todo list. As you finish each step, mark it done there and tick it off in the file with ${
|
|
489
|
+
? ` Recorded ${recorded} step${recorded === 1 ? "" : "s"} in the todo list. As you finish each step, mark it done there and tick it off in the file with ${SET_PLAN_STEPS_TOOL}.`
|
|
468
490
|
: steps.length > 0
|
|
469
|
-
? ` Could not record the steps in the todo list${error ? `: ${error}` : ""}. Keep these steps in mind and tick them off with ${
|
|
491
|
+
? ` 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.`
|
|
470
492
|
: " If the todo tool is available, record the steps with it before you start.";
|
|
471
493
|
return {
|
|
472
494
|
content: [
|
|
@@ -525,22 +547,23 @@ export function registerTools(
|
|
|
525
547
|
});
|
|
526
548
|
|
|
527
549
|
pi.registerTool({
|
|
528
|
-
name:
|
|
529
|
-
label: "Set plan
|
|
550
|
+
name: SET_PLAN_STEPS_TOOL,
|
|
551
|
+
label: "Set plan steps",
|
|
530
552
|
description:
|
|
531
|
-
"Keep an approved plan file current by setting
|
|
532
|
-
"the order the plan was approved in
|
|
533
|
-
"
|
|
534
|
-
|
|
553
|
+
"Keep an approved plan file current by setting the checkboxes of the given top-level steps in place. Each step " +
|
|
554
|
+
"number matches the order the plan was approved in; a step can be marked completed or reopened. Only the " +
|
|
555
|
+
"checkboxes change; the step text is left as written. The file is rewritten once, and nothing changes when any " +
|
|
556
|
+
"step is out of range.",
|
|
557
|
+
promptSnippet: "Tick steps off in the approved plan file.",
|
|
535
558
|
promptGuidelines: [
|
|
536
|
-
"After a plan is approved, call
|
|
559
|
+
"After a plan is approved, call set_plan_steps as you complete steps so the plan file stays current.",
|
|
537
560
|
],
|
|
538
|
-
parameters:
|
|
561
|
+
parameters: SetPlanStepsParams,
|
|
539
562
|
executionMode: "sequential",
|
|
540
563
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
541
564
|
|
|
542
565
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
543
|
-
const args = params as
|
|
566
|
+
const args = params as SetPlanStepsArgs;
|
|
544
567
|
if (runtime.isEnabled()) {
|
|
545
568
|
return {
|
|
546
569
|
content: [{ type: "text", text: "Not while planning; refine the plan with write_plan instead." }],
|
|
@@ -549,24 +572,23 @@ export function registerTools(
|
|
|
549
572
|
};
|
|
550
573
|
}
|
|
551
574
|
try {
|
|
552
|
-
const plan = await runtime.plans.
|
|
575
|
+
const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
|
|
553
576
|
const steps = extractPlanSteps(plan.content);
|
|
554
577
|
const done = steps.filter((step) => step.status === "completed").length;
|
|
555
578
|
return {
|
|
556
579
|
content: [
|
|
557
580
|
{
|
|
558
581
|
type: "text",
|
|
559
|
-
text:
|
|
582
|
+
text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}.`,
|
|
560
583
|
},
|
|
561
584
|
],
|
|
562
585
|
details: {
|
|
563
586
|
path: plan.path,
|
|
564
587
|
relativePath: plan.relativePath,
|
|
565
|
-
|
|
566
|
-
status: args.status,
|
|
588
|
+
updates: args.updates,
|
|
567
589
|
done,
|
|
568
590
|
total: steps.length,
|
|
569
|
-
} satisfies
|
|
591
|
+
} satisfies SetPlanStepsDetails,
|
|
570
592
|
};
|
|
571
593
|
} catch (error) {
|
|
572
594
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -580,24 +602,32 @@ export function registerTools(
|
|
|
580
602
|
|
|
581
603
|
renderCall(args, theme, context) {
|
|
582
604
|
const text = reuseText(context?.lastComponent);
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
|
|
605
|
+
const updates = (args as Partial<SetPlanStepsArgs>).updates;
|
|
606
|
+
const label =
|
|
607
|
+
updates && updates.length === 1 && updates[0]?.step !== undefined
|
|
608
|
+
? `step ${updates[0].step}`
|
|
609
|
+
: updates && updates.length > 1
|
|
610
|
+
? `${updates.length} steps`
|
|
611
|
+
: "steps";
|
|
612
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${SET_PLAN_STEPS_TOOL} `)) + theme.fg("muted", label));
|
|
586
613
|
return text;
|
|
587
614
|
},
|
|
588
615
|
|
|
589
616
|
renderResult(result, _options, theme, context) {
|
|
590
617
|
const text = reuseText(context?.lastComponent);
|
|
591
|
-
const details = toolDetails<
|
|
618
|
+
const details = toolDetails<SetPlanStepsDetails>(result.details);
|
|
592
619
|
if (!details) {
|
|
593
620
|
const first = result.content[0];
|
|
594
621
|
text.setText(first?.type === "text" ? first.text : "");
|
|
595
622
|
return text;
|
|
596
623
|
}
|
|
624
|
+
const completed = details.updates.filter((update) => update.status === "completed").map((u) => u.step);
|
|
625
|
+
const pending = details.updates.filter((update) => update.status === "pending").map((u) => u.step);
|
|
597
626
|
const heading =
|
|
598
|
-
|
|
599
|
-
? theme.fg("success", `✓
|
|
600
|
-
|
|
627
|
+
completed.length > 0
|
|
628
|
+
? theme.fg("success", `✓ ${stepLabel(completed)} done`) +
|
|
629
|
+
(pending.length > 0 ? theme.fg("muted", ` · ${stepLabel(pending)} reopened`) : "")
|
|
630
|
+
: theme.fg("muted", `${stepLabel(pending)} reopened`);
|
|
601
631
|
text.setText(heading + theme.fg("dim", ` · ${details.done}/${details.total}`));
|
|
602
632
|
return text;
|
|
603
633
|
},
|
package/extensions/plan/types.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Shared types for the plan-mode extension.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { StepUpdate } from "./steps.ts";
|
|
6
|
+
|
|
5
7
|
/** Persisted (custom entry) plan-mode state. */
|
|
6
8
|
export interface PlanModeEntry {
|
|
7
9
|
enabled: boolean;
|
|
@@ -32,16 +34,14 @@ export interface WritePlanDetails {
|
|
|
32
34
|
bytes: number;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
/** Structured result carried in `
|
|
36
|
-
export interface
|
|
37
|
+
/** Structured result carried in `set_plan_steps`'s `details`. */
|
|
38
|
+
export interface SetPlanStepsDetails {
|
|
37
39
|
/** Absolute path of the plan file that was updated. */
|
|
38
40
|
path: string;
|
|
39
41
|
/** Path relative to the working directory, for transcript display. */
|
|
40
42
|
relativePath: string;
|
|
41
|
-
/** The
|
|
42
|
-
|
|
43
|
-
/** The step's new status. */
|
|
44
|
-
status: "pending" | "completed";
|
|
43
|
+
/** The checkbox changes that were applied, in call order. */
|
|
44
|
+
updates: StepUpdate[];
|
|
45
45
|
/** Completed steps after the update. */
|
|
46
46
|
done: number;
|
|
47
47
|
/** Total top-level steps in the plan. */
|
package/lib/README.md
CHANGED
|
@@ -24,7 +24,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
24
24
|
| [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote adapter installs on `ctx.ui` under a string key, the `InteractionChannel` interface (`request` / `post` / `postFile`), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file/text preface) plus the shared “Other” label. |
|
|
25
25
|
| [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
|
|
26
26
|
| [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
|
|
27
|
-
| [`list-cursor.ts`](./list-cursor.ts) | Scrollable-list behavior for the `/rewind`, `/
|
|
27
|
+
| [`list-cursor.ts`](./list-cursor.ts) | Scrollable-list behavior for the `/rewind`, `/bytefly`, `/todos`, and `/jobs` screens: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` state. |
|
|
28
28
|
| [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, and doc guards share. |
|
|
29
29
|
| [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
|
|
30
30
|
| [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
|
package/lib/requirements.ts
CHANGED
|
@@ -194,9 +194,9 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
194
194
|
bytefly: {
|
|
195
195
|
configFiles: [SHARED_CONFIG_FILE],
|
|
196
196
|
notes: [
|
|
197
|
-
"TUI only for the header, footer, and turn rule; `/
|
|
198
|
-
"The
|
|
199
|
-
"`/
|
|
197
|
+
"TUI only for the header, footer, and turn rule; `/bytefly` (and the `/extensions` and `/doctor` aliases) prints a listing and report in RPC, JSON, and print modes.",
|
|
198
|
+
"The panel writes extension enablement through Pi's own settings; project scope requires a trusted project.",
|
|
199
|
+
"`/bytefly`'s list screen and the proactive missing-prerequisite warning need an interactive UI.",
|
|
200
200
|
],
|
|
201
201
|
},
|
|
202
202
|
};
|
package/lib/tui.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* todo).
|
|
4
4
|
*
|
|
5
5
|
* `FULL_SCREEN_OVERLAY` is for the one surface that must own the whole terminal:
|
|
6
|
-
* the plan read/review screen. Every list screen (`/todos`, `/jobs`,
|
|
7
|
-
* `/rewind`) stays in the dock's editor slot so they match each other and
|
|
8
|
-
*
|
|
6
|
+
* the plan read/review screen. Every list screen (`/bytefly`, `/todos`, `/jobs`,
|
|
7
|
+
* `/rewind`) stays in the dock's editor slot so they match each other and leave
|
|
8
|
+
* the transcript visible.
|
|
9
9
|
*
|
|
10
10
|
* Screens are full-width, terminal-height-aware components. These helpers build
|
|
11
11
|
* the top rule, clamp a screen's body to the terminal, and render the key-hint
|
|
@@ -128,11 +128,10 @@ export interface DockListFrame {
|
|
|
128
128
|
* its blank line, the body, an optional range row, optional detail rows, and
|
|
129
129
|
* the blank-line-padded key-hint footer.
|
|
130
130
|
*
|
|
131
|
-
* This is the convention
|
|
132
|
-
* `/doctor`
|
|
133
|
-
* after the header and no blank before the hint)
|
|
134
|
-
*
|
|
135
|
-
* fit this helper.
|
|
131
|
+
* This is the convention the bytefly panel (`/bytefly`, and the `/extensions`
|
|
132
|
+
* and `/doctor` aliases) shares with `/jobs` and `/todos`. `/rewind` (a blank
|
|
133
|
+
* after the header and no blank before the hint) places its blanks differently,
|
|
134
|
+
* so it stays hand-rolled rather than changing its output to fit this helper.
|
|
136
135
|
*/
|
|
137
136
|
export function dockListFrame(frame: DockListFrame): string[] {
|
|
138
137
|
const width = Math.max(1, frame.width);
|
package/lib/web/components.ts
CHANGED
|
@@ -113,6 +113,17 @@ th[aria-sort="descending"] .bf-sort::after{content:" \\25BC";font-size:9px;color
|
|
|
113
113
|
.bf-md pre{border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);overflow:auto;background:var(--canvas)}
|
|
114
114
|
.bf-md pre code{background:none;border:0;padding:0}
|
|
115
115
|
.bf-md blockquote{margin-left:0;padding:0 1em;border-left:3px solid var(--border);color:var(--muted)}
|
|
116
|
+
.bf-md li.task{list-style:none;margin-left:-1.4em}
|
|
117
|
+
.bf-md .md-check{color:var(--success)}
|
|
118
|
+
.bf-md table{margin:0;border-collapse:collapse;table-layout:auto;width:auto;font-size:var(--fs-sm)}
|
|
119
|
+
.bf-md .md-table-wrap{overflow-x:auto;margin:0 0 1em}
|
|
120
|
+
.bf-md th,.bf-md td{border:1px solid var(--border);padding:6px 10px;text-align:left;vertical-align:top}
|
|
121
|
+
.bf-md thead th{background:var(--surface);font-weight:var(--fw-medium)}
|
|
122
|
+
.bf-md .md-left{text-align:left}
|
|
123
|
+
.bf-md .md-center{text-align:center}
|
|
124
|
+
.bf-md .md-right{text-align:right}
|
|
125
|
+
.bf-md img{max-width:100%;height:auto;border-radius:var(--radius-sm)}
|
|
126
|
+
.bf-md hr{border:0;border-top:1px solid var(--border);margin:1.4em 0}
|
|
116
127
|
.bf-comment{padding:var(--space-3) var(--space-4);background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--warn);border-radius:var(--radius-md)}
|
|
117
128
|
.bf-comment__quote{margin-bottom:var(--space-1);color:var(--muted);font-size:var(--fs-xs);overflow-wrap:anywhere}
|
|
118
129
|
.bf-comment__body{white-space:pre-wrap;overflow-wrap:anywhere}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"themes": [
|
|
91
91
|
"./themes/*.json"
|
|
92
92
|
],
|
|
93
|
-
"image": "
|
|
93
|
+
"image": "https://raw.githubusercontent.com/gavin-hu/my-pi-agent/main/assets/bytefly-chrome.png"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"test": "bun test --parallel=4 --timeout 30000",
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `/doctor` command.
|
|
3
|
-
*
|
|
4
|
-
* One command for the whole surface: `/doctor` reports everything, `/doctor
|
|
5
|
-
* document` narrows it, `/doctor refresh` re-probes. The TUI opens a docked
|
|
6
|
-
* list screen; every other mode prints the same report as text.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { withRailsSuppressed } from "../../../lib/rails.ts";
|
|
11
|
-
import { REQUIREMENTS, type ExtensionReport } from "../../../lib/requirements.ts";
|
|
12
|
-
import { formatExtension, formatReport, hasProblems } from "./format.ts";
|
|
13
|
-
import type { DoctorRuntime } from "./runtime.ts";
|
|
14
|
-
import { DoctorScreen } from "./tui.ts";
|
|
15
|
-
|
|
16
|
-
/** The report text for a set of reports, narrowed when one extension was named. */
|
|
17
|
-
function reportText(name: string | undefined, reports: readonly ExtensionReport[]): string {
|
|
18
|
-
const first = reports[0];
|
|
19
|
-
return name && first ? formatExtension(first) : formatReport(reports);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Register the `/doctor` command. */
|
|
23
|
-
export function registerCommands(pi: ExtensionAPI, runtime: DoctorRuntime): void {
|
|
24
|
-
pi.registerCommand("doctor", {
|
|
25
|
-
description: "Check that every extension has what it needs: /doctor [extension] [refresh]",
|
|
26
|
-
getArgumentCompletions: (prefix: string) => {
|
|
27
|
-
const options = [...Object.keys(REQUIREMENTS), "refresh"].filter((option) => option.startsWith(prefix));
|
|
28
|
-
return options.length > 0 ? options.map((value) => ({ value, label: value })) : null;
|
|
29
|
-
},
|
|
30
|
-
handler: async (args, ctx) => {
|
|
31
|
-
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
32
|
-
const refresh = parts.includes("refresh");
|
|
33
|
-
const name = parts.find((part) => part !== "refresh");
|
|
34
|
-
|
|
35
|
-
const reports = await runtime.probeAll({ refresh });
|
|
36
|
-
if (name && !reports.some((report) => report.extension === name)) {
|
|
37
|
-
ctx.ui.notify(`doctor: no extension named "${name}"`, "warning");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const selected = name ? reports.filter((report) => report.extension === name) : reports;
|
|
41
|
-
|
|
42
|
-
if (ctx.mode !== "tui") {
|
|
43
|
-
ctx.ui.notify(reportText(name, selected), hasProblems(selected) ? "warning" : "info");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// The dock screen owns the editor slot; hide the rails for its lifetime.
|
|
48
|
-
let current = selected;
|
|
49
|
-
await withRailsSuppressed(pi, () =>
|
|
50
|
-
ctx.ui.custom<void>(
|
|
51
|
-
(tui, theme, _keybindings, done) =>
|
|
52
|
-
new DoctorScreen({
|
|
53
|
-
reports: () => current,
|
|
54
|
-
rows: () => tui.terminal?.rows,
|
|
55
|
-
theme,
|
|
56
|
-
requestRender: () => tui.requestRender(),
|
|
57
|
-
refresh: async () => {
|
|
58
|
-
const next = await runtime.probeAll({ refresh: true });
|
|
59
|
-
current = name ? next.filter((report) => report.extension === name) : next;
|
|
60
|
-
tui.requestRender();
|
|
61
|
-
},
|
|
62
|
-
done,
|
|
63
|
-
}),
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|