02059823ba48fa3bc072e03975c8c1806d5e7d9a
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/dot_pi/agent/extensions/pi-tree-pruning/index.ts b/dot_pi/agent/extensions/pi-tree-pruning/index.ts
2deleted file mode 100644
3index ad3aff71dd5fa0e23b191e6b8716c387aac0e5db..0000000000000000000000000000000000000000
4--- a/dot_pi/agent/extensions/pi-tree-pruning/index.ts
5+++ /dev/null
6@@ -1,264 +0,0 @@
7-import { basename } from "node:path";
8-import type {
9- ExtensionAPI,
10- ExtensionCommandContext,
11- SessionEntry,
12-} from "@earendil-works/pi-coding-agent";
13-import {
14- captureSessionFile,
15- countPruneEntries,
16- getSessionTreeEntries,
17- createBackupPath,
18- listSessionBackups,
19- pruneSessionFile,
20- restoreSessionBackup,
21- toSessionFilePath,
22- type SessionFilePath,
23-} from "./session-file.ts";
24-import { selectPruneCandidates } from "./prune-selector.ts";
25-import {
26- findInactiveBranchCandidates,
27- normalizePruneCandidates,
28- toEntryId,
29- type EntryId,
30-} from "./tree-model.ts";
31-import {
32- buildPruneTreeItems,
33- formatEntryCount,
34- formatPruneTreeItem,
35-} from "./tree-view.ts";
36-
37-const COMMAND_NAME = "tree-prune";
38-
39-export default function registerTreePruning(pi: ExtensionAPI): void {
40- pi.registerCommand(COMMAND_NAME, {
41- description: "Delete inactive session-tree branches with a backup; use /tree-prune undo to restore",
42- handler: runTreePruneCommand,
43- });
44-}
45-
46-export async function runTreePruneCommand(
47- args: string,
48- ctx: ExtensionCommandContext,
49-): Promise<void> {
50- const mode = args.trim();
51-
52- if (!ctx.hasUI) {
53- ctx.ui.notify("/tree-prune requires a user interface", "error");
54- return;
55- }
56-
57- if (mode === "") {
58- await runPrune(ctx);
59- return;
60- }
61-
62- if (mode === "undo") {
63- await runUndo(ctx);
64- return;
65- }
66-
67- ctx.ui.notify("Usage: /tree-prune [undo]", "error");
68-}
69-
70-async function runPrune(ctx: ExtensionCommandContext): Promise<void> {
71- try {
72- await ctx.waitForIdle();
73-
74- const sessionFile = requireSessionFile(ctx);
75- const currentLeafId = ctx.sessionManager.getLeafId();
76- if (!currentLeafId) {
77- ctx.ui.notify("Tree Prune needs a current active leaf", "warning");
78- return;
79- }
80-
81- const activeLeafId = toEntryId(currentLeafId);
82- const snapshot = await captureSessionFile(sessionFile);
83- const entries = getSessionTreeEntries(snapshot);
84- if (!entries.some((entry) => entry.id === activeLeafId)) {
85- ctx.ui.notify("Tree Prune cannot find the active leaf in this session file", "error");
86- return;
87- }
88-
89- const candidates = findInactiveBranchCandidates(entries, activeLeafId);
90- if (candidates.length === 0) {
91- ctx.ui.notify("Tree Prune found no inactive branches", "info");
92- return;
93- }
94-
95- const treeItems = buildPruneTreeItems(candidates, (entryId) => ctx.sessionManager.getLabel(entryId));
96- const selectedCandidates = await selectPruneCandidates(ctx, treeItems);
97- if (!selectedCandidates || selectedCandidates.length === 0) return;
98-
99- const candidatesToPrune = normalizePruneCandidates(selectedCandidates);
100- const removedEntryCount = countPruneEntries(candidatesToPrune);
101- const branchesToPrune = candidatesToPrune.map((candidate) => {
102- const item = treeItems.find((treeItem) => treeItem.candidate.rootId === candidate.rootId);
103- return `• ${item ? formatPruneTreeItem(item) : candidate.preview}`;
104- });
105- const backupPath = createBackupPath(sessionFile);
106diff --git a/dot_pi/agent/extensions/pi-tree-pruning/package-lock.json b/dot_pi/agent/extensions/pi-tree-pruning/package-lock.json
107deleted file mode 100644
108index 5602dffa2670211769a814791d54ddca57f2499d..0000000000000000000000000000000000000000
109--- a/dot_pi/agent/extensions/pi-tree-pruning/package-lock.json
110+++ /dev/null
111@@ -1,1927 +0,0 @@
112-{
113- "name": "pi-tree-pruning",
114- "version": "0.1.0",
115- "lockfileVersion": 3,
116- "requires": true,
117- "packages": {
118- "": {
119- "name": "pi-tree-pruning",
120- "version": "0.1.0",
121- "devDependencies": {
122- "@earendil-works/pi-coding-agent": "0.84.2",
123- "@earendil-works/pi-tui": "0.84.2",
124- "@types/node": "^24.5.2",
125- "typescript": "^5.9.2"
126- },
127- "peerDependencies": {
128- "@earendil-works/pi-coding-agent": "^0.84.2",
129- "@earendil-works/pi-tui": "^0.84.2"
130- }
131- },
132- "node_modules/@anthropic-ai/sdk": {
133- "version": "0.91.1",
134- "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz",
135- "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==",
136- "dev": true,
137- "license": "MIT",
138- "dependencies": {
139- "json-schema-to-ts": "^3.1.1"
140- },
141- "bin": {
142- "anthropic-ai-sdk": "bin/cli"
143- },
144- "peerDependencies": {
145- "zod": "^3.25.0 || ^4.0.0"
146- },
147- "peerDependenciesMeta": {
148- "zod": {
149- "optional": true
150- }
151- }
152- },
153- "node_modules/@aws-crypto/sha256-browser": {
154- "version": "5.2.0",
155- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
156- "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
157- "dev": true,
158- "license": "Apache-2.0",
159- "dependencies": {
160- "@aws-crypto/sha256-js": "^5.2.0",
161- "@aws-crypto/supports-web-crypto": "^5.2.0",
162- "@aws-crypto/util": "^5.2.0",
163- "@aws-sdk/types": "^3.222.0",
164- "@aws-sdk/util-locate-window": "^3.0.0",
165- "@smithy/util-utf8": "^2.0.0",
166- "tslib": "^2.6.2"
167- }
168- },
169- "node_modules/@aws-crypto/sha256-js": {
170- "version": "5.2.0",
171- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
172- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
173- "dev": true,
174- "license": "Apache-2.0",
175- "dependencies": {
176- "@aws-crypto/util": "^5.2.0",
177- "@aws-sdk/types": "^3.222.0",
178- "tslib": "^2.6.2"
179- },
180- "engines": {
181- "node": ">=16.0.0"
182- }
183- },
184- "node_modules/@aws-crypto/supports-web-crypto": {
185- "version": "5.2.0",
186- "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
187- "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
188- "dev": true,
189- "license": "Apache-2.0",
190- "dependencies": {
191- "tslib": "^2.6.2"
192- }
193- },
194- "node_modules/@aws-crypto/util": {
195- "version": "5.2.0",
196- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
197- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
198- "dev": true,
199- "license": "Apache-2.0",
200- "dependencies": {
201- "@aws-sdk/types": "^3.222.0",
202- "@smithy/util-utf8": "^2.0.0",
203- "tslib": "^2.6.2"
204- }
205- },
206- "node_modules/@aws-sdk/client-bedrock-runtime": {
207- "version": "3.1048.0",
208- "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz",
209- "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==",
210- "dev": true,
211diff --git a/dot_pi/agent/extensions/pi-tree-pruning/package.json b/dot_pi/agent/extensions/pi-tree-pruning/package.json
212deleted file mode 100644
213index 3763c9b0ac5eb8a67516a13b313a1cd7b20fb86c..0000000000000000000000000000000000000000
214--- a/dot_pi/agent/extensions/pi-tree-pruning/package.json
215+++ /dev/null
216@@ -1,25 +0,0 @@
217-{
218- "name": "pi-tree-pruning",
219- "private": true,
220- "version": "0.1.0",
221- "type": "module",
222- "scripts": {
223- "typecheck": "tsc --noEmit",
224- "check": "npm run typecheck"
225- },
226- "pi": {
227- "extensions": [
228- "./index.ts"
229- ]
230- },
231- "peerDependencies": {
232- "@earendil-works/pi-coding-agent": "^0.84.2",
233- "@earendil-works/pi-tui": "^0.84.2"
234- },
235- "devDependencies": {
236- "@earendil-works/pi-coding-agent": "0.84.2",
237- "@earendil-works/pi-tui": "0.84.2",
238- "@types/node": "^24.5.2",
239- "typescript": "^5.9.2"
240- }
241-}
242diff --git a/dot_pi/agent/extensions/pi-tree-pruning/prune-selector.ts b/dot_pi/agent/extensions/pi-tree-pruning/prune-selector.ts
243deleted file mode 100644
244index e07ad6dfc25731207d4cc4b0c20be3639f9976fc..0000000000000000000000000000000000000000
245--- a/dot_pi/agent/extensions/pi-tree-pruning/prune-selector.ts
246+++ /dev/null
247@@ -1,186 +0,0 @@
248-import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
249-import {
250- pruneCandidatesOverlap,
251- type BranchCandidate,
252-} from "./tree-model.ts";
253-import { formatPruneTreeItem, type PruneTreeItem } from "./tree-view.ts";
254-
255-export async function selectPruneCandidates(
256- ctx: ExtensionCommandContext,
257- items: readonly PruneTreeItem[],
258-): Promise<BranchCandidate[] | undefined> {
259- if (ctx.mode === "tui") {
260- return selectWithTreeUi(ctx, items);
261- }
262-
263- return selectWithFallback(ctx, items);
264-}
265-
266-async function selectWithTreeUi(
267- ctx: ExtensionCommandContext,
268- items: readonly PruneTreeItem[],
269-): Promise<BranchCandidate[] | undefined> {
270- const { Key, matchesKey, truncateToWidth } = await import("@earendil-works/pi-tui");
271- const selectedRootIds = await ctx.ui.custom<readonly string[] | undefined>((tui, theme, _keybindings, done) => {
272- let selectedIndex = 0;
273- let scrollOffset = 0;
274- let selectedRootIds = new Set<string>();
275-
276- const visibleItemCount = (): number => Math.max(1, (tui.terminal.rows ?? 24) - 5);
277- const keepSelectionVisible = (): void => {
278- const visibleCount = visibleItemCount();
279- if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
280- if (selectedIndex >= scrollOffset + visibleCount) {
281- scrollOffset = selectedIndex - visibleCount + 1;
282- }
283- };
284- const moveSelection = (offset: number): void => {
285- selectedIndex = Math.max(0, Math.min(items.length - 1, selectedIndex + offset));
286- keepSelectionVisible();
287- tui.requestRender();
288- };
289-
290- return {
291- handleInput(data: string): void {
292- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
293- done(undefined);
294- return;
295- }
296- if (matchesKey(data, Key.up)) {
297- moveSelection(-1);
298- return;
299- }
300- if (matchesKey(data, Key.down)) {
301- moveSelection(1);
302- return;
303- }
304- if (matchesKey(data, Key.space)) {
305- const candidate = items[selectedIndex]?.candidate;
306- if (!candidate) return;
307- selectedRootIds = togglePruneCandidateSelection(selectedRootIds, candidate, items);
308- tui.requestRender();
309- return;
310- }
311- if (matchesKey(data, Key.enter) && selectedRootIds.size > 0) {
312- done([...selectedRootIds]);
313- }
314- },
315- render(width: number): string[] {
316- const visibleCount = visibleItemCount();
317- const visibleItems = items.slice(scrollOffset, scrollOffset + visibleCount);
318- const lines = [
319- theme.fg("accent", theme.bold("Prune inactive branches:")),
320- theme.fg(
321- "dim",
322- `↑↓ move • space toggle • enter confirm (${formatSelectedBranchCount(selectedRootIds.size)}) • esc cancel`,
323- ),
324- ];
325-
326- if (scrollOffset > 0) lines.push(theme.fg("dim", `… ${scrollOffset} earlier branches`));
327-
328- for (let index = 0; index < visibleItems.length; index += 1) {
329- const item = visibleItems[index];
330- if (!item) continue;
331-
332- const itemIndex = scrollOffset + index;
333- const marker = selectedRootIds.has(item.candidate.rootId) ? "[x]" : "[ ]";
334- const row = `${itemIndex === selectedIndex ? "→" : " "} ${marker} ${item.treePrefix}${formatPruneTreeItem(item)}`;
335- const rendered = itemIndex === selectedIndex ? theme.fg("accent", theme.bold(row)) : row;
336- lines.push(rendered);
337- }
338-
339- const hiddenAfter = items.length - scrollOffset - visibleItems.length;
340- if (hiddenAfter > 0) lines.push(theme.fg("dim", `… ${hiddenAfter} later branches`));
341-
342- return lines.map((line) => truncateToWidth(line, width));
343- },
344- invalidate(): void {},
345- };
346- });
347diff --git a/dot_pi/agent/extensions/pi-tree-pruning/session-file.ts b/dot_pi/agent/extensions/pi-tree-pruning/session-file.ts
348deleted file mode 100644
349index e665d251c1823e08cdffdf57b5b84ced6ec7bbe4..0000000000000000000000000000000000000000
350--- a/dot_pi/agent/extensions/pi-tree-pruning/session-file.ts
351+++ /dev/null
352@@ -1,338 +0,0 @@
353-import { randomUUID } from "node:crypto";
354-import { chmod, link, lstat, open, readdir, readFile, rename, unlink } from "node:fs/promises";
355-import { basename, dirname, join, resolve } from "node:path";
356-import {
357- toEntryId,
358- treeEntryFromUnknown,
359- type BranchCandidate,
360- type EntryId,
361- type TreeEntry,
362-} from "./tree-model.ts";
363-
364-declare const sessionFilePathBrand: unique symbol;
365-
366-export type SessionFilePath = string & { readonly [sessionFilePathBrand]: "SessionFilePath" };
367-
368-export interface FileIdentity {
369- readonly dev: number;
370- readonly ino: number;
371- readonly mode: number;
372- readonly mtimeMs: number;
373- readonly size: number;
374-}
375-
376-export interface SessionFileSnapshot {
377- readonly path: SessionFilePath;
378- readonly content: string;
379- readonly identity: FileIdentity;
380-}
381-
382-interface RawLine {
383- readonly content: string;
384- readonly record?: Record<string, unknown>;
385-}
386-
387-export function toSessionFilePath(path: string): SessionFilePath {
388- return resolve(path) as SessionFilePath;
389-}
390-
391-export async function captureSessionFile(sessionFile: string): Promise<SessionFileSnapshot> {
392- const path = toSessionFilePath(sessionFile);
393-
394- for (let attempt = 0; attempt < 2; attempt += 1) {
395- const before = await getFileIdentity(path);
396- const content = await readFile(path, "utf8");
397- const after = await getFileIdentity(path);
398-
399- if (sameIdentity(before, after) && Buffer.byteLength(content) === after.size) {
400- return { path, content, identity: after };
401- }
402- }
403-
404- throw new Error("Session file changed while it was read");
405-}
406-
407-export async function assertSessionFileUnchanged(snapshot: SessionFileSnapshot): Promise<void> {
408- const current = await captureSessionFile(snapshot.path);
409- if (!sameIdentity(snapshot.identity, current.identity) || snapshot.content !== current.content) {
410- throw new Error("Session file changed while the dialog was open");
411- }
412-}
413-
414-export function getSessionTreeEntries(snapshot: SessionFileSnapshot): TreeEntry[] {
415- const entries: TreeEntry[] = [];
416- for (const rawLine of splitRawLines(snapshot.content)) {
417- const entry = treeEntryFromUnknown(rawLine.record);
418- if (entry) entries.push(entry);
419- }
420- return entries;
421-}
422-
423-export function buildPrunedSessionContent(
424- snapshot: SessionFileSnapshot,
425- candidates: readonly BranchCandidate[],
426-): string {
427- const expectedEntries = collectPruneEntries(candidates);
428- const rawLines = splitRawLines(snapshot.content);
429- const linesByEntryId = new Map<EntryId, number[]>();
430-
431- for (let index = 0; index < rawLines.length; index += 1) {
432- const rawLine = rawLines[index];
433- if (!rawLine?.record) continue;
434-
435- const id = rawLine.record.id;
436- if (typeof id !== "string" || id.length === 0) continue;
437-
438- const entryId = toEntryId(id);
439- if (!expectedEntries.has(entryId)) continue;
440-
441- const indexes = linesByEntryId.get(entryId) ?? [];
442- indexes.push(index);
443- linesByEntryId.set(entryId, indexes);
444- }
445-
446- const lineIndexesToRemove = new Set<number>();
447- for (const [id, entry] of expectedEntries) {
448- const indexes = linesByEntryId.get(id) ?? [];
449- if (indexes.length !== 1) {
450- throw new Error(`Could not safely locate exactly one JSONL line for entry ${id}`);
451- }
452diff --git a/dot_pi/agent/extensions/pi-tree-pruning/tree-model.ts b/dot_pi/agent/extensions/pi-tree-pruning/tree-model.ts
453deleted file mode 100644
454index 5107e062c9458aba64667dcd072afb848cf9e04d..0000000000000000000000000000000000000000
455--- a/dot_pi/agent/extensions/pi-tree-pruning/tree-model.ts
456+++ /dev/null
457@@ -1,246 +0,0 @@
458-import type { SessionEntry } from "@earendil-works/pi-coding-agent";
459-
460-declare const entryIdBrand: unique symbol;
461-
462-export type EntryId = string & { readonly [entryIdBrand]: "EntryId" };
463-
464-interface UnknownTreeEntry {
465- readonly id: EntryId;
466- readonly parentId: EntryId | null;
467- readonly type: string;
468- readonly message?: unknown;
469- readonly summary?: unknown;
470-}
471-
472-export type TreeEntry = (SessionEntry & {
473- readonly id: EntryId;
474- readonly parentId: EntryId | null;
475-}) | UnknownTreeEntry;
476-
477-export interface BranchCandidate {
478- readonly rootId: EntryId;
479- readonly forkPointId: EntryId;
480- readonly entries: readonly TreeEntry[];
481- readonly entryIds: readonly EntryId[];
482- readonly entryCount: number;
483- readonly preview: string;
484-}
485-
486-interface TreeIndex {
487- readonly entriesById: ReadonlyMap<EntryId, TreeEntry>;
488- readonly childrenByParentId: ReadonlyMap<EntryId, readonly EntryId[]>;
489- readonly uncertainChildrenByParentId: ReadonlySet<EntryId>;
490-}
491-
492-const PREVIEW_LIMIT = 72;
493-
494-export function findInactiveBranchCandidates(
495- entries: readonly TreeEntry[],
496- activeLeafId: EntryId,
497-): BranchCandidate[] {
498- const tree = buildTreeIndex(entries);
499- if (!tree.entriesById.has(activeLeafId)) return [];
500-
501- const candidates: BranchCandidate[] = [];
502- for (const [forkPointId, childIds] of tree.childrenByParentId) {
503- if (childIds.length < 2 || tree.uncertainChildrenByParentId.has(forkPointId)) continue;
504-
505- for (const rootId of childIds) {
506- const branchEntries = collectSubtree(rootId, tree);
507- if (!branchEntries) continue;
508- if (branchEntries.some((entry) => entry.id === activeLeafId)) continue;
509-
510- const root = tree.entriesById.get(rootId);
511- if (!root) continue;
512-
513- candidates.push({
514- rootId,
515- forkPointId,
516- entries: branchEntries,
517- entryIds: branchEntries.map((entry) => entry.id),
518- entryCount: branchEntries.length,
519- preview: previewEntry(root),
520- });
521- }
522- }
523-
524- return candidates;
525-}
526-
527-export function normalizePruneCandidates(
528- candidates: readonly BranchCandidate[],
529-): BranchCandidate[] {
530- const candidatesByRootId = new Map<EntryId, BranchCandidate>();
531- for (const candidate of candidates) {
532- if (!candidatesByRootId.has(candidate.rootId)) {
533- candidatesByRootId.set(candidate.rootId, candidate);
534- }
535- }
536-
537- const uniqueCandidates = [...candidatesByRootId.values()];
538- return uniqueCandidates.filter(
539- (candidate) =>
540- !uniqueCandidates.some(
541- (possibleParent) =>
542- possibleParent.rootId !== candidate.rootId &&
543- possibleParent.entryIds.includes(candidate.rootId),
544- ),
545- );
546-}
547-
548-export function pruneCandidatesOverlap(left: BranchCandidate, right: BranchCandidate): boolean {
549- if (left.rootId === right.rootId) return true;
550- return left.entryIds.some((entryId) => right.entryIds.includes(entryId));
551-}
552-
553-function buildTreeIndex(entries: readonly TreeEntry[]): TreeIndex {
554- const validEntries = entries.filter(isTreeEntry);
555- const occurrences = new Map<EntryId, number>();
556- const uncertainChildrenByParentId = new Set<EntryId>();
557diff --git a/dot_pi/agent/extensions/pi-tree-pruning/tree-view.ts b/dot_pi/agent/extensions/pi-tree-pruning/tree-view.ts
558deleted file mode 100644
559index acf6db3e7c8dc38338135e8db22a0fb6e20d3ddb..0000000000000000000000000000000000000000
560--- a/dot_pi/agent/extensions/pi-tree-pruning/tree-view.ts
561+++ /dev/null
562@@ -1,76 +0,0 @@
563-import type { BranchCandidate } from "./tree-model.ts";
564-
565-export interface PruneTreeItem {
566- readonly candidate: BranchCandidate;
567- readonly label: string | undefined;
568- readonly treePrefix: string;
569-}
570-
571-export function buildPruneTreeItems(
572- candidates: readonly BranchCandidate[],
573- getLabel: (entryId: string) => string | undefined,
574-): PruneTreeItem[] {
575- const parentByRootId = new Map<string, string>();
576-
577- for (const candidate of candidates) {
578- const parent = findParentCandidate(candidate, candidates);
579- if (parent) parentByRootId.set(candidate.rootId, parent.rootId);
580- }
581-
582- const childrenByRootId = new Map<string | undefined, BranchCandidate[]>();
583- for (const candidate of candidates) {
584- const parentRootId = parentByRootId.get(candidate.rootId);
585- const children = childrenByRootId.get(parentRootId) ?? [];
586- children.push(candidate);
587- childrenByRootId.set(parentRootId, children);
588- }
589-
590- const items: PruneTreeItem[] = [];
591- const visit = (candidate: BranchCandidate, ancestorIsLast: readonly boolean[], isLast: boolean): void => {
592- const parentRootId = parentByRootId.get(candidate.rootId);
593- const treePrefix = parentRootId
594- ? `${ancestorIsLast.map((last) => (last ? " " : "│ ")).join("")}${isLast ? "└─ " : "├─ "}`
595- : "";
596-
597- items.push({ candidate, label: getLabel(candidate.rootId), treePrefix });
598-
599- const children = childrenByRootId.get(candidate.rootId) ?? [];
600- for (let index = 0; index < children.length; index += 1) {
601- const child = children[index];
602- if (!child) continue;
603- visit(child, [...ancestorIsLast, isLast], index === children.length - 1);
604- }
605- };
606-
607- const roots = childrenByRootId.get(undefined) ?? [];
608- for (let index = 0; index < roots.length; index += 1) {
609- const root = roots[index];
610- if (!root) continue;
611- visit(root, [], index === roots.length - 1);
612- }
613-
614- return items;
615-}
616-
617-export function formatPruneTreeItem(item: PruneTreeItem): string {
618- const label = item.label?.replace(/\s+/g, " ").trim();
619- const preview = label ? `[${label}] ${item.candidate.preview}` : item.candidate.preview;
620- return `${preview} · ${formatEntryCount(item.candidate.entryCount)}`;
621-}
622-
623-export function formatEntryCount(count: number): string {
624- return `${count} ${count === 1 ? "entry" : "entries"}`;
625-}
626-
627-function findParentCandidate(
628- candidate: BranchCandidate,
629- candidates: readonly BranchCandidate[],
630-): BranchCandidate | undefined {
631- return candidates
632- .filter(
633- (possibleParent) =>
634- possibleParent.rootId !== candidate.rootId &&
635- possibleParent.entryIds.includes(candidate.forkPointId),
636- )
637- .sort((left, right) => left.entryCount - right.entryCount)[0];
638-}
639diff --git a/dot_pi/agent/extensions/pi-tree-pruning/tsconfig.json b/dot_pi/agent/extensions/pi-tree-pruning/tsconfig.json
640deleted file mode 100644
641index ea2b147fee7ae840125b3ade94280e9d9a8ecbcf..0000000000000000000000000000000000000000
642--- a/dot_pi/agent/extensions/pi-tree-pruning/tsconfig.json
643+++ /dev/null
644@@ -1,18 +0,0 @@
645-{
646- "compilerOptions": {
647- "target": "ESNext",
648- "module": "NodeNext",
649- "moduleResolution": "NodeNext",
650- "lib": ["ESNext"],
651- "allowImportingTsExtensions": true,
652- "noEmit": true,
653- "strict": true,
654- "exactOptionalPropertyTypes": true,
655- "noUncheckedIndexedAccess": true,
656- "noImplicitOverride": true,
657- "noFallthroughCasesInSwitch": true,
658- "skipLibCheck": true,
659- "types": ["node"]
660- },
661- "include": ["./*.ts"]
662-}
663diff --git a/dot_pi/agent/modify_settings.json b/dot_pi/agent/modify_settings.json
664index 47afe5d4feaab22245ad4c271cc42e200c961a66..075331fc58998b19a22fbab9e8179fe676acb40f 100644
665--- a/dot_pi/agent/modify_settings.json
666+++ b/dot_pi/agent/modify_settings.json
667@@ -30,6 +30,7 @@
668 "packages": [
669 "/home/pavle/dev/agent-policy-engine",
670 "git:git.theedgeofrage.com/TheEdgeOfRage/pi-llama",
671+ "git:git.theedgeofrage.com/TheEdgeOfRage/pi-tree-pruning",
672 "git:github.com/dmmulroy/pi-web-tools",
673 "npm:@plannotator/pi-extension",
674 "npm:pi-mcp-adapter",