@gavin-hu/my-pi-agent 0.17.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 +61 -45
- package/README.zh-CN.md +54 -42
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
- 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} +66 -53
- package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
- package/docs/extensions/plan.md +976 -0
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
- 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} +166 -119
- 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} +60 -38
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
- 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/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +11 -21
- 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} +13 -4
- 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} +54 -82
- package/extensions/file-browse/live/rev.ts +42 -0
- package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/view/chrome.ts +83 -0
- package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
- package/extensions/file-browse/view/diff-html.ts +94 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -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} +31 -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/goal/plan-route.ts +8 -2
- package/extensions/job/index.ts +15 -7
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +77 -21
- package/extensions/plan/feedback.ts +409 -63
- package/extensions/plan/index.ts +7 -5
- package/extensions/plan/plan-index.ts +44 -10
- package/extensions/plan/plan-text.ts +51 -24
- package/extensions/plan/plans.ts +162 -57
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +51 -95
- package/extensions/plan/runtime.ts +22 -10
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +63 -95
- package/extensions/plan/tools/enter.ts +8 -3
- package/extensions/plan/tools/exit.ts +13 -19
- package/extensions/plan/tools/write.ts +8 -3
- package/extensions/plan/tools.ts +1 -1
- package/extensions/plan/tui.ts +29 -15
- package/extensions/plan/types.ts +29 -3
- package/extensions/plan/web-chrome.ts +2 -1
- package/extensions/plan/web-client.ts +122 -178
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-page.ts +96 -17
- package/extensions/plan/web-router.ts +109 -47
- package/extensions/plan/web-share-session.ts +191 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +245 -151
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- 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 +113 -0
- package/extensions/session-bus/format.ts +101 -0
- package/extensions/session-bus/index.ts +75 -0
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +86 -0
- package/extensions/session-bus/overlap.ts +36 -0
- package/extensions/session-bus/protocol.ts +104 -0
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +208 -0
- package/extensions/session-bus/rows.ts +171 -0
- package/extensions/session-bus/runtime.ts +466 -0
- package/extensions/session-bus/store.ts +365 -0
- package/extensions/session-bus/tools.ts +192 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- 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/watch/index.ts +15 -9
- 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} +66 -51
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
- 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} +119 -56
- package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
- 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} +15 -7
- 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} +104 -40
- 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/git.ts +5 -0
- package/extensions/worktree/guard.ts +3 -5
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/status.ts +1 -3
- package/lib/README.md +12 -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/router.ts +40 -13
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +29 -13
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/interaction.ts +10 -0
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/policy.ts +12 -2
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +52 -33
- package/lib/requirements.ts +7 -9
- 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/README.md +16 -2
- package/lib/tui/dock-screen.ts +85 -17
- package/lib/tui/glyphs.ts +2 -0
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +6 -1
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +27 -14
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +77 -51
- 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 -467
- package/extensions/file-browse/server.ts +0 -134
- package/extensions/plan/README.md +0 -709
- package/extensions/plan/config.ts +0 -27
- package/extensions/wechat/outbound.ts +0 -100
- package/extensions/wechat/qrcode.ts +0 -46
- package/skills/changelog/SKILL.md +0 -148
- /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/{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
|
@@ -16,7 +16,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { type Component, Key, matchesKey, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
17
17
|
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
18
18
|
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
19
|
-
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
19
|
+
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
|
+
import { formatSize } from "../../lib/format.ts";
|
|
20
21
|
import { clip } from "../../lib/tui/columns.ts";
|
|
21
22
|
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
22
23
|
import { titleRule } from "../../lib/tui/rules.ts";
|
|
@@ -25,8 +26,12 @@ import type { PlanSummary } from "./plans.ts";
|
|
|
25
26
|
|
|
26
27
|
/** Rows the screen shows when the terminal height is unknown. */
|
|
27
28
|
const DEFAULT_ROWS = 14;
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Fixed rows around the list body: header, summary, blank, and the closing footer
|
|
31
|
+
* (blank, hint, rule). The focused path adds `detailBlockRows` on top, and `fitRows`
|
|
32
|
+
* adds one more when the range row appears.
|
|
33
|
+
*/
|
|
34
|
+
const CHROME = 6;
|
|
30
35
|
/** Longest title a row draws before the trailing meta wins the column. */
|
|
31
36
|
const TITLE_WIDTH = 56;
|
|
32
37
|
|
|
@@ -56,11 +61,33 @@ function localTime(ms: number): string {
|
|
|
56
61
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The store's own footprint for one repository: the bytes its plans hold, and the
|
|
66
|
+
* oldest of them. Derived from the summary the directory walk already produced, so
|
|
67
|
+
* reporting a size never becomes a second index that can disagree with the files.
|
|
68
|
+
*/
|
|
69
|
+
function footprint(summaries: readonly PlanSummary[]): { bytes: number; oldest: number } {
|
|
70
|
+
let bytes = 0;
|
|
71
|
+
let oldest = summaries[0]?.modified ?? 0;
|
|
72
|
+
for (const summary of summaries) {
|
|
73
|
+
bytes += summary.bytes;
|
|
74
|
+
oldest = Math.min(oldest, summary.modified);
|
|
75
|
+
}
|
|
76
|
+
return { bytes, oldest };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `2.4 kB — oldest 2d ago`, the store's footprint as the header and the screen say it. */
|
|
80
|
+
function footprintText(summaries: readonly PlanSummary[], nowMs: number): string {
|
|
81
|
+
const { bytes, oldest } = footprint(summaries);
|
|
82
|
+
return `${formatSize(bytes)} — oldest ${relativeAge(oldest, nowMs)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/**
|
|
60
86
|
* The non-TUI listing: one line per plan with its title, step progress, modified
|
|
61
|
-
* time, and path, newest first. The
|
|
87
|
+
* time, and path, newest first. The header names the count, the store's footprint,
|
|
88
|
+
* and the directory once; `now` is injectable so the age can be asserted.
|
|
62
89
|
*/
|
|
63
|
-
export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string): string {
|
|
90
|
+
export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string, now: number = Date.now()): string {
|
|
64
91
|
const where = sanitizeLine(shortenPath(dir));
|
|
65
92
|
if (summaries.length === 0) {
|
|
66
93
|
return `No plans in ${where} yet; write_plan saves one there.`;
|
|
@@ -69,7 +96,7 @@ export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string):
|
|
|
69
96
|
(summary) =>
|
|
70
97
|
` ${sanitizeLine(summary.title)} — ${stepsText(summary)} — ${localTime(summary.modified)} — ${sanitizeLine(summary.relativePath)}`,
|
|
71
98
|
);
|
|
72
|
-
return [`Plans (${summaries.length}) — ${where}`, ...lines].join("\n");
|
|
99
|
+
return [`Plans (${summaries.length}) — ${footprintText(summaries, now)} — ${where}`, ...lines].join("\n");
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
export interface PlanIndexOptions {
|
|
@@ -185,7 +212,8 @@ export class PlanIndexComponent implements Component {
|
|
|
185
212
|
private summaryLine(width: number): string {
|
|
186
213
|
const { theme } = this.options;
|
|
187
214
|
const count = this.summaries.length;
|
|
188
|
-
const
|
|
215
|
+
const size = footprintText(this.summaries, (this.options.now ?? Date.now)());
|
|
216
|
+
const text = `${paint(theme, "accent", "Plans")} ${paint(theme, "meta", `· ${count} ${count === 1 ? "plan" : "plans"} · ${size}`)}`;
|
|
189
217
|
const where = paint(theme, "faint", ` · ${sanitizeLine(shortenPath(this.options.dir))}`);
|
|
190
218
|
return clip(` ${text}${where}`, width);
|
|
191
219
|
}
|
|
@@ -202,12 +230,18 @@ export class PlanIndexComponent implements Component {
|
|
|
202
230
|
render(width: number): string[] {
|
|
203
231
|
const { theme } = this.options;
|
|
204
232
|
const w = Math.max(1, width);
|
|
205
|
-
this.cursor.visible = fitRows(this.options.viewportRows, this.summaries.length, CHROME, DEFAULT_ROWS);
|
|
206
233
|
this.cursor.sync(this.summaries.length);
|
|
234
|
+
const detail = this.detailLines(w);
|
|
235
|
+
this.cursor.visible = fitRows(
|
|
236
|
+
this.options.viewportRows,
|
|
237
|
+
this.summaries.length,
|
|
238
|
+
CHROME + detailBlockRows(detail),
|
|
239
|
+
DEFAULT_ROWS,
|
|
240
|
+
);
|
|
207
241
|
|
|
208
242
|
const body: string[] = [];
|
|
209
243
|
if (this.summaries.length === 0) {
|
|
210
|
-
body.push(...emptyState(theme, w, { label: "No plans yet. write_plan saves one
|
|
244
|
+
body.push(...emptyState(theme, w, { label: "No plans yet. write_plan saves one in the agent directory." }));
|
|
211
245
|
} else {
|
|
212
246
|
const { start, end } = this.cursor.window(this.summaries.length);
|
|
213
247
|
for (let index = start; index < end; index++) {
|
|
@@ -229,7 +263,7 @@ export class PlanIndexComponent implements Component {
|
|
|
229
263
|
range: this.cursor.clipped(this.summaries.length)
|
|
230
264
|
? formatRange(theme, w, { ...this.cursor.window(this.summaries.length), total: this.summaries.length })
|
|
231
265
|
: undefined,
|
|
232
|
-
detail
|
|
266
|
+
detail,
|
|
233
267
|
hint,
|
|
234
268
|
});
|
|
235
269
|
}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { stripControlChars } from "../../lib/format.ts";
|
|
18
|
-
import { stripFeedback } from "./feedback.ts";
|
|
19
|
-
import {
|
|
18
|
+
import { displayFeedback, stripFeedback } from "./feedback.ts";
|
|
19
|
+
import { extractSections, sectionKeys, type PlanSection } from "./sections.ts";
|
|
20
20
|
|
|
21
21
|
/** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
|
|
22
22
|
const TAB_WIDTH = 4;
|
|
@@ -38,6 +38,14 @@ export function sanitizePlanText(text: string): string {
|
|
|
38
38
|
return stripControlChars(text.replace(/\t/g, " ".repeat(TAB_WIDTH)));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The text a review *shows*: terminal-safe, with stored comments kept in place
|
|
43
|
+
* as readable quotes, so a reviewer sees what a teammate wrote.
|
|
44
|
+
*/
|
|
45
|
+
export function planDisplayText(content: string): string {
|
|
46
|
+
return displayFeedback(sanitizePlanText(content));
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/**
|
|
42
50
|
* The text a review compares: terminal-safe, with tabs expanded and stored
|
|
43
51
|
* feedback removed. A review comment a browser wrote into the file is a note to
|
|
@@ -52,9 +60,18 @@ function digestOf(text: string): string {
|
|
|
52
60
|
return createHash("sha256").update(text.trim()).digest("hex");
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A short, stable id for one plan text. The review page keys this tab's unsaved
|
|
65
|
+
* draft on it, so a draft is restored for the plan it was written against and
|
|
66
|
+
* dropped for a plan that changed underneath it.
|
|
67
|
+
*/
|
|
68
|
+
export function planDigest(text: string): string {
|
|
69
|
+
return digestOf(text).slice(0, 12);
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
/** One digest per heading section, in document order, for a persisted baseline. */
|
|
56
73
|
export function sectionDigests(content: string): SectionDigest[] {
|
|
57
|
-
return
|
|
74
|
+
return extractSections(reviewText(content)).map((section) => ({
|
|
58
75
|
title: section.title,
|
|
59
76
|
digest: digestOf(section.text),
|
|
60
77
|
}));
|
|
@@ -66,30 +83,40 @@ export function sameSections(a: readonly SectionDigest[], b: readonly SectionDig
|
|
|
66
83
|
return a.every((section, index) => section.title === b[index].title && section.digest === b[index].digest);
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
/** One section of the text as it reads now, and whether it differs from the baseline. */
|
|
87
|
+
export interface SectionMark {
|
|
88
|
+
/** The section, in the order the document lists it. */
|
|
89
|
+
section: PlanSection;
|
|
90
|
+
/** Whether its content differs from the baseline's section in the same place. */
|
|
91
|
+
changed: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
/**
|
|
70
|
-
* Per-section change
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
95
|
+
* Per-section change marks for a second review: every section of `current` with
|
|
96
|
+
* whether its content differs from the baseline's section in the same place.
|
|
97
|
+
*
|
|
98
|
+
* The marks carry their sections, so a caller cannot pair the flags with a list it
|
|
99
|
+
* parsed separately — the section list and its marks come out of one scan, in
|
|
100
|
+
* document order. Duplicate headings are told apart by occurrence rather than by
|
|
101
|
+
* title. An absent baseline marks nothing, which is what a first review of a plan
|
|
102
|
+
* looks like; `stripFeedback` only ever drops quote-marker lines, so the marks stay
|
|
103
|
+
* aligned with the sections a screen renders from the same content.
|
|
76
104
|
*/
|
|
77
|
-
export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string):
|
|
78
|
-
const sections =
|
|
79
|
-
if (baseline === undefined) return sections.map(() => false);
|
|
105
|
+
export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string): SectionMark[] {
|
|
106
|
+
const sections = extractSections(reviewText(current));
|
|
107
|
+
if (baseline === undefined) return sections.map((section) => ({ section, changed: false }));
|
|
80
108
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
// Same section across two reviews: `sectionKeys` tells a second `## Notes` from
|
|
110
|
+
// the first, so a duplicate heading is never compared with the wrong digest.
|
|
111
|
+
const before = new Map<string, string>();
|
|
112
|
+
sectionKeys(baseline).forEach((key, at) => {
|
|
113
|
+
const digest = baseline[at]?.digest;
|
|
114
|
+
if (digest !== undefined) before.set(key, digest);
|
|
115
|
+
});
|
|
87
116
|
|
|
88
|
-
const
|
|
89
|
-
return sections.map((section) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const prior = before.get(section.title)?.[index];
|
|
93
|
-
return prior === undefined || prior !== digestOf(section.text);
|
|
117
|
+
const keys = sectionKeys(sections);
|
|
118
|
+
return sections.map((section, at) => {
|
|
119
|
+
const prior = before.get(keys[at] ?? "");
|
|
120
|
+
return { section, changed: prior === undefined || prior !== digestOf(section.text) };
|
|
94
121
|
});
|
|
95
122
|
}
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* A plan is written to disk as markdown: one file per plan, named from the
|
|
5
5
|
* title and stamp. A refinement rewrites that file atomically, so the user can
|
|
6
6
|
* read the same artifact the model is working from and the plan survives
|
|
7
|
-
* outside the conversation without a partial write ever being visible.
|
|
8
|
-
* live under `<repo-root>/.pi/plans` (the worktree root inside a worktree), or
|
|
9
|
-
* under `<agent-dir>/plans` outside a repository. An extension tool sees the
|
|
10
|
-
* session cwd rather than the worktree the user entered, so the active worktree
|
|
11
|
-
* is applied before any path is resolved.
|
|
7
|
+
* outside the conversation without a partial write ever being visible.
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* Plans live under `<agent-dir>/plans/<repo-key>`, keyed by the repository's
|
|
10
|
+
* common git directory, so every worktree and every session of one repository
|
|
11
|
+
* reads one store — a plan approved in a worktree is the same plan from the main
|
|
12
|
+
* checkout. Outside a repository they land under `<agent-dir>/plans`. An
|
|
13
|
+
* extension tool sees the session cwd rather than the worktree the user entered,
|
|
14
|
+
* so the active worktree is applied before any path is resolved.
|
|
15
|
+
*
|
|
16
|
+
* Plans are outside the project, so they are never untracked files and never
|
|
17
|
+
* appear in a rewind snapshot. A store written by an older version, under
|
|
18
|
+
* `<repo>/.pi/plans`, is moved in once, and nothing is deleted before its plans
|
|
19
|
+
* have arrived.
|
|
16
20
|
*
|
|
17
21
|
* This module owns paths and bytes only; tool wiring lives in `tools.ts`.
|
|
18
22
|
*/
|
|
@@ -25,29 +29,38 @@ import {
|
|
|
25
29
|
readFileSync,
|
|
26
30
|
readdirSync,
|
|
27
31
|
renameSync,
|
|
32
|
+
rmdirSync,
|
|
28
33
|
statSync,
|
|
29
34
|
unlinkSync,
|
|
30
35
|
writeFileSync,
|
|
31
36
|
} from "node:fs";
|
|
32
|
-
import {
|
|
37
|
+
import { homedir } from "node:os";
|
|
38
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
33
39
|
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
34
40
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
35
|
-
import {
|
|
36
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
41
|
+
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
37
42
|
import { isInside, isInsideReal } from "../../lib/path.ts";
|
|
38
|
-
import {
|
|
43
|
+
import { repoKeyFor } from "../../lib/repo-key.ts";
|
|
44
|
+
import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
|
|
39
45
|
import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
|
|
40
46
|
|
|
41
47
|
/** Largest plan accepted, in UTF-8 bytes. */
|
|
42
48
|
export const MAX_PLAN_BYTES = 256 * 1024;
|
|
43
49
|
/** Longest slug derived from a plan title. */
|
|
44
50
|
const MAX_SLUG_LENGTH = 60;
|
|
51
|
+
/** The agent-directory entry plans are filed under. */
|
|
52
|
+
const PLANS_DIRNAME = "plans";
|
|
53
|
+
|
|
54
|
+
/** The plan store: `<agent-dir>/plans`, or `<agent-dir>/plans/<key>` inside a repository. */
|
|
55
|
+
function plansDir(agentDir: string, key?: string): string {
|
|
56
|
+
return key === undefined ? join(agentDir, PLANS_DIRNAME) : join(agentDir, PLANS_DIRNAME, key);
|
|
57
|
+
}
|
|
45
58
|
|
|
46
59
|
/** A plan read from disk. */
|
|
47
60
|
export interface StoredPlan {
|
|
48
61
|
/** Absolute path. */
|
|
49
62
|
path: string;
|
|
50
|
-
/**
|
|
63
|
+
/** How the path is shown to the user: `~`-shortened, since plans live outside the project. */
|
|
51
64
|
relativePath: string;
|
|
52
65
|
content: string;
|
|
53
66
|
bytes: number;
|
|
@@ -62,7 +75,7 @@ export interface WrittenPlan extends StoredPlan {
|
|
|
62
75
|
export interface PlanSummary {
|
|
63
76
|
/** Absolute path. */
|
|
64
77
|
path: string;
|
|
65
|
-
/**
|
|
78
|
+
/** How the path is shown to the user: `~`-shortened, since plans live outside the project. */
|
|
66
79
|
relativePath: string;
|
|
67
80
|
/** The plan's `# title`, or a title derived from the file name. */
|
|
68
81
|
title: string;
|
|
@@ -99,8 +112,20 @@ export interface PlanStore {
|
|
|
99
112
|
index(cwd: string): Promise<PlanSummary[]>;
|
|
100
113
|
/** Set the given top-level steps' checkboxes in an existing plan. Throws when the file or any step is invalid. */
|
|
101
114
|
setSteps(cwd: string, planPath: string, updates: StepUpdate[]): Promise<StoredPlan>;
|
|
102
|
-
/** Replace the
|
|
103
|
-
saveFeedback(
|
|
115
|
+
/** Replace the comments of `options.author` (the owner when absent) and keep every other writer's. */
|
|
116
|
+
saveFeedback(
|
|
117
|
+
cwd: string,
|
|
118
|
+
planPath: string,
|
|
119
|
+
annotations: PlanAnnotation[],
|
|
120
|
+
options?: { author?: string },
|
|
121
|
+
): Promise<StoredPlan>;
|
|
122
|
+
/**
|
|
123
|
+
* The line a one-time migration wants reported, or undefined.
|
|
124
|
+
*
|
|
125
|
+
* A moved store is the user's business — plans are not where they used to be —
|
|
126
|
+
* so the first caller that can show a notice takes it and clears it.
|
|
127
|
+
*/
|
|
128
|
+
takeNotice(): string | undefined;
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
/** Timestamp prefix the plan store adds to file names. */
|
|
@@ -155,16 +180,19 @@ function isContainedPlan(dir: string, path: string): boolean {
|
|
|
155
180
|
return isInsideReal(dir, dirname(path));
|
|
156
181
|
}
|
|
157
182
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Resolve a `plan_path` argument against the store.
|
|
185
|
+
*
|
|
186
|
+
* A plan lives outside the project, so a path relative to the working directory
|
|
187
|
+
* can no longer name one. Absolute paths and the `~/…` form the tools display are
|
|
188
|
+
* accepted, and a bare name resolves inside the store — which is what a reader of
|
|
189
|
+
* a tool result has in hand.
|
|
190
|
+
*/
|
|
191
|
+
function resolvePlanArg(dir: string, planPath: string): string {
|
|
192
|
+
const text = planPath.trim();
|
|
193
|
+
if (text === "~") return homedir();
|
|
194
|
+
if (text.startsWith("~/") || text.startsWith(`~${sep}`)) return resolve(homedir(), text.slice(2));
|
|
195
|
+
return isAbsolute(text) ? resolve(text) : resolve(dir, text);
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
/**
|
|
@@ -188,12 +216,6 @@ function writeAtomic(path: string, content: string): void {
|
|
|
188
216
|
}
|
|
189
217
|
}
|
|
190
218
|
|
|
191
|
-
/** `path` relative to `cwd` when inside it, else `path`. */
|
|
192
|
-
function relativeTo(cwd: string, path: string): string {
|
|
193
|
-
const rel = relative(cwd, path);
|
|
194
|
-
return rel && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) ? rel : path;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
219
|
function uniquePath(dir: string, base: string): string {
|
|
198
220
|
const taken = (candidate: string): boolean => {
|
|
199
221
|
try {
|
|
@@ -214,16 +236,69 @@ function uniquePath(dir: string, base: string): string {
|
|
|
214
236
|
return candidate;
|
|
215
237
|
}
|
|
216
238
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
/** A repository (or worktree) root is applied before any path here is resolved. */
|
|
240
|
+
|
|
241
|
+
/** How many legacy plans one migration moves, so a pathological directory cannot stall a tool call. */
|
|
242
|
+
const MAX_MIGRATED_PLANS = 200;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Move a store written by an older version, under `<repo>/.pi/plans`, into `dir`.
|
|
246
|
+
*
|
|
247
|
+
* Copy-then-unlink when a rename crosses a filesystem boundary, so a store on a
|
|
248
|
+
* different volume still arrives. Nothing here deletes before its plan has
|
|
249
|
+
* landed, a file that cannot move is left where it is, and the old directory
|
|
250
|
+
* goes only when what remains is the ignore file this package used to write.
|
|
251
|
+
*/
|
|
252
|
+
function migrateLegacyStore(dir: string, legacy: string): number {
|
|
253
|
+
if (!existsSync(legacy)) return 0;
|
|
254
|
+
let names: string[];
|
|
255
|
+
try {
|
|
256
|
+
names = readdirSync(legacy);
|
|
257
|
+
} catch {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
const plans = names.filter((name) => isPlanFileName(name)).slice(0, MAX_MIGRATED_PLANS);
|
|
261
|
+
let moved = 0;
|
|
262
|
+
for (const name of plans) {
|
|
263
|
+
const from = join(legacy, name);
|
|
264
|
+
try {
|
|
265
|
+
if (!statSync(from).isFile()) continue;
|
|
266
|
+
mkdirSync(dir, { recursive: true });
|
|
267
|
+
const to = uniquePath(dir, name.replace(/\.md$/i, ""));
|
|
268
|
+
try {
|
|
269
|
+
renameSync(from, to);
|
|
270
|
+
} catch {
|
|
271
|
+
writeFileSync(to, readFileSync(from), { flag: "wx" });
|
|
272
|
+
unlinkSync(from);
|
|
273
|
+
}
|
|
274
|
+
moved += 1;
|
|
275
|
+
} catch {
|
|
276
|
+
// A plan that cannot move stays where it is, and the directory stays with it.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
if (readdirSync(legacy).every((name) => name === ".gitignore")) {
|
|
281
|
+
const ignore = join(legacy, ".gitignore");
|
|
282
|
+
if (existsSync(ignore) && readFileSync(ignore, "utf-8") === "*\n") unlinkSync(ignore);
|
|
283
|
+
rmdirSync(legacy);
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// Leave a directory that still holds something.
|
|
287
|
+
}
|
|
288
|
+
return moved;
|
|
221
289
|
}
|
|
222
290
|
|
|
223
291
|
export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date } = {}): PlanStore {
|
|
224
292
|
const now = options.now ?? (() => new Date());
|
|
225
293
|
// One `git rev-parse` per effective root; a worktree switch changes that root.
|
|
226
|
-
const dirCache = new Map<string, string>();
|
|
294
|
+
const dirCache = new Map<string, { dir: string; root: string | undefined }>();
|
|
295
|
+
let notice: string | undefined;
|
|
296
|
+
|
|
297
|
+
const takeNotice = (): string | undefined => {
|
|
298
|
+
const line = notice;
|
|
299
|
+
notice = undefined;
|
|
300
|
+
return line;
|
|
301
|
+
};
|
|
227
302
|
|
|
228
303
|
/**
|
|
229
304
|
* The root every path here is resolved against.
|
|
@@ -231,21 +306,29 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
231
306
|
* A tool registered by an extension receives the session's cwd, not the
|
|
232
307
|
* worktree the user entered: only the built-ins are re-rooted. The worktree
|
|
233
308
|
* extension publishes its root in the environment, so this is where the two
|
|
234
|
-
* meet, and it is applied before the
|
|
235
|
-
* plan path resolution.
|
|
309
|
+
* meet, and it is applied before the repository lookup, the migration of an
|
|
310
|
+
* older store, and the plan path resolution.
|
|
236
311
|
*/
|
|
237
312
|
const baseOf = (cwd: string): string => resolveEffectiveCwd(cwd);
|
|
238
313
|
|
|
239
|
-
const
|
|
314
|
+
const resolve = async (cwd: string): Promise<{ dir: string; root: string | undefined }> => {
|
|
240
315
|
const base = baseOf(cwd);
|
|
241
316
|
const cached = dirCache.get(base);
|
|
242
317
|
if (cached) return cached;
|
|
243
|
-
const
|
|
244
|
-
const dir =
|
|
245
|
-
dirCache.set(base, dir);
|
|
246
|
-
|
|
318
|
+
const repo = await repoKeyFor(pi, base);
|
|
319
|
+
const dir = plansDir(getAgentDir(), repo.fromGit ? repo.key : undefined);
|
|
320
|
+
dirCache.set(base, { dir, root: repo.fromGit ? repo.root : undefined });
|
|
321
|
+
if (repo.fromGit) {
|
|
322
|
+
// The keyed directory is the store; a store an older version left in the
|
|
323
|
+
// checkout moves in once, so an upgrade does not strand its plans.
|
|
324
|
+
const moved = migrateLegacyStore(dir, join(base, CONFIG_DIR_NAME, "plans"));
|
|
325
|
+
if (moved > 0) notice = `Moved ${moved} plan${moved === 1 ? "" : "s"} to ${shortenPath(dir)}.`;
|
|
326
|
+
}
|
|
327
|
+
return { dir, root: repo.fromGit ? repo.root : undefined };
|
|
247
328
|
};
|
|
248
329
|
|
|
330
|
+
const dirFor = async (cwd: string): Promise<string> => (await resolve(cwd)).dir;
|
|
331
|
+
|
|
249
332
|
const write = async (cwd: string, input: WritePlanInput): Promise<WrittenPlan> => {
|
|
250
333
|
const base = baseOf(cwd);
|
|
251
334
|
const title = input.title.trim();
|
|
@@ -257,11 +340,10 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
257
340
|
|
|
258
341
|
const dir = await dirFor(base);
|
|
259
342
|
mkdirSync(dir, { recursive: true });
|
|
260
|
-
ensureIgnored(dir);
|
|
261
343
|
|
|
262
344
|
let path: string;
|
|
263
345
|
if (input.planPath) {
|
|
264
|
-
const resolved =
|
|
346
|
+
const resolved = resolvePlanArg(dir, input.planPath);
|
|
265
347
|
if (!isContainedPlan(dir, resolved)) {
|
|
266
348
|
throw new Error("plan_path must be a markdown plan file inside the plans directory.");
|
|
267
349
|
}
|
|
@@ -275,20 +357,30 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
275
357
|
throw new Error("The plan path is not writable inside the plans directory.");
|
|
276
358
|
}
|
|
277
359
|
|
|
278
|
-
|
|
279
|
-
|
|
360
|
+
// A refinement rewrites the body but not the review: every stored comment is
|
|
361
|
+
// carried onto the block whose text survived, so revising a plan under review
|
|
362
|
+
// does not silently discard what a teammate wrote.
|
|
363
|
+
const previous = existsSync(path) ? readFileSync(path, "utf-8") : undefined;
|
|
364
|
+
const content = previous === undefined ? carryFeedback("", input.content) : carryFeedback(previous, input.content);
|
|
365
|
+
const nextBytes = Buffer.byteLength(content, "utf-8");
|
|
366
|
+
if (nextBytes > MAX_PLAN_BYTES) {
|
|
367
|
+
throw new Error(`The plan is too large with its review comments (${nextBytes} bytes).`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
writeAtomic(path, content);
|
|
371
|
+
return { path, relativePath: shortenPath(path), title, content, bytes: nextBytes };
|
|
280
372
|
};
|
|
281
373
|
|
|
282
374
|
const read = async (cwd: string, planPath: string): Promise<StoredPlan | undefined> => {
|
|
283
375
|
const base = baseOf(cwd);
|
|
284
376
|
const dir = await dirFor(base);
|
|
285
|
-
const resolved =
|
|
377
|
+
const resolved = resolvePlanArg(dir, planPath);
|
|
286
378
|
if (!isContainedPlan(dir, resolved) || !existsSync(resolved)) return undefined;
|
|
287
379
|
try {
|
|
288
380
|
const content = readFileSync(resolved, "utf-8");
|
|
289
381
|
return {
|
|
290
382
|
path: resolved,
|
|
291
|
-
relativePath:
|
|
383
|
+
relativePath: shortenPath(resolved),
|
|
292
384
|
content,
|
|
293
385
|
bytes: Buffer.byteLength(content, "utf-8"),
|
|
294
386
|
};
|
|
@@ -333,14 +425,27 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
333
425
|
};
|
|
334
426
|
|
|
335
427
|
/**
|
|
336
|
-
* Write the browser review comments into an existing plan, replacing
|
|
337
|
-
*
|
|
338
|
-
*
|
|
428
|
+
* Write the browser review comments into an existing plan, replacing the
|
|
429
|
+
* comments of `options.author` (the owner when absent) and leaving every other
|
|
430
|
+
* writer's in place. The file must already be a contained plan, so this is the
|
|
431
|
+
* same path-safe write as `setSteps`.
|
|
339
432
|
*/
|
|
340
|
-
const saveFeedback = async (
|
|
433
|
+
const saveFeedback = async (
|
|
434
|
+
cwd: string,
|
|
435
|
+
planPath: string,
|
|
436
|
+
annotations: PlanAnnotation[],
|
|
437
|
+
options: { author?: string } = {},
|
|
438
|
+
): Promise<StoredPlan> => {
|
|
341
439
|
const plan = await read(cwd, planPath);
|
|
342
440
|
if (!plan) throw new Error("No plan file there; pass a plan_path written by write_plan.");
|
|
343
|
-
const
|
|
441
|
+
const author = normalizeAuthor(options.author);
|
|
442
|
+
// An untagged entry is the writer's own; one tagged with a different name is
|
|
443
|
+
// another writer's comment, so it is dropped rather than re-signed.
|
|
444
|
+
const own = annotations.filter((annotation) => {
|
|
445
|
+
const who = normalizeAuthor(annotation.author);
|
|
446
|
+
return who === "" || who === author;
|
|
447
|
+
});
|
|
448
|
+
const content = formatFeedback(plan.content, own, { author });
|
|
344
449
|
writeAtomic(plan.path, content);
|
|
345
450
|
return {
|
|
346
451
|
path: plan.path,
|
|
@@ -417,7 +522,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
417
522
|
|
|
418
523
|
const summary: PlanSummary = {
|
|
419
524
|
path,
|
|
420
|
-
relativePath:
|
|
525
|
+
relativePath: shortenPath(path),
|
|
421
526
|
title: planTitle(name),
|
|
422
527
|
modified: info.mtimeMs,
|
|
423
528
|
bytes: info.size,
|
|
@@ -448,5 +553,5 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
448
553
|
return summaries;
|
|
449
554
|
};
|
|
450
555
|
|
|
451
|
-
return { dirFor, write, read, latest, index, setSteps, saveFeedback };
|
|
556
|
+
return { dirFor, write, read, latest, index, setSteps, saveFeedback, takeNotice };
|
|
452
557
|
}
|
|
@@ -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. */
|