From 9ca324342cdf002250a4169d042e05c1595de366 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Fri, 12 Jun 2026 22:51:08 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20add=20/clear=20command=20=E2=80=94=20ex?= =?UTF-8?q?port=20session=20to=20transcripts/=20and=20start=20new=20sessio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers a /clear slash command as a project-local extension. Prompts for a transcript name (spaces → dashes), exports the full session HTML (with in-memory state) to transcripts/.html, then starts a new session with parent tracking. --- .pi/extensions/clear-export.ts | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .pi/extensions/clear-export.ts diff --git a/.pi/extensions/clear-export.ts b/.pi/extensions/clear-export.ts new file mode 100644 index 0000000..820bc38 --- /dev/null +++ b/.pi/extensions/clear-export.ts @@ -0,0 +1,118 @@ +import type { ExtensionAPI, ToolInfo } from "@earendil-works/pi-coding-agent"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +// Resolve the pi-coding-agent package directory +// Search in: project node_modules, then global mise node_modules +function findPkgDir(): string { + // Try project node_modules first + const projectPkg = path.join(process.cwd(), "node_modules", "@earendil-works", "pi-coding-agent"); + if (fs.existsSync(projectPkg)) return projectPkg; + + // Try global mise node_modules + const home = homedir(); + const globalPkg = path.join(home, ".local", "share", "mise", "installs", "node", "*", "lib", "node_modules", "@earendil-works", "pi-coding-agent"); + const matches = fs.readdirSync(path.join(home, ".local", "share", "mise", "installs", "node")).filter((d) => + fs.existsSync(path.join(home, ".local", "share", "mise", "installs", "node", d)), + ); + for (const version of matches) { + const candidate = path.join( + home, + ".local", + "share", + "mise", + "installs", + "node", + version, + "lib", + "node_modules", + "@earendil-works", + "pi-coding-agent", + ); + if (fs.existsSync(candidate)) return candidate; + } + + // Fallback: try the directory of the loaded module + const extPath = fileURLToPath(import.meta.url); + let dir = extPath; + for (let i = 0; i < 20; i++) { + const candidate = path.join(dir, "node_modules", "@earendil-works", "pi-coding-agent"); + if (fs.existsSync(candidate)) return candidate; + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + + throw new Error("Could not find @earendil-works/pi-coding-agent package"); +} + +const pkgDir = findPkgDir(); +const exportHtmlModule = await import(path.join(pkgDir, "dist", "core", "export-html", "index.js")); +const { exportSessionToHtml } = exportHtmlModule; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("clear", { + description: "Export session to transcripts/ and start a new session", + handler: async (_args, ctx) => { + if (ctx.mode !== "tui") { + ctx.ui.notify("clear requires interactive mode", "error"); + return; + } + + // Prompt for transcript name + const name = await ctx.ui.input("Clear & Export", "Transcript name (spaces become dashes)", { + timeout: 60000, + }); + if (!name || !name.trim()) { + ctx.ui.notify("Clear & Export cancelled", "info"); + return; + } + + // Sanitize: spaces → dashes, ensure .html extension + const sanitizedName = name.trim().replace(/\s+/g, "-"); + const fileName = sanitizedName.endsWith(".html") ? sanitizedName : `${sanitizedName}.html`; + + // Build output path in transcripts/ + const transcriptsDir = path.join(ctx.cwd, "transcripts"); + const outputPath = path.join(transcriptsDir, fileName); + + // Ensure transcripts directory exists + if (!fs.existsSync(transcriptsDir)) { + fs.mkdirSync(transcriptsDir, { recursive: true }); + } + + // Get session file path + const sessionFile = ctx.sessionManager.getSessionFile(); + if (!sessionFile) { + ctx.ui.notify("No session to export", "error"); + return; + } + + // Build state from in-memory data (not available in a separate pi process) + const state = { + systemPrompt: ctx.getSystemPrompt(), + tools: ctx.getAllTools(), + }; + + // Export session to HTML with full in-memory state + await exportSessionToHtml(ctx.sessionManager, state, { outputPath }); + + ctx.ui.notify(`Exported to: transcripts/${fileName}`, "info"); + + // Start a new session + const currentSessionFile = ctx.sessionManager.getSessionFile(); + const newResult = await ctx.newSession({ + parentSession: currentSessionFile, + withSession: (replacementCtx) => { + replacementCtx.ui.notify("Session cleared and exported", "info"); + }, + }); + + if (newResult.cancelled) { + ctx.ui.notify("New session cancelled", "info"); + } + }, + }); +}