import type { ExtensionAPI } from "@jaeswift/jae-coding-agent"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; interface ReplayEvent { time: number; type: string; text: string; } export default function (pi: ExtensionAPI) { let recording = false; let events: ReplayEvent[] = []; let startTime = 0; pi.on("message_end", async (event, ctx) => { if (!recording) return; const msg = event.message; if (!msg) return; let text = ""; if ("content" in msg && typeof msg.content === "string") { text = msg.content; } else if ("content" in msg && Array.isArray(msg.content)) { text = (msg.content as any[]) .filter((b: any) => b.type === "text") .map((b: any) => b.text) .join(""); } if (text) { events.push({ time: (Date.now() - startTime) / 1000, type: (msg as any).role || "unknown", text, }); } }); pi.on("tool_execution_end", async (event, _ctx) => { if (!recording) return; const resultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result).substring(0, 500); events.push({ time: (Date.now() - startTime) / 1000, type: `tool:${event.toolName}`, text: resultText, }); }); pi.registerCommand("replay", { description: "Session replay: /replay start | /replay stop | /replay export [file.cast]", handler: async (args, ctx) => { const parts = args.trim().split(/\s+/); const sub = parts[0] || "status"; if (sub === "start") { if (recording) { ctx.ui.notify("Already recording.", "warning"); return; } recording = true; events = []; startTime = Date.now(); ctx.ui.setStatus("replay", "\u{1F534} REC"); ctx.ui.notify("\u{1F3AC} Replay recording started.", "info"); return; } if (sub === "stop") { if (!recording) { ctx.ui.notify("Not recording.", "warning"); return; } recording = false; ctx.ui.setStatus("replay", undefined); ctx.ui.notify(`\u{1F3AC} Recording stopped. ${events.length} events captured. Use /replay export to save.`, "info"); return; } if (sub === "export") { if (events.length === 0) { ctx.ui.notify("No events recorded. Start a recording first.", "warning"); return; } const filename = parts[1] || `jae-replay-${Date.now()}.cast`; const outPath = join(ctx.cwd, filename); // asciicast v2 format const header = JSON.stringify({ version: 2, width: 120, height: 40, timestamp: Math.floor(startTime / 1000), title: "JAE Session Replay", env: { TERM: "xterm-256color" }, }); const lines = [header]; for (const ev of events) { const prefix = ev.type === "assistant" ? "\x1b[36m" : ev.type.startsWith("tool:") ? "\x1b[33m" : "\x1b[37m"; const label = ev.type === "assistant" ? "JAE" : ev.type === "user" ? "YOU" : ev.type; const displayText = `${prefix}[${label}]\x1b[0m ${ev.text}\r\n`; lines.push(JSON.stringify([ev.time, "o", displayText])); } writeFileSync(outPath, lines.join("\n") + "\n", "utf-8"); ctx.ui.notify(`\u{1F3AC} Exported ${events.length} events to ${outPath}\nPlay with: asciinema play ${filename}`, "info"); return; } if (sub === "status") { ctx.ui.notify( recording ? `\u{1F534} Recording in progress: ${events.length} events (${((Date.now() - startTime) / 1000).toFixed(0)}s)` : `\u{1F3AC} Not recording. ${events.length} events in buffer.`, "info", ); return; } ctx.ui.notify("Usage: /replay start | stop | export [file.cast] | status", "info"); }, }); }