/ extensions / ephemeral-mode.ts
ephemeral-mode.ts
1 /** 2 * Ephemeral Mode Extension 3 * 4 * Toggle session persistence on/off mid-session. 5 * When ephemeral mode is enabled, the session file is deleted on exit. 6 * 7 * Usage: 8 * /ephemeral - Toggle ephemeral mode 9 * Alt+E - Toggle ephemeral mode (shortcut) 10 */ 11 12 import { SessionManager, type ExtensionAPI, type SessionEntry } from "@mariozechner/pi-coding-agent"; 13 import { existsSync, unlinkSync } from "node:fs"; 14 15 export default function (pi: ExtensionAPI) { 16 let ephemeralMode = false; 17 18 const isEphemeralEnabled = (entries: SessionEntry[]) => { 19 let enabled = false; 20 21 for (const entry of entries) { 22 if (entry.type === "custom" && entry.customType === "ephemeral-mode") { 23 enabled = entry.data?.enabled ?? false; 24 } 25 } 26 27 return enabled; 28 }; 29 30 // Reconstruct state from the *current branch* so /tree navigation properly restores branch-specific state 31 const reconstructStateFromBranch = (ctx: any) => { 32 ephemeralMode = isEphemeralEnabled(ctx.sessionManager.getBranch()); 33 34 if (!ctx.hasUI) { 35 return; 36 } 37 38 if (ephemeralMode) { 39 ctx.ui.setStatus("ephemeral", ctx.ui.theme.fg("warning", "儚")); 40 } else { 41 ctx.ui.setStatus("ephemeral", undefined); 42 } 43 }; 44 45 const toggleEphemeral = async (ctx: any) => { 46 ephemeralMode = !ephemeralMode; 47 48 // Persist the state in the session branch (for /tree navigation) 49 pi.appendEntry("ephemeral-mode", { enabled: ephemeralMode }); 50 51 if (!ctx.hasUI) { 52 return; 53 } 54 55 if (ephemeralMode) { 56 ctx.ui.setStatus("ephemeral", ctx.ui.theme.fg("warning", "儚")); 57 ctx.ui.notify("Ephemeral mode ON - session will be deleted on exit", "warning"); 58 } else { 59 ctx.ui.setStatus("ephemeral", undefined); 60 ctx.ui.notify("Ephemeral mode OFF - session will be preserved", "info"); 61 } 62 }; 63 64 const clearIndicator = (ctx: any) => { 65 ephemeralMode = false; 66 if (ctx?.hasUI) { 67 ctx.ui.setStatus("ephemeral", undefined); 68 } 69 }; 70 71 const deleteSessionFileBestEffort = async (sessionFile: string) => { 72 try { 73 const { code } = await pi.exec("trash", [sessionFile], { timeout: 5000 }); 74 if (code === 0) { 75 return; 76 } 77 } catch { 78 // ignore 79 } 80 81 // Fallback to direct deletion 82 try { 83 if (existsSync(sessionFile)) { 84 unlinkSync(sessionFile); 85 } 86 } catch { 87 // ignore 88 } 89 }; 90 91 const deletePreviousEphemeralSessionIfNeeded = async (previousSessionFile?: string, currentSessionFile?: string) => { 92 if (!previousSessionFile || previousSessionFile === currentSessionFile) { 93 return; 94 } 95 96 try { 97 const previousSession = SessionManager.open(previousSessionFile); 98 if (!isEphemeralEnabled(previousSession.getBranch())) { 99 return; 100 } 101 } catch { 102 return; 103 } 104 105 await deleteSessionFileBestEffort(previousSessionFile); 106 }; 107 108 pi.on("session_start", async (event, ctx) => { 109 if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") { 110 await deletePreviousEphemeralSessionIfNeeded( 111 event.previousSessionFile, 112 ctx.sessionManager.getSessionFile() ?? undefined, 113 ); 114 } 115 116 reconstructStateFromBranch(ctx); 117 }); 118 119 pi.on("session_before_switch", async (_event, ctx) => { 120 // Clear the indicator immediately so it never visually leaks into the switcher UI or next session 121 clearIndicator(ctx); 122 }); 123 124 pi.on("session_tree", async (_event, ctx) => { 125 reconstructStateFromBranch(ctx); 126 }); 127 128 pi.on("session_before_fork", async (_event, ctx) => { 129 // Clear the indicator immediately so it never visually leaks into the picker UI or next session 130 clearIndicator(ctx); 131 }); 132 133 pi.registerCommand("ephemeral", { 134 description: "Toggle ephemeral mode (delete session file on exit)", 135 handler: async (_args, ctx) => { 136 await toggleEphemeral(ctx); 137 }, 138 }); 139 140 pi.registerShortcut("alt+e", { 141 description: "Toggle ephemeral mode", 142 handler: async (ctx) => { 143 await toggleEphemeral(ctx); 144 }, 145 }); 146 147 pi.on("session_shutdown", async (_event, ctx) => { 148 if (!ephemeralMode) return; 149 150 const sessionFile = ctx.sessionManager.getSessionFile(); 151 if (!sessionFile) return; // Already in-memory mode 152 153 await deleteSessionFileBestEffort(sessionFile); 154 }); 155 }