import { randomUUID } from "node:crypto"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { HerdrTimeoutError, SocketHerdrClient, type HerdrPing, type LaunchSidetrackInput, type SidetrackLaunch, } from "../../src/herdr.ts"; import { cloneActiveSession, type CloneActiveSessionInput, type ClonedSession } from "../../src/session.ts"; interface SidetrackClient { ping(signal?: AbortSignal): Promise; launchSidetrack(input: LaunchSidetrackInput, signal?: AbortSignal): Promise; } export interface SidetrackDependencies { createClient(socketPath: string): SidetrackClient; cloneSession(input: CloneActiveSessionInput): Promise; makeName(): string; } function preCloneError(error: unknown): string { if (error instanceof Error && error.message) return error.message; return "Sidetrack could not be created."; } function postCloneError(error: unknown, childSessionFile: string): string { const reason = error instanceof HerdrTimeoutError ? "Herdr did not confirm the launch in time; the pane may still have been created." : "Herdr did not confirm the launch."; return `Sidetrack launch failed. ${reason} The cloned session was preserved at ${childSessionFile}`; } export function createSidetrackExtension(dependencies: SidetrackDependencies) { return function herdrSidetrack(pi: ExtensionAPI): void { const socketPath = process.env.HERDR_SOCKET_PATH; const workspaceId = process.env.HERDR_WORKSPACE_ID; const tabId = process.env.HERDR_TAB_ID; const parentPaneId = process.env.HERDR_PANE_ID; if (!socketPath || !workspaceId || !tabId || !parentPaneId) { let notified = false; pi.on("session_start", (_event, ctx) => { if (notified || !ctx.hasUI) return; notified = true; ctx.ui.notify("Herdr sidetrack inactive: start Pi through Herdr to enable /sidetrack.", "info"); }); return; } const client = dependencies.createClient(socketPath); pi.registerCommand("sidetrack", { description: "Clone the active conversation tip into a focused right-hand Herdr pane immediately", handler: async (args, ctx) => { if (ctx.mode !== "tui") { ctx.ui.notify("Sidetrack requires Pi's interactive TUI.", "error"); return; } if (!args.trim()) { ctx.ui.notify("Usage: /sidetrack ", "error"); return; } let childSessionFile: string | undefined; try { // Snapshot immediately so a busy parent does not delay or advance the fork tip. const parentSessionFile = ctx.sessionManager.getSessionFile(); const leafId = ctx.sessionManager.getLeafId(); const sessionDir = ctx.sessionManager.getSessionDir(); const cwd = ctx.cwd; if (!parentSessionFile) { ctx.ui.notify("Sidetrack requires a persisted Pi session. Start Pi without --no-session and try again.", "error"); return; } if (!leafId) { ctx.ui.notify("The current Pi session has no saved conversation to clone. Continue the conversation first.", "error"); return; } const ping = await client.ping(); const name = dependencies.makeName(); const cloned = await dependencies.cloneSession({ parentSessionFile, sessionDir, cwd, leafId }); childSessionFile = cloned.childSessionFile; const launch = await client.launchSidetrack({ ping, name, childSessionFile, prompt: args, cwd, workspaceId, tabId, parentPaneId, }); ctx.ui.notify(`Sidetrack opened in Herdr pane ${launch.paneId}. Child session: ${childSessionFile}`, "info"); } catch (error) { ctx.ui.notify( childSessionFile ? postCloneError(error, childSessionFile) : preCloneError(error), "error", ); } }, }); }; } const extension = createSidetrackExtension({ createClient: (socketPath) => new SocketHerdrClient({ socketPath }), cloneSession: cloneActiveSession, makeName: () => `sidetrack-${randomUUID().replaceAll("-", "").slice(0, 12)}`, }); export default extension;