@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
package/extensions/memory/tui.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* without the component owning any authoritative state. Navigation and scroll
|
|
8
8
|
* math come from `lib/tui/cursor.ts` and the screen chrome from `lib/tui/chrome.ts`;
|
|
9
9
|
* the inline note editor is the shared `Editor` from `@earendil-works/pi-tui`.
|
|
10
|
+
*
|
|
11
|
+
* `m` marks one note as a merge source and `m` on a second note in the same
|
|
12
|
+
* scope opens the editor, pre-filled with both notes so the user writes the
|
|
13
|
+
* merged wording. The merge is one write: the store replaces the first note and
|
|
14
|
+
* drops the second. `p` moves the focused note to the other scope, which is two
|
|
15
|
+
* writes ordered target-first so a failure leaves a duplicate, never a loss.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -53,6 +59,10 @@ export interface MemoryPickerCallbacks {
|
|
|
53
59
|
add(scope: MemoryScope, text: string): Promise<{ changed: boolean; existing?: string }>;
|
|
54
60
|
update(scope: MemoryScope, from: string, to: string): Promise<boolean>;
|
|
55
61
|
forget(scope: MemoryScope, entry: string): Promise<boolean>;
|
|
62
|
+
/** Merge two notes in `scope` into one, in a single write. */
|
|
63
|
+
merge(scope: MemoryScope, first: string, second: string, merged: string): Promise<boolean>;
|
|
64
|
+
/** Move a note to the other scope: written there first, then removed here. */
|
|
65
|
+
move(from: MemoryScope, to: MemoryScope, entry: string): Promise<{ moved: boolean; existing?: string }>;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
/** One note in the flat, filtered list. */
|
|
@@ -72,11 +82,15 @@ export class MemoryPickerComponent implements Component {
|
|
|
72
82
|
private readonly editor: Editor;
|
|
73
83
|
|
|
74
84
|
private filter: MemoryFilter = "all";
|
|
75
|
-
private mode: "list" | "edit" | "add" = "list";
|
|
85
|
+
private mode: "list" | "edit" | "add" | "merge" = "list";
|
|
76
86
|
/** Scope the inline editor writes to. */
|
|
77
87
|
private editorScope: MemoryScope = "project";
|
|
78
88
|
/** Note being edited, or undefined while adding. */
|
|
79
89
|
private editing: Note | undefined;
|
|
90
|
+
/** The two notes a merge editor will combine. */
|
|
91
|
+
private mergePair: { first: Note; second: Note } | undefined;
|
|
92
|
+
/** The note marked with `m`, awaiting a second `m` to merge into it. */
|
|
93
|
+
private mergeSource: Note | undefined;
|
|
80
94
|
/** Note awaiting a y/N forget confirmation. */
|
|
81
95
|
private confirm: Note | undefined;
|
|
82
96
|
/** Transient feedback shown in place of the hint. */
|
|
@@ -161,15 +175,55 @@ export class MemoryPickerComponent implements Component {
|
|
|
161
175
|
}
|
|
162
176
|
this.mode = "edit";
|
|
163
177
|
this.editing = note;
|
|
178
|
+
this.mergePair = undefined;
|
|
164
179
|
this.editorScope = note.scope;
|
|
165
180
|
this.editor.setText(note.text);
|
|
166
181
|
this.syncEditorFocus();
|
|
167
182
|
this.requestRender();
|
|
168
183
|
}
|
|
169
184
|
|
|
185
|
+
/**
|
|
186
|
+
* `m`: mark the focused note, unmark it, or open the merge editor for a marked
|
|
187
|
+
* pair. The mark survives navigation so the second note can be found.
|
|
188
|
+
*/
|
|
189
|
+
private toggleMerge(note: Note | undefined): void {
|
|
190
|
+
if (!note) return;
|
|
191
|
+
const source = this.mergeSource;
|
|
192
|
+
if (source && noteKey(source) === noteKey(note)) {
|
|
193
|
+
this.mergeSource = undefined;
|
|
194
|
+
this.requestRender();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (source) {
|
|
198
|
+
this.startMerge(source, note);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.mergeSource = note;
|
|
202
|
+
this.notice = undefined;
|
|
203
|
+
this.requestRender();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Open the editor for a marked pair, pre-filled with both notes to rewrite. */
|
|
207
|
+
private startMerge(first: Note, second: Note): void {
|
|
208
|
+
if (first.scope !== second.scope) {
|
|
209
|
+
this.notice = "A merge combines two notes in the same scope.";
|
|
210
|
+
this.requestRender();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.mode = "merge";
|
|
214
|
+
this.editing = undefined;
|
|
215
|
+
this.mergePair = { first, second };
|
|
216
|
+
this.editorScope = first.scope;
|
|
217
|
+
// The user writes the merged wording; the seed only saves retyping both notes.
|
|
218
|
+
this.editor.setText(`${first.text}; ${second.text}`.slice(0, MAX_NOTE_CHARS));
|
|
219
|
+
this.syncEditorFocus();
|
|
220
|
+
this.requestRender();
|
|
221
|
+
}
|
|
222
|
+
|
|
170
223
|
private startAdd(): void {
|
|
171
224
|
this.mode = "add";
|
|
172
225
|
this.editing = undefined;
|
|
226
|
+
this.mergePair = undefined;
|
|
173
227
|
this.editorScope = this.activeScope();
|
|
174
228
|
this.editor.setText("");
|
|
175
229
|
this.syncEditorFocus();
|
|
@@ -179,6 +233,8 @@ export class MemoryPickerComponent implements Component {
|
|
|
179
233
|
private cancelEditor(): void {
|
|
180
234
|
this.mode = "list";
|
|
181
235
|
this.editing = undefined;
|
|
236
|
+
this.mergePair = undefined;
|
|
237
|
+
this.mergeSource = undefined;
|
|
182
238
|
this.syncEditorFocus();
|
|
183
239
|
this.notice = undefined;
|
|
184
240
|
this.requestRender();
|
|
@@ -188,8 +244,11 @@ export class MemoryPickerComponent implements Component {
|
|
|
188
244
|
const text = sanitizeLine(value).trim();
|
|
189
245
|
const editing = this.editing;
|
|
190
246
|
const scope = this.editorScope;
|
|
247
|
+
const pair = this.mergePair;
|
|
191
248
|
this.mode = "list";
|
|
192
249
|
this.editing = undefined;
|
|
250
|
+
this.mergePair = undefined;
|
|
251
|
+
this.mergeSource = undefined;
|
|
193
252
|
this.syncEditorFocus();
|
|
194
253
|
|
|
195
254
|
if (text.length === 0) {
|
|
@@ -204,6 +263,12 @@ export class MemoryPickerComponent implements Component {
|
|
|
204
263
|
}
|
|
205
264
|
|
|
206
265
|
this.run(async () => {
|
|
266
|
+
if (pair) {
|
|
267
|
+
await this.callbacks.merge(pair.first.scope, pair.first.text, pair.second.text, text);
|
|
268
|
+
this.notice = `Merged 2 ${scopeLabel(pair.first.scope)} notes.`;
|
|
269
|
+
this.selectedKey = noteKey({ scope: pair.first.scope, text });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
207
272
|
if (editing) {
|
|
208
273
|
if (text === editing.text) {
|
|
209
274
|
this.notice = "Unchanged.";
|
|
@@ -236,6 +301,30 @@ export class MemoryPickerComponent implements Component {
|
|
|
236
301
|
});
|
|
237
302
|
}
|
|
238
303
|
|
|
304
|
+
/** `p`: move the focused note to the other scope, target first. */
|
|
305
|
+
private moveNote(note: Note | undefined): void {
|
|
306
|
+
if (!note) return;
|
|
307
|
+
const to: MemoryScope = note.scope === "project" ? "global" : "project";
|
|
308
|
+
if (to === "project" && !this.callbacks.trusted()) {
|
|
309
|
+
this.notice = "Project memory is unavailable for an untrusted project.";
|
|
310
|
+
this.requestRender();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const from = note.scope;
|
|
314
|
+
const text = note.text;
|
|
315
|
+
this.run(async () => {
|
|
316
|
+
const result = await this.callbacks.move(from, to, text);
|
|
317
|
+
if (!result.moved) {
|
|
318
|
+
this.notice = `Added to ${scopeLabel(to)}; the ${scopeLabel(from)} note was already gone.`;
|
|
319
|
+
} else if (result.existing !== undefined) {
|
|
320
|
+
this.notice = `Moved to ${scopeLabel(to)} (it was already there).`;
|
|
321
|
+
} else {
|
|
322
|
+
this.notice = `Moved to ${scopeLabel(to)}.`;
|
|
323
|
+
}
|
|
324
|
+
this.selectedKey = noteKey({ scope: to, text });
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
239
328
|
private reload(): void {
|
|
240
329
|
this.notice = this.callbacks.refresh() ? "Reloaded memory." : "Memory is already up to date.";
|
|
241
330
|
this.requestRender();
|
|
@@ -248,11 +337,21 @@ export class MemoryPickerComponent implements Component {
|
|
|
248
337
|
.catch((error: unknown) => {
|
|
249
338
|
this.notice = `Memory not saved: ${messageOf(error)}`;
|
|
250
339
|
})
|
|
251
|
-
.then(() =>
|
|
340
|
+
.then(() => {
|
|
341
|
+
this.dropStaleMergeSource();
|
|
342
|
+
this.requestRender();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** A merge source that a mutation removed is no longer a valid target. */
|
|
347
|
+
private dropStaleMergeSource(): void {
|
|
348
|
+
const source = this.mergeSource;
|
|
349
|
+
if (!source) return;
|
|
350
|
+
if (!this.notes().some((note) => noteKey(note) === noteKey(source))) this.mergeSource = undefined;
|
|
252
351
|
}
|
|
253
352
|
|
|
254
353
|
private syncEditorFocus(): void {
|
|
255
|
-
this.editor.focused = this.componentFocused &&
|
|
354
|
+
this.editor.focused = this.componentFocused && this.mode !== "list";
|
|
256
355
|
}
|
|
257
356
|
|
|
258
357
|
private handleConfirm(data: string): void {
|
|
@@ -270,7 +369,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
270
369
|
}
|
|
271
370
|
|
|
272
371
|
handleInput(data: string): void {
|
|
273
|
-
if (this.mode
|
|
372
|
+
if (this.mode !== "list") {
|
|
274
373
|
if (isCloseKey(data) || matchesKey(data, Key.escape)) {
|
|
275
374
|
this.cancelEditor();
|
|
276
375
|
return;
|
|
@@ -284,6 +383,12 @@ export class MemoryPickerComponent implements Component {
|
|
|
284
383
|
return;
|
|
285
384
|
}
|
|
286
385
|
if (isCloseKey(data) || data === "q") {
|
|
386
|
+
// With a mark set, the first Esc cancels the merge instead of closing the screen.
|
|
387
|
+
if (this.mergeSource) {
|
|
388
|
+
this.mergeSource = undefined;
|
|
389
|
+
this.requestRender();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
287
392
|
this.onClose();
|
|
288
393
|
return;
|
|
289
394
|
}
|
|
@@ -308,6 +413,8 @@ export class MemoryPickerComponent implements Component {
|
|
|
308
413
|
if (matchesKey(data, Key.tab) || data === "s") this.cycleFilter();
|
|
309
414
|
else if (matchesKey(data, Key.enter) || data === "e") this.startEdit(focused);
|
|
310
415
|
else if (data === "a") this.startAdd();
|
|
416
|
+
else if (data === "m") this.toggleMerge(focused);
|
|
417
|
+
else if (data === "p") this.moveNote(focused);
|
|
311
418
|
else if (data === "d") {
|
|
312
419
|
if (!focused) return;
|
|
313
420
|
this.confirm = focused;
|
|
@@ -355,9 +462,11 @@ export class MemoryPickerComponent implements Component {
|
|
|
355
462
|
/** One note row: selection marker, scope tag, and the sanitized note text. */
|
|
356
463
|
private noteRow(note: Note, selected: boolean, width: number): string {
|
|
357
464
|
const tag = `${paint(this.theme, "meta", scopeLabel(note.scope))} ${paint(this.theme, "faint", "·")}`;
|
|
465
|
+
const source = this.mergeSource;
|
|
466
|
+
const marked = source !== undefined && noteKey(source) === noteKey(note);
|
|
358
467
|
return listRow(this.theme, width, {
|
|
359
468
|
selected,
|
|
360
|
-
leading: tag,
|
|
469
|
+
leading: marked ? `${tag} ${paint(this.theme, "warn", "merge")}` : tag,
|
|
361
470
|
label: sanitizeLine(note.text),
|
|
362
471
|
labelRole: selected ? "accent" : "label",
|
|
363
472
|
});
|
|
@@ -372,17 +481,20 @@ export class MemoryPickerComponent implements Component {
|
|
|
372
481
|
}
|
|
373
482
|
|
|
374
483
|
private renderEditor(width: number): string[] {
|
|
375
|
-
const verb = this.mode === "edit" ? "Edit" : "Add";
|
|
484
|
+
const verb = this.mode === "edit" ? "Edit" : this.mode === "merge" ? "Merge" : "Add";
|
|
376
485
|
const scope = scopeLabel(this.editorScope);
|
|
377
486
|
const pad = width > 1 ? " " : "";
|
|
487
|
+
const summary =
|
|
488
|
+
this.mode === "edit"
|
|
489
|
+
? "Editing the focused note"
|
|
490
|
+
: this.mode === "merge"
|
|
491
|
+
? `Merging 2 ${scope} notes into one`
|
|
492
|
+
: `New ${scope} note`;
|
|
378
493
|
const body: string[] = [];
|
|
379
494
|
for (const line of this.editor.render(Math.max(1, width - 2))) body.push(`${pad}${line}`);
|
|
380
495
|
return dockListFrame({
|
|
381
496
|
header: titleRule(this.theme, width, `Memory · ${verb} ${scope}`),
|
|
382
|
-
summary: clip(
|
|
383
|
-
` ${paint(this.theme, "meta", this.mode === "edit" ? "Editing the focused note" : `New ${scope} note`)}`,
|
|
384
|
-
width,
|
|
385
|
-
),
|
|
497
|
+
summary: clip(` ${paint(this.theme, "meta", summary)}`, width),
|
|
386
498
|
body,
|
|
387
499
|
hint: screenHint(this.theme, width, ["Enter save", "Esc cancel"]),
|
|
388
500
|
});
|
|
@@ -391,7 +503,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
391
503
|
render(width: number): string[] {
|
|
392
504
|
const w = Math.max(1, width);
|
|
393
505
|
this.syncEditorFocus();
|
|
394
|
-
if (this.mode
|
|
506
|
+
if (this.mode !== "list") return this.renderEditor(w);
|
|
395
507
|
|
|
396
508
|
const notes = this.notes();
|
|
397
509
|
this.cursor.visible = fitRows(this.viewportRowsSource, notes.length, CHROME, SCREEN_DEFAULT_ROWS);
|
|
@@ -411,7 +523,19 @@ export class MemoryPickerComponent implements Component {
|
|
|
411
523
|
? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
|
|
412
524
|
: this.notice !== undefined
|
|
413
525
|
? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
|
|
414
|
-
:
|
|
526
|
+
: this.mergeSource
|
|
527
|
+
? screenHint(this.theme, w, ["m merge into the focused note", "Esc cancel merge", "↑/↓"])
|
|
528
|
+
: screenHint(this.theme, w, [
|
|
529
|
+
"Esc close",
|
|
530
|
+
"Enter/e edit",
|
|
531
|
+
"a add",
|
|
532
|
+
"d forget",
|
|
533
|
+
"m merge",
|
|
534
|
+
"p move",
|
|
535
|
+
"s/Tab scope",
|
|
536
|
+
"↑/↓",
|
|
537
|
+
"g/G",
|
|
538
|
+
]);
|
|
415
539
|
|
|
416
540
|
return dockListFrame({
|
|
417
541
|
header: titleRule(this.theme, w, "Memory"),
|
|
@@ -42,19 +42,28 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
42
42
|
`<repo-root>/.pi/plans/<YYYY-MM-DD-HHmm>-<slug>.md` (or `<agent-dir>/plans`
|
|
43
43
|
outside a repository). The plans directory is self-ignoring, so plan files
|
|
44
44
|
never show up as untracked files or in `rewind` snapshots.
|
|
45
|
+
- **A discoverable index.** `/plan list` walks the plans directory and shows
|
|
46
|
+
every plan with its title, modified time, and step progress, newest first — in
|
|
47
|
+
the TUI as a docked list where `Enter` opens the plan's review, elsewhere as a
|
|
48
|
+
notice with each plan's path. The directory stays the index: no registry file,
|
|
49
|
+
one bounded read per plan.
|
|
45
50
|
- **A shape that reads.** `write_plan` documents a canonical outline — a
|
|
46
51
|
`# <Title>`, then `## Goal`, `## Context`, `## Approach`, `## Steps`,
|
|
47
52
|
`## Risks`, and `## Verification` — and the model is guided to follow it. This
|
|
48
53
|
is guidance only: any markdown is accepted, stored, and rendered as written.
|
|
49
54
|
- **Trackable steps.** Steps are written as `- [ ]` checkboxes in the `## Steps`
|
|
50
|
-
section; approval seeds them into `todo
|
|
55
|
+
section; approval seeds them into `todo`, with the `## Verification` section as
|
|
56
|
+
the last item so the definition of done travels with the work. A plan with more
|
|
57
|
+
than 50 top-level steps is refused rather than truncated, so approval never
|
|
58
|
+
drops a step you reviewed.
|
|
51
59
|
- **A navigable review.** The review screen splits the body at its headings,
|
|
52
60
|
names the section you are in, and jumps between sections with `n`/`p`.
|
|
53
61
|
- **What changed since the last review.** After a refinement, the review screen
|
|
54
62
|
names how many sections changed, marks the current one in the scroll row, and
|
|
55
63
|
the browser outline dots each changed section, so a re-review does not mean
|
|
56
|
-
re-reading the whole plan. The baseline is
|
|
57
|
-
|
|
64
|
+
re-reading the whole plan. The baseline is one digest per heading section of
|
|
65
|
+
what the previous review showed, kept as a session entry, so a resumed session
|
|
66
|
+
or a `/tree` restore marks only what changed on its own branch.
|
|
58
67
|
- **Kept current.** After approval, `set_plan_steps` flips the checkboxes of
|
|
59
68
|
the steps you name in the file, so the plan mirrors what has actually been
|
|
60
69
|
done. Pass the step's text as `expected` and an update whose step number moved
|
|
@@ -83,19 +92,28 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
83
92
|
nested sub-bullets are treated as notes. The list is a merge, not a
|
|
84
93
|
replacement: existing items keep their place and status, a step whose text is
|
|
85
94
|
already there is skipped, and the tool's item cap defers the rest instead of
|
|
86
|
-
dropping anything.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
dropping anything. The plan's `## Verification` section follows as the **last**
|
|
96
|
+
todo item, summarized on one line (`Verify: …`) — it is not a plan step, so it is
|
|
97
|
+
ticked in the todo list rather than with `set_plan_steps`, and it never counts
|
|
98
|
+
against the 50-step cap. A plan without a `Steps` heading keeps the older rule —
|
|
99
|
+
the shallowest list in the document — so prose bullets in a plan that follows the
|
|
100
|
+
outline never become todos, and its verification bullets are already covered.
|
|
101
|
+
- **Goal-aware.** While planning, the injected context restates an active
|
|
102
|
+
session goal as one bullet, so the plan is designed against the objective
|
|
103
|
+
instead of beside it. On approval, when the session has no goal at all, the user
|
|
104
|
+
is offered one confirm to record the plan's title as the goal — never replacing
|
|
105
|
+
an existing goal, and with no extra model call.
|
|
106
|
+
- **Branch-aware.** The enabled flag and the review baseline are custom session
|
|
107
|
+
entries, so `/resume` and `/tree` restore the right mode and keep the
|
|
108
|
+
changed-section markers.
|
|
91
109
|
|
|
92
110
|
## Tools
|
|
93
111
|
|
|
94
112
|
| Tool | Active | Purpose |
|
|
95
113
|
|---|---|---|
|
|
96
114
|
| `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
|
|
97
|
-
| `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. |
|
|
98
|
-
| `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. |
|
|
115
|
+
| `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. A plan with more than 50 top-level steps in its `Steps` section is refused, with the cap and the fix named, and nothing is written. |
|
|
116
|
+
| `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. Refuses a file whose step list is over the cap, before the review screen. |
|
|
99
117
|
| `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and optionally `expected` — the step's text, so a reordered plan is refused) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. |
|
|
100
118
|
|
|
101
119
|
### Commands and flags
|
|
@@ -105,6 +123,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
105
123
|
| `enter_plan_mode` tool | the model, before a non-trivial task | `ctx.ui.confirm` |
|
|
106
124
|
| `/plan` | the user | immediate |
|
|
107
125
|
| `/plan <prompt>` | the user, with the task | immediate |
|
|
126
|
+
| `/plan list` | the user, browsing the plans directory | immediate |
|
|
108
127
|
| `/plan review [path]` | the user, reviewing a plan in the browser | immediate |
|
|
109
128
|
| `Ctrl+Alt+P` | the user | immediate |
|
|
110
129
|
| `--plan` | the user, at launch | immediate |
|
|
@@ -143,7 +162,7 @@ actions (`a approve & run` in `success`, `r refine` in `accent`).
|
|
|
143
162
|
mounted and opens an inline editor beneath it (`Enter` submits, an empty buffer
|
|
144
163
|
does nothing, `Esc` returns to the plan); `Esc` keeps planning and tells the
|
|
145
164
|
model the plan was not approved. When the plan changed since the previous review
|
|
146
|
-
|
|
165
|
+
on this branch, a summary line under the title names how many sections changed
|
|
147
166
|
and the scroll row appends `· changed` to the current section. Scroll with `↑`/`↓` or `j`/`k`, `space`/`b` or
|
|
148
167
|
`PgUp`/`PgDn`, `d`/`u`, `g`/`G`, and the mouse wheel. Press `n`/`p` to jump to
|
|
149
168
|
the next or previous heading; the scroll row names the current section when the
|
|
@@ -174,6 +193,10 @@ instead of opening a server nobody can use. The browser's **Approve & run** is
|
|
|
174
193
|
offered only for the review `exit_plan_mode` opens, since only that flow owns
|
|
175
194
|
leaving plan mode; `/plan review` stays comment-only.
|
|
176
195
|
|
|
196
|
+
Aborting the turn closes a browser review. `exit_plan_mode` forwards the call's
|
|
197
|
+
abort signal to the review server, so an aborted turn returns "not approved"
|
|
198
|
+
and stops listening instead of waiting on a page nobody is watching.
|
|
199
|
+
|
|
177
200
|
### Transcript
|
|
178
201
|
|
|
179
202
|
The three tool rows reuse the transcript component, and `exit_plan_mode`
|
|
@@ -227,8 +250,11 @@ command. The read-only guarantee is layered:
|
|
|
227
250
|
transcript and in non-TUI notices; a plan file cannot drive the terminal.
|
|
228
251
|
- **Loopback browser review.** The review server binds `127.0.0.1` and requires a
|
|
229
252
|
random per-session token in the URL, rejects a missing or foreign `Host`
|
|
230
|
-
header, serves only `GET
|
|
231
|
-
with DOM text nodes, never `innerHTML`, so a
|
|
253
|
+
header, serves only `GET`, `HEAD`, and `POST`, and caps the request body; the
|
|
254
|
+
page renders plan and comment text with DOM text nodes, never `innerHTML`, so a
|
|
255
|
+
plan cannot script the reviewer. The page also refuses to be framed
|
|
256
|
+
(`frame-ancestors 'none'` and `X-Frame-Options: DENY`), so a local page cannot
|
|
257
|
+
clickjack **Approve & run**.
|
|
232
258
|
|
|
233
259
|
## Read-only git shell
|
|
234
260
|
|
|
@@ -314,7 +340,7 @@ file and returns them to the model.
|
|
|
314
340
|
links, an active-section highlight, and a per-section comment count; the panel
|
|
315
341
|
above it reports the `Steps` section's progress (`n/m`, the same list approval
|
|
316
342
|
seeds) and total comments. A
|
|
317
|
-
section that changed since the previous review
|
|
343
|
+
section that changed since the previous review on this branch carries a dot
|
|
318
344
|
(its `title` and accessible label say so); an unchanged plan shows none.
|
|
319
345
|
- **Inline threads.** A comment renders directly under the block it anchors to,
|
|
320
346
|
labelled and indented as a thread, so it reads in place and the plan keeps one
|
|
@@ -356,12 +382,13 @@ file and returns them to the model.
|
|
|
356
382
|
update, depending on the mode).
|
|
357
383
|
- **Loopback and tokenized.** The server binds `127.0.0.1`, carries a random
|
|
358
384
|
per-session token in the URL, rejects a missing or foreign `Host` header,
|
|
359
|
-
serves only `GET` and `POST`, and caps the request body. A save or cancel
|
|
385
|
+
serves only `GET`, `HEAD`, and `POST`, and caps the request body. A save or cancel
|
|
360
386
|
resolves the session only after its HTTP response is delivered, so the browser
|
|
361
387
|
never sees a false "Failed to fetch". The last bound port is remembered per
|
|
362
388
|
plan, so reviewing the same plan again reuses the origin (and the browser's
|
|
363
|
-
saved theme with it). It is closed on save, cancel,
|
|
364
|
-
`session_shutdown
|
|
389
|
+
saved theme with it). It is closed on save, cancel, a dropped event stream, or
|
|
390
|
+
`session_shutdown`; a reload reconnects inside that grace, so refreshing or
|
|
391
|
+
reloading a conflicted page does not end the review.
|
|
365
392
|
|
|
366
393
|
| Route | Result |
|
|
367
394
|
|---|---|
|
|
@@ -378,14 +405,15 @@ file and returns them to the model.
|
|
|
378
405
|
Git Bash (`bash`), which must be installed for any shell command to run.
|
|
379
406
|
- The read-only guarantee is a guard rail against accidental writes, not a
|
|
380
407
|
security boundary (see [Security](#security)).
|
|
381
|
-
- The plans directory is the only index
|
|
382
|
-
plan
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
shortened in the todo list and
|
|
388
|
-
|
|
408
|
+
- The plans directory is the only index. `/plan list` walks it on every open and
|
|
409
|
+
keeps no registry, so it sees an external edit or a hand-copied plan
|
|
410
|
+
immediately; it lists a file over the 256 KiB plan limit without reading it,
|
|
411
|
+
and does not index the agent directory's plans while a worktree is active.
|
|
412
|
+
- Approval tracks at most 50 steps: `write_plan` refuses a larger plan, and
|
|
413
|
+
`exit_plan_mode` refuses to present one, so no reviewed step is silently
|
|
414
|
+
dropped. A step longer than 200 characters is shortened in the todo list and
|
|
415
|
+
the approval listing — a display rule only, because the plan file always keeps
|
|
416
|
+
the full text.
|
|
389
417
|
- Browser review needs a local browser: the server is loopback-only, so a remote
|
|
390
418
|
turn cannot drive it.
|
|
391
419
|
- A browser that never connects leaves the review waiting until the user
|
|
@@ -406,14 +434,21 @@ file and returns them to the model.
|
|
|
406
434
|
|
|
407
435
|
| Integration point | How |
|
|
408
436
|
|---|---|
|
|
409
|
-
| Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`); no `outputSchema`/`structuredContent`. |
|
|
410
|
-
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
411
|
-
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })
|
|
412
|
-
| Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context
|
|
437
|
+
| Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`), and `ExitPlanModeDetails` carries the review's `annotations` when a browser approval or save supplied comments; no `outputSchema`/`structuredContent`. |
|
|
438
|
+
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
439
|
+
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan never writes a goal itself: an approval offer records one through `ctx.executeTool("goal", { objective })`. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
|
|
440
|
+
| Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with an active session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
|
|
413
441
|
| Lifecycle | `session_start`/`session_tree` restore state; `session_shutdown` clears the footer status and closes any browser review server. |
|
|
414
442
|
|
|
415
443
|
## Design notes
|
|
416
444
|
|
|
445
|
+
- **One review loop owns the human gate.** `review.ts` asks however the session
|
|
446
|
+
is attached — TUI screen, dialog plus editor, remote file plus digest, or
|
|
447
|
+
browser — and returns one `ReviewDecision`, so `exit_plan_mode` and
|
|
448
|
+
`/plan review` no longer carry two copies of the browser lifecycle and the
|
|
449
|
+
tool's own `for(;;)` with a `continue` per outcome. The loop also owns the
|
|
450
|
+
baseline rule (mark what the screen showed, after it showed it) and forwards
|
|
451
|
+
the call's abort signal, which is what closes a review on an aborted turn.
|
|
417
452
|
- **Two tools, not one.** `EnterPlanMode`/`ExitPlanMode` are a pair: entering is
|
|
418
453
|
consent to explore read-only, exiting is consent to act. A single toggle tool
|
|
419
454
|
cannot express "the model wants to plan but the user declines".
|
|
@@ -441,12 +476,21 @@ file and returns them to the model.
|
|
|
441
476
|
channel; no import from that extension) and the steps are merged into them.
|
|
442
477
|
A rejected seed reports the nested tool's own reason in the approval message
|
|
443
478
|
instead of failing silently.
|
|
479
|
+
- **A cap is enforced where it can still be fixed.** Both refusal points —
|
|
480
|
+
`write_plan` and `exit_plan_mode` — sit before the write and before the review,
|
|
481
|
+
because those are the moments the model can still regroup the steps. A note on
|
|
482
|
+
a successful write would arrive after the file existed and the review had
|
|
483
|
+
already listed every step, which is exactly when the overflow becomes lost
|
|
484
|
+
work.
|
|
444
485
|
- **Changed markers compare what was shown, not revisions on disk.** A review
|
|
445
|
-
result is only meaningful next to the text the reviewer already read, so
|
|
446
|
-
|
|
447
|
-
`
|
|
486
|
+
result is only meaningful next to the text the reviewer already read, so every
|
|
487
|
+
review marks the sections it presented (keyed by resolved path) and
|
|
488
|
+
`plan-text.ts` compares the next plan against those digests. Stored feedback is
|
|
448
489
|
stripped from both sides, so a review comment never reads as an edit, and the
|
|
449
|
-
same helper feeds the terminal summary and the browser outline.
|
|
490
|
+
same helper feeds the terminal summary and the browser outline. The digests are
|
|
491
|
+
a session entry rather than the previous text, so the markers survive a resume
|
|
492
|
+
without copying the plan file into the branch, and the reader and the writer
|
|
493
|
+
share one normalization so a tab cannot read as a change.
|
|
450
494
|
- **The outline guides, it does not enforce.** `write_plan` documents the
|
|
451
495
|
canonical shape in its description and stores whatever markdown it is given;
|
|
452
496
|
no section is required and nothing is rewritten. That keeps the model free to
|
|
@@ -499,11 +543,14 @@ file and returns them to the model.
|
|
|
499
543
|
| `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
|
|
500
544
|
| `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
|
|
501
545
|
| `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
|
|
502
|
-
| `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections,
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
546
|
+
| `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, and the checkbox edit. |
|
|
547
|
+
| `plan-text.ts` | The plan text a review shows and compares: `sanitizePlanText`, the one normalization both sides use, the per-section `sectionDigests`, and `changedSections`. |
|
|
548
|
+
| `runtime.ts` | Enabled state, tool gating, persistence (mode and review baseline), control-tool identity, footer status. |
|
|
549
|
+
| `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`: schemas, results, transcripts, and the approval-time todo seed. |
|
|
550
|
+
| `review.ts` | The review gate: one decision from the TUI screen, a dialog, or the browser, the shared browser lifecycle, and the "mark what was shown" baseline rule. |
|
|
505
551
|
| `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
|
|
506
552
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, changed-section summary and marker, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
553
|
+
| `plan-index.ts` | `/plan list`: the notice text and the docked index screen (title, step progress, age, focused path; Enter reviews). |
|
|
507
554
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
508
555
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
509
556
|
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline (with changed-section flags) and code highlighting, and the save/approve/cancel/abort session. |
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* step.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { FENCE, HEADING } from "./steps.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";
|
|
@@ -30,10 +30,6 @@ export interface PlanBlock {
|
|
|
30
30
|
lines: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** ATX heading outside a fence. */
|
|
34
|
-
const HEADING = /^ {0,3}#{1,6}(?:\s|$)/;
|
|
35
|
-
/** Code-fence opener or closer (backticks or tildes). */
|
|
36
|
-
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
37
33
|
/** A list marker with its leading indentation and trailing gap. */
|
|
38
34
|
const LIST_ITEM = /^( *)([-*+]|\d+[.)])(\s+)/;
|
|
39
35
|
/** A blockquote line. */
|
|
@@ -190,36 +186,6 @@ export function blockAt(blocks: PlanBlock[], line: number): PlanBlock | undefine
|
|
|
190
186
|
return candidate;
|
|
191
187
|
}
|
|
192
188
|
|
|
193
|
-
/** A heading in a plan, for the review page's section outline. */
|
|
194
|
-
export interface PlanHeading {
|
|
195
|
-
/** 0-based source line of the heading. */
|
|
196
|
-
line: number;
|
|
197
|
-
/** Heading level, 1-6. */
|
|
198
|
-
level: number;
|
|
199
|
-
/** Heading text with inline markdown removed. */
|
|
200
|
-
text: string;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** ATX heading with its captured level and raw text. */
|
|
204
|
-
const HEADING_PARTS = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
205
|
-
|
|
206
|
-
/** Strip the inline markdown an outline label should not show. */
|
|
207
|
-
function headingText(raw: string): string {
|
|
208
|
-
return normalizeHeadingText(raw);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** The plan's headings in order, for the outline. */
|
|
212
|
-
export function planHeadings(blocks: PlanBlock[]): PlanHeading[] {
|
|
213
|
-
const headings: PlanHeading[] = [];
|
|
214
|
-
for (const block of blocks) {
|
|
215
|
-
if (block.kind !== "heading") continue;
|
|
216
|
-
const match = block.lines[0]?.match(HEADING_PARTS);
|
|
217
|
-
if (!match) continue;
|
|
218
|
-
headings.push({ line: block.start, level: match[1].length, text: headingText(match[2]) });
|
|
219
|
-
}
|
|
220
|
-
return headings;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
189
|
/** The source inside a fenced code block, without its opening and closing fence lines. */
|
|
224
190
|
export function codeBody(block: PlanBlock): string[] {
|
|
225
191
|
if (block.kind !== "code") return block.lines.slice();
|