@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -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 +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- 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 +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { stripControlChars } from "../../lib/format.ts";
|
|
18
|
-
import { stripFeedback } from "./feedback.ts";
|
|
18
|
+
import { displayFeedback, stripFeedback } from "./feedback.ts";
|
|
19
19
|
import { extractPlanSections } from "./steps.ts";
|
|
20
20
|
|
|
21
21
|
/** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
|
|
@@ -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,6 +60,15 @@ 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
74
|
return extractPlanSections(reviewText(content)).map((section) => ({
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -4,13 +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.
|
|
7
|
+
* outside the conversation without a partial write ever being visible.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
14
20
|
*
|
|
15
21
|
* This module owns paths and bytes only; tool wiring lives in `tools.ts`.
|
|
16
22
|
*/
|
|
@@ -23,16 +29,20 @@ import {
|
|
|
23
29
|
readFileSync,
|
|
24
30
|
readdirSync,
|
|
25
31
|
renameSync,
|
|
32
|
+
rmdirSync,
|
|
26
33
|
statSync,
|
|
27
34
|
unlinkSync,
|
|
28
35
|
writeFileSync,
|
|
29
36
|
} from "node:fs";
|
|
30
|
-
import {
|
|
37
|
+
import { homedir } from "node:os";
|
|
38
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
31
39
|
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
32
|
-
import {
|
|
33
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
40
|
+
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
41
|
+
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
34
42
|
import { isInside, isInsideReal } from "../../lib/path.ts";
|
|
35
|
-
import {
|
|
43
|
+
import { plansDir } from "../../lib/plans/store.ts";
|
|
44
|
+
import { repoKeyFor } from "../../lib/repo-key.ts";
|
|
45
|
+
import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
|
|
36
46
|
import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
|
|
37
47
|
|
|
38
48
|
/** Largest plan accepted, in UTF-8 bytes. */
|
|
@@ -44,7 +54,7 @@ const MAX_SLUG_LENGTH = 60;
|
|
|
44
54
|
export interface StoredPlan {
|
|
45
55
|
/** Absolute path. */
|
|
46
56
|
path: string;
|
|
47
|
-
/**
|
|
57
|
+
/** How the path is shown to the user: `~`-shortened, since plans live outside the project. */
|
|
48
58
|
relativePath: string;
|
|
49
59
|
content: string;
|
|
50
60
|
bytes: number;
|
|
@@ -59,7 +69,7 @@ export interface WrittenPlan extends StoredPlan {
|
|
|
59
69
|
export interface PlanSummary {
|
|
60
70
|
/** Absolute path. */
|
|
61
71
|
path: string;
|
|
62
|
-
/**
|
|
72
|
+
/** How the path is shown to the user: `~`-shortened, since plans live outside the project. */
|
|
63
73
|
relativePath: string;
|
|
64
74
|
/** The plan's `# title`, or a title derived from the file name. */
|
|
65
75
|
title: string;
|
|
@@ -84,6 +94,13 @@ export interface WritePlanInput {
|
|
|
84
94
|
export interface PlanStore {
|
|
85
95
|
/** Directory plans are written to for `cwd`. */
|
|
86
96
|
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>;
|
|
87
104
|
write(cwd: string, input: WritePlanInput): Promise<WrittenPlan>;
|
|
88
105
|
/** Read a plan inside the plans directory, or undefined when missing/outside. */
|
|
89
106
|
read(cwd: string, planPath: string): Promise<StoredPlan | undefined>;
|
|
@@ -96,8 +113,20 @@ export interface PlanStore {
|
|
|
96
113
|
index(cwd: string): Promise<PlanSummary[]>;
|
|
97
114
|
/** Set the given top-level steps' checkboxes in an existing plan. Throws when the file or any step is invalid. */
|
|
98
115
|
setSteps(cwd: string, planPath: string, updates: StepUpdate[]): Promise<StoredPlan>;
|
|
99
|
-
/** Replace the
|
|
100
|
-
saveFeedback(
|
|
116
|
+
/** Replace the comments of `options.author` (the owner when absent) and keep every other writer's. */
|
|
117
|
+
saveFeedback(
|
|
118
|
+
cwd: string,
|
|
119
|
+
planPath: string,
|
|
120
|
+
annotations: PlanAnnotation[],
|
|
121
|
+
options?: { author?: string },
|
|
122
|
+
): Promise<StoredPlan>;
|
|
123
|
+
/**
|
|
124
|
+
* The line a one-time migration wants reported, or undefined.
|
|
125
|
+
*
|
|
126
|
+
* A moved store is the user's business — plans are not where they used to be —
|
|
127
|
+
* so the first caller that can show a notice takes it and clears it.
|
|
128
|
+
*/
|
|
129
|
+
takeNotice(): string | undefined;
|
|
101
130
|
}
|
|
102
131
|
|
|
103
132
|
/** Timestamp prefix the plan store adds to file names. */
|
|
@@ -152,16 +181,19 @@ function isContainedPlan(dir: string, path: string): boolean {
|
|
|
152
181
|
return isInsideReal(dir, dirname(path));
|
|
153
182
|
}
|
|
154
183
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Resolve a `plan_path` argument against the store.
|
|
186
|
+
*
|
|
187
|
+
* A plan lives outside the project, so a path relative to the working directory
|
|
188
|
+
* can no longer name one. Absolute paths and the `~/…` form the tools display are
|
|
189
|
+
* accepted, and a bare name resolves inside the store — which is what a reader of
|
|
190
|
+
* a tool result has in hand.
|
|
191
|
+
*/
|
|
192
|
+
function resolvePlanArg(dir: string, planPath: string): string {
|
|
193
|
+
const text = planPath.trim();
|
|
194
|
+
if (text === "~") return homedir();
|
|
195
|
+
if (text.startsWith("~/") || text.startsWith(`~${sep}`)) return resolve(homedir(), text.slice(2));
|
|
196
|
+
return isAbsolute(text) ? resolve(text) : resolve(dir, text);
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
/**
|
|
@@ -185,12 +217,6 @@ function writeAtomic(path: string, content: string): void {
|
|
|
185
217
|
}
|
|
186
218
|
}
|
|
187
219
|
|
|
188
|
-
/** `path` relative to `cwd` when inside it, else `path`. */
|
|
189
|
-
function relativeTo(cwd: string, path: string): string {
|
|
190
|
-
const rel = relative(cwd, path);
|
|
191
|
-
return rel && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) ? rel : path;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
220
|
function uniquePath(dir: string, base: string): string {
|
|
195
221
|
const taken = (candidate: string): boolean => {
|
|
196
222
|
try {
|
|
@@ -211,27 +237,103 @@ function uniquePath(dir: string, base: string): string {
|
|
|
211
237
|
return candidate;
|
|
212
238
|
}
|
|
213
239
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
/** A repository (or worktree) root is applied before any path here is resolved. */
|
|
241
|
+
|
|
242
|
+
/** How many legacy plans one migration moves, so a pathological directory cannot stall a tool call. */
|
|
243
|
+
const MAX_MIGRATED_PLANS = 200;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Move a store written by an older version, under `<repo>/.pi/plans`, into `dir`.
|
|
247
|
+
*
|
|
248
|
+
* Copy-then-unlink when a rename crosses a filesystem boundary, so a store on a
|
|
249
|
+
* different volume still arrives. Nothing here deletes before its plan has
|
|
250
|
+
* landed, a file that cannot move is left where it is, and the old directory
|
|
251
|
+
* goes only when what remains is the ignore file this package used to write.
|
|
252
|
+
*/
|
|
253
|
+
function migrateLegacyStore(dir: string, legacy: string): number {
|
|
254
|
+
if (!existsSync(legacy)) return 0;
|
|
255
|
+
let names: string[];
|
|
256
|
+
try {
|
|
257
|
+
names = readdirSync(legacy);
|
|
258
|
+
} catch {
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
const plans = names.filter((name) => isPlanFileName(name)).slice(0, MAX_MIGRATED_PLANS);
|
|
262
|
+
let moved = 0;
|
|
263
|
+
for (const name of plans) {
|
|
264
|
+
const from = join(legacy, name);
|
|
265
|
+
try {
|
|
266
|
+
if (!statSync(from).isFile()) continue;
|
|
267
|
+
mkdirSync(dir, { recursive: true });
|
|
268
|
+
const to = uniquePath(dir, name.replace(/\.md$/i, ""));
|
|
269
|
+
try {
|
|
270
|
+
renameSync(from, to);
|
|
271
|
+
} catch {
|
|
272
|
+
writeFileSync(to, readFileSync(from), { flag: "wx" });
|
|
273
|
+
unlinkSync(from);
|
|
274
|
+
}
|
|
275
|
+
moved += 1;
|
|
276
|
+
} catch {
|
|
277
|
+
// A plan that cannot move stays where it is, and the directory stays with it.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
if (readdirSync(legacy).every((name) => name === ".gitignore")) {
|
|
282
|
+
const ignore = join(legacy, ".gitignore");
|
|
283
|
+
if (existsSync(ignore) && readFileSync(ignore, "utf-8") === "*\n") unlinkSync(ignore);
|
|
284
|
+
rmdirSync(legacy);
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Leave a directory that still holds something.
|
|
288
|
+
}
|
|
289
|
+
return moved;
|
|
218
290
|
}
|
|
219
291
|
|
|
220
292
|
export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date } = {}): PlanStore {
|
|
221
293
|
const now = options.now ?? (() => new Date());
|
|
222
|
-
// One `git rev-parse` per
|
|
223
|
-
const dirCache = new Map<string, string>();
|
|
294
|
+
// One `git rev-parse` per effective root; a worktree switch changes that root.
|
|
295
|
+
const dirCache = new Map<string, { dir: string; root: string | undefined }>();
|
|
296
|
+
let notice: string | undefined;
|
|
297
|
+
|
|
298
|
+
const takeNotice = (): string | undefined => {
|
|
299
|
+
const line = notice;
|
|
300
|
+
notice = undefined;
|
|
301
|
+
return line;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The root every path here is resolved against.
|
|
306
|
+
*
|
|
307
|
+
* A tool registered by an extension receives the session's cwd, not the
|
|
308
|
+
* worktree the user entered: only the built-ins are re-rooted. The worktree
|
|
309
|
+
* extension publishes its root in the environment, so this is where the two
|
|
310
|
+
* meet, and it is applied before the repository lookup, the migration of an
|
|
311
|
+
* older store, and the plan path resolution.
|
|
312
|
+
*/
|
|
313
|
+
const baseOf = (cwd: string): string => resolveEffectiveCwd(cwd);
|
|
224
314
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
315
|
+
const resolve = async (cwd: string): Promise<{ dir: string; root: string | undefined }> => {
|
|
316
|
+
const base = baseOf(cwd);
|
|
317
|
+
const cached = dirCache.get(base);
|
|
227
318
|
if (cached) return cached;
|
|
228
|
-
const
|
|
229
|
-
const dir =
|
|
230
|
-
dirCache.set(
|
|
231
|
-
|
|
319
|
+
const repo = await repoKeyFor(pi, base);
|
|
320
|
+
const dir = plansDir(getAgentDir(), repo.fromGit ? repo.key : undefined);
|
|
321
|
+
dirCache.set(base, { dir, root: repo.fromGit ? repo.root : undefined });
|
|
322
|
+
if (repo.fromGit) {
|
|
323
|
+
// The keyed directory is the store; a store an older version left in the
|
|
324
|
+
// checkout moves in once, so an upgrade does not strand its plans.
|
|
325
|
+
const moved = migrateLegacyStore(dir, join(base, CONFIG_DIR_NAME, "plans"));
|
|
326
|
+
if (moved > 0) notice = `Moved ${moved} plan${moved === 1 ? "" : "s"} to ${shortenPath(dir)}.`;
|
|
327
|
+
}
|
|
328
|
+
return { dir, root: repo.fromGit ? repo.root : undefined };
|
|
232
329
|
};
|
|
233
330
|
|
|
331
|
+
const dirFor = async (cwd: string): Promise<string> => (await resolve(cwd)).dir;
|
|
332
|
+
|
|
333
|
+
const rootFor = async (cwd: string): Promise<string | undefined> => (await resolve(cwd)).root;
|
|
334
|
+
|
|
234
335
|
const write = async (cwd: string, input: WritePlanInput): Promise<WrittenPlan> => {
|
|
336
|
+
const base = baseOf(cwd);
|
|
235
337
|
const title = input.title.trim();
|
|
236
338
|
if (!title) throw new Error("A plan title is required.");
|
|
237
339
|
const bytes = Buffer.byteLength(input.content, "utf-8");
|
|
@@ -239,13 +341,12 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
239
341
|
throw new Error(`The plan is too large (${bytes} bytes; the limit is ${MAX_PLAN_BYTES}).`);
|
|
240
342
|
}
|
|
241
343
|
|
|
242
|
-
const dir = await dirFor(
|
|
344
|
+
const dir = await dirFor(base);
|
|
243
345
|
mkdirSync(dir, { recursive: true });
|
|
244
|
-
ensureIgnored(dir);
|
|
245
346
|
|
|
246
347
|
let path: string;
|
|
247
348
|
if (input.planPath) {
|
|
248
|
-
const resolved =
|
|
349
|
+
const resolved = resolvePlanArg(dir, input.planPath);
|
|
249
350
|
if (!isContainedPlan(dir, resolved)) {
|
|
250
351
|
throw new Error("plan_path must be a markdown plan file inside the plans directory.");
|
|
251
352
|
}
|
|
@@ -259,19 +360,30 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
259
360
|
throw new Error("The plan path is not writable inside the plans directory.");
|
|
260
361
|
}
|
|
261
362
|
|
|
262
|
-
|
|
263
|
-
|
|
363
|
+
// A refinement rewrites the body but not the review: every stored comment is
|
|
364
|
+
// carried onto the block whose text survived, so revising a plan under review
|
|
365
|
+
// does not silently discard what a teammate wrote.
|
|
366
|
+
const previous = existsSync(path) ? readFileSync(path, "utf-8") : undefined;
|
|
367
|
+
const content = previous === undefined ? carryFeedback("", input.content) : carryFeedback(previous, input.content);
|
|
368
|
+
const nextBytes = Buffer.byteLength(content, "utf-8");
|
|
369
|
+
if (nextBytes > MAX_PLAN_BYTES) {
|
|
370
|
+
throw new Error(`The plan is too large with its review comments (${nextBytes} bytes).`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
writeAtomic(path, content);
|
|
374
|
+
return { path, relativePath: shortenPath(path), title, content, bytes: nextBytes };
|
|
264
375
|
};
|
|
265
376
|
|
|
266
377
|
const read = async (cwd: string, planPath: string): Promise<StoredPlan | undefined> => {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
378
|
+
const base = baseOf(cwd);
|
|
379
|
+
const dir = await dirFor(base);
|
|
380
|
+
const resolved = resolvePlanArg(dir, planPath);
|
|
269
381
|
if (!isContainedPlan(dir, resolved) || !existsSync(resolved)) return undefined;
|
|
270
382
|
try {
|
|
271
383
|
const content = readFileSync(resolved, "utf-8");
|
|
272
384
|
return {
|
|
273
385
|
path: resolved,
|
|
274
|
-
relativePath:
|
|
386
|
+
relativePath: shortenPath(resolved),
|
|
275
387
|
content,
|
|
276
388
|
bytes: Buffer.byteLength(content, "utf-8"),
|
|
277
389
|
};
|
|
@@ -316,14 +428,27 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
316
428
|
};
|
|
317
429
|
|
|
318
430
|
/**
|
|
319
|
-
* Write the browser review comments into an existing plan, replacing
|
|
320
|
-
*
|
|
321
|
-
*
|
|
431
|
+
* Write the browser review comments into an existing plan, replacing the
|
|
432
|
+
* comments of `options.author` (the owner when absent) and leaving every other
|
|
433
|
+
* writer's in place. The file must already be a contained plan, so this is the
|
|
434
|
+
* same path-safe write as `setSteps`.
|
|
322
435
|
*/
|
|
323
|
-
const saveFeedback = async (
|
|
436
|
+
const saveFeedback = async (
|
|
437
|
+
cwd: string,
|
|
438
|
+
planPath: string,
|
|
439
|
+
annotations: PlanAnnotation[],
|
|
440
|
+
options: { author?: string } = {},
|
|
441
|
+
): Promise<StoredPlan> => {
|
|
324
442
|
const plan = await read(cwd, planPath);
|
|
325
443
|
if (!plan) throw new Error("No plan file there; pass a plan_path written by write_plan.");
|
|
326
|
-
const
|
|
444
|
+
const author = normalizeAuthor(options.author);
|
|
445
|
+
// An untagged entry is the writer's own; one tagged with a different name is
|
|
446
|
+
// another writer's comment, so it is dropped rather than re-signed.
|
|
447
|
+
const own = annotations.filter((annotation) => {
|
|
448
|
+
const who = normalizeAuthor(annotation.author);
|
|
449
|
+
return who === "" || who === author;
|
|
450
|
+
});
|
|
451
|
+
const content = formatFeedback(plan.content, own, { author });
|
|
327
452
|
writeAtomic(plan.path, content);
|
|
328
453
|
return {
|
|
329
454
|
path: plan.path,
|
|
@@ -375,7 +500,8 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
375
500
|
* dotfile, or a file that vanishes mid-walk is simply not a plan.
|
|
376
501
|
*/
|
|
377
502
|
const index = async (cwd: string): Promise<PlanSummary[]> => {
|
|
378
|
-
const
|
|
503
|
+
const base = baseOf(cwd);
|
|
504
|
+
const dir = await dirFor(base);
|
|
379
505
|
let names: string[];
|
|
380
506
|
try {
|
|
381
507
|
names = readdirSync(dir);
|
|
@@ -399,7 +525,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
399
525
|
|
|
400
526
|
const summary: PlanSummary = {
|
|
401
527
|
path,
|
|
402
|
-
relativePath:
|
|
528
|
+
relativePath: shortenPath(path),
|
|
403
529
|
title: planTitle(name),
|
|
404
530
|
modified: info.mtimeMs,
|
|
405
531
|
bytes: info.size,
|
|
@@ -430,5 +556,5 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
430
556
|
return summaries;
|
|
431
557
|
};
|
|
432
558
|
|
|
433
|
-
return { dirFor, write, read, latest, index, setSteps, saveFeedback };
|
|
559
|
+
return { dirFor, rootFor, write, read, latest, index, setSteps, saveFeedback, takeNotice };
|
|
434
560
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rendering and parsing for the compact remote plan review.
|
|
3
|
+
*
|
|
4
|
+
* The prompt and its reply share one action table, so changing a number or a
|
|
5
|
+
* localized label cannot leave the parser behind. File delivery and the review
|
|
6
|
+
* lifecycle stay in `review.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { InteractionLanguage } from "../../lib/interaction.ts";
|
|
10
|
+
import { extractPlanSteps } from "./steps.ts";
|
|
11
|
+
|
|
12
|
+
/** The actions the compact prompt can resolve without a refinement. */
|
|
13
|
+
export type PlanReviewAction = "approve" | "keep" | "cancel";
|
|
14
|
+
|
|
15
|
+
/** One editor reply: an action, or free text the model should apply. */
|
|
16
|
+
export type PlanReviewReply =
|
|
17
|
+
| { action: "approve" }
|
|
18
|
+
| { action: "keep" }
|
|
19
|
+
| { action: "cancel" }
|
|
20
|
+
| { action: "refine"; refinement: string };
|
|
21
|
+
|
|
22
|
+
interface ReviewAction {
|
|
23
|
+
key: string;
|
|
24
|
+
action: PlanReviewAction;
|
|
25
|
+
zh: string;
|
|
26
|
+
en: string;
|
|
27
|
+
words: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The action line and the parser read this one table, in display order. */
|
|
31
|
+
const REVIEW_ACTIONS: readonly ReviewAction[] = [
|
|
32
|
+
{ key: "1", action: "approve", zh: "批准", en: "approve", words: ["approve", "批准", "同意"] },
|
|
33
|
+
{
|
|
34
|
+
key: "2",
|
|
35
|
+
action: "keep",
|
|
36
|
+
zh: "继续规划",
|
|
37
|
+
en: "keep planning",
|
|
38
|
+
words: ["keep", "keep planning", "继续", "继续规划"],
|
|
39
|
+
},
|
|
40
|
+
{ key: "3", action: "cancel", zh: "退出", en: "exit", words: ["exit", "cancel", "quit", "退出", "取消"] },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** How much of one step the remote digest shows. */
|
|
44
|
+
const DIGEST_STEP_CHARS = 90;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One compact remote prompt: title, first steps, mode status, and the action
|
|
48
|
+
* line. The plan file is sent before it; a non-action reply refines the plan.
|
|
49
|
+
*/
|
|
50
|
+
export function planReviewPrompt(content: string, language: InteractionLanguage, attached: boolean): string {
|
|
51
|
+
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
|
|
52
|
+
const steps = extractPlanSteps(content);
|
|
53
|
+
const count = steps.length;
|
|
54
|
+
const head =
|
|
55
|
+
language === "zh"
|
|
56
|
+
? `计划${title ? `:${title}` : ""}${count > 0 ? `(${count} 步)` : ""}`
|
|
57
|
+
: `Plan${title ? `: ${title}` : ""}${count > 0 ? ` (${count} step${count === 1 ? "" : "s"})` : ""}`;
|
|
58
|
+
const preview = steps.slice(0, 3).map((step, index) => {
|
|
59
|
+
const text = step.content.replace(/\s+/g, " ").trim();
|
|
60
|
+
const shown = text.length > DIGEST_STEP_CHARS ? `${text.slice(0, DIGEST_STEP_CHARS - 1)}…` : text;
|
|
61
|
+
return `${index + 1}. ${shown}`;
|
|
62
|
+
});
|
|
63
|
+
const more =
|
|
64
|
+
count > preview.length
|
|
65
|
+
? language === "zh"
|
|
66
|
+
? `… 还有 ${count - preview.length} 步`
|
|
67
|
+
: `… (${count - preview.length} more)`
|
|
68
|
+
: "";
|
|
69
|
+
const status = language === "zh" ? "Plan 模式开启,写入已关闭。" : "Plan mode is on — write access is off.";
|
|
70
|
+
const attachment = attached ? (language === "zh" ? "计划已在上方。" : "The plan is above.") : "";
|
|
71
|
+
const choices = REVIEW_ACTIONS.map((action) => `${action.key} ${action[language]}`).join(
|
|
72
|
+
language === "zh" ? "," : ", ",
|
|
73
|
+
);
|
|
74
|
+
const actions =
|
|
75
|
+
language === "zh"
|
|
76
|
+
? `回复 ${choices};或直接输入修改意见(想做别的先退出或批准)。`
|
|
77
|
+
: `Reply ${choices} — or type feedback to revise. To ask something else, exit or approve first.`;
|
|
78
|
+
return [head, ...preview, more, status, attachment, actions].filter((line) => line !== "").join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse the one editor reply. Only an exact number or action word is an action;
|
|
83
|
+
* any other non-empty text is a refinement request.
|
|
84
|
+
*/
|
|
85
|
+
export function parsePlanReviewReply(raw: string): PlanReviewReply {
|
|
86
|
+
const text = raw.trim();
|
|
87
|
+
if (text === "") return { action: "cancel" };
|
|
88
|
+
const key = text.toLowerCase();
|
|
89
|
+
for (const action of REVIEW_ACTIONS) {
|
|
90
|
+
if (key === action.key || action.words.includes(key)) return { action: action.action };
|
|
91
|
+
}
|
|
92
|
+
return { action: "refine", refinement: text };
|
|
93
|
+
}
|