@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
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The plan review gate: ask the human about a plan file and return one decision.
|
|
3
3
|
*
|
|
4
|
-
* A review has three presentations — the scrollable TUI screen, a
|
|
5
|
-
* editor
|
|
6
|
-
*
|
|
4
|
+
* A review has three presentations — the scrollable TUI screen, a dialog-only
|
|
5
|
+
* editor, and a remote turn's compact editor prompt — and one optional browser
|
|
6
|
+
* server. This module owns the loop that switches between
|
|
7
7
|
* them, so `exit_plan_mode` and `/plan review` share one lifecycle and only map
|
|
8
8
|
* the returned {@link ReviewDecision} to their own result.
|
|
9
9
|
*
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
* signal reaches the browser server so an aborted turn closes it.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
16
15
|
import { basename } from "node:path";
|
|
17
|
-
import {
|
|
16
|
+
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
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
20
|
import type { PlanStore, StoredPlan } from "./plans.ts";
|
|
21
21
|
import type { SectionDigest } from "./plan-text.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { planReviewPrompt, parsePlanReviewReply } from "./review-prompt.ts";
|
|
23
23
|
import { PlanViewComponent, type PlanViewAction } from "./tui.ts";
|
|
24
24
|
import type { PlanReviewResult, PlanReviewSession, PlanWebReview } from "./web.ts";
|
|
25
25
|
|
|
@@ -29,9 +29,6 @@ interface PlanReviewChoice {
|
|
|
29
29
|
refinement?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** A dialog-only choice: post the full plan text, then ask again. Never returned. */
|
|
33
|
-
type ReviewChoice = PlanReviewChoice | { action: "send" };
|
|
34
|
-
|
|
35
32
|
/** What a review decided, and the plan state it decided on. */
|
|
36
33
|
export type ReviewDecision =
|
|
37
34
|
| { action: "approve"; plan: StoredPlan; comments: PlanAnnotation[] }
|
|
@@ -78,98 +75,45 @@ const REFINE_PROMPT = "Refine the plan — what should change? (Enter to save, E
|
|
|
78
75
|
*/
|
|
79
76
|
export const PLAN_REVIEW_TIMEOUT_MS = 600_000;
|
|
80
77
|
|
|
81
|
-
/** How much of one step the remote digest shows. */
|
|
82
|
-
const DIGEST_STEP_CHARS = 90;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* A one-message digest of the plan for a remote review.
|
|
86
|
-
*
|
|
87
|
-
* The file is the primary artifact, but a phone client may never render or open
|
|
88
|
-
* it, and the peer still has to choose approve / refine. The digest carries the
|
|
89
|
-
* title, the step count, and the first few steps; the full text follows as a
|
|
90
|
-
* file or as the fallback message.
|
|
91
|
-
*/
|
|
92
|
-
function planReviewDigest(content: string): string {
|
|
93
|
-
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
|
|
94
|
-
const steps = extractPlanSteps(content);
|
|
95
|
-
const count = steps.length;
|
|
96
|
-
const head = `Plan${title ? `: ${title}` : ""}${count > 0 ? ` (${count} step${count === 1 ? "" : "s"})` : ""}`;
|
|
97
|
-
const preview = steps.slice(0, 3).map((step, index) => {
|
|
98
|
-
const text = step.content.replace(/\s+/g, " ").trim();
|
|
99
|
-
const shown = text.length > DIGEST_STEP_CHARS ? `${text.slice(0, DIGEST_STEP_CHARS - 1)}…` : text;
|
|
100
|
-
return `${index + 1}. ${shown}`;
|
|
101
|
-
});
|
|
102
|
-
const more = count > preview.length ? `… (${count - preview.length} more)` : "";
|
|
103
|
-
return [
|
|
104
|
-
head,
|
|
105
|
-
...preview,
|
|
106
|
-
more,
|
|
107
|
-
"Plan mode is on — write access is off. Choose “Exit plan mode” to leave without approving.",
|
|
108
|
-
"The full plan is attached above, or sent as text if the file failed.",
|
|
109
|
-
]
|
|
110
|
-
.filter((line) => line !== "")
|
|
111
|
-
.join("\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
78
|
/**
|
|
115
|
-
* Ask the user to review the plan file. The TUI opens the scrollable
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* first. Asking for the full plan re-enters with the text only, then shows the
|
|
119
|
-
* choices again.
|
|
79
|
+
* Ask the user to review the plan file. The TUI opens the scrollable screen;
|
|
80
|
+
* dialog-capable modes (RPC/WeChat) get one editor prompt. A reply that is not an
|
|
81
|
+
* action is a refinement.
|
|
120
82
|
*/
|
|
121
83
|
async function askPlanReview(
|
|
122
84
|
ctx: ExtensionToolContext,
|
|
123
85
|
plan: StoredPlan,
|
|
124
86
|
baseline: SectionDigest[] | undefined,
|
|
125
87
|
): Promise<PlanReviewChoice> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (choice === "Refine the plan") return { action: "refine" };
|
|
156
|
-
if (choice === "Keep planning (stay in plan mode)") return { action: "keep" };
|
|
157
|
-
// Dismissing the plan gate means leaving plan mode, not silently staying
|
|
158
|
-
// in it. The explicit Keep choice above is the only way to stay.
|
|
159
|
-
return { action: "cancel" };
|
|
160
|
-
},
|
|
161
|
-
// A remote turn gets the plan file up front, falling back to the full text;
|
|
162
|
-
// a local TUI turn ignores both and renders the component. The digest goes
|
|
163
|
-
// out either way so the choices stay answerable when a phone client does not
|
|
164
|
-
// render the file bubble.
|
|
165
|
-
file: sentFullText ? undefined : { path: plan.path, name: basename(plan.relativePath) },
|
|
166
|
-
body: plan.content,
|
|
167
|
-
note: sentFullText ? undefined : planReviewDigest(plan.content),
|
|
168
|
-
timeoutMs: PLAN_REVIEW_TIMEOUT_MS,
|
|
169
|
-
});
|
|
170
|
-
if (outcome.action !== "send") return outcome;
|
|
171
|
-
sentFullText = true;
|
|
172
|
-
}
|
|
88
|
+
const language = remoteLanguage(ctx) ?? "en";
|
|
89
|
+
const remote = isRemoteTurn(ctx);
|
|
90
|
+
return askHuman<PlanReviewChoice>(ctx, {
|
|
91
|
+
custom: async () => {
|
|
92
|
+
const choice = await ctx.ui.custom<PlanReviewChoice | undefined>(
|
|
93
|
+
(tui, theme, _keybindings, done) =>
|
|
94
|
+
new PlanViewComponent({
|
|
95
|
+
plan,
|
|
96
|
+
baseline,
|
|
97
|
+
theme,
|
|
98
|
+
onClose: (action, refinement) => done({ action, refinement }),
|
|
99
|
+
requestRender: () => tui.requestRender(),
|
|
100
|
+
viewportRows: () => tui.terminal?.rows,
|
|
101
|
+
tui,
|
|
102
|
+
}),
|
|
103
|
+
FULL_SCREEN_OVERLAY,
|
|
104
|
+
);
|
|
105
|
+
return choice ?? { action: "keep" };
|
|
106
|
+
},
|
|
107
|
+
dialogs: async () => {
|
|
108
|
+
const answer = await ctx.ui.editor(planReviewPrompt(plan.content, language, remote), "");
|
|
109
|
+
return parsePlanReviewReply(answer ?? "");
|
|
110
|
+
},
|
|
111
|
+
// The full plan is the artifact under review, so it goes out before the
|
|
112
|
+
// choices; if the upload fails, the same text is the fallback.
|
|
113
|
+
file: { path: plan.path, name: basename(plan.relativePath) },
|
|
114
|
+
body: plan.content,
|
|
115
|
+
timeoutMs: PLAN_REVIEW_TIMEOUT_MS,
|
|
116
|
+
});
|
|
173
117
|
}
|
|
174
118
|
|
|
175
119
|
/** How a browser review may decide, and what to report when it opens. */
|
|
@@ -182,6 +126,11 @@ export interface BrowserReviewOptions {
|
|
|
182
126
|
signal?: AbortSignal;
|
|
183
127
|
/** Called once the server is listening and the browser was opened. */
|
|
184
128
|
onOpened?: (session: PlanReviewSession) => void;
|
|
129
|
+
/**
|
|
130
|
+
* Called when a shared reviewer's post lands, so the owner learns about it where
|
|
131
|
+
* they are waiting: the page updates itself, the terminal has to be told.
|
|
132
|
+
*/
|
|
133
|
+
onReviewerComment?: (author: string, count: number) => void;
|
|
185
134
|
}
|
|
186
135
|
|
|
187
136
|
/**
|
|
@@ -196,7 +145,12 @@ export async function runBrowserReview(
|
|
|
196
145
|
): Promise<PlanReviewResult | undefined> {
|
|
197
146
|
if (options.signal?.aborted) return { status: "cancelled" };
|
|
198
147
|
const session = await web
|
|
199
|
-
.start(cwd, plan, {
|
|
148
|
+
.start(cwd, plan, {
|
|
149
|
+
canApprove: options.canApprove,
|
|
150
|
+
baseline: options.baseline,
|
|
151
|
+
signal: options.signal,
|
|
152
|
+
...(options.onReviewerComment === undefined ? {} : { onReviewerComment: options.onReviewerComment }),
|
|
153
|
+
})
|
|
200
154
|
.catch(() => undefined);
|
|
201
155
|
if (!session) return undefined;
|
|
202
156
|
options.onOpened?.(session);
|
|
@@ -242,6 +196,8 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
|
|
|
242
196
|
baseline,
|
|
243
197
|
signal,
|
|
244
198
|
onOpened: (session) => ctx.ui.notify(`Plan review: opened ${session.url}`, "info"),
|
|
199
|
+
onReviewerComment: (author, count) =>
|
|
200
|
+
ctx.ui.notify(`Plan review: @${author} posted ${count === 1 ? "a comment" : `${count} comments`}.`, "info"),
|
|
245
201
|
});
|
|
246
202
|
if (!result) {
|
|
247
203
|
ctx.ui.notify("Plan review: could not start the browser review server.", "error");
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* Enabling plan mode removes every tool the policy does not consider read-only
|
|
5
5
|
* from the active set and activates the plan-mode control tools: `exit_plan_mode`
|
|
6
6
|
* (present the plan for approval) and `write_plan` (save the plan artifact under
|
|
7
|
-
*
|
|
7
|
+
* the plans directory). Disabling restores exactly what it hid. The enabled flag is
|
|
8
8
|
* persisted as a custom session entry so `/resume` and `/tree` follow the
|
|
9
9
|
* branch, and it is also mirrored into the footer status. The review baseline
|
|
10
10
|
* travels the same way, as one digest per heading section, so the changed-section
|
|
11
11
|
* markers survive a resume too.
|
|
12
12
|
*
|
|
13
13
|
* `write_plan` is the one sanctioned write while planning. The read-only guard
|
|
14
|
-
* in `index.ts` permits
|
|
15
|
-
* extension's
|
|
14
|
+
* in `index.ts` permits plan's own tools only when the registered tool is this
|
|
15
|
+
* extension's (matched by source path), so a same-named tool from another
|
|
16
16
|
* extension cannot borrow the exemption.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -48,9 +48,17 @@ export const BASELINE_TYPE = "plan-baseline";
|
|
|
48
48
|
/** Longest plan-file basename shown in the footer chip. */
|
|
49
49
|
const STATUS_NAME_LENGTH = 28;
|
|
50
50
|
|
|
51
|
-
/** Tools plan mode activates itself
|
|
51
|
+
/** Tools plan mode activates itself. */
|
|
52
52
|
const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Tools the read-only guard exempts when this extension registered them.
|
|
56
|
+
* `enter_plan_mode` is here only so a call already in flight when plan mode came on
|
|
57
|
+
* is answered by the tool ("already in plan mode"); the policy still denies it, so
|
|
58
|
+
* it stays hidden while planning.
|
|
59
|
+
*/
|
|
60
|
+
const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
|
|
61
|
+
|
|
54
62
|
export interface PlanRuntime {
|
|
55
63
|
/** Whether plan mode is currently on. */
|
|
56
64
|
isEnabled(): boolean;
|
|
@@ -61,8 +69,8 @@ export interface PlanRuntime {
|
|
|
61
69
|
restore(ctx: ExtensionContext): void;
|
|
62
70
|
/** Plan-file storage for `write_plan` and `exit_plan_mode`. */
|
|
63
71
|
plans: PlanStore;
|
|
64
|
-
/** Whether `toolName` is
|
|
65
|
-
|
|
72
|
+
/** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
|
|
73
|
+
isOwnTool(toolName: string): boolean;
|
|
66
74
|
/** Path of the most recently written plan, for the footer chip. */
|
|
67
75
|
lastPlanPath(): string | undefined;
|
|
68
76
|
/** Remember a written plan and repaint the footer. */
|
|
@@ -159,11 +167,15 @@ export function createPlanRuntime(
|
|
|
159
167
|
|
|
160
168
|
const plans = createPlanStore(pi);
|
|
161
169
|
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Whether `toolName` is plan's own registration, matched by the entry path the
|
|
172
|
+
* host recorded. Older hosts may not record a source; only a recorded mismatch
|
|
173
|
+
* is rejected.
|
|
174
|
+
*/
|
|
175
|
+
const isOwnTool = (toolName: string): boolean => {
|
|
176
|
+
if (!OWNED_TOOLS.includes(toolName)) return false;
|
|
164
177
|
const info = pi.getAllTools().find((tool) => tool.name === toolName);
|
|
165
178
|
const source = info?.sourceInfo?.path;
|
|
166
|
-
// Older hosts may not record a source; only reject a recorded mismatch.
|
|
167
179
|
return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
|
|
168
180
|
};
|
|
169
181
|
|
|
@@ -241,7 +253,7 @@ export function createPlanRuntime(
|
|
|
241
253
|
if (ctx) setStatus(ctx);
|
|
242
254
|
},
|
|
243
255
|
plans,
|
|
244
|
-
|
|
256
|
+
isOwnTool,
|
|
245
257
|
lastPlanPath: () => lastPlanPath,
|
|
246
258
|
setLastPlan: (ctx, path) => {
|
|
247
259
|
lastPlanPath = path;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-document sections (pure).
|
|
3
|
+
*
|
|
4
|
+
* One fence-aware scan of a plan's markdown: the review screen navigates by
|
|
5
|
+
* section, `set_plan_steps` looks up `## Steps`, a carried comment finds the
|
|
6
|
+
* block it belongs to, and the browser outline lists the same headings. A second
|
|
7
|
+
* scanner would disagree about a heading inside a fence, so every reader goes
|
|
8
|
+
* through this module.
|
|
9
|
+
*
|
|
10
|
+
* The scan is deliberately smaller than CommonMark: a heading is up to three
|
|
11
|
+
* leading spaces, a space after the hashes, and an optional closing run; a fence
|
|
12
|
+
* is three backticks or tildes anywhere a fence may start. Inside a fence
|
|
13
|
+
* nothing is a heading, which is what keeps a `# comment` in a shell example out
|
|
14
|
+
* of the outline. Every section keeps its raw source lines, so a caller can edit
|
|
15
|
+
* exactly what it read.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A heading-delimited chunk of a plan, for navigation and in-place edits. */
|
|
19
|
+
export interface PlanSection {
|
|
20
|
+
/** Plain heading text, or `""` for the content before the first heading. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** The section's markdown, including its heading line. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** 0-based source line of the section's heading (0 for the preamble). */
|
|
25
|
+
line: number;
|
|
26
|
+
/** Heading level, or 0 for the preamble before the first heading. */
|
|
27
|
+
level: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A markdown heading outside a fence, capturing its level and text: up to three
|
|
32
|
+
* leading spaces, a space after the hashes, and a closing hash run only when a
|
|
33
|
+
* space separates it. Shared with the block model, so the section split and the
|
|
34
|
+
* browser outline read a heading line the same way, and `## C#` keeps its title.
|
|
35
|
+
*/
|
|
36
|
+
export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A markdown code-fence opener or closer (backticks or tildes). Shared with the
|
|
40
|
+
* block model: a tab-indented fence is prose for both parsers, never code for one
|
|
41
|
+
* and prose for the other.
|
|
42
|
+
*/
|
|
43
|
+
export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A heading or step label as the review surfaces compare it: inline emphasis and
|
|
47
|
+
* code removed, whitespace collapsed. Exported so a renderer labels an outline
|
|
48
|
+
* entry the same way {@link extractSections} labels its section, which is what
|
|
49
|
+
* lets the browser mark a changed section.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeHeadingText(text: string): string {
|
|
52
|
+
return text
|
|
53
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
54
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
55
|
+
.replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
|
|
56
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sections of a plan, split at heading lines outside code fences. The content
|
|
63
|
+
* before the first heading is a section titled `""`. Pi's `Markdown` renderer
|
|
64
|
+
* exposes no source-line map, so the review screen renders these chunks
|
|
65
|
+
* separately to know where each section begins.
|
|
66
|
+
*/
|
|
67
|
+
export function extractSections(plan: string): PlanSection[] {
|
|
68
|
+
const sections: PlanSection[] = [];
|
|
69
|
+
let current: PlanSection | undefined;
|
|
70
|
+
let fence: string | undefined;
|
|
71
|
+
|
|
72
|
+
const flush = (): void => {
|
|
73
|
+
if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
|
|
74
|
+
current = undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
plan.split("\n").forEach((line, index) => {
|
|
78
|
+
const fenceMatch = line.match(FENCE);
|
|
79
|
+
if (fenceMatch) {
|
|
80
|
+
if (fence === undefined) fence = fenceMatch[1][0];
|
|
81
|
+
else if (fence === fenceMatch[1][0]) fence = undefined;
|
|
82
|
+
} else if (fence === undefined) {
|
|
83
|
+
const heading = line.match(HEADING);
|
|
84
|
+
if (heading) {
|
|
85
|
+
flush();
|
|
86
|
+
current = { title: normalizeHeadingText(heading[2]), text: line, line: index, level: heading[1].length };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
|
|
91
|
+
else current.text += `\n${line}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
flush();
|
|
95
|
+
return sections;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One key per section that tells two identically titled headings apart: the title
|
|
100
|
+
* and which occurrence of it this is, in document order. The review pairs a
|
|
101
|
+
* baseline digest with the section it belongs to by it, and the feedback codec
|
|
102
|
+
* matches a comment's section across a rewrite, so a second `## Notes` never
|
|
103
|
+
* collects the first one's lines.
|
|
104
|
+
*/
|
|
105
|
+
export function sectionKeys(sections: readonly { title: string }[]): string[] {
|
|
106
|
+
const seen = new Map<string, number>();
|
|
107
|
+
return sections.map((section) => {
|
|
108
|
+
const occurrence = seen.get(section.title) ?? 0;
|
|
109
|
+
seen.set(section.title, occurrence + 1);
|
|
110
|
+
return `${occurrence}\u0000${section.title}`;
|
|
111
|
+
});
|
|
112
|
+
}
|