02059823ba48fa3bc072e03975c8c1806d5e7d9a

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Move tree pruning extension to its own repo

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",