@gavin-hu/my-pi-agent 0.16.0 → 0.17.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 +14 -8
- package/README.zh-CN.md +14 -9
- package/extensions/bytefly/README.md +174 -101
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +10 -27
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +29 -1
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/html.ts +7 -5
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/index.ts +20 -45
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +10 -10
- package/extensions/plan/tools/exit.ts +54 -43
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +22 -29
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +7 -6
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/report.ts +75 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +16 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +3 -1
- package/skills/changelog/SKILL.md +57 -45
- package/skills/next/SKILL.md +184 -0
|
@@ -18,6 +18,8 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
|
18
18
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
19
19
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
20
|
import { callLine, detailsOrLine, moreHint } from "../../../lib/tui/tool.ts";
|
|
21
|
+
import { failedResult } from "../../../lib/tool-result.ts";
|
|
22
|
+
import { loadConfig } from "../config.ts";
|
|
21
23
|
import { feedbackSummary } from "../feedback.ts";
|
|
22
24
|
import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
|
|
23
25
|
import { sanitizePlanText } from "../plan-text.ts";
|
|
@@ -51,7 +53,8 @@ interface GoalOutcome {
|
|
|
51
53
|
* Finalise the session goal from the approved plan.
|
|
52
54
|
*
|
|
53
55
|
* A `planning` goal needs no dialog — approving the plan *is* the decision — and a
|
|
54
|
-
* session with no goal at all gets one confirm
|
|
56
|
+
* session with no goal at all gets one confirm, unless `plan.goalPrompt` is off,
|
|
57
|
+
* which records the objective without asking. A final goal (`active` or
|
|
55
58
|
* `achieved`) is the user's own: approving ordinary work never rewrites it.
|
|
56
59
|
* Recording runs through the goal tool, so validation and the widget stay goal's,
|
|
57
60
|
* and no model turn is spent.
|
|
@@ -62,8 +65,10 @@ async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promis
|
|
|
62
65
|
if (current !== null && current.status !== "planning") return { objective };
|
|
63
66
|
if (current === null) {
|
|
64
67
|
if (!ctx.hasUI) return { objective };
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
if (loadConfig(ctx.cwd).goalPrompt) {
|
|
69
|
+
const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
|
|
70
|
+
if (!approved) return { objective };
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
|
|
69
74
|
const action = current === null ? "recorded" : "promoted";
|
|
@@ -99,65 +104,47 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
99
104
|
const planPath = (params as ExitPlanModeArgs).plan_path.trim();
|
|
100
105
|
|
|
101
106
|
if (!runtime.isEnabled()) {
|
|
102
|
-
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
+
return failedResult("Not in plan mode; there is no plan to approve.", {
|
|
108
|
+
approved: false,
|
|
109
|
+
plan: "",
|
|
110
|
+
planPath,
|
|
111
|
+
} satisfies ExitPlanModeDetails);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const file = await runtime.plans.read(ctx.cwd, planPath);
|
|
110
115
|
if (!file) {
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
text: `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
|
|
119
|
-
isError: true,
|
|
120
|
-
};
|
|
116
|
+
return failedResult(
|
|
117
|
+
`No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
|
|
118
|
+
{ approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
|
|
119
|
+
);
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
const total = planStepSummary(file.content).total;
|
|
124
123
|
if (total > MAX_STEPS) {
|
|
125
124
|
// A hand-edited or older plan file can still be over the cap; refuse it here
|
|
126
125
|
// too, so approval never seeds a list it cannot hold in full.
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
type: "text",
|
|
131
|
-
text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
details: {
|
|
126
|
+
return failedResult(
|
|
127
|
+
`${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
|
|
128
|
+
{
|
|
135
129
|
approved: false,
|
|
136
130
|
plan: file.content,
|
|
137
131
|
planPath: file.path,
|
|
138
132
|
relativePath: file.relativePath,
|
|
139
133
|
} satisfies ExitPlanModeDetails,
|
|
140
|
-
|
|
141
|
-
};
|
|
134
|
+
);
|
|
142
135
|
}
|
|
143
136
|
|
|
144
137
|
if (!ctx.hasUI) {
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
type: "text",
|
|
149
|
-
text: `No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
details: {
|
|
138
|
+
return failedResult(
|
|
139
|
+
`No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
|
|
140
|
+
{
|
|
153
141
|
approved: false,
|
|
154
142
|
plan: file.content,
|
|
155
143
|
planPath: file.path,
|
|
156
144
|
relativePath: file.relativePath,
|
|
157
145
|
unavailable: true,
|
|
158
146
|
} satisfies ExitPlanModeDetails,
|
|
159
|
-
|
|
160
|
-
};
|
|
147
|
+
);
|
|
161
148
|
}
|
|
162
149
|
|
|
163
150
|
// One decision, however the session is attached; `review.ts` owns the loop,
|
|
@@ -208,6 +195,28 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
208
195
|
} satisfies ExitPlanModeDetails,
|
|
209
196
|
};
|
|
210
197
|
}
|
|
198
|
+
if (decision.action === "cancel") {
|
|
199
|
+
runtime.disable(ctx);
|
|
200
|
+
const shown = decision.plan.relativePath;
|
|
201
|
+
ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text:
|
|
207
|
+
"Plan mode is off and write access is restored. The plan was not approved and no steps were recorded. " +
|
|
208
|
+
`The plan file ${shown} remains if the user wants to review it later.`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
details: {
|
|
212
|
+
approved: false,
|
|
213
|
+
cancelled: true,
|
|
214
|
+
plan: decision.plan.content,
|
|
215
|
+
planPath: decision.plan.path,
|
|
216
|
+
relativePath: decision.plan.relativePath,
|
|
217
|
+
} satisfies ExitPlanModeDetails,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
211
220
|
|
|
212
221
|
runtime.disable(ctx);
|
|
213
222
|
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
|
|
@@ -269,11 +278,13 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
269
278
|
const details = outcome.details;
|
|
270
279
|
const heading = details.unavailable
|
|
271
280
|
? paint(theme, "warn", "No UI to approve the plan")
|
|
272
|
-
: details.
|
|
273
|
-
? paint(theme, "
|
|
274
|
-
: details.
|
|
275
|
-
? paint(theme, "
|
|
276
|
-
:
|
|
281
|
+
: details.cancelled
|
|
282
|
+
? paint(theme, "warn", "Plan mode off — plan not approved")
|
|
283
|
+
: details.approved
|
|
284
|
+
? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
|
|
285
|
+
: details.refined
|
|
286
|
+
? paint(theme, "accent", "Refining the plan")
|
|
287
|
+
: paint(theme, "warn", "Plan not approved");
|
|
277
288
|
const extra =
|
|
278
289
|
details.refined && details.refinement
|
|
279
290
|
? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
|
|
@@ -13,6 +13,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
|
13
13
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
14
14
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
15
15
|
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
16
|
+
import { failedResult, textResult } from "../../../lib/tool-result.ts";
|
|
16
17
|
import { describeUpdates, stepLabel } from "../format.ts";
|
|
17
18
|
import { SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
18
19
|
import { extractPlanSteps } from "../steps.ts";
|
|
@@ -66,39 +67,26 @@ export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime)
|
|
|
66
67
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
67
68
|
const args = params as SetPlanStepsArgs;
|
|
68
69
|
if (runtime.isEnabled()) {
|
|
69
|
-
return
|
|
70
|
-
content: [{ type: "text", text: "Not while planning; refine the plan with write_plan instead." }],
|
|
71
|
-
details: undefined,
|
|
72
|
-
isError: true,
|
|
73
|
-
};
|
|
70
|
+
return failedResult("Not while planning; refine the plan with write_plan instead.", undefined);
|
|
74
71
|
}
|
|
75
72
|
try {
|
|
76
73
|
const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
|
|
77
74
|
const steps = extractPlanSteps(plan.content);
|
|
78
75
|
const done = steps.filter((step) => step.status === "completed").length;
|
|
79
76
|
const todoNote = await mirrorTodos(ctx, args.updates);
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
type: "text",
|
|
84
|
-
text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
details: {
|
|
77
|
+
return textResult(
|
|
78
|
+
`${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
|
|
79
|
+
{
|
|
88
80
|
path: plan.path,
|
|
89
81
|
relativePath: plan.relativePath,
|
|
90
82
|
updates: args.updates,
|
|
91
83
|
done,
|
|
92
84
|
total: steps.length,
|
|
93
85
|
} satisfies SetPlanStepsDetails,
|
|
94
|
-
|
|
86
|
+
);
|
|
95
87
|
} catch (error) {
|
|
96
88
|
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
-
return {
|
|
98
|
-
content: [{ type: "text", text: `Could not update the plan: ${message}` }],
|
|
99
|
-
details: undefined,
|
|
100
|
-
isError: true,
|
|
101
|
-
};
|
|
89
|
+
return failedResult(`Could not update the plan: ${message}`, undefined);
|
|
102
90
|
}
|
|
103
91
|
},
|
|
104
92
|
|
|
@@ -12,6 +12,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
|
12
12
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
13
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
14
14
|
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
15
|
+
import { failedResult, textResult } from "../../../lib/tool-result.ts";
|
|
15
16
|
import { overCap, stepElisionNote } from "../format.ts";
|
|
16
17
|
import { WRITE_PLAN_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
17
18
|
import { MAX_STEPS, PLAN_OUTLINE, planStepSummary } from "../steps.ts";
|
|
@@ -42,11 +43,22 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
42
43
|
"Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
|
|
43
44
|
"repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
|
|
44
45
|
`${PLAN_OUTLINE}\n\n` +
|
|
46
|
+
"Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
|
|
47
|
+
"sentences, give each step one verifiable outcome with the detail in a nested note, and put the sketch, files, " +
|
|
48
|
+
"and commands in inline code or a fence rather than pasted prose. " +
|
|
49
|
+
"When the change alters a surface that already exists, Visual shows it before and after in one fence each; " +
|
|
50
|
+
"tag a fence only when every line in it is code. " +
|
|
51
|
+
"The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
|
|
52
|
+
"Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
|
|
53
|
+
"anywhere becomes the steps), and Verification becomes the last todo item. " +
|
|
54
|
+
"In Steps, each top-level bullet is one outcome in the order you will do them, and deeper bullets are notes. " +
|
|
45
55
|
"Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
|
|
46
56
|
`A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
|
|
47
57
|
promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
|
|
48
58
|
promptGuidelines: [
|
|
49
59
|
"While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
|
|
60
|
+
"When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
|
|
61
|
+
"so a rename reads as an edit.",
|
|
50
62
|
],
|
|
51
63
|
parameters: WritePlanParams,
|
|
52
64
|
defaultActive: false,
|
|
@@ -57,27 +69,17 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
57
69
|
|
|
58
70
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
59
71
|
if (!runtime.isEnabled()) {
|
|
60
|
-
return
|
|
61
|
-
content: [{ type: "text", text: "Not in plan mode; call enter_plan_mode first." }],
|
|
62
|
-
details: undefined,
|
|
63
|
-
isError: true,
|
|
64
|
-
};
|
|
72
|
+
return failedResult("Not in plan mode; call enter_plan_mode first.", undefined);
|
|
65
73
|
}
|
|
66
74
|
const args = params as WritePlanArgs;
|
|
67
75
|
const summary = planStepSummary(args.content);
|
|
68
76
|
if (summary.total > MAX_STEPS) {
|
|
69
77
|
// Refuse before writing: a note after the fact arrives once the file exists and
|
|
70
78
|
// the review has listed every step, so the overflow is already lost.
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
details: undefined,
|
|
79
|
-
isError: true,
|
|
80
|
-
};
|
|
79
|
+
return failedResult(
|
|
80
|
+
`${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
|
|
81
|
+
undefined,
|
|
82
|
+
);
|
|
81
83
|
}
|
|
82
84
|
try {
|
|
83
85
|
const file = await runtime.plans.write(ctx.cwd, {
|
|
@@ -86,22 +88,13 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
86
88
|
planPath: args.plan_path,
|
|
87
89
|
});
|
|
88
90
|
runtime.setLastPlan(ctx, file.path);
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
details: { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
97
|
-
};
|
|
91
|
+
return textResult(
|
|
92
|
+
`Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
93
|
+
{ path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
94
|
+
);
|
|
98
95
|
} catch (error) {
|
|
99
96
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
return {
|
|
101
|
-
content: [{ type: "text", text: `Could not write the plan: ${message}` }],
|
|
102
|
-
details: undefined,
|
|
103
|
-
isError: true,
|
|
104
|
-
};
|
|
97
|
+
return failedResult(`Could not write the plan: ${message}`, undefined);
|
|
105
98
|
}
|
|
106
99
|
},
|
|
107
100
|
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `enter_plan_mode` lets the model ask to plan before touching code; it needs
|
|
5
5
|
* user confirmation. `write_plan` saves the plan artifact under `.pi/plans` and
|
|
6
|
-
* returns its path. `
|
|
7
|
-
*
|
|
6
|
+
* returns its path. `cancel_plan_mode` leaves planning without approving, so a
|
|
7
|
+
* phone user is never trapped in read-only mode. `exit_plan_mode` reads that file
|
|
8
|
+
* back, presents it for approval (a scrollable review screen in the TUI, a select dialog elsewhere),
|
|
8
9
|
* and on approval seeds the `todo` tool with the plan's steps (through
|
|
9
10
|
* `ctx.executeTool`, so the todo widget and validation run normally).
|
|
10
11
|
* `set_plan_steps` then keeps the plan current: after approval the model ticks
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
|
|
23
24
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
25
|
import type { PlanRuntime } from "./runtime.ts";
|
|
26
|
+
import { registerCancelTool } from "./tools/cancel.ts";
|
|
25
27
|
import { registerEnterTool } from "./tools/enter.ts";
|
|
26
28
|
import { registerExitTool } from "./tools/exit.ts";
|
|
27
29
|
import { registerSetPlanStepsTool } from "./tools/steps.ts";
|
|
@@ -34,6 +36,7 @@ export function registerTools(
|
|
|
34
36
|
web: PlanWebReview = createPlanWebReview(runtime.plans),
|
|
35
37
|
): void {
|
|
36
38
|
registerEnterTool(pi, runtime);
|
|
39
|
+
registerCancelTool(pi, runtime);
|
|
37
40
|
registerWriteTool(pi, runtime);
|
|
38
41
|
registerExitTool(pi, runtime, web);
|
|
39
42
|
registerSetPlanStepsTool(pi, runtime);
|
package/extensions/plan/tui.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
import { isCloseKey } from "../../lib/tui/cursor.ts";
|
|
38
38
|
import { gaugeBar } from "../../lib/tui/gauge.ts";
|
|
39
39
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
40
|
-
import { titleRule } from "../../lib/tui/rules.ts";
|
|
40
|
+
import { plainRule, titleRule } from "../../lib/tui/rules.ts";
|
|
41
41
|
import { emptyState, checkbox } from "../../lib/tui/rows.ts";
|
|
42
42
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
43
43
|
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
@@ -45,7 +45,7 @@ import { changedSections, sanitizePlanText, type SectionDigest } from "./plan-te
|
|
|
45
45
|
import { extractPlanSections } from "./steps.ts";
|
|
46
46
|
|
|
47
47
|
/** What the user chose from the read/review screen. */
|
|
48
|
-
export type PlanViewAction = "approve" | "refine" | "keep" | "browser";
|
|
48
|
+
export type PlanViewAction = "approve" | "refine" | "keep" | "browser" | "cancel";
|
|
49
49
|
|
|
50
50
|
/** A plan section's rendered start line, for navigation and the current-section label. */
|
|
51
51
|
interface SectionOffset {
|
|
@@ -87,7 +87,7 @@ export interface PlanViewOptions {
|
|
|
87
87
|
|
|
88
88
|
/** Rows the screen shows when the terminal height is unknown. */
|
|
89
89
|
const DEFAULT_ROWS = 20;
|
|
90
|
-
/** Header, blanks, footer, and
|
|
90
|
+
/** Header, blanks, footer, and closing rule around a non-empty plan. */
|
|
91
91
|
const CHROME_ROWS = 5;
|
|
92
92
|
/** Narrowest width where Markdown's 2-column padding still fits the body. */
|
|
93
93
|
const MIN_PADDED_WIDTH = 6;
|
|
@@ -97,13 +97,14 @@ const BODY_PADDING = 2;
|
|
|
97
97
|
const GAUGE_SEGMENTS = 10;
|
|
98
98
|
/** Footer hints shared by the review and the inline editor's primary actions. */
|
|
99
99
|
const PRIMARY_HINTS: ScreenHint[] = [
|
|
100
|
-
"Esc keep
|
|
101
|
-
{ text: "a approve
|
|
100
|
+
"Esc keep plan",
|
|
101
|
+
{ text: "a approve", color: "success" },
|
|
102
102
|
{ text: "r refine", color: "accent" },
|
|
103
103
|
{ text: "w browser", color: "accent" },
|
|
104
|
+
{ text: "x exit plan", color: "warning" },
|
|
104
105
|
];
|
|
105
106
|
/** Scroll keys appended to the review footer while the plan overflows. */
|
|
106
|
-
const SCROLL_HINTS: ScreenHint[] = ["
|
|
107
|
+
const SCROLL_HINTS: ScreenHint[] = ["j/k scroll", "space page", "g/G ends"];
|
|
107
108
|
/** Section-jump hint, shown when the plan has more than one heading section. */
|
|
108
109
|
const SECTION_HINTS: ScreenHint[] = ["n/p section"];
|
|
109
110
|
|
|
@@ -332,6 +333,7 @@ export class PlanViewComponent implements Component {
|
|
|
332
333
|
else if (data === "a") this.options.onClose("approve");
|
|
333
334
|
else if (data === "r") this.startRefine();
|
|
334
335
|
else if (data === "w") this.options.onClose("browser");
|
|
336
|
+
else if (data === "x") this.options.onClose("cancel");
|
|
335
337
|
}
|
|
336
338
|
|
|
337
339
|
/** Wheel scrolling in fullscreen; regular mode leaves the wheel to the terminal. */
|
|
@@ -403,7 +405,7 @@ export class PlanViewComponent implements Component {
|
|
|
403
405
|
this.maxOffset > 0 ? [...PRIMARY_HINTS, ...SCROLL_HINTS, ...sectionHints] : PRIMARY_HINTS;
|
|
404
406
|
lines.push(screenHint(theme, w, hints));
|
|
405
407
|
}
|
|
406
|
-
lines.push(
|
|
408
|
+
lines.push(plainRule(theme, w));
|
|
407
409
|
return lines;
|
|
408
410
|
}
|
|
409
411
|
}
|
package/extensions/plan/types.ts
CHANGED
|
@@ -74,6 +74,14 @@ export interface ExitPlanModeDetails {
|
|
|
74
74
|
refinement?: string;
|
|
75
75
|
/** Comments the review carried when it approved or requested a refinement. */
|
|
76
76
|
annotations?: PlanAnnotation[];
|
|
77
|
+
/** True when the user left plan mode without approving the plan. */
|
|
78
|
+
cancelled?: boolean;
|
|
77
79
|
/** True when no interactive UI was available to ask for approval. */
|
|
78
80
|
unavailable?: boolean;
|
|
79
81
|
}
|
|
82
|
+
|
|
83
|
+
/** Structured result carried in `cancel_plan_mode`'s `details`. */
|
|
84
|
+
export interface CancelPlanModeDetails {
|
|
85
|
+
/** True when plan mode was on and has now been turned off. */
|
|
86
|
+
cancelled: boolean;
|
|
87
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's chrome: what the page says about itself.
|
|
3
|
+
*
|
|
4
|
+
* It is a fragment of the same IIFE `web-client.ts` ships (`WEB_CHROME_JS` is
|
|
5
|
+
* interpolated into `REVIEW_CLIENT_JS`), kept in its own module so the state
|
|
6
|
+
* chip, the status line, and the header are read together and away from the
|
|
7
|
+
* document rendering. It paints only text it was handed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
|
|
11
|
+
if (!statusEl) return;
|
|
12
|
+
statusEl.textContent = text || "";
|
|
13
|
+
statusEl.title = text || "";
|
|
14
|
+
statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The always-visible header chip: connection, comment count, and whether
|
|
18
|
+
// there are edits the file does not have yet.
|
|
19
|
+
function updateState() {
|
|
20
|
+
if (!stateEl) return;
|
|
21
|
+
var label;
|
|
22
|
+
var tone = "";
|
|
23
|
+
if (approved) {
|
|
24
|
+
label = "Approved";
|
|
25
|
+
tone = "success";
|
|
26
|
+
} else if (saved) {
|
|
27
|
+
label = "Saved";
|
|
28
|
+
tone = "success";
|
|
29
|
+
} else if (closed) {
|
|
30
|
+
// Settled by the user, not by a dropped stream: never flash Offline.
|
|
31
|
+
label = "Closed";
|
|
32
|
+
} else if (connected === true) {
|
|
33
|
+
label = "Connected";
|
|
34
|
+
tone = "success";
|
|
35
|
+
} else if (everConnected) {
|
|
36
|
+
// Only after the stream has opened once: an error before that is a
|
|
37
|
+
// transient initial connect, so keep showing Connecting rather than Offline.
|
|
38
|
+
label = "Offline";
|
|
39
|
+
tone = "danger";
|
|
40
|
+
} else {
|
|
41
|
+
label = "Connecting\u2026";
|
|
42
|
+
}
|
|
43
|
+
var count = annotations.length;
|
|
44
|
+
label += " \u00b7 " + count + (count === 1 ? " comment" : " comments");
|
|
45
|
+
if (dirty && !saved && !approved && !closed) {
|
|
46
|
+
label += " \u00b7 unsaved";
|
|
47
|
+
if (!tone) tone = "warn";
|
|
48
|
+
}
|
|
49
|
+
stateEl.textContent = label;
|
|
50
|
+
stateEl.className = "bf-chip bf-chip--accent review-state" + (tone ? " bf-chip--" + tone : "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderHeader() {
|
|
54
|
+
var titleEl = document.getElementById("plan-title");
|
|
55
|
+
if (titleEl && data.title) {
|
|
56
|
+
titleEl.textContent = data.title;
|
|
57
|
+
titleEl.title = data.title;
|
|
58
|
+
}
|
|
59
|
+
if (data.title) document.title = "Plan review \u00b7 " + data.title;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateProgress() {
|
|
63
|
+
if (!progressEl) return;
|
|
64
|
+
var doc = document.scrollingElement || document.documentElement;
|
|
65
|
+
var top = doc ? doc.scrollTop : 0;
|
|
66
|
+
var height = doc ? doc.scrollHeight : 0;
|
|
67
|
+
var view = doc ? doc.clientHeight : 0;
|
|
68
|
+
if (planEl && planEl.scrollHeight > planEl.clientHeight + 4) {
|
|
69
|
+
top = planEl.scrollTop; height = planEl.scrollHeight; view = planEl.clientHeight;
|
|
70
|
+
}
|
|
71
|
+
var max = height - view;
|
|
72
|
+
var percent = max > 0 ? Math.min(100, Math.max(0, (top / max) * 100)) : 0;
|
|
73
|
+
progressEl.style.width = percent + "%";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function lockActions() {
|
|
77
|
+
if (approveButton) approveButton.disabled = true;
|
|
78
|
+
if (saveButton) saveButton.disabled = true;
|
|
79
|
+
if (cancelButton) cancelButton.disabled = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A conflict means the file moved under the page; only a reload can re-anchor.
|
|
83
|
+
function offerReload() {
|
|
84
|
+
if (document.getElementById("reload")) return;
|
|
85
|
+
var controls = document.querySelector(".review-controls");
|
|
86
|
+
if (!controls) return;
|
|
87
|
+
var reload = el("button");
|
|
88
|
+
reload.type = "button";
|
|
89
|
+
reload.id = "reload";
|
|
90
|
+
reload.className = "bf-btn bf-btn--primary";
|
|
91
|
+
reload.textContent = "Reload";
|
|
92
|
+
reload.addEventListener("click", function () { window.location.reload(); });
|
|
93
|
+
controls.appendChild(reload);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The stream exists only to notice a closed tab. Once the review is settled it
|
|
97
|
+
// has no job, and leaving it open retries a dead port until the tab closes.
|
|
98
|
+
function settleStream() {
|
|
99
|
+
if (stream && stream.close) { stream.close(); stream = null; }
|
|
100
|
+
}
|
|
101
|
+
`;
|