@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- 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 +561 -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} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- 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 +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -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} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -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 +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- 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/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- 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 -17
- 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/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- 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/job/runtime.ts +4 -4
- 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 +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- 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} +15 -14
- 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 +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- 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 +106 -251
- 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/intake.ts +245 -0
- 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/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -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/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- 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
|
@@ -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,8 +230,14 @@ 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) {
|
|
@@ -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
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { stripControlChars } from "../../lib/format.ts";
|
|
18
18
|
import { displayFeedback, stripFeedback } from "./feedback.ts";
|
|
19
|
-
import {
|
|
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;
|
|
@@ -71,7 +71,7 @@ export function planDigest(text: string): string {
|
|
|
71
71
|
|
|
72
72
|
/** One digest per heading section, in document order, for a persisted baseline. */
|
|
73
73
|
export function sectionDigests(content: string): SectionDigest[] {
|
|
74
|
-
return
|
|
74
|
+
return extractSections(reviewText(content)).map((section) => ({
|
|
75
75
|
title: section.title,
|
|
76
76
|
digest: digestOf(section.text),
|
|
77
77
|
}));
|
|
@@ -83,30 +83,40 @@ export function sameSections(a: readonly SectionDigest[], b: readonly SectionDig
|
|
|
83
83
|
return a.every((section, index) => section.title === b[index].title && section.digest === b[index].digest);
|
|
84
84
|
}
|
|
85
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
|
+
|
|
86
94
|
/**
|
|
87
|
-
* Per-section change
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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.
|
|
93
104
|
*/
|
|
94
|
-
export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string):
|
|
95
|
-
const sections =
|
|
96
|
-
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 }));
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
});
|
|
104
116
|
|
|
105
|
-
const
|
|
106
|
-
return sections.map((section) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const prior = before.get(section.title)?.[index];
|
|
110
|
-
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) };
|
|
111
121
|
});
|
|
112
122
|
}
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* This module owns paths and bytes only; tool wiring lives in `tools.ts`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { randomBytes } from "node:crypto";
|
|
25
24
|
import {
|
|
26
25
|
existsSync,
|
|
27
26
|
lstatSync,
|
|
@@ -38,9 +37,9 @@ import { homedir } from "node:os";
|
|
|
38
37
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
39
38
|
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
40
39
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
40
|
+
import { keepRevision, writeAtomic } from "./plan-files.ts";
|
|
41
41
|
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
42
42
|
import { isInside, isInsideReal } from "../../lib/path.ts";
|
|
43
|
-
import { plansDir } from "../../lib/plans/store.ts";
|
|
44
43
|
import { repoKeyFor } from "../../lib/repo-key.ts";
|
|
45
44
|
import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
|
|
46
45
|
import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
|
|
@@ -49,6 +48,13 @@ import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } f
|
|
|
49
48
|
export const MAX_PLAN_BYTES = 256 * 1024;
|
|
50
49
|
/** Longest slug derived from a plan title. */
|
|
51
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
|
+
}
|
|
52
58
|
|
|
53
59
|
/** A plan read from disk. */
|
|
54
60
|
export interface StoredPlan {
|
|
@@ -63,6 +69,11 @@ export interface StoredPlan {
|
|
|
63
69
|
/** A plan written to disk, carrying the title that named it. */
|
|
64
70
|
export interface WrittenPlan extends StoredPlan {
|
|
65
71
|
title: string;
|
|
72
|
+
/**
|
|
73
|
+
* The revision this write replaced, when it refined an existing plan: the text as the
|
|
74
|
+
* reviewer last read it, kept beside the plan.
|
|
75
|
+
*/
|
|
76
|
+
replaced?: { path: string; relativePath: string };
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
/** One row of the plan index: enough to choose a plan without opening it. */
|
|
@@ -94,13 +105,6 @@ export interface WritePlanInput {
|
|
|
94
105
|
export interface PlanStore {
|
|
95
106
|
/** Directory plans are written to for `cwd`. */
|
|
96
107
|
dirFor(cwd: string): Promise<string>;
|
|
97
|
-
/**
|
|
98
|
-
* The repository root `cwd` belongs to, or undefined outside a repository.
|
|
99
|
-
*
|
|
100
|
-
* A repository-scoped setting — the split switch — is read at the root, so a
|
|
101
|
-
* session started in a subdirectory still sees what the project turned on.
|
|
102
|
-
*/
|
|
103
|
-
rootFor(cwd: string): Promise<string | undefined>;
|
|
104
108
|
write(cwd: string, input: WritePlanInput): Promise<WrittenPlan>;
|
|
105
109
|
/** Read a plan inside the plans directory, or undefined when missing/outside. */
|
|
106
110
|
read(cwd: string, planPath: string): Promise<StoredPlan | undefined>;
|
|
@@ -196,27 +200,6 @@ function resolvePlanArg(dir: string, planPath: string): string {
|
|
|
196
200
|
return isAbsolute(text) ? resolve(text) : resolve(dir, text);
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
/**
|
|
200
|
-
* Write a plan atomically: a hidden sibling temp file, then a rename. A crash
|
|
201
|
-
* or a full disk mid-write leaves the previous file intact instead of a
|
|
202
|
-
* truncated plan, and the temp name is a dotfile, so neither `latest()` nor a
|
|
203
|
-
* plan lookup ever mistakes it for a plan.
|
|
204
|
-
*/
|
|
205
|
-
function writeAtomic(path: string, content: string): void {
|
|
206
|
-
const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
207
|
-
try {
|
|
208
|
-
writeFileSync(temp, content, { encoding: "utf-8", flag: "wx" });
|
|
209
|
-
renameSync(temp, path);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
try {
|
|
212
|
-
unlinkSync(temp);
|
|
213
|
-
} catch {
|
|
214
|
-
// The temp file was never created, or the rename already moved it.
|
|
215
|
-
}
|
|
216
|
-
throw error;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
203
|
function uniquePath(dir: string, base: string): string {
|
|
221
204
|
const taken = (candidate: string): boolean => {
|
|
222
205
|
try {
|
|
@@ -330,8 +313,6 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
330
313
|
|
|
331
314
|
const dirFor = async (cwd: string): Promise<string> => (await resolve(cwd)).dir;
|
|
332
315
|
|
|
333
|
-
const rootFor = async (cwd: string): Promise<string | undefined> => (await resolve(cwd)).root;
|
|
334
|
-
|
|
335
316
|
const write = async (cwd: string, input: WritePlanInput): Promise<WrittenPlan> => {
|
|
336
317
|
const base = baseOf(cwd);
|
|
337
318
|
const title = input.title.trim();
|
|
@@ -370,8 +351,20 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
370
351
|
throw new Error(`The plan is too large with its review comments (${nextBytes} bytes).`);
|
|
371
352
|
}
|
|
372
353
|
|
|
354
|
+
// Both are kept: the plan is what the review and the model read from now on, and the
|
|
355
|
+
// revision is what they read before this call — the only thing a diff, or a comment
|
|
356
|
+
// carried onto a block that moved, can be answered against.
|
|
357
|
+
// A refinement that changed nothing has nothing to keep: the copy would be the plan.
|
|
358
|
+
const replaced = previous === undefined || previous === content ? undefined : keepRevision(path, previous);
|
|
373
359
|
writeAtomic(path, content);
|
|
374
|
-
return {
|
|
360
|
+
return {
|
|
361
|
+
path,
|
|
362
|
+
relativePath: shortenPath(path),
|
|
363
|
+
title,
|
|
364
|
+
content,
|
|
365
|
+
bytes: nextBytes,
|
|
366
|
+
...(replaced === undefined ? {} : { replaced: { path: replaced, relativePath: shortenPath(replaced) } }),
|
|
367
|
+
};
|
|
375
368
|
};
|
|
376
369
|
|
|
377
370
|
const read = async (cwd: string, planPath: string): Promise<StoredPlan | undefined> => {
|
|
@@ -556,5 +549,5 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
556
549
|
return summaries;
|
|
557
550
|
};
|
|
558
551
|
|
|
559
|
-
return { dirFor,
|
|
552
|
+
return { dirFor, write, read, latest, index, setSteps, saveFeedback, takeNotice };
|
|
560
553
|
}
|
|
@@ -14,36 +14,49 @@
|
|
|
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
|
+
BOARD_TOOL,
|
|
21
|
+
GOAL_TOOL,
|
|
22
|
+
SUBAGENT_TOOL,
|
|
23
|
+
TODO_TOOL,
|
|
24
|
+
WEB_FETCH_TOOL,
|
|
25
|
+
WEB_SEARCH_TOOL,
|
|
26
|
+
} from "../../lib/tool-names.ts";
|
|
27
|
+
|
|
19
28
|
import { ENTER_TOOL } from "./runtime.ts";
|
|
20
29
|
|
|
21
30
|
/**
|
|
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.
|
|
31
|
+
* Tools that stay available while planning: structured readers (documents
|
|
32
|
+
* included), the trackers, the git-only `bash`, and the known web/question
|
|
33
|
+
* readers. They are also the tools the path backstop does not second-guess.
|
|
25
34
|
*/
|
|
26
35
|
export const PLAN_SAFE_TOOLS = [
|
|
27
|
-
|
|
28
|
-
"grep",
|
|
29
|
-
"find",
|
|
30
|
-
"ls",
|
|
36
|
+
...WORKSPACE_READERS,
|
|
31
37
|
TODO_TOOL,
|
|
32
38
|
GOAL_TOOL,
|
|
33
39
|
"bash",
|
|
34
40
|
ASK_USER_QUESTION_TOOL,
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
WEB_SEARCH_TOOL,
|
|
42
|
+
WEB_FETCH_TOOL,
|
|
37
43
|
];
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
|
-
* Tools that may stay active while planning.
|
|
41
|
-
* filtered out,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
46
|
+
* Tools that may stay active while planning. Two of them are here so they are not
|
|
47
|
+
* filtered out, without being in {@link PLAN_SAFE_TOOLS}: the `tool_call` guard
|
|
48
|
+
* narrows each before the path backstop would see it. `subagent` is forced
|
|
49
|
+
* read-only per call, and `board` admits only its `list` action — a model planning
|
|
50
|
+
* work should be able to read the board it is about to add cards to, and the hint
|
|
51
|
+
* cannot say that because the same tool writes. `powershell` is deliberately
|
|
52
|
+
* absent — it is default-denied like any other shell.
|
|
53
|
+
*/
|
|
54
|
+
export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL, BOARD_TOOL];
|
|
55
|
+
/**
|
|
56
|
+
* Tools always blocked while planning, even when annotated read-only.
|
|
57
|
+
* `enter_plan_mode` is denied so it stays hidden while planning; the guard exempts
|
|
58
|
+
* plan's own registration so a call already in flight can still answer "already".
|
|
44
59
|
*/
|
|
45
|
-
export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL];
|
|
46
|
-
/** Tools always blocked while planning, even when annotated read-only. */
|
|
47
60
|
const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
|
|
48
61
|
|
|
49
62
|
/** 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
|
|
|
@@ -28,7 +28,7 @@ import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
|
28
28
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
29
29
|
import { createPlanStore, type PlanStore } from "./plans.ts";
|
|
30
30
|
import { sameSections, sectionDigests, type SectionDigest } from "./plan-text.ts";
|
|
31
|
-
import type { PlanModeEntry, ReviewBaselineEntry } from "./types.ts";
|
|
31
|
+
import type { PlanModeEntry, ReviewBaselineEntry, SeededStepsEntry } from "./types.ts";
|
|
32
32
|
|
|
33
33
|
/** Tool the model calls to leave plan mode after approval. */
|
|
34
34
|
export const EXIT_TOOL = "exit_plan_mode";
|
|
@@ -44,13 +44,23 @@ export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
|
|
|
44
44
|
export const STATE_TYPE = PLAN_MODE_ENTRY_TYPE;
|
|
45
45
|
/** Custom-entry type used for a plan's review baseline. */
|
|
46
46
|
export const BASELINE_TYPE = "plan-baseline";
|
|
47
|
+
/** Custom-entry type used for the steps one approval seeded, so a re-approval can replace them. */
|
|
48
|
+
export const SEEDED_TYPE = "plan-seeded";
|
|
47
49
|
|
|
48
50
|
/** Longest plan-file basename shown in the footer chip. */
|
|
49
51
|
const STATUS_NAME_LENGTH = 28;
|
|
50
52
|
|
|
51
|
-
/** Tools plan mode activates itself
|
|
53
|
+
/** Tools plan mode activates itself. */
|
|
52
54
|
const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Tools the read-only guard exempts when this extension registered them.
|
|
58
|
+
* `enter_plan_mode` is here only so a call already in flight when plan mode came on
|
|
59
|
+
* is answered by the tool ("already in plan mode"); the policy still denies it, so
|
|
60
|
+
* it stays hidden while planning.
|
|
61
|
+
*/
|
|
62
|
+
const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
|
|
63
|
+
|
|
54
64
|
export interface PlanRuntime {
|
|
55
65
|
/** Whether plan mode is currently on. */
|
|
56
66
|
isEnabled(): boolean;
|
|
@@ -61,8 +71,8 @@ export interface PlanRuntime {
|
|
|
61
71
|
restore(ctx: ExtensionContext): void;
|
|
62
72
|
/** Plan-file storage for `write_plan` and `exit_plan_mode`. */
|
|
63
73
|
plans: PlanStore;
|
|
64
|
-
/** Whether `toolName` is
|
|
65
|
-
|
|
74
|
+
/** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
|
|
75
|
+
isOwnTool(toolName: string): boolean;
|
|
66
76
|
/** Path of the most recently written plan, for the footer chip. */
|
|
67
77
|
lastPlanPath(): string | undefined;
|
|
68
78
|
/** Remember a written plan and repaint the footer. */
|
|
@@ -71,12 +81,18 @@ export interface PlanRuntime {
|
|
|
71
81
|
reviewed(path: string): SectionDigest[] | undefined;
|
|
72
82
|
/** Remember what a review just showed, as a session entry on this branch. */
|
|
73
83
|
markReviewed(path: string, content: string): void;
|
|
84
|
+
/** The step texts the last approval of this plan appended, or undefined. */
|
|
85
|
+
seeded(path: string): string[] | undefined;
|
|
86
|
+
/** Record what this approval appended, so a re-approval can replace it. */
|
|
87
|
+
markSeeded(path: string, steps: readonly string[]): void;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
/** What the branch carries: the last plan-mode state, and a baseline per plan. */
|
|
77
91
|
interface BranchState {
|
|
78
92
|
mode: PlanModeEntry | undefined;
|
|
79
93
|
baselines: Map<string, SectionDigest[]>;
|
|
94
|
+
/** The step texts each approval appended, per plan path. */
|
|
95
|
+
seeded: Map<string, string[]>;
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
/** Well-formed `{ title, digest }` pairs from untrusted branch data, or undefined. */
|
|
@@ -98,6 +114,7 @@ function readSections(value: unknown): SectionDigest[] | undefined {
|
|
|
98
114
|
function readBranchState(ctx: ExtensionContext): BranchState {
|
|
99
115
|
let mode: PlanModeEntry | undefined;
|
|
100
116
|
const baselines = new Map<string, SectionDigest[]>();
|
|
117
|
+
const seeded = new Map<string, string[]>();
|
|
101
118
|
for (const raw of ctx.sessionManager.getBranch()) {
|
|
102
119
|
const entry = raw as { type?: string; customType?: string; data?: unknown };
|
|
103
120
|
if (entry.type !== "custom") continue;
|
|
@@ -111,6 +128,16 @@ function readBranchState(ctx: ExtensionContext): BranchState {
|
|
|
111
128
|
mode = removed ? { enabled: data.enabled, removed } : { enabled: data.enabled };
|
|
112
129
|
continue;
|
|
113
130
|
}
|
|
131
|
+
if (entry.customType === SEEDED_TYPE) {
|
|
132
|
+
const data = entry.data as { path?: unknown; steps?: unknown } | null | undefined;
|
|
133
|
+
if (typeof data?.path !== "string" || !Array.isArray(data.steps)) continue;
|
|
134
|
+
// Branch data is untrusted: keep only the strings a step can be.
|
|
135
|
+
seeded.set(
|
|
136
|
+
resolve(data.path),
|
|
137
|
+
data.steps.filter((text): text is string => typeof text === "string"),
|
|
138
|
+
);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
114
141
|
if (entry.customType !== BASELINE_TYPE) continue;
|
|
115
142
|
const data = entry.data as { path?: unknown; sections?: unknown } | null | undefined;
|
|
116
143
|
if (typeof data?.path !== "string") continue;
|
|
@@ -120,7 +147,7 @@ function readBranchState(ctx: ExtensionContext): BranchState {
|
|
|
120
147
|
// still answers a lookup for the plan it names.
|
|
121
148
|
if (sections) baselines.set(resolve(data.path), sections);
|
|
122
149
|
}
|
|
123
|
-
return { mode, baselines };
|
|
150
|
+
return { mode, baselines, seeded };
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
/** Compare two paths, resolving symlinks when possible so `/tmp` and `/private/tmp` match. */
|
|
@@ -153,17 +180,23 @@ export function createPlanRuntime(
|
|
|
153
180
|
// can mark what changed. Persisted with the branch, so `/resume` and `/tree`
|
|
154
181
|
// compare against the review this branch actually showed.
|
|
155
182
|
const reviewedPlans = new Map<string, SectionDigest[]>();
|
|
183
|
+
/** The steps each approval appended, so the next approval can replace its own leftovers. */
|
|
184
|
+
const seededPlans = new Map<string, string[]>();
|
|
156
185
|
// Tools plan mode deactivated, so disabling restores exactly those and does
|
|
157
186
|
// not re-activate a tool another extension intentionally hid.
|
|
158
187
|
let removedForPlan: string[] = [];
|
|
159
188
|
|
|
160
189
|
const plans = createPlanStore(pi);
|
|
161
190
|
|
|
162
|
-
|
|
163
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Whether `toolName` is plan's own registration, matched by the entry path the
|
|
193
|
+
* host recorded. Older hosts may not record a source; only a recorded mismatch
|
|
194
|
+
* is rejected.
|
|
195
|
+
*/
|
|
196
|
+
const isOwnTool = (toolName: string): boolean => {
|
|
197
|
+
if (!OWNED_TOOLS.includes(toolName)) return false;
|
|
164
198
|
const info = pi.getAllTools().find((tool) => tool.name === toolName);
|
|
165
199
|
const source = info?.sourceInfo?.path;
|
|
166
|
-
// Older hosts may not record a source; only reject a recorded mismatch.
|
|
167
200
|
return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
|
|
168
201
|
};
|
|
169
202
|
|
|
@@ -224,6 +257,8 @@ export function createPlanRuntime(
|
|
|
224
257
|
// never showed has no digest here, so nothing looks changed until it does.
|
|
225
258
|
reviewedPlans.clear();
|
|
226
259
|
for (const [path, sections] of branch.baselines) reviewedPlans.set(path, sections);
|
|
260
|
+
seededPlans.clear();
|
|
261
|
+
for (const [path, steps] of branch.seeded) seededPlans.set(path, steps);
|
|
227
262
|
const persisted = branch.mode;
|
|
228
263
|
// `--plan` starts a fresh session in plan mode; an explicit persisted
|
|
229
264
|
// choice (including a later disable) wins over the launch flag.
|
|
@@ -241,13 +276,19 @@ export function createPlanRuntime(
|
|
|
241
276
|
if (ctx) setStatus(ctx);
|
|
242
277
|
},
|
|
243
278
|
plans,
|
|
244
|
-
|
|
279
|
+
isOwnTool,
|
|
245
280
|
lastPlanPath: () => lastPlanPath,
|
|
246
281
|
setLastPlan: (ctx, path) => {
|
|
247
282
|
lastPlanPath = path;
|
|
248
283
|
setStatus(ctx);
|
|
249
284
|
},
|
|
250
285
|
reviewed: (path) => reviewedPlans.get(resolve(path)),
|
|
286
|
+
seeded: (path) => seededPlans.get(resolve(path)),
|
|
287
|
+
markSeeded: (path, steps) => {
|
|
288
|
+
const key = resolve(path);
|
|
289
|
+
seededPlans.set(key, [...steps]);
|
|
290
|
+
pi.appendEntry(SEEDED_TYPE, { path: key, steps: [...steps] } satisfies SeededStepsEntry);
|
|
291
|
+
},
|
|
251
292
|
markReviewed: (path, content) => {
|
|
252
293
|
const key = resolve(path);
|
|
253
294
|
const sections = sectionDigests(content);
|