@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.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-long-subject.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-running.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-rejected.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/plan/tools.ts
CHANGED
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
|
|
18
18
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
19
19
|
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { basename } from "node:path";
|
|
21
20
|
import { Type, type Static } from "typebox";
|
|
22
21
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
23
|
-
import {
|
|
24
|
-
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
25
|
-
import {
|
|
22
|
+
import { lastGoal } from "../../lib/goal-state.ts";
|
|
23
|
+
import { GOAL_TOOL, TODO_TOOL } from "../../lib/tool-names.ts";
|
|
24
|
+
import { lastTodos } from "../../lib/todo-state.ts";
|
|
25
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
26
26
|
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
27
27
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
28
28
|
import { expandHint, GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
29
29
|
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
30
|
-
import { feedbackSummary
|
|
30
|
+
import { feedbackSummary } from "./feedback.ts";
|
|
31
31
|
import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
|
|
32
32
|
import {
|
|
33
33
|
MAX_STEP_LENGTH,
|
|
@@ -35,14 +35,17 @@ import {
|
|
|
35
35
|
PLAN_OUTLINE,
|
|
36
36
|
extractPlanSteps,
|
|
37
37
|
planStepSummary,
|
|
38
|
+
verificationItem,
|
|
38
39
|
type PlanStep,
|
|
39
|
-
type PlanStepSummary,
|
|
40
40
|
type StepUpdate,
|
|
41
41
|
} from "./steps.ts";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
42
|
+
import { basename } from "node:path";
|
|
43
|
+
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
44
|
+
import { DEFAULT_TODO_MAX, mergeTodos, type TodoMerge } from "./todos.ts";
|
|
45
|
+
import { reviewPlan } from "./review.ts";
|
|
46
|
+
import { sanitizePlanText } from "./plan-text.ts";
|
|
44
47
|
import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
|
|
45
|
-
import { createPlanWebReview, type
|
|
48
|
+
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
46
49
|
|
|
47
50
|
const ExitPlanModeParams = Type.Object({
|
|
48
51
|
plan_path: Type.String({
|
|
@@ -116,20 +119,21 @@ function preview(plan: string): { body: string; more: number } {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
|
-
* One sentence
|
|
120
|
-
*
|
|
122
|
+
* One sentence when a step is longer than the display cap. The text stays in the
|
|
123
|
+
* plan file either way; only the todo list and the approval listing shorten it.
|
|
121
124
|
*/
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
function stepElisionNote(elided: number): string {
|
|
126
|
+
if (elided === 0) return "";
|
|
127
|
+
const plural = elided === 1 ? " is" : "s are";
|
|
128
|
+
return ` Note: ${elided} step${plural} longer than ${MAX_STEP_LENGTH} characters and will be shortened in the todo list.`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The diagnosis both cap refusals share: a step list longer than approval can
|
|
133
|
+
* track. Each caller appends the fix that is available from where it stands.
|
|
134
|
+
*/
|
|
135
|
+
function overCap(total: number): string {
|
|
136
|
+
return `The plan has ${total} top-level steps but approval tracks at most ${MAX_STEPS}.`;
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/** "Step 2" or "Steps 1, 2" for a set of step numbers. */
|
|
@@ -171,11 +175,12 @@ async function seedTodos(
|
|
|
171
175
|
plan: string,
|
|
172
176
|
): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
|
|
173
177
|
const steps = extractPlanSteps(plan);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
178
|
+
const verification = verificationItem(plan);
|
|
179
|
+
if (steps.length === 0 && !verification) return { steps };
|
|
180
|
+
const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
|
|
181
|
+
// Nothing new to write: every step (and the verification item) is already in
|
|
182
|
+
// the list, or the list is full, so a rewrite would only churn the widget.
|
|
183
|
+
if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
|
|
179
184
|
const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
|
|
180
185
|
if (!outcome.isError) return { steps, merged };
|
|
181
186
|
// The nested call fails both when the tool is unavailable and when it rejects
|
|
@@ -185,6 +190,56 @@ async function seedTodos(
|
|
|
185
190
|
return { steps, merged, error: firstLine ? sanitizeLine(firstLine) : undefined };
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
/** What the goal offer did: `recorded` undefined when there was nothing to offer. */
|
|
194
|
+
interface GoalOffer {
|
|
195
|
+
objective: string;
|
|
196
|
+
recorded?: boolean;
|
|
197
|
+
error?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Offer to record the approved plan as the session goal — only when the session
|
|
202
|
+
* has no goal at all (an active or achieved one is the user's, never replaced) and
|
|
203
|
+
* the session can ask. The objective is the plan's own `# title`, or the file-name
|
|
204
|
+
* slug; recording runs through the goal tool, so validation and the widget stay
|
|
205
|
+
* goal's, and no model turn is spent.
|
|
206
|
+
*/
|
|
207
|
+
async function offerSessionGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOffer> {
|
|
208
|
+
const objective = planObjective(plan);
|
|
209
|
+
if (!ctx.hasUI) return { objective };
|
|
210
|
+
// The branch already carries a goal: leave it alone, and say nothing.
|
|
211
|
+
if (lastGoal(ctx.sessionManager.getBranch()) !== null) return { objective };
|
|
212
|
+
const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
|
|
213
|
+
if (!approved) return { objective };
|
|
214
|
+
const outcome = await ctx.executeTool(GOAL_TOOL, { objective });
|
|
215
|
+
if (!outcome.isError) return { objective, recorded: true };
|
|
216
|
+
const part = outcome.result?.content?.find((entry) => entry.type === "text");
|
|
217
|
+
const firstLine = part?.type === "text" ? part.text.split("\n", 1)[0] : undefined;
|
|
218
|
+
return { objective, recorded: false, error: firstLine ? sanitizeLine(firstLine) : undefined };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The plan's own title: its first `# heading`, else the file-name slug. */
|
|
222
|
+
function planObjective(plan: StoredPlan): string {
|
|
223
|
+
const heading = /^ {0,3}#\s+(.+)$/m.exec(plan.content)?.[1];
|
|
224
|
+
const title = heading === undefined ? "" : sanitizeLine(heading).trim();
|
|
225
|
+
return title || planTitle(basename(plan.path));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* One sentence about the plan's verification item, or "" when the plan has none
|
|
230
|
+
* (or it was already in the list). The item is a todo, not a plan step, so it is
|
|
231
|
+
* ticked in the list rather than in the file.
|
|
232
|
+
*/
|
|
233
|
+
function verificationNote(merged: TodoMerge | undefined): string {
|
|
234
|
+
if (merged?.trailing === "added") {
|
|
235
|
+
return "\nThe plan's verification section is the last todo item; tick it in the todo list, not in the file.";
|
|
236
|
+
}
|
|
237
|
+
if (merged?.trailing === "deferred") {
|
|
238
|
+
return "\nThe plan's verification section did not fit in the full todo list; keep it in mind.";
|
|
239
|
+
}
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
/** What the merge did, for the approval message; `steps` is the plan's step count. */
|
|
189
244
|
function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
|
|
190
245
|
const total = steps.length;
|
|
@@ -204,103 +259,6 @@ function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
|
|
|
204
259
|
return ` Merged ${counted} into the todo list${suffix}. ${tick}`;
|
|
205
260
|
}
|
|
206
261
|
|
|
207
|
-
/** The review choice plus the refinement the user typed in the TUI, if any. */
|
|
208
|
-
interface PlanReviewOutcome {
|
|
209
|
-
action: PlanViewAction;
|
|
210
|
-
refinement?: string;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** A dialog-only choice: post the full plan text, then ask again. Never returned. */
|
|
214
|
-
type ReviewChoice = PlanReviewOutcome | { action: "send" };
|
|
215
|
-
|
|
216
|
-
/** Prompt shown by the dialog-based refine editor in non-TUI modes. */
|
|
217
|
-
const REFINE_PROMPT = "Refine the plan — what should change? (Enter to save, Esc to cancel)";
|
|
218
|
-
|
|
219
|
-
/** How much of one step the remote digest shows. */
|
|
220
|
-
const DIGEST_STEP_CHARS = 90;
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* A one-message digest of the plan for a remote review.
|
|
224
|
-
*
|
|
225
|
-
* The file is the primary artifact, but a phone client may never render or open
|
|
226
|
-
* it, and the peer still has to choose approve / refine. The digest carries the
|
|
227
|
-
* title, the step count, and the first few steps; the full text follows as a
|
|
228
|
-
* file or as the fallback message.
|
|
229
|
-
*/
|
|
230
|
-
function planReviewDigest(content: string): string {
|
|
231
|
-
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
|
|
232
|
-
const steps = extractPlanSteps(content);
|
|
233
|
-
const count = steps.length;
|
|
234
|
-
const head = `Plan${title ? `: ${title}` : ""}${count > 0 ? ` (${count} step${count === 1 ? "" : "s"})` : ""}`;
|
|
235
|
-
const preview = steps.slice(0, 3).map((step, index) => {
|
|
236
|
-
const text = step.content.replace(/\s+/g, " ").trim();
|
|
237
|
-
const shown = text.length > DIGEST_STEP_CHARS ? `${text.slice(0, DIGEST_STEP_CHARS - 1)}…` : text;
|
|
238
|
-
return `${index + 1}. ${shown}`;
|
|
239
|
-
});
|
|
240
|
-
const more = count > preview.length ? `… (${count - preview.length} more)` : "";
|
|
241
|
-
return [head, ...preview, more, "The full plan is attached above, or sent as text if the file failed."]
|
|
242
|
-
.filter((line) => line !== "")
|
|
243
|
-
.join("\n");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Ask the user to review the plan file. The TUI opens the scrollable review
|
|
248
|
-
* screen (with an inline refine editor); dialog-capable modes (RPC) fall back
|
|
249
|
-
* to a select plus the refine editor; the caller has already refused when there
|
|
250
|
-
* is no UI.
|
|
251
|
-
*/
|
|
252
|
-
async function reviewPlan(
|
|
253
|
-
ctx: ExtensionToolContext,
|
|
254
|
-
plan: { path: string; relativePath: string; content: string; bytes: number },
|
|
255
|
-
previous?: string,
|
|
256
|
-
): Promise<PlanReviewOutcome> {
|
|
257
|
-
// The first pass sends the file (or the full text when it fails) and the
|
|
258
|
-
// digest; asking for the full plan re-enters with the text only, then shows
|
|
259
|
-
// the choices again.
|
|
260
|
-
let sentFullText = false;
|
|
261
|
-
for (;;) {
|
|
262
|
-
const outcome = await askHuman<ReviewChoice>(ctx, {
|
|
263
|
-
custom: async () => {
|
|
264
|
-
const outcome = await ctx.ui.custom<PlanReviewOutcome | undefined>(
|
|
265
|
-
(tui, theme, _keybindings, done) =>
|
|
266
|
-
new PlanViewComponent({
|
|
267
|
-
plan,
|
|
268
|
-
previous,
|
|
269
|
-
theme,
|
|
270
|
-
onClose: (action, refinement) => done({ action, refinement }),
|
|
271
|
-
requestRender: () => tui.requestRender(),
|
|
272
|
-
viewportRows: () => tui.terminal?.rows,
|
|
273
|
-
tui,
|
|
274
|
-
}),
|
|
275
|
-
FULL_SCREEN_OVERLAY,
|
|
276
|
-
);
|
|
277
|
-
return outcome ?? { action: "keep" };
|
|
278
|
-
},
|
|
279
|
-
dialogs: async () => {
|
|
280
|
-
const choice = await ctx.ui.select("Plan mode — what next?", [
|
|
281
|
-
"Approve and execute",
|
|
282
|
-
"Send the full plan",
|
|
283
|
-
"Refine the plan",
|
|
284
|
-
"Keep planning",
|
|
285
|
-
]);
|
|
286
|
-
if (choice === "Approve and execute") return { action: "approve" };
|
|
287
|
-
if (choice === "Send the full plan") return { action: "send" };
|
|
288
|
-
if (choice === "Refine the plan") return { action: "refine" };
|
|
289
|
-
return { action: "keep" };
|
|
290
|
-
},
|
|
291
|
-
// A remote turn gets the plan file up front, falling back to the full text;
|
|
292
|
-
// a local TUI turn ignores both and renders the component. The digest goes
|
|
293
|
-
// out either way so the choices stay answerable when a phone client does not
|
|
294
|
-
// render the file bubble.
|
|
295
|
-
file: sentFullText ? undefined : { path: plan.path, name: basename(plan.relativePath) },
|
|
296
|
-
body: plan.content,
|
|
297
|
-
note: sentFullText ? undefined : planReviewDigest(plan.content),
|
|
298
|
-
});
|
|
299
|
-
if (outcome.action !== "send") return outcome;
|
|
300
|
-
sentFullText = true;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
262
|
export function registerTools(
|
|
305
263
|
pi: ExtensionAPI,
|
|
306
264
|
runtime: PlanRuntime,
|
|
@@ -320,7 +278,7 @@ export function registerTools(
|
|
|
320
278
|
executionMode: "sequential",
|
|
321
279
|
// readOnlyHint stays true: entering plan mode changes only the session's
|
|
322
280
|
// tool set, never files or system state, so it needs no approval.
|
|
323
|
-
annotations: { readOnlyHint: true,
|
|
281
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
324
282
|
|
|
325
283
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
326
284
|
if (runtime.isEnabled()) {
|
|
@@ -384,7 +342,8 @@ export function registerTools(
|
|
|
384
342
|
"Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
|
|
385
343
|
"repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
|
|
386
344
|
`${PLAN_OUTLINE}\n\n` +
|
|
387
|
-
"Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining."
|
|
345
|
+
"Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
|
|
346
|
+
`A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
|
|
388
347
|
promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
|
|
389
348
|
promptGuidelines: [
|
|
390
349
|
"While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
|
|
@@ -405,6 +364,21 @@ export function registerTools(
|
|
|
405
364
|
};
|
|
406
365
|
}
|
|
407
366
|
const args = params as WritePlanArgs;
|
|
367
|
+
const summary = planStepSummary(args.content);
|
|
368
|
+
if (summary.total > MAX_STEPS) {
|
|
369
|
+
// Refuse before writing: a note after the fact arrives once the file exists and
|
|
370
|
+
// the review has listed every step, so the overflow is already lost.
|
|
371
|
+
return {
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
details: undefined,
|
|
379
|
+
isError: true,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
408
382
|
try {
|
|
409
383
|
const file = await runtime.plans.write(ctx.cwd, {
|
|
410
384
|
title: args.title,
|
|
@@ -416,7 +390,7 @@ export function registerTools(
|
|
|
416
390
|
content: [
|
|
417
391
|
{
|
|
418
392
|
type: "text",
|
|
419
|
-
text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${
|
|
393
|
+
text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
420
394
|
},
|
|
421
395
|
],
|
|
422
396
|
details: { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
@@ -464,9 +438,9 @@ export function registerTools(
|
|
|
464
438
|
executionMode: "sequential",
|
|
465
439
|
// readOnlyHint stays true: it writes no files, and the review dialog inside
|
|
466
440
|
// execute is the human gate, so an outer prompt would only double-ask.
|
|
467
|
-
annotations: { readOnlyHint: true,
|
|
441
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
468
442
|
|
|
469
|
-
async execute(_toolCallId, params,
|
|
443
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
470
444
|
const planPath = (params as ExitPlanModeArgs).plan_path.trim();
|
|
471
445
|
|
|
472
446
|
if (!runtime.isEnabled()) {
|
|
@@ -477,7 +451,7 @@ export function registerTools(
|
|
|
477
451
|
};
|
|
478
452
|
}
|
|
479
453
|
|
|
480
|
-
|
|
454
|
+
const file = await runtime.plans.read(ctx.cwd, planPath);
|
|
481
455
|
if (!file) {
|
|
482
456
|
return {
|
|
483
457
|
content: [
|
|
@@ -491,6 +465,27 @@ export function registerTools(
|
|
|
491
465
|
};
|
|
492
466
|
}
|
|
493
467
|
|
|
468
|
+
const total = planStepSummary(file.content).total;
|
|
469
|
+
if (total > MAX_STEPS) {
|
|
470
|
+
// A hand-edited or older plan file can still be over the cap; refuse it here
|
|
471
|
+
// too, so approval never seeds a list it cannot hold in full.
|
|
472
|
+
return {
|
|
473
|
+
content: [
|
|
474
|
+
{
|
|
475
|
+
type: "text",
|
|
476
|
+
text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
details: {
|
|
480
|
+
approved: false,
|
|
481
|
+
plan: file.content,
|
|
482
|
+
planPath: file.path,
|
|
483
|
+
relativePath: file.relativePath,
|
|
484
|
+
} satisfies ExitPlanModeDetails,
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
494
489
|
if (!ctx.hasUI) {
|
|
495
490
|
return {
|
|
496
491
|
content: [
|
|
@@ -510,117 +505,58 @@ export function registerTools(
|
|
|
510
505
|
};
|
|
511
506
|
}
|
|
512
507
|
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
runtime
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
content: [
|
|
561
|
-
{
|
|
562
|
-
type: "text",
|
|
563
|
-
text: `The user reviewed the plan in the browser and left feedback:\n${refinement}\nStay in plan mode, revise the plan (write_plan, reusing plan_path), then call exit_plan_mode again.`,
|
|
564
|
-
},
|
|
565
|
-
],
|
|
566
|
-
details: {
|
|
567
|
-
approved: false,
|
|
568
|
-
plan: result.plan.content,
|
|
569
|
-
planPath: result.plan.path,
|
|
570
|
-
relativePath: result.plan.relativePath,
|
|
571
|
-
refined: true,
|
|
572
|
-
refinement,
|
|
573
|
-
} satisfies ExitPlanModeDetails,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
// Cancelled or the tab closed: re-read the file and review again.
|
|
577
|
-
file = (await runtime.plans.read(ctx.cwd, file.path)) ?? file;
|
|
578
|
-
outcome = await present(file);
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
if (outcome.action === "refine" && !outcome.refinement) {
|
|
582
|
-
const refinement = (await ctx.ui.editor(REFINE_PROMPT, ""))?.trim();
|
|
583
|
-
if (refinement) outcome = { action: "refine", refinement };
|
|
584
|
-
else outcome = await present(file);
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
if (outcome.action === "refine") {
|
|
588
|
-
const refinement = outcome.refinement ?? "";
|
|
589
|
-
return {
|
|
590
|
-
content: [
|
|
591
|
-
{
|
|
592
|
-
type: "text",
|
|
593
|
-
text: `The user asked to refine the plan: ${refinement}\nStay in plan mode, revise the plan (write_plan, reusing plan_path), then call exit_plan_mode again.`,
|
|
594
|
-
},
|
|
595
|
-
],
|
|
596
|
-
details: {
|
|
597
|
-
approved: false,
|
|
598
|
-
plan: file.content,
|
|
599
|
-
planPath: file.path,
|
|
600
|
-
relativePath: file.relativePath,
|
|
601
|
-
refined: true,
|
|
602
|
-
refinement,
|
|
603
|
-
} satisfies ExitPlanModeDetails,
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
if (outcome.action !== "approve") {
|
|
607
|
-
return {
|
|
608
|
-
content: [
|
|
609
|
-
{ type: "text", text: "Plan not approved. Stay in plan mode, ask what to change, and revise the plan." },
|
|
610
|
-
],
|
|
611
|
-
details: {
|
|
612
|
-
approved: false,
|
|
613
|
-
plan: file.content,
|
|
614
|
-
planPath: file.path,
|
|
615
|
-
relativePath: file.relativePath,
|
|
616
|
-
} satisfies ExitPlanModeDetails,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
break;
|
|
508
|
+
// One decision, however the session is attached; `review.ts` owns the loop,
|
|
509
|
+
// the baseline, and closing the browser review when the turn is aborted.
|
|
510
|
+
const decision = await reviewPlan({
|
|
511
|
+
ctx,
|
|
512
|
+
web,
|
|
513
|
+
plans: runtime.plans,
|
|
514
|
+
baselines: runtime,
|
|
515
|
+
plan: file,
|
|
516
|
+
signal,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (decision.action === "refine") {
|
|
520
|
+
// A browser save already wrote its comments into the file; either way the
|
|
521
|
+
// model revises the same plan and presents it again.
|
|
522
|
+
const feedback = decision.fromBrowser
|
|
523
|
+
? `The user reviewed the plan in the browser and left feedback:\n${decision.refinement}`
|
|
524
|
+
: `The user asked to refine the plan: ${decision.refinement}`;
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: `${feedback}\nStay in plan mode, revise the plan (write_plan, reusing plan_path), then call exit_plan_mode again.`,
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
details: {
|
|
533
|
+
approved: false,
|
|
534
|
+
plan: decision.plan.content,
|
|
535
|
+
planPath: decision.plan.path,
|
|
536
|
+
relativePath: decision.plan.relativePath,
|
|
537
|
+
refined: true,
|
|
538
|
+
refinement: decision.refinement,
|
|
539
|
+
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
540
|
+
} satisfies ExitPlanModeDetails,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (decision.action === "keep") {
|
|
544
|
+
return {
|
|
545
|
+
content: [
|
|
546
|
+
{ type: "text", text: "Plan not approved. Stay in plan mode, ask what to change, and revise the plan." },
|
|
547
|
+
],
|
|
548
|
+
details: {
|
|
549
|
+
approved: false,
|
|
550
|
+
plan: decision.plan.content,
|
|
551
|
+
planPath: decision.plan.path,
|
|
552
|
+
relativePath: decision.plan.relativePath,
|
|
553
|
+
} satisfies ExitPlanModeDetails,
|
|
554
|
+
};
|
|
620
555
|
}
|
|
621
556
|
|
|
622
557
|
runtime.disable(ctx);
|
|
623
|
-
const { steps, merged, error } = await seedTodos(pi, ctx,
|
|
558
|
+
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
|
|
559
|
+
const goal = await offerSessionGoal(ctx, decision.plan);
|
|
624
560
|
const listing =
|
|
625
561
|
steps.length > 0
|
|
626
562
|
? `\n\n${steps
|
|
@@ -635,21 +571,29 @@ export function registerTools(
|
|
|
635
571
|
: merged
|
|
636
572
|
? mergeSummary(merged, steps)
|
|
637
573
|
: ` Could not read the todo list. Record these steps with the todo tool before you start, and tick them off with ${SET_PLAN_STEPS_TOOL}.`;
|
|
638
|
-
const
|
|
639
|
-
|
|
574
|
+
const verification = verificationNote(merged);
|
|
575
|
+
const goalNote =
|
|
576
|
+
goal.recorded === undefined
|
|
577
|
+
? ""
|
|
578
|
+
: goal.recorded
|
|
579
|
+
? ` Recorded the session goal: ${goal.objective}.`
|
|
580
|
+
: ` Could not record the session goal: ${goal.error ?? "unknown error"}. Use the goal tool to set it.`;
|
|
581
|
+
const browserNote = decision.comments.length
|
|
582
|
+
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
|
|
640
583
|
: "";
|
|
641
584
|
return {
|
|
642
585
|
content: [
|
|
643
586
|
{
|
|
644
587
|
type: "text",
|
|
645
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${
|
|
588
|
+
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
|
|
646
589
|
},
|
|
647
590
|
],
|
|
648
591
|
details: {
|
|
649
592
|
approved: true,
|
|
650
|
-
plan:
|
|
651
|
-
planPath:
|
|
652
|
-
relativePath:
|
|
593
|
+
plan: decision.plan.content,
|
|
594
|
+
planPath: decision.plan.path,
|
|
595
|
+
relativePath: decision.plan.relativePath,
|
|
596
|
+
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
653
597
|
} satisfies ExitPlanModeDetails,
|
|
654
598
|
};
|
|
655
599
|
},
|