@gavin-hu/my-pi-agent 0.18.0 → 0.19.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 +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +554 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +76 -92
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/guard.ts +3 -5
- package/lib/README.md +8 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -14,26 +14,30 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { createReadOnlyPolicy, type ReadOnlyPolicy } from "../../lib/policy.ts";
|
|
18
|
-
import {
|
|
17
|
+
import { createReadOnlyPolicy, WORKSPACE_READERS, type ReadOnlyPolicy } from "../../lib/policy.ts";
|
|
18
|
+
import {
|
|
19
|
+
ASK_USER_QUESTION_TOOL,
|
|
20
|
+
GOAL_TOOL,
|
|
21
|
+
SUBAGENT_TOOL,
|
|
22
|
+
TODO_TOOL,
|
|
23
|
+
WEB_FETCH_TOOL,
|
|
24
|
+
WEB_SEARCH_TOOL,
|
|
25
|
+
} from "../../lib/tool-names.ts";
|
|
19
26
|
import { ENTER_TOOL } from "./runtime.ts";
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
|
-
* Tools that stay available while planning: structured readers
|
|
23
|
-
* the git-only `bash`, and the known web/question
|
|
24
|
-
* tools the path backstop does not second-guess.
|
|
29
|
+
* Tools that stay available while planning: structured readers (documents
|
|
30
|
+
* included), the trackers, the git-only `bash`, and the known web/question
|
|
31
|
+
* readers. They are also the tools the path backstop does not second-guess.
|
|
25
32
|
*/
|
|
26
33
|
export const PLAN_SAFE_TOOLS = [
|
|
27
|
-
|
|
28
|
-
"grep",
|
|
29
|
-
"find",
|
|
30
|
-
"ls",
|
|
34
|
+
...WORKSPACE_READERS,
|
|
31
35
|
TODO_TOOL,
|
|
32
36
|
GOAL_TOOL,
|
|
33
37
|
"bash",
|
|
34
38
|
ASK_USER_QUESTION_TOOL,
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
WEB_SEARCH_TOOL,
|
|
40
|
+
WEB_FETCH_TOOL,
|
|
37
41
|
];
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -43,7 +47,11 @@ export const PLAN_SAFE_TOOLS = [
|
|
|
43
47
|
* deliberately absent — it is default-denied like any other shell.
|
|
44
48
|
*/
|
|
45
49
|
export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL];
|
|
46
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Tools always blocked while planning, even when annotated read-only.
|
|
52
|
+
* `enter_plan_mode` is denied so it stays hidden while planning; the guard exempts
|
|
53
|
+
* plan's own registration so a call already in flight can still answer "already".
|
|
54
|
+
*/
|
|
47
55
|
const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
|
|
48
56
|
|
|
49
57
|
/** Shown when a tool call is refused while planning. */
|
|
@@ -17,7 +17,6 @@ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import { askHuman, isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
|
|
18
18
|
import { FULL_SCREEN_OVERLAY } from "../../lib/tui/chrome.ts";
|
|
19
19
|
import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
|
|
20
|
-
import type { PlanUnit } from "../../lib/plans/team.ts";
|
|
21
20
|
import type { PlanStore, StoredPlan } from "./plans.ts";
|
|
22
21
|
import type { SectionDigest } from "./plan-text.ts";
|
|
23
22
|
import { planReviewPrompt, parsePlanReviewReply } from "./review-prompt.ts";
|
|
@@ -64,15 +63,6 @@ export interface ReviewPlanOptions {
|
|
|
64
63
|
plan: StoredPlan;
|
|
65
64
|
/** Aborting the turn closes a browser review and returns the plan unapproved. */
|
|
66
65
|
signal?: AbortSignal;
|
|
67
|
-
/** The split approval would write, so the screen shows what is being approved. */
|
|
68
|
-
split?: ReviewSplit;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** A validated split, as the review screen displays it. */
|
|
72
|
-
export interface ReviewSplit {
|
|
73
|
-
lead: string;
|
|
74
|
-
units: readonly PlanUnit[];
|
|
75
|
-
verdict: "pair" | "team";
|
|
76
66
|
}
|
|
77
67
|
|
|
78
68
|
/** Prompt shown by the dialog-based refine editor in non-TUI modes. */
|
|
@@ -94,7 +84,6 @@ async function askPlanReview(
|
|
|
94
84
|
ctx: ExtensionToolContext,
|
|
95
85
|
plan: StoredPlan,
|
|
96
86
|
baseline: SectionDigest[] | undefined,
|
|
97
|
-
split: ReviewSplit | undefined,
|
|
98
87
|
): Promise<PlanReviewChoice> {
|
|
99
88
|
const language = remoteLanguage(ctx) ?? "en";
|
|
100
89
|
const remote = isRemoteTurn(ctx);
|
|
@@ -105,7 +94,6 @@ async function askPlanReview(
|
|
|
105
94
|
new PlanViewComponent({
|
|
106
95
|
plan,
|
|
107
96
|
baseline,
|
|
108
|
-
...(split === undefined ? {} : { split }),
|
|
109
97
|
theme,
|
|
110
98
|
onClose: (action, refinement) => done({ action, refinement }),
|
|
111
99
|
requestRender: () => tui.requestRender(),
|
|
@@ -143,8 +131,6 @@ export interface BrowserReviewOptions {
|
|
|
143
131
|
* they are waiting: the page updates itself, the terminal has to be told.
|
|
144
132
|
*/
|
|
145
133
|
onReviewerComment?: (author: string, count: number) => void;
|
|
146
|
-
/** A line the page shows the owner, such as why approval belongs in the terminal. */
|
|
147
|
-
note?: string;
|
|
148
134
|
}
|
|
149
135
|
|
|
150
136
|
/**
|
|
@@ -163,7 +149,6 @@ export async function runBrowserReview(
|
|
|
163
149
|
canApprove: options.canApprove,
|
|
164
150
|
baseline: options.baseline,
|
|
165
151
|
signal: options.signal,
|
|
166
|
-
...(options.note === undefined ? {} : { note: options.note }),
|
|
167
152
|
...(options.onReviewerComment === undefined ? {} : { onReviewerComment: options.onReviewerComment }),
|
|
168
153
|
})
|
|
169
154
|
.catch(() => undefined);
|
|
@@ -182,7 +167,7 @@ export async function runBrowserReview(
|
|
|
182
167
|
* cancelled returns to the screen it was opened from.
|
|
183
168
|
*/
|
|
184
169
|
export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDecision> {
|
|
185
|
-
const { ctx, web, plans, baselines, signal
|
|
170
|
+
const { ctx, web, plans, baselines, signal } = options;
|
|
186
171
|
let plan = options.plan;
|
|
187
172
|
for (;;) {
|
|
188
173
|
// An aborted turn has nobody left to answer the screen.
|
|
@@ -190,7 +175,7 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
|
|
|
190
175
|
// Read the baseline before the screen and mark it after, so what the next
|
|
191
176
|
// review compares against is the content this one actually showed.
|
|
192
177
|
const baseline = baselines.reviewed(plan.path);
|
|
193
|
-
const choice = await askPlanReview(ctx, plan, baseline
|
|
178
|
+
const choice = await askPlanReview(ctx, plan, baseline);
|
|
194
179
|
baselines.markReviewed(plan.path, plan.content);
|
|
195
180
|
|
|
196
181
|
if (choice.action === "approve") return { action: "approve", plan, comments: [] };
|
|
@@ -206,17 +191,8 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
|
|
|
206
191
|
// An unrecognized action keeps planning rather than opening a server.
|
|
207
192
|
if (choice.action !== "browser") return { action: "keep", plan };
|
|
208
193
|
|
|
209
|
-
// A split section exists only after approval, and the page anchors comments to
|
|
210
|
-
// source lines, so a page cannot show — or approve — a plan it cannot carry.
|
|
211
|
-
// The terminal review is where the split is approved.
|
|
212
|
-
const splitNote =
|
|
213
|
-
split === undefined
|
|
214
|
-
? undefined
|
|
215
|
-
: "This plan has a split, which the page cannot show; approve it in the terminal review.";
|
|
216
|
-
if (splitNote !== undefined) ctx.ui.notify(splitNote, "info");
|
|
217
194
|
const result = await runBrowserReview(web, ctx.cwd, plan, {
|
|
218
|
-
canApprove:
|
|
219
|
-
...(splitNote === undefined ? {} : { note: splitNote }),
|
|
195
|
+
canApprove: true,
|
|
220
196
|
baseline,
|
|
221
197
|
signal,
|
|
222
198
|
onOpened: (session) => ctx.ui.notify(`Plan review: opened ${session.url}`, "info"),
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* markers survive a resume too.
|
|
12
12
|
*
|
|
13
13
|
* `write_plan` is the one sanctioned write while planning. The read-only guard
|
|
14
|
-
* in `index.ts` permits
|
|
15
|
-
* extension's
|
|
14
|
+
* in `index.ts` permits plan's own tools only when the registered tool is this
|
|
15
|
+
* extension's (matched by source path), so a same-named tool from another
|
|
16
16
|
* extension cannot borrow the exemption.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -48,9 +48,17 @@ export const BASELINE_TYPE = "plan-baseline";
|
|
|
48
48
|
/** Longest plan-file basename shown in the footer chip. */
|
|
49
49
|
const STATUS_NAME_LENGTH = 28;
|
|
50
50
|
|
|
51
|
-
/** Tools plan mode activates itself
|
|
51
|
+
/** Tools plan mode activates itself. */
|
|
52
52
|
const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Tools the read-only guard exempts when this extension registered them.
|
|
56
|
+
* `enter_plan_mode` is here only so a call already in flight when plan mode came on
|
|
57
|
+
* is answered by the tool ("already in plan mode"); the policy still denies it, so
|
|
58
|
+
* it stays hidden while planning.
|
|
59
|
+
*/
|
|
60
|
+
const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
|
|
61
|
+
|
|
54
62
|
export interface PlanRuntime {
|
|
55
63
|
/** Whether plan mode is currently on. */
|
|
56
64
|
isEnabled(): boolean;
|
|
@@ -61,8 +69,8 @@ export interface PlanRuntime {
|
|
|
61
69
|
restore(ctx: ExtensionContext): void;
|
|
62
70
|
/** Plan-file storage for `write_plan` and `exit_plan_mode`. */
|
|
63
71
|
plans: PlanStore;
|
|
64
|
-
/** Whether `toolName` is
|
|
65
|
-
|
|
72
|
+
/** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
|
|
73
|
+
isOwnTool(toolName: string): boolean;
|
|
66
74
|
/** Path of the most recently written plan, for the footer chip. */
|
|
67
75
|
lastPlanPath(): string | undefined;
|
|
68
76
|
/** Remember a written plan and repaint the footer. */
|
|
@@ -159,11 +167,15 @@ export function createPlanRuntime(
|
|
|
159
167
|
|
|
160
168
|
const plans = createPlanStore(pi);
|
|
161
169
|
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Whether `toolName` is plan's own registration, matched by the entry path the
|
|
172
|
+
* host recorded. Older hosts may not record a source; only a recorded mismatch
|
|
173
|
+
* is rejected.
|
|
174
|
+
*/
|
|
175
|
+
const isOwnTool = (toolName: string): boolean => {
|
|
176
|
+
if (!OWNED_TOOLS.includes(toolName)) return false;
|
|
164
177
|
const info = pi.getAllTools().find((tool) => tool.name === toolName);
|
|
165
178
|
const source = info?.sourceInfo?.path;
|
|
166
|
-
// Older hosts may not record a source; only reject a recorded mismatch.
|
|
167
179
|
return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
|
|
168
180
|
};
|
|
169
181
|
|
|
@@ -241,7 +253,7 @@ export function createPlanRuntime(
|
|
|
241
253
|
if (ctx) setStatus(ctx);
|
|
242
254
|
},
|
|
243
255
|
plans,
|
|
244
|
-
|
|
256
|
+
isOwnTool,
|
|
245
257
|
lastPlanPath: () => lastPlanPath,
|
|
246
258
|
setLastPlan: (ctx, path) => {
|
|
247
259
|
lastPlanPath = path;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-document sections (pure).
|
|
3
|
+
*
|
|
4
|
+
* One fence-aware scan of a plan's markdown: the review screen navigates by
|
|
5
|
+
* section, `set_plan_steps` looks up `## Steps`, a carried comment finds the
|
|
6
|
+
* block it belongs to, and the browser outline lists the same headings. A second
|
|
7
|
+
* scanner would disagree about a heading inside a fence, so every reader goes
|
|
8
|
+
* through this module.
|
|
9
|
+
*
|
|
10
|
+
* The scan is deliberately smaller than CommonMark: a heading is up to three
|
|
11
|
+
* leading spaces, a space after the hashes, and an optional closing run; a fence
|
|
12
|
+
* is three backticks or tildes anywhere a fence may start. Inside a fence
|
|
13
|
+
* nothing is a heading, which is what keeps a `# comment` in a shell example out
|
|
14
|
+
* of the outline. Every section keeps its raw source lines, so a caller can edit
|
|
15
|
+
* exactly what it read.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A heading-delimited chunk of a plan, for navigation and in-place edits. */
|
|
19
|
+
export interface PlanSection {
|
|
20
|
+
/** Plain heading text, or `""` for the content before the first heading. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** The section's markdown, including its heading line. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** 0-based source line of the section's heading (0 for the preamble). */
|
|
25
|
+
line: number;
|
|
26
|
+
/** Heading level, or 0 for the preamble before the first heading. */
|
|
27
|
+
level: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A markdown heading outside a fence, capturing its level and text: up to three
|
|
32
|
+
* leading spaces, a space after the hashes, and a closing hash run only when a
|
|
33
|
+
* space separates it. Shared with the block model, so the section split and the
|
|
34
|
+
* browser outline read a heading line the same way, and `## C#` keeps its title.
|
|
35
|
+
*/
|
|
36
|
+
export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A markdown code-fence opener or closer (backticks or tildes). Shared with the
|
|
40
|
+
* block model: a tab-indented fence is prose for both parsers, never code for one
|
|
41
|
+
* and prose for the other.
|
|
42
|
+
*/
|
|
43
|
+
export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A heading or step label as the review surfaces compare it: inline emphasis and
|
|
47
|
+
* code removed, whitespace collapsed. Exported so a renderer labels an outline
|
|
48
|
+
* entry the same way {@link extractSections} labels its section, which is what
|
|
49
|
+
* lets the browser mark a changed section.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeHeadingText(text: string): string {
|
|
52
|
+
return text
|
|
53
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
54
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
55
|
+
.replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
|
|
56
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sections of a plan, split at heading lines outside code fences. The content
|
|
63
|
+
* before the first heading is a section titled `""`. Pi's `Markdown` renderer
|
|
64
|
+
* exposes no source-line map, so the review screen renders these chunks
|
|
65
|
+
* separately to know where each section begins.
|
|
66
|
+
*/
|
|
67
|
+
export function extractSections(plan: string): PlanSection[] {
|
|
68
|
+
const sections: PlanSection[] = [];
|
|
69
|
+
let current: PlanSection | undefined;
|
|
70
|
+
let fence: string | undefined;
|
|
71
|
+
|
|
72
|
+
const flush = (): void => {
|
|
73
|
+
if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
|
|
74
|
+
current = undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
plan.split("\n").forEach((line, index) => {
|
|
78
|
+
const fenceMatch = line.match(FENCE);
|
|
79
|
+
if (fenceMatch) {
|
|
80
|
+
if (fence === undefined) fence = fenceMatch[1][0];
|
|
81
|
+
else if (fence === fenceMatch[1][0]) fence = undefined;
|
|
82
|
+
} else if (fence === undefined) {
|
|
83
|
+
const heading = line.match(HEADING);
|
|
84
|
+
if (heading) {
|
|
85
|
+
flush();
|
|
86
|
+
current = { title: normalizeHeadingText(heading[2]), text: line, line: index, level: heading[1].length };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
|
|
91
|
+
else current.text += `\n${line}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
flush();
|
|
95
|
+
return sections;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One key per section that tells two identically titled headings apart: the title
|
|
100
|
+
* and which occurrence of it this is, in document order. The review pairs a
|
|
101
|
+
* baseline digest with the section it belongs to by it, and the feedback codec
|
|
102
|
+
* matches a comment's section across a rewrite, so a second `## Notes` never
|
|
103
|
+
* collects the first one's lines.
|
|
104
|
+
*/
|
|
105
|
+
export function sectionKeys(sections: readonly { title: string }[]): string[] {
|
|
106
|
+
const seen = new Map<string, number>();
|
|
107
|
+
return sections.map((section) => {
|
|
108
|
+
const occurrence = seen.get(section.title) ?? 0;
|
|
109
|
+
seen.set(section.title, occurrence + 1);
|
|
110
|
+
return `${occurrence}\u0000${section.title}`;
|
|
111
|
+
});
|
|
112
|
+
}
|
package/extensions/plan/steps.ts
CHANGED
|
@@ -18,26 +18,7 @@
|
|
|
18
18
|
* checkbox or a `[DONE:n]` marker sets the status.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
extractSections as extractPlanSections,
|
|
23
|
-
FENCE,
|
|
24
|
-
HEADING,
|
|
25
|
-
normalizeHeadingText,
|
|
26
|
-
} from "../../lib/plans/sections.ts";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The section machinery is `lib/plans/sections.ts`, because the split writes
|
|
30
|
-
* `## Team` with the same fence-aware scan this module navigates with. These
|
|
31
|
-
* re-exports keep the plan extension's public surface one import.
|
|
32
|
-
*/
|
|
33
|
-
export {
|
|
34
|
-
extractSections as extractPlanSections,
|
|
35
|
-
FENCE,
|
|
36
|
-
HEADING,
|
|
37
|
-
normalizeHeadingText,
|
|
38
|
-
replaceSection,
|
|
39
|
-
} from "../../lib/plans/sections.ts";
|
|
40
|
-
export type { PlanSection } from "../../lib/plans/sections.ts";
|
|
21
|
+
import { extractSections, FENCE, HEADING, normalizeHeadingText } from "./sections.ts";
|
|
41
22
|
|
|
42
23
|
/** Most steps seeded from one plan. */
|
|
43
24
|
export const MAX_STEPS = 50;
|
|
@@ -295,7 +276,7 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
|
|
|
295
276
|
*/
|
|
296
277
|
export function verificationItem(plan: string): PlanStep | undefined {
|
|
297
278
|
if (stepsSection(plan) === undefined) return undefined;
|
|
298
|
-
const section =
|
|
279
|
+
const section = extractSections(plan).find(
|
|
299
280
|
(candidate) => candidate.level > 0 && candidate.title.toLowerCase() === "verification",
|
|
300
281
|
);
|
|
301
282
|
if (!section) return undefined;
|
|
@@ -394,7 +375,7 @@ export function setStepStatuses(plan: string, updates: readonly StepEdit[]): str
|
|
|
394
375
|
* `## Goal` section, so the caller can fall back to the title.
|
|
395
376
|
*/
|
|
396
377
|
export function goalSection(plan: string): string | undefined {
|
|
397
|
-
const section =
|
|
378
|
+
const section = extractSections(plan).find(
|
|
398
379
|
(candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === "goal",
|
|
399
380
|
);
|
|
400
381
|
if (section === undefined) return undefined;
|
|
@@ -12,7 +12,6 @@ import { basename } from "node:path";
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, type Static } from "typebox";
|
|
14
14
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
15
|
-
import { messageOf } from "../../../lib/errors.ts";
|
|
16
15
|
import { lastGoal } from "../../../lib/goal-state.ts";
|
|
17
16
|
import { GOAL_TOOL } from "../../../lib/tool-names.ts";
|
|
18
17
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
@@ -25,8 +24,6 @@ import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
|
|
|
25
24
|
import { sanitizePlanText } from "../plan-text.ts";
|
|
26
25
|
import { planTitle, type StoredPlan } from "../plans.ts";
|
|
27
26
|
import { reviewPlan } from "../review.ts";
|
|
28
|
-
import { decideSplit, rosterCard, SPLIT_OFF_NOTE } from "../split.ts";
|
|
29
|
-
import { writeTeam } from "../../../lib/plans/team.ts";
|
|
30
27
|
import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
31
28
|
import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
|
|
32
29
|
import type { ExitPlanModeDetails } from "../types.ts";
|
|
@@ -34,27 +31,11 @@ import type { PlanWebReview } from "../web.ts";
|
|
|
34
31
|
import { firstTextLine } from "./nested.ts";
|
|
35
32
|
import { seedTodos } from "./todo-seed.ts";
|
|
36
33
|
|
|
37
|
-
const PlanUnitParams = Type.Object({
|
|
38
|
-
name: Type.String({ description: "Lowercase slug; the owner session takes it as its label." }),
|
|
39
|
-
paths: Type.Array(Type.String(), { description: "Repository-relative path prefixes this unit owns." }),
|
|
40
|
-
test: Type.String({ description: "The command that proves the unit is done." }),
|
|
41
|
-
dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Units that must land first." })),
|
|
42
|
-
lead: Type.Optional(Type.Boolean({ description: "True for the one unit this session keeps." })),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
34
|
const ExitPlanModeParams = Type.Object({
|
|
46
35
|
plan_path: Type.String({
|
|
47
36
|
description:
|
|
48
37
|
"Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
|
|
49
38
|
}),
|
|
50
|
-
units: Type.Optional(
|
|
51
|
-
Type.Array(PlanUnitParams, {
|
|
52
|
-
description:
|
|
53
|
-
"The plan's slices, when it has two or more independent ones the user will run in separate sessions. " +
|
|
54
|
-
"Only used when the plan.split setting is on; validation refuses an overlapping, looping, or oversized " +
|
|
55
|
-
"split before the review opens. Omit it to approve as one flow.",
|
|
56
|
-
}),
|
|
57
|
-
),
|
|
58
39
|
});
|
|
59
40
|
|
|
60
41
|
type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
|
|
@@ -73,13 +54,15 @@ interface GoalOutcome {
|
|
|
73
54
|
* Approving the plan *is* the decision, so neither a `planning` goal nor a
|
|
74
55
|
* session with none is asked again. A final goal (`active` or `achieved`) is the
|
|
75
56
|
* user's own: approving ordinary work never rewrites it. Recording runs through
|
|
76
|
-
* the goal tool
|
|
77
|
-
* spent
|
|
57
|
+
* the goal tool when it is registered — validation and the widget stay goal's,
|
|
58
|
+
* and no model turn is spent — and stays silent when that sibling extension is
|
|
59
|
+
* not installed, because there is nothing to report and nothing to act on.
|
|
78
60
|
*/
|
|
79
|
-
async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
|
|
61
|
+
async function finalizeGoal(pi: ExtensionAPI, ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
|
|
80
62
|
const objective = planObjective(plan);
|
|
81
63
|
const current = lastGoal(ctx.sessionManager.getBranch());
|
|
82
64
|
if (current !== null && current.status !== "planning") return { objective };
|
|
65
|
+
if (!pi.getAllTools().some((tool) => tool.name === GOAL_TOOL)) return { objective };
|
|
83
66
|
const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
|
|
84
67
|
const action = current === null ? "recorded" : "promoted";
|
|
85
68
|
return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
|
|
@@ -145,26 +128,6 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
145
128
|
);
|
|
146
129
|
}
|
|
147
130
|
|
|
148
|
-
// The split is decided before the review opens, so a refused or rejected split
|
|
149
|
-
// never spends the user's review — and the screen can show what it approves.
|
|
150
|
-
const units = args.units?.map((unit) => ({
|
|
151
|
-
name: unit.name,
|
|
152
|
-
paths: [...unit.paths],
|
|
153
|
-
test: unit.test,
|
|
154
|
-
dependsOn: [...(unit.dependsOn ?? [])],
|
|
155
|
-
...(unit.lead ? { lead: true } : {}),
|
|
156
|
-
}));
|
|
157
|
-
const split = decideSplit(ctx.cwd, await runtime.plans.rootFor(ctx.cwd), units);
|
|
158
|
-
if (split.kind === "refused") {
|
|
159
|
-
return failedResult(`Refused: ${split.reason}`, {
|
|
160
|
-
approved: false,
|
|
161
|
-
plan: file.content,
|
|
162
|
-
planPath: file.path,
|
|
163
|
-
relativePath: file.relativePath,
|
|
164
|
-
} satisfies ExitPlanModeDetails);
|
|
165
|
-
}
|
|
166
|
-
const pending = split.kind === "team" ? split : undefined;
|
|
167
|
-
|
|
168
131
|
if (!ctx.hasUI) {
|
|
169
132
|
return failedResult(
|
|
170
133
|
`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.`,
|
|
@@ -187,7 +150,6 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
187
150
|
baselines: runtime,
|
|
188
151
|
plan: file,
|
|
189
152
|
signal,
|
|
190
|
-
...(pending === undefined ? {} : { split: { ...pending.team, verdict: pending.verdict } }),
|
|
191
153
|
});
|
|
192
154
|
|
|
193
155
|
if (decision.action === "refine") {
|
|
@@ -250,33 +212,9 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
250
212
|
};
|
|
251
213
|
}
|
|
252
214
|
|
|
253
|
-
// The team becomes real here, never before: the section is what a joining
|
|
254
|
-
// session reads, so it appears only after the user approved the split. The
|
|
255
|
-
// write happens while plan mode is still on, so a failure leaves the session
|
|
256
|
-
// able to retry instead of approved-but-empty.
|
|
257
|
-
let approved = decision.plan;
|
|
258
|
-
if (pending !== undefined) {
|
|
259
|
-
try {
|
|
260
|
-
approved = await runtime.plans.write(ctx.cwd, {
|
|
261
|
-
title: planTitle(basename(decision.plan.path)),
|
|
262
|
-
content: writeTeam(decision.plan.content, { lead: pending.team.lead, units: pending.team.units }),
|
|
263
|
-
planPath: decision.plan.path,
|
|
264
|
-
});
|
|
265
|
-
} catch (writeError) {
|
|
266
|
-
return failedResult(
|
|
267
|
-
`The plan was approved but its team could not be written: ${messageOf(writeError)}. Plan mode is still on and no steps were seeded; fix the plan file and call exit_plan_mode again.`,
|
|
268
|
-
{
|
|
269
|
-
approved: false,
|
|
270
|
-
plan: decision.plan.content,
|
|
271
|
-
planPath: decision.plan.path,
|
|
272
|
-
relativePath: decision.plan.relativePath,
|
|
273
|
-
} satisfies ExitPlanModeDetails,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
215
|
runtime.disable(ctx);
|
|
278
|
-
const { steps, merged, error } = await seedTodos(pi, ctx,
|
|
279
|
-
const goal = await finalizeGoal(ctx,
|
|
216
|
+
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
|
|
217
|
+
const goal = await finalizeGoal(pi, ctx, decision.plan);
|
|
280
218
|
const listing =
|
|
281
219
|
steps.length > 0
|
|
282
220
|
? `\n\n${steps
|
|
@@ -303,41 +241,19 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
303
241
|
const browserNote = decision.comments.length
|
|
304
242
|
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
|
|
305
243
|
: "";
|
|
306
|
-
const splitNote =
|
|
307
|
-
pending === undefined
|
|
308
|
-
? split.kind === "off"
|
|
309
|
-
? `\n\n${SPLIT_OFF_NOTE}`
|
|
310
|
-
: ""
|
|
311
|
-
: `\n\n${rosterCard(pending.team, approved.path, pi.getSessionName())}\n\nWork only your own unit (${pending.team.lead}); the other units belong to the sessions the user starts.`;
|
|
312
244
|
return {
|
|
313
245
|
content: [
|
|
314
246
|
{
|
|
315
247
|
type: "text",
|
|
316
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${
|
|
248
|
+
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}`,
|
|
317
249
|
},
|
|
318
250
|
],
|
|
319
251
|
details: {
|
|
320
252
|
approved: true,
|
|
321
|
-
plan:
|
|
322
|
-
planPath:
|
|
323
|
-
relativePath:
|
|
253
|
+
plan: decision.plan.content,
|
|
254
|
+
planPath: decision.plan.path,
|
|
255
|
+
relativePath: decision.plan.relativePath,
|
|
324
256
|
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
325
|
-
...(pending === undefined
|
|
326
|
-
? split.kind === "off"
|
|
327
|
-
? { splitOff: true }
|
|
328
|
-
: {}
|
|
329
|
-
: {
|
|
330
|
-
team: {
|
|
331
|
-
lead: pending.team.lead,
|
|
332
|
-
verdict: pending.verdict,
|
|
333
|
-
units: pending.team.units.map((unit) => ({
|
|
334
|
-
name: unit.name,
|
|
335
|
-
paths: unit.paths,
|
|
336
|
-
test: unit.test,
|
|
337
|
-
dependsOn: unit.dependsOn,
|
|
338
|
-
})),
|
|
339
|
-
},
|
|
340
|
-
}),
|
|
341
257
|
} satisfies ExitPlanModeDetails,
|
|
342
258
|
};
|
|
343
259
|
},
|
package/extensions/plan/tui.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type TuiMouseEventResult,
|
|
30
30
|
} from "@earendil-works/pi-tui";
|
|
31
31
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
32
|
-
import { renderTeam, type PlanUnit } from "../../lib/plans/team.ts";
|
|
33
32
|
import {
|
|
34
33
|
editorTheme,
|
|
35
34
|
screenHint,
|
|
@@ -46,7 +45,6 @@ import { emptyState, checkbox } from "../../lib/tui/rows.ts";
|
|
|
46
45
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
47
46
|
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
48
47
|
import { changedSections, planDisplayText, type SectionDigest } from "./plan-text.ts";
|
|
49
|
-
import { extractPlanSections } from "./steps.ts";
|
|
50
48
|
|
|
51
49
|
/** What the user chose from the read/review screen. */
|
|
52
50
|
export type PlanViewAction = "approve" | "refine" | "keep" | "browser" | "cancel";
|
|
@@ -87,11 +85,6 @@ export interface PlanViewOptions {
|
|
|
87
85
|
tui?: TUI;
|
|
88
86
|
/** Markdown theme for the body; defaults to Pi's active theme. */
|
|
89
87
|
markdownTheme?: MarkdownTheme;
|
|
90
|
-
/**
|
|
91
|
-
* The split approval would write. Rendered as a read-only section after the
|
|
92
|
-
* plan, because the `## Team` section itself only exists after approval.
|
|
93
|
-
*/
|
|
94
|
-
split?: { lead: string; units: readonly PlanUnit[]; verdict: "pair" | "team" };
|
|
95
88
|
}
|
|
96
89
|
|
|
97
90
|
/** Rows the screen shows when the terminal height is unknown. */
|
|
@@ -207,11 +200,10 @@ export class PlanViewComponent implements Component {
|
|
|
207
200
|
const base = this.options.markdownTheme ?? getMarkdownTheme();
|
|
208
201
|
const theme = planMarkdownTheme(this.theme, base);
|
|
209
202
|
const current = planDisplayText(this.options.plan.content);
|
|
210
|
-
const
|
|
211
|
-
const chunks = extractPlanSections(current);
|
|
203
|
+
const marks = changedSections(this.options.baseline, current);
|
|
212
204
|
const lines: string[] = [];
|
|
213
205
|
const sections: SectionOffset[] = [];
|
|
214
|
-
|
|
206
|
+
marks.forEach(({ section: chunk, changed }, index) => {
|
|
215
207
|
// Markdown pads every line to the full width for backgrounds; this screen
|
|
216
208
|
// draws no background, so the padding is dropped.
|
|
217
209
|
const rendered = new Markdown(chunk.text, padding, 0, theme)
|
|
@@ -220,38 +212,17 @@ export class PlanViewComponent implements Component {
|
|
|
220
212
|
sections.push({
|
|
221
213
|
title: chunk.title,
|
|
222
214
|
offset: lines.length,
|
|
223
|
-
...(changed
|
|
215
|
+
...(changed ? { changed: true } : {}),
|
|
224
216
|
});
|
|
225
217
|
lines.push(...rendered);
|
|
226
218
|
// Sections render independently, so restore the blank line a heading-only
|
|
227
219
|
// chunk omits before the next section.
|
|
228
|
-
if (index <
|
|
220
|
+
if (index < marks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
229
221
|
});
|
|
230
|
-
|
|
231
|
-
if (split !== undefined) {
|
|
232
|
-
if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
233
|
-
const rendered = new Markdown(split, padding, 0, theme)
|
|
234
|
-
.render(width)
|
|
235
|
-
.map((line) => truncateToWidth(line.trimEnd(), width));
|
|
236
|
-
sections.push({ title: "Split", offset: lines.length });
|
|
237
|
-
lines.push(...rendered);
|
|
238
|
-
}
|
|
239
|
-
this.cache = { width, lines, sections, changedCount: changed.filter(Boolean).length };
|
|
222
|
+
this.cache = { width, lines, sections, changedCount: marks.filter((mark) => mark.changed).length };
|
|
240
223
|
return this.cache;
|
|
241
224
|
}
|
|
242
225
|
|
|
243
|
-
/**
|
|
244
|
-
* The split as a read-only section: the same table approval writes, so what the
|
|
245
|
-
* user approves is what the plan file will carry.
|
|
246
|
-
*/
|
|
247
|
-
private splitSection(): string | undefined {
|
|
248
|
-
const split = this.options.split;
|
|
249
|
-
if (split === undefined) return undefined;
|
|
250
|
-
const owners = split.units.filter((unit) => !unit.lead).length;
|
|
251
|
-
const summary = `${split.verdict} · lead + ${owners} owner${owners === 1 ? "" : "s"} · written on approval`;
|
|
252
|
-
return ["## Split", "", summary, "", renderTeam({ lead: split.lead, units: [...split.units] })].join("\n");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
226
|
/** The section whose heading is at or above the viewport top. */
|
|
256
227
|
private currentSection(sections: SectionOffset[]): SectionOffset | undefined {
|
|
257
228
|
let current: SectionOffset | undefined;
|
package/extensions/plan/types.ts
CHANGED
|
@@ -104,14 +104,6 @@ export interface ExitPlanModeDetails {
|
|
|
104
104
|
cancelled?: boolean;
|
|
105
105
|
/** True when no interactive UI was available to ask for approval. */
|
|
106
106
|
unavailable?: boolean;
|
|
107
|
-
/** The team approval wrote into the plan, when the plan was split. */
|
|
108
|
-
team?: {
|
|
109
|
-
lead: string;
|
|
110
|
-
units: Array<{ name: string; paths: string[]; test: string; dependsOn: string[] }>;
|
|
111
|
-
verdict: "pair" | "team";
|
|
112
|
-
};
|
|
113
|
-
/** True when the model proposed a split while `plan.split` was off. */
|
|
114
|
-
splitOff?: boolean;
|
|
115
107
|
}
|
|
116
108
|
|
|
117
109
|
/** Structured result carried in `cancel_plan_mode`'s `details`. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The browser review client, served as `/client.js`.
|
|
3
3
|
*
|
|
4
|
-
* Kept as one self-contained script (like `file-browse/client.ts`) with no
|
|
4
|
+
* Kept as one self-contained script (like `file-browse/view/client.ts`) with no
|
|
5
5
|
* build step. Its document half — the inline and block Markdown rendering — is
|
|
6
6
|
* `web-markdown.ts`, a fragment of the same IIFE interpolated below. It renders
|
|
7
7
|
* the plan from the JSON data island using DOM text nodes only — plan and
|