@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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-document sections (pure).
|
|
3
|
+
*
|
|
4
|
+
* One fence-aware scan of a plan's markdown: the review screen navigates by
|
|
5
|
+
* section, `set_plan_steps` looks up `## Steps`, a carried comment finds the
|
|
6
|
+
* block it belongs to, and the browser outline lists the same headings. A second
|
|
7
|
+
* scanner would disagree about a heading inside a fence, so every reader goes
|
|
8
|
+
* through this module.
|
|
9
|
+
*
|
|
10
|
+
* The scan is deliberately smaller than CommonMark: a heading is up to three
|
|
11
|
+
* leading spaces, a space after the hashes, and an optional closing run; a fence
|
|
12
|
+
* is three backticks or tildes anywhere a fence may start. Inside a fence
|
|
13
|
+
* nothing is a heading, which is what keeps a `# comment` in a shell example out
|
|
14
|
+
* of the outline. Every section keeps its raw source lines, so a caller can edit
|
|
15
|
+
* exactly what it read.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A heading-delimited chunk of a plan, for navigation and in-place edits. */
|
|
19
|
+
export interface PlanSection {
|
|
20
|
+
/** Plain heading text, or `""` for the content before the first heading. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** The section's markdown, including its heading line. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** 0-based source line of the section's heading (0 for the preamble). */
|
|
25
|
+
line: number;
|
|
26
|
+
/** Heading level, or 0 for the preamble before the first heading. */
|
|
27
|
+
level: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A markdown heading outside a fence, capturing its level and text: up to three
|
|
32
|
+
* leading spaces, a space after the hashes, and a closing hash run only when a
|
|
33
|
+
* space separates it. Shared with the block model, so the section split and the
|
|
34
|
+
* browser outline read a heading line the same way, and `## C#` keeps its title.
|
|
35
|
+
*/
|
|
36
|
+
export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A markdown code-fence opener or closer (backticks or tildes). Shared with the
|
|
40
|
+
* block model: a tab-indented fence is prose for both parsers, never code for one
|
|
41
|
+
* and prose for the other.
|
|
42
|
+
*/
|
|
43
|
+
export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A heading or step label as the review surfaces compare it: inline emphasis and
|
|
47
|
+
* code removed, whitespace collapsed. Exported so a renderer labels an outline
|
|
48
|
+
* entry the same way {@link extractSections} labels its section, which is what
|
|
49
|
+
* lets the browser mark a changed section.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeHeadingText(text: string): string {
|
|
52
|
+
return text
|
|
53
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
54
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
55
|
+
.replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
|
|
56
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sections of a plan, split at heading lines outside code fences. The content
|
|
63
|
+
* before the first heading is a section titled `""`. Pi's `Markdown` renderer
|
|
64
|
+
* exposes no source-line map, so the review screen renders these chunks
|
|
65
|
+
* separately to know where each section begins.
|
|
66
|
+
*/
|
|
67
|
+
export function extractSections(plan: string): PlanSection[] {
|
|
68
|
+
const sections: PlanSection[] = [];
|
|
69
|
+
let current: PlanSection | undefined;
|
|
70
|
+
let fence: string | undefined;
|
|
71
|
+
|
|
72
|
+
const flush = (): void => {
|
|
73
|
+
if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
|
|
74
|
+
current = undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
plan.split("\n").forEach((line, index) => {
|
|
78
|
+
const fenceMatch = line.match(FENCE);
|
|
79
|
+
if (fenceMatch) {
|
|
80
|
+
if (fence === undefined) fence = fenceMatch[1][0];
|
|
81
|
+
else if (fence === fenceMatch[1][0]) fence = undefined;
|
|
82
|
+
} else if (fence === undefined) {
|
|
83
|
+
const heading = line.match(HEADING);
|
|
84
|
+
if (heading) {
|
|
85
|
+
flush();
|
|
86
|
+
current = { title: normalizeHeadingText(heading[2]), text: line, line: index, level: heading[1].length };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
|
|
91
|
+
else current.text += `\n${line}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
flush();
|
|
95
|
+
return sections;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One key per section that tells two identically titled headings apart: the title
|
|
100
|
+
* and which occurrence of it this is, in document order. The review pairs a
|
|
101
|
+
* baseline digest with the section it belongs to by it, and the feedback codec
|
|
102
|
+
* matches a comment's section across a rewrite, so a second `## Notes` never
|
|
103
|
+
* collects the first one's lines.
|
|
104
|
+
*/
|
|
105
|
+
export function sectionKeys(sections: readonly { title: string }[]): string[] {
|
|
106
|
+
const seen = new Map<string, number>();
|
|
107
|
+
return sections.map((section) => {
|
|
108
|
+
const occurrence = seen.get(section.title) ?? 0;
|
|
109
|
+
seen.set(section.title, occurrence + 1);
|
|
110
|
+
return `${occurrence}\u0000${section.title}`;
|
|
111
|
+
});
|
|
112
|
+
}
|
package/extensions/plan/steps.ts
CHANGED
|
@@ -18,26 +18,7 @@
|
|
|
18
18
|
* checkbox or a `[DONE:n]` marker sets the status.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
extractSections as extractPlanSections,
|
|
23
|
-
FENCE,
|
|
24
|
-
HEADING,
|
|
25
|
-
normalizeHeadingText,
|
|
26
|
-
} from "../../lib/plans/sections.ts";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The section machinery is `lib/plans/sections.ts`, because the split writes
|
|
30
|
-
* `## Team` with the same fence-aware scan this module navigates with. These
|
|
31
|
-
* re-exports keep the plan extension's public surface one import.
|
|
32
|
-
*/
|
|
33
|
-
export {
|
|
34
|
-
extractSections as extractPlanSections,
|
|
35
|
-
FENCE,
|
|
36
|
-
HEADING,
|
|
37
|
-
normalizeHeadingText,
|
|
38
|
-
replaceSection,
|
|
39
|
-
} from "../../lib/plans/sections.ts";
|
|
40
|
-
export type { PlanSection } from "../../lib/plans/sections.ts";
|
|
21
|
+
import { extractSections, FENCE, HEADING, normalizeHeadingText } from "./sections.ts";
|
|
41
22
|
|
|
42
23
|
/** Most steps seeded from one plan. */
|
|
43
24
|
export const MAX_STEPS = 50;
|
|
@@ -295,7 +276,7 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
|
|
|
295
276
|
*/
|
|
296
277
|
export function verificationItem(plan: string): PlanStep | undefined {
|
|
297
278
|
if (stepsSection(plan) === undefined) return undefined;
|
|
298
|
-
const section =
|
|
279
|
+
const section = extractSections(plan).find(
|
|
299
280
|
(candidate) => candidate.level > 0 && candidate.title.toLowerCase() === "verification",
|
|
300
281
|
);
|
|
301
282
|
if (!section) return undefined;
|
|
@@ -394,7 +375,7 @@ export function setStepStatuses(plan: string, updates: readonly StepEdit[]): str
|
|
|
394
375
|
* `## Goal` section, so the caller can fall back to the title.
|
|
395
376
|
*/
|
|
396
377
|
export function goalSection(plan: string): string | undefined {
|
|
397
|
-
const section =
|
|
378
|
+
const section = extractSections(plan).find(
|
|
398
379
|
(candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === "goal",
|
|
399
380
|
);
|
|
400
381
|
if (section === undefined) return undefined;
|
package/extensions/plan/todos.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface TodoMerge {
|
|
|
22
22
|
skipped: number;
|
|
23
23
|
/** Existing items kept as they were. */
|
|
24
24
|
kept: number;
|
|
25
|
+
/**
|
|
26
|
+
* Untouched items dropped because they were seeded by an earlier approval of this
|
|
27
|
+
* plan and are no longer one of its steps: the plan was renamed or rewritten.
|
|
28
|
+
*/
|
|
29
|
+
replaced: number;
|
|
30
|
+
/**
|
|
31
|
+
* Older seeds kept because someone had already started or finished them. Deleting one
|
|
32
|
+
* would erase work a reader did, so they are reported instead of replaced.
|
|
33
|
+
*/
|
|
34
|
+
stale: number;
|
|
25
35
|
/** Plan steps left out because the list reached its cap. */
|
|
26
36
|
deferred: number;
|
|
27
37
|
/**
|
|
@@ -89,9 +99,38 @@ function contentKey(content: string): string {
|
|
|
89
99
|
* (still subject to the same duplicate and cap rules) but is not counted as a
|
|
90
100
|
* step, because it is not one.
|
|
91
101
|
*/
|
|
92
|
-
export function mergeTodos(
|
|
102
|
+
export function mergeTodos(
|
|
103
|
+
existing: ExistingTodo[],
|
|
104
|
+
steps: PlanStep[],
|
|
105
|
+
max: number,
|
|
106
|
+
trailing?: PlanStep,
|
|
107
|
+
/** What an earlier approval of this plan seeded, when there was one. */
|
|
108
|
+
previous?: readonly string[],
|
|
109
|
+
): TodoMerge {
|
|
93
110
|
const limit = Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
|
|
94
|
-
|
|
111
|
+
// A plan refined after approval is approved again over the list its first approval
|
|
112
|
+
// made. An item this plan seeded that is no longer one of its steps is a leftover, and
|
|
113
|
+
// only an item *this plan seeded* is a candidate: a user's own item is never touched.
|
|
114
|
+
const ours = new Set((previous ?? []).map(contentKey));
|
|
115
|
+
const current = new Set(steps.map((step) => contentKey(step.content)));
|
|
116
|
+
if (trailing) current.add(contentKey(trailing.content));
|
|
117
|
+
let replaced = 0;
|
|
118
|
+
let stale = 0;
|
|
119
|
+
const kept: ExistingTodo[] = [];
|
|
120
|
+
for (const todo of existing) {
|
|
121
|
+
const key = contentKey(todo.content);
|
|
122
|
+
if (ours.has(key) && !current.has(key)) {
|
|
123
|
+
// An untouched leftover is bookkeeping and goes; one someone acted on is
|
|
124
|
+
// evidence of work, and is named rather than deleted.
|
|
125
|
+
if (todo.status === "pending") {
|
|
126
|
+
replaced += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
stale += 1;
|
|
130
|
+
}
|
|
131
|
+
kept.push(todo);
|
|
132
|
+
}
|
|
133
|
+
const todos: ExistingTodo[] = kept.map((todo) =>
|
|
95
134
|
todo.status === "in_progress" && !todo.activeForm ? { ...todo, activeForm: todo.content } : todo,
|
|
96
135
|
);
|
|
97
136
|
const seen = new Set(todos.map((todo) => contentKey(todo.content)));
|
|
@@ -126,5 +165,14 @@ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: num
|
|
|
126
165
|
}
|
|
127
166
|
}
|
|
128
167
|
|
|
129
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
todos,
|
|
170
|
+
added,
|
|
171
|
+
skipped,
|
|
172
|
+
kept: existing.length - replaced,
|
|
173
|
+
replaced,
|
|
174
|
+
stale,
|
|
175
|
+
deferred,
|
|
176
|
+
trailing: trailingOutcome,
|
|
177
|
+
};
|
|
130
178
|
}
|
|
@@ -12,7 +12,6 @@ import { basename } from "node:path";
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, type Static } from "typebox";
|
|
14
14
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
15
|
-
import { messageOf } from "../../../lib/errors.ts";
|
|
16
15
|
import { lastGoal } from "../../../lib/goal-state.ts";
|
|
17
16
|
import { GOAL_TOOL } from "../../../lib/tool-names.ts";
|
|
18
17
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
@@ -25,8 +24,6 @@ import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
|
|
|
25
24
|
import { sanitizePlanText } from "../plan-text.ts";
|
|
26
25
|
import { planTitle, type StoredPlan } from "../plans.ts";
|
|
27
26
|
import { reviewPlan } from "../review.ts";
|
|
28
|
-
import { decideSplit, rosterCard, SPLIT_OFF_NOTE } from "../split.ts";
|
|
29
|
-
import { writeTeam } from "../../../lib/plans/team.ts";
|
|
30
27
|
import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
31
28
|
import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
|
|
32
29
|
import type { ExitPlanModeDetails } from "../types.ts";
|
|
@@ -34,27 +31,11 @@ import type { PlanWebReview } from "../web.ts";
|
|
|
34
31
|
import { firstTextLine } from "./nested.ts";
|
|
35
32
|
import { seedTodos } from "./todo-seed.ts";
|
|
36
33
|
|
|
37
|
-
const PlanUnitParams = Type.Object({
|
|
38
|
-
name: Type.String({ description: "Lowercase slug; the owner session takes it as its label." }),
|
|
39
|
-
paths: Type.Array(Type.String(), { description: "Repository-relative path prefixes this unit owns." }),
|
|
40
|
-
test: Type.String({ description: "The command that proves the unit is done." }),
|
|
41
|
-
dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Units that must land first." })),
|
|
42
|
-
lead: Type.Optional(Type.Boolean({ description: "True for the one unit this session keeps." })),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
34
|
const ExitPlanModeParams = Type.Object({
|
|
46
35
|
plan_path: Type.String({
|
|
47
36
|
description:
|
|
48
37
|
"Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
|
|
49
38
|
}),
|
|
50
|
-
units: Type.Optional(
|
|
51
|
-
Type.Array(PlanUnitParams, {
|
|
52
|
-
description:
|
|
53
|
-
"The plan's slices, when it has two or more independent ones the user will run in separate sessions. " +
|
|
54
|
-
"Only used when the plan.split setting is on; validation refuses an overlapping, looping, or oversized " +
|
|
55
|
-
"split before the review opens. Omit it to approve as one flow.",
|
|
56
|
-
}),
|
|
57
|
-
),
|
|
58
39
|
});
|
|
59
40
|
|
|
60
41
|
type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
|
|
@@ -73,13 +54,15 @@ interface GoalOutcome {
|
|
|
73
54
|
* Approving the plan *is* the decision, so neither a `planning` goal nor a
|
|
74
55
|
* session with none is asked again. A final goal (`active` or `achieved`) is the
|
|
75
56
|
* user's own: approving ordinary work never rewrites it. Recording runs through
|
|
76
|
-
* the goal tool
|
|
77
|
-
* spent
|
|
57
|
+
* the goal tool when it is registered — validation and the widget stay goal's,
|
|
58
|
+
* and no model turn is spent — and stays silent when that sibling extension is
|
|
59
|
+
* not installed, because there is nothing to report and nothing to act on.
|
|
78
60
|
*/
|
|
79
|
-
async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
|
|
61
|
+
async function finalizeGoal(pi: ExtensionAPI, ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
|
|
80
62
|
const objective = planObjective(plan);
|
|
81
63
|
const current = lastGoal(ctx.sessionManager.getBranch());
|
|
82
64
|
if (current !== null && current.status !== "planning") return { objective };
|
|
65
|
+
if (!pi.getAllTools().some((tool) => tool.name === GOAL_TOOL)) return { objective };
|
|
83
66
|
const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
|
|
84
67
|
const action = current === null ? "recorded" : "promoted";
|
|
85
68
|
return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
|
|
@@ -145,26 +128,6 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
145
128
|
);
|
|
146
129
|
}
|
|
147
130
|
|
|
148
|
-
// The split is decided before the review opens, so a refused or rejected split
|
|
149
|
-
// never spends the user's review — and the screen can show what it approves.
|
|
150
|
-
const units = args.units?.map((unit) => ({
|
|
151
|
-
name: unit.name,
|
|
152
|
-
paths: [...unit.paths],
|
|
153
|
-
test: unit.test,
|
|
154
|
-
dependsOn: [...(unit.dependsOn ?? [])],
|
|
155
|
-
...(unit.lead ? { lead: true } : {}),
|
|
156
|
-
}));
|
|
157
|
-
const split = decideSplit(ctx.cwd, await runtime.plans.rootFor(ctx.cwd), units);
|
|
158
|
-
if (split.kind === "refused") {
|
|
159
|
-
return failedResult(`Refused: ${split.reason}`, {
|
|
160
|
-
approved: false,
|
|
161
|
-
plan: file.content,
|
|
162
|
-
planPath: file.path,
|
|
163
|
-
relativePath: file.relativePath,
|
|
164
|
-
} satisfies ExitPlanModeDetails);
|
|
165
|
-
}
|
|
166
|
-
const pending = split.kind === "team" ? split : undefined;
|
|
167
|
-
|
|
168
131
|
if (!ctx.hasUI) {
|
|
169
132
|
return failedResult(
|
|
170
133
|
`No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
|
|
@@ -187,7 +150,6 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
187
150
|
baselines: runtime,
|
|
188
151
|
plan: file,
|
|
189
152
|
signal,
|
|
190
|
-
...(pending === undefined ? {} : { split: { ...pending.team, verdict: pending.verdict } }),
|
|
191
153
|
});
|
|
192
154
|
|
|
193
155
|
if (decision.action === "refine") {
|
|
@@ -250,33 +212,18 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
250
212
|
};
|
|
251
213
|
}
|
|
252
214
|
|
|
253
|
-
// The team becomes real here, never before: the section is what a joining
|
|
254
|
-
// session reads, so it appears only after the user approved the split. The
|
|
255
|
-
// write happens while plan mode is still on, so a failure leaves the session
|
|
256
|
-
// able to retry instead of approved-but-empty.
|
|
257
|
-
let approved = decision.plan;
|
|
258
|
-
if (pending !== undefined) {
|
|
259
|
-
try {
|
|
260
|
-
approved = await runtime.plans.write(ctx.cwd, {
|
|
261
|
-
title: planTitle(basename(decision.plan.path)),
|
|
262
|
-
content: writeTeam(decision.plan.content, { lead: pending.team.lead, units: pending.team.units }),
|
|
263
|
-
planPath: decision.plan.path,
|
|
264
|
-
});
|
|
265
|
-
} catch (writeError) {
|
|
266
|
-
return failedResult(
|
|
267
|
-
`The plan was approved but its team could not be written: ${messageOf(writeError)}. Plan mode is still on and no steps were seeded; fix the plan file and call exit_plan_mode again.`,
|
|
268
|
-
{
|
|
269
|
-
approved: false,
|
|
270
|
-
plan: decision.plan.content,
|
|
271
|
-
planPath: decision.plan.path,
|
|
272
|
-
relativePath: decision.plan.relativePath,
|
|
273
|
-
} satisfies ExitPlanModeDetails,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
215
|
runtime.disable(ctx);
|
|
278
|
-
|
|
279
|
-
|
|
216
|
+
// A plan refined and approved again seeds over the list its first approval made:
|
|
217
|
+
// the steps that approval appended are the ones this one may replace.
|
|
218
|
+
const previous = runtime.seeded(decision.plan.path);
|
|
219
|
+
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content, previous);
|
|
220
|
+
if (error === undefined && steps.length > 0) {
|
|
221
|
+
runtime.markSeeded(
|
|
222
|
+
decision.plan.path,
|
|
223
|
+
steps.map((step) => step.content),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const goal = await finalizeGoal(pi, ctx, decision.plan);
|
|
280
227
|
const listing =
|
|
281
228
|
steps.length > 0
|
|
282
229
|
? `\n\n${steps
|
|
@@ -303,41 +250,19 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
303
250
|
const browserNote = decision.comments.length
|
|
304
251
|
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
|
|
305
252
|
: "";
|
|
306
|
-
const splitNote =
|
|
307
|
-
pending === undefined
|
|
308
|
-
? split.kind === "off"
|
|
309
|
-
? `\n\n${SPLIT_OFF_NOTE}`
|
|
310
|
-
: ""
|
|
311
|
-
: `\n\n${rosterCard(pending.team, approved.path, pi.getSessionName())}\n\nWork only your own unit (${pending.team.lead}); the other units belong to the sessions the user starts.`;
|
|
312
253
|
return {
|
|
313
254
|
content: [
|
|
314
255
|
{
|
|
315
256
|
type: "text",
|
|
316
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${
|
|
257
|
+
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
|
|
317
258
|
},
|
|
318
259
|
],
|
|
319
260
|
details: {
|
|
320
261
|
approved: true,
|
|
321
|
-
plan:
|
|
322
|
-
planPath:
|
|
323
|
-
relativePath:
|
|
262
|
+
plan: decision.plan.content,
|
|
263
|
+
planPath: decision.plan.path,
|
|
264
|
+
relativePath: decision.plan.relativePath,
|
|
324
265
|
annotations: decision.comments.length > 0 ? decision.comments : undefined,
|
|
325
|
-
...(pending === undefined
|
|
326
|
-
? split.kind === "off"
|
|
327
|
-
? { splitOff: true }
|
|
328
|
-
: {}
|
|
329
|
-
: {
|
|
330
|
-
team: {
|
|
331
|
-
lead: pending.team.lead,
|
|
332
|
-
verdict: pending.verdict,
|
|
333
|
-
units: pending.team.units.map((unit) => ({
|
|
334
|
-
name: unit.name,
|
|
335
|
-
paths: unit.paths,
|
|
336
|
-
test: unit.test,
|
|
337
|
-
dependsOn: unit.dependsOn,
|
|
338
|
-
})),
|
|
339
|
-
},
|
|
340
|
-
}),
|
|
341
266
|
} satisfies ExitPlanModeDetails,
|
|
342
267
|
};
|
|
343
268
|
},
|
|
@@ -36,14 +36,16 @@ export async function seedTodos(
|
|
|
36
36
|
pi: ExtensionAPI,
|
|
37
37
|
ctx: ExtensionToolContext,
|
|
38
38
|
plan: string,
|
|
39
|
+
/** What an earlier approval of the same plan appended, when it was approved before. */
|
|
40
|
+
previous?: readonly string[],
|
|
39
41
|
): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
|
|
40
42
|
const steps = extractPlanSteps(plan);
|
|
41
43
|
const verification = verificationItem(plan);
|
|
42
44
|
if (steps.length === 0 && !verification) return { steps };
|
|
43
|
-
const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
|
|
45
|
+
const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification, previous);
|
|
44
46
|
// Nothing new to write: every step (and the verification item) is already in
|
|
45
47
|
// the list, or the list is full, so a rewrite would only churn the widget.
|
|
46
|
-
if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
|
|
48
|
+
if (merged.added === 0 && merged.trailing !== "added" && merged.replaced === 0) return { steps, merged };
|
|
47
49
|
const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
|
|
48
50
|
if (!outcome.isError) return { steps, merged };
|
|
49
51
|
// The nested call fails both when the tool is unavailable and when it rejects
|
|
@@ -93,8 +93,19 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
93
93
|
// write; say so once, where the user is looking.
|
|
94
94
|
const moved = runtime.plans.takeNotice();
|
|
95
95
|
if (moved) ctx.ui.notify(moved, "info");
|
|
96
|
+
// A refinement says where the text it replaced went: a reviewer's comment that was
|
|
97
|
+
// carried to a block that moved can be checked against what she actually read.
|
|
98
|
+
const kept =
|
|
99
|
+
file.replaced === undefined ? "" : `, keeping the revision it replaced at ${file.replaced.relativePath}`;
|
|
100
|
+
// A plan that was approved before is now out of step with the todo list, and
|
|
101
|
+
// the list is only re-seeded by approving it again. Say so here, where the
|
|
102
|
+
// refinement is made, instead of letting the two disagree in silence.
|
|
103
|
+
const reseed =
|
|
104
|
+
runtime.seeded(file.path) === undefined
|
|
105
|
+
? ""
|
|
106
|
+
: " This plan was approved before: its todo items are the ones that approval seeded, and approving it again replaces the steps that changed (an item you started or finished is left alone and named).";
|
|
96
107
|
return textResult(
|
|
97
|
-
`Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
108
|
+
`Wrote the plan to ${file.relativePath}${kept}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}${reseed}`,
|
|
98
109
|
{ path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
99
110
|
);
|
|
100
111
|
} catch (error) {
|
package/extensions/plan/tui.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type TuiMouseEventResult,
|
|
30
30
|
} from "@earendil-works/pi-tui";
|
|
31
31
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
32
|
-
import { renderTeam, type PlanUnit } from "../../lib/plans/team.ts";
|
|
33
32
|
import {
|
|
34
33
|
editorTheme,
|
|
35
34
|
screenHint,
|
|
@@ -46,7 +45,6 @@ import { emptyState, checkbox } from "../../lib/tui/rows.ts";
|
|
|
46
45
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
47
46
|
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
48
47
|
import { changedSections, planDisplayText, type SectionDigest } from "./plan-text.ts";
|
|
49
|
-
import { extractPlanSections } from "./steps.ts";
|
|
50
48
|
|
|
51
49
|
/** What the user chose from the read/review screen. */
|
|
52
50
|
export type PlanViewAction = "approve" | "refine" | "keep" | "browser" | "cancel";
|
|
@@ -87,11 +85,6 @@ export interface PlanViewOptions {
|
|
|
87
85
|
tui?: TUI;
|
|
88
86
|
/** Markdown theme for the body; defaults to Pi's active theme. */
|
|
89
87
|
markdownTheme?: MarkdownTheme;
|
|
90
|
-
/**
|
|
91
|
-
* The split approval would write. Rendered as a read-only section after the
|
|
92
|
-
* plan, because the `## Team` section itself only exists after approval.
|
|
93
|
-
*/
|
|
94
|
-
split?: { lead: string; units: readonly PlanUnit[]; verdict: "pair" | "team" };
|
|
95
88
|
}
|
|
96
89
|
|
|
97
90
|
/** Rows the screen shows when the terminal height is unknown. */
|
|
@@ -207,11 +200,10 @@ export class PlanViewComponent implements Component {
|
|
|
207
200
|
const base = this.options.markdownTheme ?? getMarkdownTheme();
|
|
208
201
|
const theme = planMarkdownTheme(this.theme, base);
|
|
209
202
|
const current = planDisplayText(this.options.plan.content);
|
|
210
|
-
const
|
|
211
|
-
const chunks = extractPlanSections(current);
|
|
203
|
+
const marks = changedSections(this.options.baseline, current);
|
|
212
204
|
const lines: string[] = [];
|
|
213
205
|
const sections: SectionOffset[] = [];
|
|
214
|
-
|
|
206
|
+
marks.forEach(({ section: chunk, changed }, index) => {
|
|
215
207
|
// Markdown pads every line to the full width for backgrounds; this screen
|
|
216
208
|
// draws no background, so the padding is dropped.
|
|
217
209
|
const rendered = new Markdown(chunk.text, padding, 0, theme)
|
|
@@ -220,38 +212,17 @@ export class PlanViewComponent implements Component {
|
|
|
220
212
|
sections.push({
|
|
221
213
|
title: chunk.title,
|
|
222
214
|
offset: lines.length,
|
|
223
|
-
...(changed
|
|
215
|
+
...(changed ? { changed: true } : {}),
|
|
224
216
|
});
|
|
225
217
|
lines.push(...rendered);
|
|
226
218
|
// Sections render independently, so restore the blank line a heading-only
|
|
227
219
|
// chunk omits before the next section.
|
|
228
|
-
if (index <
|
|
220
|
+
if (index < marks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
229
221
|
});
|
|
230
|
-
|
|
231
|
-
if (split !== undefined) {
|
|
232
|
-
if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
233
|
-
const rendered = new Markdown(split, padding, 0, theme)
|
|
234
|
-
.render(width)
|
|
235
|
-
.map((line) => truncateToWidth(line.trimEnd(), width));
|
|
236
|
-
sections.push({ title: "Split", offset: lines.length });
|
|
237
|
-
lines.push(...rendered);
|
|
238
|
-
}
|
|
239
|
-
this.cache = { width, lines, sections, changedCount: changed.filter(Boolean).length };
|
|
222
|
+
this.cache = { width, lines, sections, changedCount: marks.filter((mark) => mark.changed).length };
|
|
240
223
|
return this.cache;
|
|
241
224
|
}
|
|
242
225
|
|
|
243
|
-
/**
|
|
244
|
-
* The split as a read-only section: the same table approval writes, so what the
|
|
245
|
-
* user approves is what the plan file will carry.
|
|
246
|
-
*/
|
|
247
|
-
private splitSection(): string | undefined {
|
|
248
|
-
const split = this.options.split;
|
|
249
|
-
if (split === undefined) return undefined;
|
|
250
|
-
const owners = split.units.filter((unit) => !unit.lead).length;
|
|
251
|
-
const summary = `${split.verdict} · lead + ${owners} owner${owners === 1 ? "" : "s"} · written on approval`;
|
|
252
|
-
return ["## Split", "", summary, "", renderTeam({ lead: split.lead, units: [...split.units] })].join("\n");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
226
|
/** The section whose heading is at or above the viewport top. */
|
|
256
227
|
private currentSection(sections: SectionOffset[]): SectionOffset | undefined {
|
|
257
228
|
let current: SectionOffset | undefined;
|
package/extensions/plan/types.ts
CHANGED
|
@@ -50,6 +50,21 @@ export interface ReviewBaselineEntry {
|
|
|
50
50
|
sections: SectionDigest[];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* What one approval put into the todo list, so the next approval can replace it.
|
|
55
|
+
*
|
|
56
|
+
* A plan refined after it was approved is approved again, and the list still holds the
|
|
57
|
+
* items seeded the first time. Their texts are the only record of which items came from
|
|
58
|
+
* this plan, so a re-approval keeps them to recognise — and replace — its own leftovers
|
|
59
|
+
* without touching an item the user was already tracking.
|
|
60
|
+
*/
|
|
61
|
+
export interface SeededStepsEntry {
|
|
62
|
+
/** Resolved path of the approved plan file. */
|
|
63
|
+
path: string;
|
|
64
|
+
/** The step texts the approval appended, in order. */
|
|
65
|
+
steps: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/** Structured result carried in `enter_plan_mode`'s `details`. */
|
|
54
69
|
export interface EnterPlanModeDetails {
|
|
55
70
|
/** Whether the user agreed to enter plan mode. */
|
|
@@ -104,14 +119,6 @@ export interface ExitPlanModeDetails {
|
|
|
104
119
|
cancelled?: boolean;
|
|
105
120
|
/** True when no interactive UI was available to ask for approval. */
|
|
106
121
|
unavailable?: boolean;
|
|
107
|
-
/** The team approval wrote into the plan, when the plan was split. */
|
|
108
|
-
team?: {
|
|
109
|
-
lead: string;
|
|
110
|
-
units: Array<{ name: string; paths: string[]; test: string; dependsOn: string[] }>;
|
|
111
|
-
verdict: "pair" | "team";
|
|
112
|
-
};
|
|
113
|
-
/** True when the model proposed a split while `plan.split` was off. */
|
|
114
|
-
splitOff?: boolean;
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
/** Structured result carried in `cancel_plan_mode`'s `details`. */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's block navigation: which block is active, and how a key walks them.
|
|
3
|
+
*
|
|
4
|
+
* The fourth capability out of the review client's template literal. Each function takes
|
|
5
|
+
* the document it reads and returns the index it computed, so the client keeps the one
|
|
6
|
+
* variable (`activeIndex`) and these own the rules: clamp to the blocks that exist, start
|
|
7
|
+
* at the first or last when nothing is active, and land beside the reader rather than
|
|
8
|
+
* jumping to the middle of the page.
|
|
9
|
+
*
|
|
10
|
+
* `el` comes from the runtime, declared here because this source is spliced into the same
|
|
11
|
+
* bundle; the declaration emits nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
15
|
+
|
|
16
|
+
/** The island's view of one block, as far as navigation needs it. */
|
|
17
|
+
export interface IslandBlock {
|
|
18
|
+
start: number;
|
|
19
|
+
kind?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The rendered blocks.
|
|
24
|
+
*
|
|
25
|
+
* A rebuild replaces them, so this is read fresh rather than remembered: the array a
|
|
26
|
+
* caller holds goes stale the moment `renderPlan` throws the DOM away.
|
|
27
|
+
*/
|
|
28
|
+
export function blockElements(planEl: ClientNode | null): ClientNode[] {
|
|
29
|
+
if (!planEl) return [];
|
|
30
|
+
const nodes = planEl.querySelectorAll(".bf-block");
|
|
31
|
+
const blocks: ClientNode[] = [];
|
|
32
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
33
|
+
const block = nodes[i];
|
|
34
|
+
if (block) blocks.push(block);
|
|
35
|
+
}
|
|
36
|
+
return blocks;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Make one block active and bring it into view, or say nothing when there are no blocks.
|
|
41
|
+
*
|
|
42
|
+
* `null` means "nothing changed": a page with no blocks has no active block, and the
|
|
43
|
+
* caller keeps the index it had rather than being reset to zero.
|
|
44
|
+
*/
|
|
45
|
+
export function setActive(planEl: ClientNode | null, index: number): number | null {
|
|
46
|
+
const blocks = blockElements(planEl);
|
|
47
|
+
if (blocks.length === 0) return null;
|
|
48
|
+
const next = Math.max(0, Math.min(blocks.length - 1, index));
|
|
49
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
50
|
+
const block = blocks[i];
|
|
51
|
+
if (block) block.classList.toggle("bf-block--active", i === next);
|
|
52
|
+
}
|
|
53
|
+
const current = blocks[next];
|
|
54
|
+
if (current && current.scrollIntoView) current.scrollIntoView({ block: "nearest" });
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The index one step from `activeIndex`, or `null` when there is nothing to walk.
|
|
60
|
+
*
|
|
61
|
+
* With nothing active yet, `j` starts at the top and `k` at the bottom, which is what a
|
|
62
|
+
* reader pressing either key on a fresh page expects.
|
|
63
|
+
*/
|
|
64
|
+
export function moveActive(planEl: ClientNode | null, activeIndex: number, delta: number): number | null {
|
|
65
|
+
const blocks = blockElements(planEl);
|
|
66
|
+
if (blocks.length === 0) return null;
|
|
67
|
+
const next = activeIndex < 0 ? (delta < 0 ? blocks.length - 1 : 0) : activeIndex + delta;
|
|
68
|
+
return setActive(planEl, next);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The rendered block and its island block for a source line, when both exist.
|
|
73
|
+
*
|
|
74
|
+
* Both halves are required: a rendered block with no island entry has nothing to comment
|
|
75
|
+
* on, and an island entry with no rendered block is a comment the page cannot place.
|
|
76
|
+
*/
|
|
77
|
+
export function blockAtLine(
|
|
78
|
+
planEl: ClientNode | null,
|
|
79
|
+
blocks: readonly IslandBlock[],
|
|
80
|
+
line: number,
|
|
81
|
+
): { wrap: ClientNode; block: IslandBlock } | null {
|
|
82
|
+
const wrap = planEl ? planEl.querySelector('.bf-block[data-line="' + line + '"]') : null;
|
|
83
|
+
let block: IslandBlock | undefined;
|
|
84
|
+
for (const candidate of blocks) if (candidate.start === line) block = candidate;
|
|
85
|
+
return wrap && block ? { wrap, block } : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The block rules as source, for the client bundle. */
|
|
89
|
+
export const WEB_BLOCKS_JS = [blockElements, setActive, moveActive, blockAtLine].map(String).join("\n\n");
|