@gavin-hu/my-pi-agent 0.14.0 → 0.15.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 +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
package/extensions/plan/steps.ts
CHANGED
|
@@ -281,15 +281,19 @@ export function planStepSummary(plan: string): PlanStepSummary {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/** One checkbox change: a 1-based step number and the state to set. */
|
|
284
|
-
export interface
|
|
284
|
+
export interface StepEdit {
|
|
285
285
|
step: number;
|
|
286
286
|
status: PlanStep["status"];
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A guarded checkbox change: the edit plus the step's text as it was listed at
|
|
291
|
+
* approval. The store refuses the batch when the step at that number no longer
|
|
292
|
+
* carries the text, so a reordered or rewritten plan is caught instead of
|
|
293
|
+
* ticking the wrong step.
|
|
294
|
+
*/
|
|
295
|
+
export interface StepUpdate extends StepEdit {
|
|
296
|
+
expected: string;
|
|
293
297
|
}
|
|
294
298
|
|
|
295
299
|
/** Whether the step text now at a number still matches what the caller expected. */
|
|
@@ -308,7 +312,7 @@ export function matchesStepText(actual: string, expected: string): boolean {
|
|
|
308
312
|
* otherwise left exactly as written. When a step appears twice, the last entry
|
|
309
313
|
* wins.
|
|
310
314
|
*/
|
|
311
|
-
export function setStepStatuses(plan: string, updates:
|
|
315
|
+
export function setStepStatuses(plan: string, updates: readonly StepEdit[]): string | undefined {
|
|
312
316
|
const items = topLevelItems(plan);
|
|
313
317
|
for (const update of updates) {
|
|
314
318
|
if (!Number.isInteger(update.step) || update.step < 1 || !items[update.step - 1]) return undefined;
|
|
@@ -364,3 +368,28 @@ export function extractPlanSections(plan: string): PlanSection[] {
|
|
|
364
368
|
flush();
|
|
365
369
|
return sections;
|
|
366
370
|
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The plan's own objective: the first paragraph of its `## Goal` section, with
|
|
374
|
+
* inline markdown and a leading list marker removed.
|
|
375
|
+
*
|
|
376
|
+
* Approval records this as the session goal, so the goal is what the plan says it
|
|
377
|
+
* achieves rather than the file-name slug. `undefined` when the plan has no
|
|
378
|
+
* `## Goal` section, so the caller can fall back to the title.
|
|
379
|
+
*/
|
|
380
|
+
export function goalSection(plan: string): string | undefined {
|
|
381
|
+
const section = extractPlanSections(plan).find(
|
|
382
|
+
(candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === "goal",
|
|
383
|
+
);
|
|
384
|
+
if (section === undefined) return undefined;
|
|
385
|
+
const paragraph: string[] = [];
|
|
386
|
+
for (const line of section.text.split("\n").slice(1)) {
|
|
387
|
+
if (!line.trim()) {
|
|
388
|
+
if (paragraph.length > 0) break;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
paragraph.push(line);
|
|
392
|
+
}
|
|
393
|
+
const text = normalizeHeadingText(paragraph.join(" ").replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, ""));
|
|
394
|
+
return text || undefined;
|
|
395
|
+
}
|
package/extensions/plan/todos.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The plan↔todo bridge (pure).
|
|
3
3
|
*
|
|
4
4
|
* `todo` is whole-list replacement, so seeding an approved plan straight over
|
|
5
5
|
* the list would discard whatever the user was already tracking. This module
|
|
6
6
|
* merges the plan's steps into the list `lib/todo-state.ts` replays from the
|
|
7
7
|
* session branch: existing items keep their place and status, a step whose text
|
|
8
8
|
* is already present is skipped, and the tool's item cap is respected without
|
|
9
|
-
* dropping anything the user had.
|
|
9
|
+
* dropping anything the user had. It also mirrors a step's new state back onto
|
|
10
|
+
* the item it seeded, so ticking the plan file keeps the list honest.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import type { ExistingTodo } from "../../lib/todo-state.ts";
|
|
13
|
-
import type { PlanStep } from "./steps.ts";
|
|
14
|
+
import type { PlanStep, StepUpdate } from "./steps.ts";
|
|
14
15
|
|
|
15
16
|
/** The merged list plus the counts the approval message reports. */
|
|
16
17
|
export interface TodoMerge {
|
|
@@ -33,6 +34,46 @@ export interface TodoMerge {
|
|
|
33
34
|
/** Fallback list cap when the registered todo tool does not advertise one. */
|
|
34
35
|
export const DEFAULT_TODO_MAX = 50;
|
|
35
36
|
|
|
37
|
+
/** What the tick-time mirror did for one `set_plan_steps` batch. */
|
|
38
|
+
export interface TodoMirror {
|
|
39
|
+
/** The list after the matching items were brought in line with the plan. */
|
|
40
|
+
todos: ExistingTodo[];
|
|
41
|
+
/** Items whose status the batch changed. */
|
|
42
|
+
updated: number;
|
|
43
|
+
/** Steps in the batch with no matching item, so nothing was written for them. */
|
|
44
|
+
unmatched: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bring the todo items seeded at approval in line with the plan's new step
|
|
49
|
+
* statuses.
|
|
50
|
+
*
|
|
51
|
+
* Matching is on the text the approval seeded, so an item the user renamed is
|
|
52
|
+
* left alone and counted as unmatched rather than overwritten. Only `status`
|
|
53
|
+
* changes, and the last update for a step number wins, the same rule the plan
|
|
54
|
+
* file's own edit follows. An update that changes nothing is not counted, so a
|
|
55
|
+
* refused or repeated call does not churn the widget.
|
|
56
|
+
*/
|
|
57
|
+
export function mirrorStepStatuses(existing: ExistingTodo[], updates: readonly StepUpdate[]): TodoMirror {
|
|
58
|
+
const byStep = new Map<number, StepUpdate>();
|
|
59
|
+
for (const update of updates) byStep.set(update.step, update);
|
|
60
|
+
const todos = existing.map((todo) => ({ ...todo }));
|
|
61
|
+
let updated = 0;
|
|
62
|
+
let unmatched = 0;
|
|
63
|
+
for (const update of byStep.values()) {
|
|
64
|
+
const index = todos.findIndex((todo) => contentKey(todo.content) === contentKey(update.expected));
|
|
65
|
+
if (index === -1) {
|
|
66
|
+
unmatched += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const todo = todos[index];
|
|
70
|
+
if (todo === undefined || todo.status === update.status) continue;
|
|
71
|
+
todos[index] = { ...todo, status: update.status };
|
|
72
|
+
updated += 1;
|
|
73
|
+
}
|
|
74
|
+
return { todos, updated, unmatched };
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
/** Case- and whitespace-insensitive key for recognizing a repeated item. */
|
|
37
78
|
function contentKey(content: string): string {
|
|
38
79
|
return content.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `enter_plan_mode` tool registration.
|
|
3
|
+
*
|
|
4
|
+
* It asks for confirmation, then turns the runtime's enabled flag on; the tool
|
|
5
|
+
* gating and persistence belong to `runtime.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
11
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
12
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
13
|
+
import { rejectionLine, reuseCallText, toolCallTitle } from "../../../lib/tui/tool.ts";
|
|
14
|
+
import { PLAN_CONSENT, READ_ONLY_SUMMARY } from "../../../lib/plan-entry.ts";
|
|
15
|
+
import { ENTER_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
16
|
+
import type { EnterPlanModeDetails } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
const ENTERED_TEXT =
|
|
19
|
+
"You are now in plan mode. Explore read-only, then save the full plan with write_plan (a short title and the markdown) " +
|
|
20
|
+
"and call exit_plan_mode with the returned path so the user can read the file and choose. " +
|
|
21
|
+
`While planning, ${READ_ONLY_SUMMARY}.`;
|
|
22
|
+
|
|
23
|
+
export function registerEnterTool(pi: ExtensionAPI, runtime: PlanRuntime): void {
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
name: ENTER_TOOL,
|
|
26
|
+
label: "Enter plan mode",
|
|
27
|
+
description:
|
|
28
|
+
"Enter read-only plan mode: investigate and design, then save the plan with write_plan and present it with " +
|
|
29
|
+
"exit_plan_mode. Entering switches the session's active tool set to read-only and requires the user to confirm.",
|
|
30
|
+
promptSnippet: "Ask the user to enter read-only plan mode.",
|
|
31
|
+
promptGuidelines: [
|
|
32
|
+
"Call enter_plan_mode before a non-trivial implementation task; skip it for small, obvious changes.",
|
|
33
|
+
],
|
|
34
|
+
parameters: Type.Object({}),
|
|
35
|
+
executionMode: "sequential",
|
|
36
|
+
// readOnlyHint stays true: entering plan mode changes only the session's
|
|
37
|
+
// tool set, never files or system state, so it needs no approval.
|
|
38
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
39
|
+
|
|
40
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
41
|
+
if (runtime.isEnabled()) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: "Already in plan mode." }],
|
|
44
|
+
details: { entered: true, already: true } satisfies EnterPlanModeDetails,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!ctx.hasUI) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: "No interactive UI is available to confirm entering plan mode. Ask the user whether they want a plan.",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: { entered: false, unavailable: true } satisfies EnterPlanModeDetails,
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const approved = await ctx.ui.confirm("Enter plan mode?", PLAN_CONSENT);
|
|
61
|
+
if (!approved) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: "Stayed in normal mode. Proceed, or ask the user how to continue." }],
|
|
64
|
+
details: { entered: false } satisfies EnterPlanModeDetails,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
runtime.enable(ctx);
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: ENTERED_TEXT }],
|
|
70
|
+
details: { entered: true } satisfies EnterPlanModeDetails,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
renderCall(_args, theme, context) {
|
|
75
|
+
const text = reuseCallText(context?.lastComponent);
|
|
76
|
+
text.setText(toolCallTitle(theme, `${ENTER_TOOL} `) + paint(theme, "meta", "requested plan mode"));
|
|
77
|
+
return text;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
renderResult(result, options, theme, context) {
|
|
81
|
+
const text = reuseText(context?.lastComponent);
|
|
82
|
+
const details = toolDetails<EnterPlanModeDetails>(result.details);
|
|
83
|
+
if (!details) {
|
|
84
|
+
text.setText(rejectionLine(theme, result, options.expanded));
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
if (details.unavailable) text.setText(paint(theme, "warn", "No UI to confirm plan mode"));
|
|
88
|
+
else if (details.already) text.setText(paint(theme, "meta", "Already in plan mode"));
|
|
89
|
+
else if (details.entered) text.setText(paint(theme, "ok", `${GLYPHS.check} Plan mode enabled`));
|
|
90
|
+
else text.setText(paint(theme, "meta", "Stayed in normal mode"));
|
|
91
|
+
return text;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `exit_plan_mode` tool registration.
|
|
3
|
+
*
|
|
4
|
+
* It presents the saved plan for approval, then seeds the approved steps into
|
|
5
|
+
* the todo list and finalizes the session goal: a `planning` goal becomes the
|
|
6
|
+
* plan's own `## Goal`, and a session with no goal is offered the same. The
|
|
7
|
+
* review loop and the browser lifecycle belong to `review.ts`; the nested writes
|
|
8
|
+
* belong to `todo-seed.ts` and the goal tool.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { basename } from "node:path";
|
|
12
|
+
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Type, type Static } from "typebox";
|
|
14
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
15
|
+
import { lastGoal } from "../../../lib/goal-state.ts";
|
|
16
|
+
import { GOAL_TOOL } from "../../../lib/tool-names.ts";
|
|
17
|
+
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
18
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
19
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
|
+
import { moreHint, rejectionLine, reuseCallText, toolCallTitle } from "../../../lib/tui/tool.ts";
|
|
21
|
+
import { feedbackSummary } from "../feedback.ts";
|
|
22
|
+
import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
|
|
23
|
+
import { sanitizePlanText } from "../plan-text.ts";
|
|
24
|
+
import { planTitle, type StoredPlan } from "../plans.ts";
|
|
25
|
+
import { reviewPlan } from "../review.ts";
|
|
26
|
+
import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
27
|
+
import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
|
|
28
|
+
import type { ExitPlanModeDetails } from "../types.ts";
|
|
29
|
+
import type { PlanWebReview } from "../web.ts";
|
|
30
|
+
import { firstTextLine } from "./nested.ts";
|
|
31
|
+
import { seedTodos } from "./todo-seed.ts";
|
|
32
|
+
|
|
33
|
+
const ExitPlanModeParams = Type.Object({
|
|
34
|
+
plan_path: Type.String({
|
|
35
|
+
description:
|
|
36
|
+
"Path to the plan file written by write_plan (absolute, or relative to the working directory). The user reviews this file.",
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
|
|
41
|
+
|
|
42
|
+
/** How the goal ended up: no action when it was already final. */
|
|
43
|
+
interface GoalOutcome {
|
|
44
|
+
objective: string;
|
|
45
|
+
/** `recorded` for a session with no goal, `promoted` for one that was planning. */
|
|
46
|
+
action?: "recorded" | "promoted";
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Finalise the session goal from the approved plan.
|
|
52
|
+
*
|
|
53
|
+
* A `planning` goal needs no dialog — approving the plan *is* the decision — and a
|
|
54
|
+
* session with no goal at all gets one confirm. A final goal (`active` or
|
|
55
|
+
* `achieved`) is the user's own: approving ordinary work never rewrites it.
|
|
56
|
+
* Recording runs through the goal tool, so validation and the widget stay goal's,
|
|
57
|
+
* and no model turn is spent.
|
|
58
|
+
*/
|
|
59
|
+
async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
|
|
60
|
+
const objective = planObjective(plan);
|
|
61
|
+
const current = lastGoal(ctx.sessionManager.getBranch());
|
|
62
|
+
if (current !== null && current.status !== "planning") return { objective };
|
|
63
|
+
if (current === null) {
|
|
64
|
+
if (!ctx.hasUI) return { objective };
|
|
65
|
+
const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
|
|
66
|
+
if (!approved) return { objective };
|
|
67
|
+
}
|
|
68
|
+
const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
|
|
69
|
+
const action = current === null ? "recorded" : "promoted";
|
|
70
|
+
return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The plan's own objective: its `## Goal` section, else the `# heading`, else the slug. */
|
|
74
|
+
function planObjective(plan: StoredPlan): string {
|
|
75
|
+
const stated = goalSection(plan.content);
|
|
76
|
+
if (stated) return sanitizeLine(stated);
|
|
77
|
+
const heading = /^ {0,3}#\s+(.+)$/m.exec(plan.content)?.[1];
|
|
78
|
+
const title = heading === undefined ? "" : sanitizeLine(heading).trim();
|
|
79
|
+
return title || planTitle(basename(plan.path));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: PlanWebReview): void {
|
|
83
|
+
pi.registerTool({
|
|
84
|
+
name: EXIT_TOOL,
|
|
85
|
+
label: "Exit plan mode",
|
|
86
|
+
description:
|
|
87
|
+
"Present the saved plan and ask the user to approve it. Reads the file written by write_plan, so call it with " +
|
|
88
|
+
"plan_path. On approval plan mode ends, write access returns, and the plan's steps go into the todo list; the " +
|
|
89
|
+
"user may instead keep planning or request a refinement. The plan file stays the source of truth.",
|
|
90
|
+
promptSnippet: "Present the saved plan and ask the user to approve leaving plan mode.",
|
|
91
|
+
parameters: ExitPlanModeParams,
|
|
92
|
+
defaultActive: false,
|
|
93
|
+
executionMode: "sequential",
|
|
94
|
+
// readOnlyHint stays true: it writes no files, and the review dialog inside
|
|
95
|
+
// execute is the human gate, so an outer prompt would only double-ask.
|
|
96
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
97
|
+
|
|
98
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
99
|
+
const planPath = (params as ExitPlanModeArgs).plan_path.trim();
|
|
100
|
+
|
|
101
|
+
if (!runtime.isEnabled()) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: "Not in plan mode; there is no plan to approve." }],
|
|
104
|
+
details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const file = await runtime.plans.read(ctx.cwd, planPath);
|
|
110
|
+
if (!file) {
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
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
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const total = planStepSummary(file.content).total;
|
|
124
|
+
if (total > MAX_STEPS) {
|
|
125
|
+
// A hand-edited or older plan file can still be over the cap; refuse it here
|
|
126
|
+
// too, so approval never seeds a list it cannot hold in full.
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
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: {
|
|
135
|
+
approved: false,
|
|
136
|
+
plan: file.content,
|
|
137
|
+
planPath: file.path,
|
|
138
|
+
relativePath: file.relativePath,
|
|
139
|
+
} satisfies ExitPlanModeDetails,
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!ctx.hasUI) {
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
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: {
|
|
153
|
+
approved: false,
|
|
154
|
+
plan: file.content,
|
|
155
|
+
planPath: file.path,
|
|
156
|
+
relativePath: file.relativePath,
|
|
157
|
+
unavailable: true,
|
|
158
|
+
} satisfies ExitPlanModeDetails,
|
|
159
|
+
isError: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// One decision, however the session is attached; `review.ts` owns the loop,
|
|
164
|
+
// the baseline, and closing the browser review when the turn is aborted.
|
|
165
|
+
const decision = await reviewPlan({
|
|
166
|
+
ctx,
|
|
167
|
+
web,
|
|
168
|
+
plans: runtime.plans,
|
|
169
|
+
baselines: runtime,
|
|
170
|
+
plan: file,
|
|
171
|
+
signal,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (decision.action === "refine") {
|
|
175
|
+
// A browser save already wrote its comments into the file; either way the
|
|
176
|
+
// model revises the same plan and presents it again.
|
|
177
|
+
const feedback = decision.fromBrowser
|
|
178
|
+
? `The user reviewed the plan in the browser and left feedback:\n${decision.refinement}`
|
|
179
|
+
: `The user asked to refine the plan: ${decision.refinement}`;
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: `${feedback}\nStay in plan mode, revise the plan (write_plan, reusing plan_path), then call exit_plan_mode again.`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
details: {
|
|
188
|
+
approved: false,
|
|
189
|
+
plan: decision.plan.content,
|
|
190
|
+
planPath: decision.plan.path,
|
|
191
|
+
relativePath: decision.plan.relativePath,
|
|
192
|
+
refined: true,
|
|
193
|
+
refinement: decision.refinement,
|
|
194
|
+
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
195
|
+
} satisfies ExitPlanModeDetails,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (decision.action === "keep") {
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{ type: "text", text: "Plan not approved. Stay in plan mode, ask what to change, and revise the plan." },
|
|
202
|
+
],
|
|
203
|
+
details: {
|
|
204
|
+
approved: false,
|
|
205
|
+
plan: decision.plan.content,
|
|
206
|
+
planPath: decision.plan.path,
|
|
207
|
+
relativePath: decision.plan.relativePath,
|
|
208
|
+
} satisfies ExitPlanModeDetails,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
runtime.disable(ctx);
|
|
213
|
+
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
|
|
214
|
+
const goal = await finalizeGoal(ctx, decision.plan);
|
|
215
|
+
const listing =
|
|
216
|
+
steps.length > 0
|
|
217
|
+
? `\n\n${steps
|
|
218
|
+
.map((step, index) => `${index + 1}. [${step.status === "completed" ? "x" : " "}] ${step.content}`)
|
|
219
|
+
.join("\n")}`
|
|
220
|
+
: "";
|
|
221
|
+
const tail =
|
|
222
|
+
steps.length === 0
|
|
223
|
+
? " The plan has no steps to record."
|
|
224
|
+
: error
|
|
225
|
+
? ` Could not record the steps in the todo list: ${error}. Keep these steps in mind and tick them off with ${SET_PLAN_STEPS_TOOL} as you go.`
|
|
226
|
+
: merged
|
|
227
|
+
? mergeSummary(merged, steps)
|
|
228
|
+
: ` 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}.`;
|
|
229
|
+
const verification = verificationNote(merged);
|
|
230
|
+
const goalNote =
|
|
231
|
+
goal.action === undefined
|
|
232
|
+
? ""
|
|
233
|
+
: goal.error !== undefined
|
|
234
|
+
? ` Could not ${goal.action === "recorded" ? "record" : "update"} the session goal: ${goal.error}. Use the goal tool to set it.`
|
|
235
|
+
: goal.action === "recorded"
|
|
236
|
+
? ` Recorded the session goal: ${goal.objective}.`
|
|
237
|
+
: ` Set the session goal from the plan: ${goal.objective}.`;
|
|
238
|
+
const browserNote = decision.comments.length
|
|
239
|
+
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
|
|
240
|
+
: "";
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "text",
|
|
245
|
+
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}`,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
details: {
|
|
249
|
+
approved: true,
|
|
250
|
+
plan: decision.plan.content,
|
|
251
|
+
planPath: decision.plan.path,
|
|
252
|
+
relativePath: decision.plan.relativePath,
|
|
253
|
+
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
254
|
+
} satisfies ExitPlanModeDetails,
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
renderCall(_args, theme, context) {
|
|
259
|
+
const text = reuseCallText(context?.lastComponent);
|
|
260
|
+
text.setText(toolCallTitle(theme, `${EXIT_TOOL} `) + paint(theme, "meta", "submitted a plan"));
|
|
261
|
+
return text;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
renderResult(result, { expanded }, theme, context) {
|
|
265
|
+
const text = reuseText(context?.lastComponent);
|
|
266
|
+
const details = toolDetails<ExitPlanModeDetails>(result.details);
|
|
267
|
+
if (!details) {
|
|
268
|
+
text.setText(rejectionLine(theme, result, expanded));
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
271
|
+
const heading = details.unavailable
|
|
272
|
+
? paint(theme, "warn", "No UI to approve the plan")
|
|
273
|
+
: details.approved
|
|
274
|
+
? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
|
|
275
|
+
: details.refined
|
|
276
|
+
? paint(theme, "accent", "Refining the plan")
|
|
277
|
+
: paint(theme, "warn", "Plan not approved");
|
|
278
|
+
const extra =
|
|
279
|
+
details.refined && details.refinement
|
|
280
|
+
? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
|
|
281
|
+
: "";
|
|
282
|
+
const shown = details.relativePath ?? details.planPath;
|
|
283
|
+
const path = shown ? `\n${paint(theme, "meta", shown)}` : "";
|
|
284
|
+
const planText = sanitizePlanText(details.plan);
|
|
285
|
+
let out = `${heading}${extra}${path}`;
|
|
286
|
+
if (planText) {
|
|
287
|
+
const { body, more } = expanded ? { body: planText, more: 0 } : preview(planText);
|
|
288
|
+
// A blank line separates the header block from the plan body.
|
|
289
|
+
out += `\n\n${theme.fg("toolOutput", body)}`;
|
|
290
|
+
if (more > 0) out += `\n${moreHint(theme, more)}`;
|
|
291
|
+
}
|
|
292
|
+
text.setText(out);
|
|
293
|
+
return text;
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a nested `ctx.executeTool` result.
|
|
3
|
+
*
|
|
4
|
+
* The plan tools delegate to `todo` and `goal` so their validation and widgets
|
|
5
|
+
* run normally; when the nested call fails, its first text line is the reason
|
|
6
|
+
* worth reporting, sanitized because a tool message is untrusted text.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
10
|
+
|
|
11
|
+
/** The shape of a nested tool outcome the plan tools read. */
|
|
12
|
+
interface NestedOutcome {
|
|
13
|
+
result?: { content?: ReadonlyArray<{ type: string; text?: string }> };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The first text line of a nested result, sanitized, or undefined. */
|
|
17
|
+
export function firstTextLine(outcome: NestedOutcome): string | undefined {
|
|
18
|
+
const part = outcome.result?.content?.find((entry) => entry.type === "text");
|
|
19
|
+
const text = part?.type === "text" && typeof part.text === "string" ? part.text.split("\n", 1)[0] : undefined;
|
|
20
|
+
return text ? sanitizeLine(text) : undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `set_plan_steps` tool registration.
|
|
3
|
+
*
|
|
4
|
+
* It rewrites the plan file's checkboxes in one call, then mirrors the same
|
|
5
|
+
* batch into the todo items approval seeded. A rejected batch leaves both
|
|
6
|
+
* artifacts untouched.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
11
|
+
import { Type, type Static } from "typebox";
|
|
12
|
+
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
13
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
14
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
15
|
+
import { rejectionLine, reuseCallText, toolCallTitle } from "../../../lib/tui/tool.ts";
|
|
16
|
+
import { describeUpdates, stepLabel } from "../format.ts";
|
|
17
|
+
import { SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
18
|
+
import { extractPlanSteps } from "../steps.ts";
|
|
19
|
+
import type { SetPlanStepsDetails } from "../types.ts";
|
|
20
|
+
import { mirrorTodos } from "./todo-seed.ts";
|
|
21
|
+
|
|
22
|
+
const StepUpdateParams = Type.Object({
|
|
23
|
+
step: Type.Integer({
|
|
24
|
+
minimum: 1,
|
|
25
|
+
description: "1-based step number, in the order the plan was approved.",
|
|
26
|
+
}),
|
|
27
|
+
status: StringEnum(["pending", "completed"] as const, {
|
|
28
|
+
description: "The step's new state.",
|
|
29
|
+
}),
|
|
30
|
+
expected: Type.String({
|
|
31
|
+
description:
|
|
32
|
+
"The step's text as it was listed when the plan was approved. A mismatch refuses the whole batch, so a reordered plan is caught instead of mis-ticked.",
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const SetPlanStepsParams = Type.Object({
|
|
37
|
+
plan_path: Type.String({
|
|
38
|
+
description: "Path to the approved plan file, as returned by write_plan or exit_plan_mode.",
|
|
39
|
+
}),
|
|
40
|
+
updates: Type.Array(StepUpdateParams, {
|
|
41
|
+
minItems: 1,
|
|
42
|
+
description:
|
|
43
|
+
"The steps to set; each entry sets one step's checkbox, and the last entry wins for a duplicate step. Pass expected as well to refuse a stale step number.",
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type SetPlanStepsArgs = Static<typeof SetPlanStepsParams>;
|
|
48
|
+
|
|
49
|
+
export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime): void {
|
|
50
|
+
pi.registerTool({
|
|
51
|
+
name: SET_PLAN_STEPS_TOOL,
|
|
52
|
+
label: "Set plan steps",
|
|
53
|
+
description:
|
|
54
|
+
"Keep an approved plan file current by setting the checkboxes of the given top-level steps in place. Each step " +
|
|
55
|
+
"number matches the order the plan was approved in; a step can be marked completed or reopened. Only the " +
|
|
56
|
+
"checkboxes change; the step text is left as written. The file is rewritten once, and nothing changes when any " +
|
|
57
|
+
"step is out of range or an entry's expected text no longer matches the file.",
|
|
58
|
+
promptSnippet: "Tick steps off in the approved plan file.",
|
|
59
|
+
promptGuidelines: [
|
|
60
|
+
"After a plan is approved, call set_plan_steps as you complete steps, passing each step's text as expected so a reordered plan is refused instead of mis-ticked.",
|
|
61
|
+
],
|
|
62
|
+
parameters: SetPlanStepsParams,
|
|
63
|
+
executionMode: "sequential",
|
|
64
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
65
|
+
|
|
66
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
67
|
+
const args = params as SetPlanStepsArgs;
|
|
68
|
+
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
|
+
};
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
|
|
77
|
+
const steps = extractPlanSteps(plan.content);
|
|
78
|
+
const done = steps.filter((step) => step.status === "completed").length;
|
|
79
|
+
const todoNote = await mirrorTodos(ctx, args.updates);
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
details: {
|
|
88
|
+
path: plan.path,
|
|
89
|
+
relativePath: plan.relativePath,
|
|
90
|
+
updates: args.updates,
|
|
91
|
+
done,
|
|
92
|
+
total: steps.length,
|
|
93
|
+
} satisfies SetPlanStepsDetails,
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
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
|
+
};
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
renderCall(args, theme, context) {
|
|
106
|
+
const text = reuseCallText(context?.lastComponent);
|
|
107
|
+
const updates = (args as Partial<SetPlanStepsArgs>).updates;
|
|
108
|
+
const label =
|
|
109
|
+
updates && updates.length === 1 && updates[0]?.step !== undefined
|
|
110
|
+
? `step ${updates[0].step}`
|
|
111
|
+
: updates && updates.length > 1
|
|
112
|
+
? `${updates.length} steps`
|
|
113
|
+
: "steps";
|
|
114
|
+
text.setText(toolCallTitle(theme, `${SET_PLAN_STEPS_TOOL} `) + paint(theme, "meta", label));
|
|
115
|
+
return text;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
renderResult(result, options, theme, context) {
|
|
119
|
+
const text = reuseText(context?.lastComponent);
|
|
120
|
+
const details = toolDetails<SetPlanStepsDetails>(result.details);
|
|
121
|
+
if (!details) {
|
|
122
|
+
text.setText(rejectionLine(theme, result, options.expanded));
|
|
123
|
+
return text;
|
|
124
|
+
}
|
|
125
|
+
const completed = details.updates.filter((update) => update.status === "completed").map((u) => u.step);
|
|
126
|
+
const pending = details.updates.filter((update) => update.status === "pending").map((u) => u.step);
|
|
127
|
+
const heading =
|
|
128
|
+
completed.length > 0
|
|
129
|
+
? paint(theme, "ok", `${GLYPHS.check} ${stepLabel(completed)} done`) +
|
|
130
|
+
(pending.length > 0 ? paint(theme, "meta", ` · ${stepLabel(pending)} reopened`) : "")
|
|
131
|
+
: paint(theme, "meta", `${stepLabel(pending)} reopened`);
|
|
132
|
+
text.setText(heading + paint(theme, "faint", ` · ${details.done}/${details.total}`));
|
|
133
|
+
return text;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|