@gavin-hu/my-pi-agent 0.18.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 +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- 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} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- 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 +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- 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} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
- 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/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- 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} +1 -1
- 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} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
- 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/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -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} +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/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- 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} +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 +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 +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- 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/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} +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/{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/guard.ts +3 -5
- package/lib/README.md +8 -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/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- 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/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -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/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- 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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* step.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { FENCE, HEADING } from "./
|
|
17
|
+
import { FENCE, HEADING } from "./sections.ts";
|
|
18
18
|
|
|
19
19
|
/** How a block is rendered and grouped. */
|
|
20
20
|
export type PlanBlockKind = "heading" | "paragraph" | "list" | "code" | "quote" | "table" | "hr" | "blank";
|
|
@@ -176,7 +176,7 @@ export function planBlocks(plan: string): PlanBlock[] {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/** The block containing source line `line`, or the nearest block before it. */
|
|
179
|
-
export function blockAt(blocks: PlanBlock[], line: number): PlanBlock | undefined {
|
|
179
|
+
export function blockAt(blocks: readonly PlanBlock[], line: number): PlanBlock | undefined {
|
|
180
180
|
let candidate: PlanBlock | undefined;
|
|
181
181
|
for (const block of blocks) {
|
|
182
182
|
if (block.start <= line && line <= block.end) return block;
|
|
@@ -186,6 +186,27 @@ export function blockAt(blocks: PlanBlock[], line: number): PlanBlock | undefine
|
|
|
186
186
|
return candidate;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* The nearest block with content at or before `line`, else the first one after
|
|
191
|
+
* it: what a comment in blank space belongs to.
|
|
192
|
+
*
|
|
193
|
+
* A blank line is a block, so {@link blockAt} may name one, but a comment cannot
|
|
194
|
+
* hang on nothing: the review page renders no line for a blank block, so an
|
|
195
|
+
* anchor there is one it can never attach a thread to.
|
|
196
|
+
*/
|
|
197
|
+
export function contentBlockAt(blocks: readonly PlanBlock[], line: number): PlanBlock | undefined {
|
|
198
|
+
let before: PlanBlock | undefined;
|
|
199
|
+
for (const block of blocks) {
|
|
200
|
+
const content = block.kind !== "blank";
|
|
201
|
+
if (block.start <= line) {
|
|
202
|
+
if (content) before = block;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (content) return before ?? block;
|
|
206
|
+
}
|
|
207
|
+
return before;
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
/** The source inside a fenced code block, without its opening and closing fence lines. */
|
|
190
211
|
export function codeBody(block: PlanBlock): string[] {
|
|
191
212
|
if (block.kind !== "code") return block.lines.slice();
|
|
@@ -13,6 +13,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
13
13
|
import { isRemoteTurn } from "../../lib/interaction.ts";
|
|
14
14
|
import { READ_ONLY_SUMMARY } from "../../lib/plan-entry.ts";
|
|
15
15
|
import { qrLines } from "../../lib/qr.ts";
|
|
16
|
+
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
16
17
|
import { feedbackSummary } from "./feedback.ts";
|
|
17
18
|
import { PlanIndexComponent, formatPlanIndex } from "./plan-index.ts";
|
|
18
19
|
import { runBrowserReview } from "./review.ts";
|
|
@@ -192,22 +193,26 @@ async function openPlanIndex(
|
|
|
192
193
|
|
|
193
194
|
// The review runs after the screen closes, so the input focus is free again.
|
|
194
195
|
let selected: (PlanSummary | undefined) | undefined;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
196
|
+
// A dock list shares the editor slot with the goal and todo rails, so it takes
|
|
197
|
+
// the dock to itself for as long as it is open, like the other list screens.
|
|
198
|
+
await withRailsSuppressed(pi, () =>
|
|
199
|
+
ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
200
|
+
const component = new PlanIndexComponent({
|
|
201
|
+
summaries,
|
|
202
|
+
dir,
|
|
203
|
+
theme,
|
|
204
|
+
onSelect: (summary) => {
|
|
205
|
+
selected = summary;
|
|
206
|
+
done();
|
|
207
|
+
},
|
|
208
|
+
onClose: () => done(),
|
|
209
|
+
reload: summarise,
|
|
210
|
+
requestRender: () => tui.requestRender(),
|
|
211
|
+
viewportRows: () => tui.terminal?.rows,
|
|
212
|
+
});
|
|
213
|
+
return component;
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
211
216
|
if (selected) await reviewInBrowser(pi, runtime, web, selected.path, ctx);
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
24
|
-
import { blockAt, planBlocks, type PlanBlock } from "./blocks.ts";
|
|
25
|
-
import {
|
|
24
|
+
import { blockAt, contentBlockAt, planBlocks, type PlanBlock } from "./blocks.ts";
|
|
25
|
+
import { extractSections, sectionKeys } from "./sections.ts";
|
|
26
26
|
|
|
27
27
|
/** Marker that opens every stored feedback line. */
|
|
28
28
|
export const REVIEW_MARKER = "[!REVIEW]";
|
|
@@ -191,19 +191,43 @@ export function stripFeedback(plan: string): string {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
194
|
+
* A plan read the way the review reads it: its body, its blocks, and its
|
|
195
|
+
* comments, out of one parse.
|
|
196
196
|
*/
|
|
197
|
-
export
|
|
197
|
+
export interface FeedbackRead {
|
|
198
|
+
/** The plan body with every stored marker removed. */
|
|
199
|
+
body: string;
|
|
200
|
+
/** The body split into source-line blocks: the shape an anchor names. */
|
|
201
|
+
blocks: PlanBlock[];
|
|
202
|
+
/** The comments the markers carried, each anchored to a content block's start. */
|
|
203
|
+
annotations: PlanAnnotation[];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Read a plan once. An anchor is the start of the block the marker follows, so a
|
|
208
|
+
* comment keeps its place across reloads — and it is always a block with
|
|
209
|
+
* content, because the page renders no line for a blank one and so can never
|
|
210
|
+
* attach a thread to it. A plan whose every line is blank has no such block: its
|
|
211
|
+
* comments keep anchor `0` and the page lists them as unattached, which is the
|
|
212
|
+
* truth there.
|
|
213
|
+
*/
|
|
214
|
+
export function readFeedback(plan: string): FeedbackRead {
|
|
198
215
|
const { clean, markers } = scanMarkers(plan);
|
|
199
|
-
if (markers.length === 0) return [];
|
|
200
216
|
const blocks = planBlocks(clean);
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
217
|
+
const annotations = markers.map((marker) => {
|
|
218
|
+
const annotation: PlanAnnotation = {
|
|
219
|
+
anchor: contentBlockAt(blocks, marker.at)?.start ?? 0,
|
|
220
|
+
comment: marker.comment,
|
|
221
|
+
};
|
|
204
222
|
if (marker.author) annotation.author = marker.author;
|
|
205
223
|
return annotation;
|
|
206
224
|
});
|
|
225
|
+
return { body: clean, blocks, annotations };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The comments a plan carries, in the order they were written. */
|
|
229
|
+
export function parseFeedback(plan: string): PlanAnnotation[] {
|
|
230
|
+
return readFeedback(plan).annotations;
|
|
207
231
|
}
|
|
208
232
|
|
|
209
233
|
/** Sanitized, non-empty comments with the anchor they land on. */
|
|
@@ -230,7 +254,9 @@ export function formatFeedback(plan: string, annotations: PlanAnnotation[], opti
|
|
|
230
254
|
.map((marker) => ({ at: marker.at, line: markerLine(marker.author, marker.comment) }));
|
|
231
255
|
const blocks = planBlocks(clean);
|
|
232
256
|
const added = storableComments(annotations).map((item) => ({
|
|
233
|
-
|
|
257
|
+
// A marker beside a block, never after a blank line: an anchor that names
|
|
258
|
+
// blank space is written where the page can read it back as a thread.
|
|
259
|
+
at: contentBlockAt(blocks, item.anchor)?.end ?? -1,
|
|
234
260
|
line: markerLine(author, item.comment),
|
|
235
261
|
}));
|
|
236
262
|
return compose(clean, [...kept, ...added]);
|
|
@@ -247,30 +273,92 @@ function blockKey(block: PlanBlock): string {
|
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
|
|
276
|
+
* Words a block is compared by when it may merely have been reworded: lowercased,
|
|
277
|
+
* with the list marker and task checkbox dropped. Every step of a `## Steps`
|
|
278
|
+
* section would otherwise match every other one through its `- [ ]` alone.
|
|
279
|
+
*/
|
|
280
|
+
function blockWords(block: PlanBlock): Set<string> {
|
|
281
|
+
const text = block.lines
|
|
282
|
+
.join(" ")
|
|
283
|
+
.toLowerCase()
|
|
284
|
+
.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, " ")
|
|
285
|
+
.replace(/\[[ x]\]/g, " ");
|
|
286
|
+
return new Set(text.split(/[^\p{L}\p{N}]+/u).filter((word) => word !== ""));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Share of the longer block's words that both blocks have. */
|
|
290
|
+
function sharedShare(a: ReadonlySet<string>, b: ReadonlySet<string>): number {
|
|
291
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
292
|
+
let shared = 0;
|
|
293
|
+
for (const word of a) if (b.has(word)) shared += 1;
|
|
294
|
+
return shared / Math.max(a.size, b.size);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Share of its words a block keeps, at or above which it is the same block reworded. */
|
|
298
|
+
const MATCH_SHARE = 0.5;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Which block of `next` each block of `old` became, keeping both lists in
|
|
302
|
+
* document order: the longest set of pairs whose wording overlaps enough to be one
|
|
303
|
+
* block reworded. A block left without a pair is `-1` — deleted, or replaced by
|
|
304
|
+
* text that shares nothing — which is the gap its comment belongs in.
|
|
305
|
+
*/
|
|
306
|
+
function alignBlocks(old: readonly PlanBlock[], next: readonly PlanBlock[]): number[] {
|
|
307
|
+
const before = old.map(blockWords);
|
|
308
|
+
const after = next.map(blockWords);
|
|
309
|
+
const matches = before.map((words) => after.map((other) => (sharedShare(words, other) >= MATCH_SHARE ? 1 : 0)));
|
|
310
|
+
// A longest common subsequence over the match grid, so a block that moved keeps
|
|
311
|
+
// its own pairing instead of pulling the blocks around it along.
|
|
312
|
+
const width = next.length + 1;
|
|
313
|
+
const counts = new Array<number>((old.length + 1) * width).fill(0);
|
|
314
|
+
for (let row = old.length - 1; row >= 0; row--) {
|
|
315
|
+
for (let column = next.length - 1; column >= 0; column--) {
|
|
316
|
+
const paired = matches[row][column] === 1 ? 1 + counts[(row + 1) * width + column + 1] : 0;
|
|
317
|
+
counts[row * width + column] = Math.max(
|
|
318
|
+
paired,
|
|
319
|
+
counts[(row + 1) * width + column],
|
|
320
|
+
counts[row * width + column + 1],
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const aligned = new Array<number>(old.length).fill(-1);
|
|
325
|
+
let row = 0;
|
|
326
|
+
let column = 0;
|
|
327
|
+
while (row < old.length && column < next.length) {
|
|
328
|
+
if (matches[row][column] === 1 && counts[row * width + column] === 1 + counts[(row + 1) * width + column + 1]) {
|
|
329
|
+
aligned[row] = column;
|
|
330
|
+
row += 1;
|
|
331
|
+
column += 1;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (counts[(row + 1) * width + column] >= counts[row * width + column + 1]) row += 1;
|
|
335
|
+
else column += 1;
|
|
336
|
+
}
|
|
337
|
+
return aligned;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A plan's heading sections, keyed by `sectionKeys`, each with the blocks a
|
|
342
|
+
* comment can hang on and the last line of that content. Blank lines are left out
|
|
343
|
+
* on purpose: a position among them would move when one is added or removed
|
|
344
|
+
* above, and a reworded step would lose its comment to the next block. A section
|
|
345
|
+
* with no content block ends at its own heading, which the page renders.
|
|
254
346
|
*/
|
|
255
347
|
function sectionIndex(plan: string, blocks: readonly PlanBlock[]): Map<string, { blocks: PlanBlock[]; end: number }> {
|
|
256
|
-
const sections =
|
|
257
|
-
const lineCount = plan === "" ? 0 : plan.split("\n").length;
|
|
258
|
-
const seen = new Map<string, number>();
|
|
348
|
+
const sections = extractSections(plan);
|
|
259
349
|
const index = new Map<string, { blocks: PlanBlock[]; end: number }>();
|
|
260
|
-
const keys = sections
|
|
261
|
-
|
|
262
|
-
seen.set(section.title, occurrence + 1);
|
|
263
|
-
const key = `${occurrence}\u0000${section.title}`;
|
|
264
|
-
index.set(key, { blocks: [], end: (sections[position + 1]?.line ?? lineCount) - 1 });
|
|
265
|
-
return key;
|
|
266
|
-
});
|
|
350
|
+
const keys = sectionKeys(sections);
|
|
351
|
+
keys.forEach((key, at) => index.set(key, { blocks: [], end: sections[at]?.line ?? 0 }));
|
|
267
352
|
|
|
268
353
|
let position = 0;
|
|
269
354
|
for (const block of blocks) {
|
|
270
355
|
while (position + 1 < sections.length && (sections[position + 1]?.line ?? 0) <= block.start) position += 1;
|
|
271
356
|
const key = keys[position];
|
|
272
357
|
if (key === undefined) continue;
|
|
273
|
-
index.get(key)
|
|
358
|
+
const entry = index.get(key);
|
|
359
|
+
if (!entry || block.kind === "blank") continue;
|
|
360
|
+
entry.blocks.push(block);
|
|
361
|
+
entry.end = block.end;
|
|
274
362
|
}
|
|
275
363
|
return index;
|
|
276
364
|
}
|
|
@@ -280,9 +368,11 @@ function sectionIndex(plan: string, blocks: readonly PlanBlock[]): Map<string, {
|
|
|
280
368
|
* Comments are not plan content, so `next` is stripped of any marker it carries,
|
|
281
369
|
* and every old one is re-attached by the strongest evidence that survives: the
|
|
282
370
|
* block's text (per occurrence, so two identical steps keep their own comments),
|
|
283
|
-
* then the
|
|
284
|
-
*
|
|
285
|
-
*
|
|
371
|
+
* then the block the alignment says it became when the wording changed, then the
|
|
372
|
+
* gap it left between the blocks that survived, then the end of that section's
|
|
373
|
+
* content. A comment whose section is gone too moves to the plan's last content
|
|
374
|
+
* block, so the page keeps showing it under a block rather than listing it as
|
|
375
|
+
* unattached; only a plan with no block at all leaves it in that list.
|
|
286
376
|
*/
|
|
287
377
|
export function carryFeedback(previous: string, next: string): string {
|
|
288
378
|
const { clean: oldClean, markers } = scanMarkers(previous);
|
|
@@ -303,16 +393,35 @@ export function carryFeedback(previous: string, next: string): string {
|
|
|
303
393
|
const oldSections = sectionIndex(oldClean, oldBlocks);
|
|
304
394
|
const nextSections = sectionIndex(cleanNext, nextBlocks);
|
|
305
395
|
const lastLine = cleanNext === "" ? -1 : cleanNext.split("\n").length - 1;
|
|
396
|
+
// Where a comment goes when nothing about it survived: beside the plan's last
|
|
397
|
+
// block, never after the blank line a trailing newline leaves.
|
|
398
|
+
const tail = contentBlockAt(nextBlocks, lastLine)?.end ?? lastLine;
|
|
399
|
+
|
|
400
|
+
/** One alignment per section, reused by every comment that lands in it. */
|
|
401
|
+
const alignments = new Map<string, number[]>();
|
|
306
402
|
|
|
307
|
-
/**
|
|
403
|
+
/**
|
|
404
|
+
* Where a comment goes when its own block's text is gone: the block the alignment
|
|
405
|
+
* says it became, else the gap it left, else its section's end.
|
|
406
|
+
*/
|
|
308
407
|
const bySection = (block: PlanBlock): number | undefined => {
|
|
309
|
-
for (const [key,
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
408
|
+
for (const [key, before] of oldSections) {
|
|
409
|
+
const index = before.blocks.indexOf(block);
|
|
410
|
+
if (index < 0) continue;
|
|
411
|
+
const after = nextSections.get(key);
|
|
412
|
+
if (after === undefined) return undefined;
|
|
413
|
+
let aligned = alignments.get(key);
|
|
414
|
+
if (aligned === undefined) {
|
|
415
|
+
aligned = alignBlocks(before.blocks, after.blocks);
|
|
416
|
+
alignments.set(key, aligned);
|
|
417
|
+
}
|
|
418
|
+
const became = aligned[index];
|
|
419
|
+
if (became >= 0) return after.blocks[became].end;
|
|
420
|
+
// The gap it left is right after the block before its nearest surviving
|
|
421
|
+
// successor: for the first block of a section, that is the heading.
|
|
422
|
+
const successor = aligned.findIndex((at, from) => from > index && at >= 0);
|
|
423
|
+
const gap = successor >= 0 ? after.blocks[aligned[successor] - 1] : undefined;
|
|
424
|
+
return gap?.end ?? after.end;
|
|
316
425
|
}
|
|
317
426
|
return undefined;
|
|
318
427
|
};
|
|
@@ -320,12 +429,12 @@ export function carryFeedback(previous: string, next: string): string {
|
|
|
320
429
|
const placed = markers.map((marker) => {
|
|
321
430
|
const block = blockAt(oldBlocks, marker.at);
|
|
322
431
|
const line = markerLine(marker.author, marker.comment);
|
|
323
|
-
if (block === undefined) return { at:
|
|
432
|
+
if (block === undefined) return { at: tail, line };
|
|
324
433
|
if (block.kind !== "blank") {
|
|
325
434
|
const match = queues.get(blockKey(block))?.shift();
|
|
326
435
|
if (match !== undefined) return { at: match.end, line };
|
|
327
436
|
}
|
|
328
|
-
return { at: bySection(block) ??
|
|
437
|
+
return { at: bySection(block) ?? tail, line };
|
|
329
438
|
});
|
|
330
439
|
return compose(cleanNext, placed);
|
|
331
440
|
}
|
package/extensions/plan/index.ts
CHANGED
|
@@ -149,10 +149,12 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
149
149
|
* the path backstop (a hint on a path-carrying tool is only a claim).
|
|
150
150
|
*/
|
|
151
151
|
const blockedReason = (toolName: string, input: unknown): string | undefined => {
|
|
152
|
-
// Plan-owned
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
152
|
+
// Plan-owned tools are the sanctioned exception: `write_plan` saves the
|
|
153
|
+
// artifact, `exit_plan_mode` presents it, `cancel_plan_mode` restores access,
|
|
154
|
+
// and `enter_plan_mode` answers "already" to a call that was in flight when
|
|
155
|
+
// plan mode came on. The source-path check keeps a same-named tool from
|
|
156
|
+
// another extension from borrowing the exemption.
|
|
157
|
+
if (runtime.isOwnTool(toolName)) return undefined;
|
|
156
158
|
// `subagent` stays active, but only as read-only delegation. The flag is
|
|
157
159
|
// forced in place (the child enforces it through `--tools`), so the model
|
|
158
160
|
// cannot forget it; a `subagent` that does not declare the parameter is
|
|
@@ -16,7 +16,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { type Component, Key, matchesKey, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
17
17
|
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
18
18
|
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
19
|
-
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
19
|
+
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
|
+
import { formatSize } from "../../lib/format.ts";
|
|
20
21
|
import { clip } from "../../lib/tui/columns.ts";
|
|
21
22
|
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
22
23
|
import { titleRule } from "../../lib/tui/rules.ts";
|
|
@@ -25,8 +26,12 @@ import type { PlanSummary } from "./plans.ts";
|
|
|
25
26
|
|
|
26
27
|
/** Rows the screen shows when the terminal height is unknown. */
|
|
27
28
|
const DEFAULT_ROWS = 14;
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Fixed rows around the list body: header, summary, blank, and the closing footer
|
|
31
|
+
* (blank, hint, rule). The focused path adds `detailBlockRows` on top, and `fitRows`
|
|
32
|
+
* adds one more when the range row appears.
|
|
33
|
+
*/
|
|
34
|
+
const CHROME = 6;
|
|
30
35
|
/** Longest title a row draws before the trailing meta wins the column. */
|
|
31
36
|
const TITLE_WIDTH = 56;
|
|
32
37
|
|
|
@@ -56,11 +61,33 @@ function localTime(ms: number): string {
|
|
|
56
61
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The store's own footprint for one repository: the bytes its plans hold, and the
|
|
66
|
+
* oldest of them. Derived from the summary the directory walk already produced, so
|
|
67
|
+
* reporting a size never becomes a second index that can disagree with the files.
|
|
68
|
+
*/
|
|
69
|
+
function footprint(summaries: readonly PlanSummary[]): { bytes: number; oldest: number } {
|
|
70
|
+
let bytes = 0;
|
|
71
|
+
let oldest = summaries[0]?.modified ?? 0;
|
|
72
|
+
for (const summary of summaries) {
|
|
73
|
+
bytes += summary.bytes;
|
|
74
|
+
oldest = Math.min(oldest, summary.modified);
|
|
75
|
+
}
|
|
76
|
+
return { bytes, oldest };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `2.4 kB — oldest 2d ago`, the store's footprint as the header and the screen say it. */
|
|
80
|
+
function footprintText(summaries: readonly PlanSummary[], nowMs: number): string {
|
|
81
|
+
const { bytes, oldest } = footprint(summaries);
|
|
82
|
+
return `${formatSize(bytes)} — oldest ${relativeAge(oldest, nowMs)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/**
|
|
60
86
|
* The non-TUI listing: one line per plan with its title, step progress, modified
|
|
61
|
-
* time, and path, newest first. The
|
|
87
|
+
* time, and path, newest first. The header names the count, the store's footprint,
|
|
88
|
+
* and the directory once; `now` is injectable so the age can be asserted.
|
|
62
89
|
*/
|
|
63
|
-
export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string): string {
|
|
90
|
+
export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string, now: number = Date.now()): string {
|
|
64
91
|
const where = sanitizeLine(shortenPath(dir));
|
|
65
92
|
if (summaries.length === 0) {
|
|
66
93
|
return `No plans in ${where} yet; write_plan saves one there.`;
|
|
@@ -69,7 +96,7 @@ export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string):
|
|
|
69
96
|
(summary) =>
|
|
70
97
|
` ${sanitizeLine(summary.title)} — ${stepsText(summary)} — ${localTime(summary.modified)} — ${sanitizeLine(summary.relativePath)}`,
|
|
71
98
|
);
|
|
72
|
-
return [`Plans (${summaries.length}) — ${where}`, ...lines].join("\n");
|
|
99
|
+
return [`Plans (${summaries.length}) — ${footprintText(summaries, now)} — ${where}`, ...lines].join("\n");
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
export interface PlanIndexOptions {
|
|
@@ -185,7 +212,8 @@ export class PlanIndexComponent implements Component {
|
|
|
185
212
|
private summaryLine(width: number): string {
|
|
186
213
|
const { theme } = this.options;
|
|
187
214
|
const count = this.summaries.length;
|
|
188
|
-
const
|
|
215
|
+
const size = footprintText(this.summaries, (this.options.now ?? Date.now)());
|
|
216
|
+
const text = `${paint(theme, "accent", "Plans")} ${paint(theme, "meta", `· ${count} ${count === 1 ? "plan" : "plans"} · ${size}`)}`;
|
|
189
217
|
const where = paint(theme, "faint", ` · ${sanitizeLine(shortenPath(this.options.dir))}`);
|
|
190
218
|
return clip(` ${text}${where}`, width);
|
|
191
219
|
}
|
|
@@ -202,8 +230,14 @@ export class PlanIndexComponent implements Component {
|
|
|
202
230
|
render(width: number): string[] {
|
|
203
231
|
const { theme } = this.options;
|
|
204
232
|
const w = Math.max(1, width);
|
|
205
|
-
this.cursor.visible = fitRows(this.options.viewportRows, this.summaries.length, CHROME, DEFAULT_ROWS);
|
|
206
233
|
this.cursor.sync(this.summaries.length);
|
|
234
|
+
const detail = this.detailLines(w);
|
|
235
|
+
this.cursor.visible = fitRows(
|
|
236
|
+
this.options.viewportRows,
|
|
237
|
+
this.summaries.length,
|
|
238
|
+
CHROME + detailBlockRows(detail),
|
|
239
|
+
DEFAULT_ROWS,
|
|
240
|
+
);
|
|
207
241
|
|
|
208
242
|
const body: string[] = [];
|
|
209
243
|
if (this.summaries.length === 0) {
|
|
@@ -229,7 +263,7 @@ export class PlanIndexComponent implements Component {
|
|
|
229
263
|
range: this.cursor.clipped(this.summaries.length)
|
|
230
264
|
? formatRange(theme, w, { ...this.cursor.window(this.summaries.length), total: this.summaries.length })
|
|
231
265
|
: undefined,
|
|
232
|
-
detail
|
|
266
|
+
detail,
|
|
233
267
|
hint,
|
|
234
268
|
});
|
|
235
269
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { stripControlChars } from "../../lib/format.ts";
|
|
18
18
|
import { displayFeedback, stripFeedback } from "./feedback.ts";
|
|
19
|
-
import {
|
|
19
|
+
import { extractSections, sectionKeys, type PlanSection } from "./sections.ts";
|
|
20
20
|
|
|
21
21
|
/** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
|
|
22
22
|
const TAB_WIDTH = 4;
|
|
@@ -71,7 +71,7 @@ export function planDigest(text: string): string {
|
|
|
71
71
|
|
|
72
72
|
/** One digest per heading section, in document order, for a persisted baseline. */
|
|
73
73
|
export function sectionDigests(content: string): SectionDigest[] {
|
|
74
|
-
return
|
|
74
|
+
return extractSections(reviewText(content)).map((section) => ({
|
|
75
75
|
title: section.title,
|
|
76
76
|
digest: digestOf(section.text),
|
|
77
77
|
}));
|
|
@@ -83,30 +83,40 @@ export function sameSections(a: readonly SectionDigest[], b: readonly SectionDig
|
|
|
83
83
|
return a.every((section, index) => section.title === b[index].title && section.digest === b[index].digest);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** One section of the text as it reads now, and whether it differs from the baseline. */
|
|
87
|
+
export interface SectionMark {
|
|
88
|
+
/** The section, in the order the document lists it. */
|
|
89
|
+
section: PlanSection;
|
|
90
|
+
/** Whether its content differs from the baseline's section in the same place. */
|
|
91
|
+
changed: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
/**
|
|
87
|
-
* Per-section change
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
95
|
+
* Per-section change marks for a second review: every section of `current` with
|
|
96
|
+
* whether its content differs from the baseline's section in the same place.
|
|
97
|
+
*
|
|
98
|
+
* The marks carry their sections, so a caller cannot pair the flags with a list it
|
|
99
|
+
* parsed separately — the section list and its marks come out of one scan, in
|
|
100
|
+
* document order. Duplicate headings are told apart by occurrence rather than by
|
|
101
|
+
* title. An absent baseline marks nothing, which is what a first review of a plan
|
|
102
|
+
* looks like; `stripFeedback` only ever drops quote-marker lines, so the marks stay
|
|
103
|
+
* aligned with the sections a screen renders from the same content.
|
|
93
104
|
*/
|
|
94
|
-
export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string):
|
|
95
|
-
const sections =
|
|
96
|
-
if (baseline === undefined) return sections.map(() => false);
|
|
105
|
+
export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string): SectionMark[] {
|
|
106
|
+
const sections = extractSections(reviewText(current));
|
|
107
|
+
if (baseline === undefined) return sections.map((section) => ({ section, changed: false }));
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
// Same section across two reviews: `sectionKeys` tells a second `## Notes` from
|
|
110
|
+
// the first, so a duplicate heading is never compared with the wrong digest.
|
|
111
|
+
const before = new Map<string, string>();
|
|
112
|
+
sectionKeys(baseline).forEach((key, at) => {
|
|
113
|
+
const digest = baseline[at]?.digest;
|
|
114
|
+
if (digest !== undefined) before.set(key, digest);
|
|
115
|
+
});
|
|
104
116
|
|
|
105
|
-
const
|
|
106
|
-
return sections.map((section) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const prior = before.get(section.title)?.[index];
|
|
110
|
-
return prior === undefined || prior !== digestOf(section.text);
|
|
117
|
+
const keys = sectionKeys(sections);
|
|
118
|
+
return sections.map((section, at) => {
|
|
119
|
+
const prior = before.get(keys[at] ?? "");
|
|
120
|
+
return { section, changed: prior === undefined || prior !== digestOf(section.text) };
|
|
111
121
|
});
|
|
112
122
|
}
|
package/extensions/plan/plans.ts
CHANGED
|
@@ -40,7 +40,6 @@ import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works
|
|
|
40
40
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
41
41
|
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
42
42
|
import { isInside, isInsideReal } from "../../lib/path.ts";
|
|
43
|
-
import { plansDir } from "../../lib/plans/store.ts";
|
|
44
43
|
import { repoKeyFor } from "../../lib/repo-key.ts";
|
|
45
44
|
import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
|
|
46
45
|
import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
|
|
@@ -49,6 +48,13 @@ import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } f
|
|
|
49
48
|
export const MAX_PLAN_BYTES = 256 * 1024;
|
|
50
49
|
/** Longest slug derived from a plan title. */
|
|
51
50
|
const MAX_SLUG_LENGTH = 60;
|
|
51
|
+
/** The agent-directory entry plans are filed under. */
|
|
52
|
+
const PLANS_DIRNAME = "plans";
|
|
53
|
+
|
|
54
|
+
/** The plan store: `<agent-dir>/plans`, or `<agent-dir>/plans/<key>` inside a repository. */
|
|
55
|
+
function plansDir(agentDir: string, key?: string): string {
|
|
56
|
+
return key === undefined ? join(agentDir, PLANS_DIRNAME) : join(agentDir, PLANS_DIRNAME, key);
|
|
57
|
+
}
|
|
52
58
|
|
|
53
59
|
/** A plan read from disk. */
|
|
54
60
|
export interface StoredPlan {
|
|
@@ -94,13 +100,6 @@ export interface WritePlanInput {
|
|
|
94
100
|
export interface PlanStore {
|
|
95
101
|
/** Directory plans are written to for `cwd`. */
|
|
96
102
|
dirFor(cwd: string): Promise<string>;
|
|
97
|
-
/**
|
|
98
|
-
* The repository root `cwd` belongs to, or undefined outside a repository.
|
|
99
|
-
*
|
|
100
|
-
* A repository-scoped setting — the split switch — is read at the root, so a
|
|
101
|
-
* session started in a subdirectory still sees what the project turned on.
|
|
102
|
-
*/
|
|
103
|
-
rootFor(cwd: string): Promise<string | undefined>;
|
|
104
103
|
write(cwd: string, input: WritePlanInput): Promise<WrittenPlan>;
|
|
105
104
|
/** Read a plan inside the plans directory, or undefined when missing/outside. */
|
|
106
105
|
read(cwd: string, planPath: string): Promise<StoredPlan | undefined>;
|
|
@@ -330,8 +329,6 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
330
329
|
|
|
331
330
|
const dirFor = async (cwd: string): Promise<string> => (await resolve(cwd)).dir;
|
|
332
331
|
|
|
333
|
-
const rootFor = async (cwd: string): Promise<string | undefined> => (await resolve(cwd)).root;
|
|
334
|
-
|
|
335
332
|
const write = async (cwd: string, input: WritePlanInput): Promise<WrittenPlan> => {
|
|
336
333
|
const base = baseOf(cwd);
|
|
337
334
|
const title = input.title.trim();
|
|
@@ -556,5 +553,5 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
|
|
|
556
553
|
return summaries;
|
|
557
554
|
};
|
|
558
555
|
|
|
559
|
-
return { dirFor,
|
|
556
|
+
return { dirFor, write, read, latest, index, setSteps, saveFeedback, takeNotice };
|
|
560
557
|
}
|