// Extension: where-was-i // Interrupt Recovery canvas — helps developers resume mental context after interruption. import { createServer } from "node:http"; import { execFile } from "node:child_process"; import { randomBytes, timingSafeEqual } from "node:crypto"; import { writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { joinSession, createCanvas, CanvasError } from "@github/copilot-sdk/extension"; import { gatherGitContext, getFileDiff } from "./git-context.mjs"; const servers = new Map(); const sseClients = new Map(); // instanceId → Set const contextCache = new Map(); // instanceId → contextData let workspaceCwd = null; function captureCwd(ctx) { const dir = ctx?.session?.workingDirectory; if (typeof dir === "string" && dir.trim()) workspaceCwd = dir; } async function activeCwd(ctx) { captureCwd(ctx); if (!workspaceCwd && sessionRef) { const snapshot = await sessionRef.rpc.metadata.snapshot(); const dir = snapshot?.workingDirectory; if (typeof dir === "string" && dir.trim()) workspaceCwd = dir; } if (!workspaceCwd) { throw new CanvasError( "workspace_unavailable", "No repository working directory is attached to this session.", ); } return workspaceCwd; } function runGhJson(cwd, args) { return new Promise((resolve, reject) => { execFile("gh", args, { cwd, timeout: 15000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error((stderr || error.message || "GitHub CLI command failed").trim())); return; } try { resolve({ data: JSON.parse(stdout || "[]"), warning: (stderr || "").trim(), }); } catch (parseError) { reject(new Error(`GitHub CLI returned invalid JSON: ${parseError.message}`)); } }); }); } async function gatherContext(cwd) { const gitContext = await gatherGitContext(cwd); const [prs, issues] = await Promise.allSettled([ runGhJson(cwd, [ "pr", "list", "--author=@me", "--state=open", "--limit=10", "--json", "number,title,url,updatedAt,comments", ]), runGhJson(cwd, [ "issue", "list", "--assignee=@me", "--state=open", "--limit=10", "--json", "number,title,url,updatedAt", ]), ]); return { ...gitContext, openPrs: prs.status === "fulfilled" ? prs.value.data : [], assignedIssues: issues.status === "fulfilled" ? issues.value.data : [], warnings: [prs, issues] .map((result) => result.status === "fulfilled" ? result.value.warning : result.reason.message) .filter(Boolean), gatheredAt: new Date().toISOString(), }; } // --- Persistence --- async function saveContext(workspacePath, data) { if (!workspacePath) return; const dir = join(workspacePath, "files"); await mkdir(dir, { recursive: true }); await writeFile(join(dir, "where-was-i-context.json"), JSON.stringify(data, null, 2)); } // --- SSE --- function broadcast(instanceId, data) { const clients = sseClients.get(instanceId); if (!clients) return; const payload = `data: ${JSON.stringify(data)}\n\n`; for (const res of clients) { try { res.write(payload); } catch { clients.delete(res); } } } // --- HTML renderer --- function renderHtml(instanceId, scriptNonce) { return ` Where Was I?
Reconstructing your context…
`; } // --- Server --- function tokenMatches(provided, expected) { if (typeof provided !== "string" || provided.length !== expected.length) return false; try { return timingSafeEqual(Buffer.from(provided), Buffer.from(expected)); } catch { return false; } } function requestIsAuthorized(req, url, entry) { const host = String(req.headers.host || "").toLowerCase(); if (!entry.canonicalHost || host !== entry.canonicalHost) return false; const origin = req.headers.origin; if (origin && origin !== entry.origin) return false; return tokenMatches(url.searchParams.get("k") || "", entry.token); } async function startServer(instanceId, cwd, workspacePath) { const entry = { server: null, url: "", cwd, token: randomBytes(32).toString("base64url"), canonicalHost: "", origin: "", }; entry.server = createServer(async (req, res) => { const url = new URL(req.url, "http://localhost"); if (!requestIsAuthorized(req, url, entry)) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); return; } if (url.pathname === "/events" && req.method === "GET") { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }); res.write(":\n\n"); let clients = sseClients.get(instanceId); if (!clients) { clients = new Set(); sseClients.set(instanceId, clients); } clients.add(res); req.on("close", () => { clients.delete(res); }); return; } if (url.pathname === "/context" && req.method === "GET") { const data = contextCache.get(instanceId) || {}; res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); return; } if (url.pathname === "/file-diff" && req.method === "GET") { const path = url.searchParams.get("path"); const code = url.searchParams.get("code"); if (!path) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "A file path is required." })); return; } try { const data = await getFileDiff(entry.cwd, path, code && code.length === 2 ? code : null); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); } catch (error) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: error.message || "Unable to load this diff." })); } return; } if (url.pathname === "/refresh" && req.method === "POST") { try { const data = await gatherContext(entry.cwd); contextCache.set(instanceId, data); await saveContext(workspacePath, data); broadcast(instanceId, data); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); } catch (error) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: error.message || "Unable to refresh context." })); } return; } if (url.pathname === "/resume" && req.method === "POST") { let body = ""; for await (const chunk of req) body += chunk; let thread = null; try { thread = JSON.parse(body).thread; } catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "The resume request body must be valid JSON." })); return; } const ctx = contextCache.get(instanceId) || {}; const commits = ctx.branchCommits?.length ? ctx.branchCommits : (ctx.recentCommits || []); let prompt; if (thread) { prompt = `I was working on ${thread} and got interrupted. Here's my current context:\n\n` + `**Worktree:** ${ctx.worktreeRoot || "unknown"}\n` + `**Branch:** ${ctx.branch || "unknown"}\n` + `**Worktree commits:** ${commits.join(", ")}\n` + `**Uncommitted changes:** ${(ctx.uncommitted || []).join(", ")}\n` + `**Open PRs:** ${(ctx.openPrs || []).map(p => "#" + p.number + " " + p.title).join(", ")}\n\n` + `Help me pick up where I left off on this specific thread.`; } else { prompt = `I got interrupted and need to resume my work. Here's my full context:\n\n` + `**Worktree:** ${ctx.worktreeRoot || "unknown"}\n` + `**Branch:** ${ctx.branch || "unknown"}\n` + `**Worktree commits:**\n${commits.map(c => "- " + c).join("\n")}\n\n` + `**Uncommitted changes:**\n${(ctx.uncommitted || []).map(f => "- " + f).join("\n")}\n\n` + `**Diff stat:**\n${ctx.diffStat || "none"}\n\n` + `**Open PRs:** ${(ctx.openPrs || []).map(p => "#" + p.number + " " + p.title).join(", ") || "none"}\n` + `**Assigned issues:** ${(ctx.assignedIssues || []).map(i => "#" + i.number + " " + i.title).join(", ") || "none"}\n\n` + `Help me pick up where I left off. What should I focus on first?`; } try { if (!sessionRef) throw new Error("The Copilot session is unavailable."); await sessionRef.send(prompt); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); } catch (error) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: error.message || "Unable to send the resume prompt." })); } return; } if (url.pathname === "/" && req.method === "GET") { const scriptNonce = randomBytes(16).toString("base64url"); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": [ "default-src 'none'", `script-src 'nonce-${scriptNonce}'`, "style-src 'unsafe-inline' https://fonts.googleapis.com", "font-src https://fonts.gstatic.com", "connect-src 'self'", "base-uri 'none'", "object-src 'none'", ].join("; "), }); res.end(renderHtml(instanceId, scriptNonce)); return; } res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }); await new Promise((resolve) => entry.server.listen(0, "127.0.0.1", resolve)); const address = entry.server.address(); const port = typeof address === "object" && address ? address.port : 0; entry.canonicalHost = `127.0.0.1:${port}`; entry.origin = `http://${entry.canonicalHost}`; entry.url = `${entry.origin}/?k=${entry.token}`; return entry; } // --- Extension --- let sessionRef = null; const session = await joinSession({ canvases: [ createCanvas({ id: "where-was-i", displayName: "Where Was I?", description: "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", actions: [ { name: "refresh", description: "Re-gather all git/project context and push updates to the canvas", handler: async (ctx) => { const cwd = await activeCwd(ctx); const entry = servers.get(ctx.instanceId); if (entry) entry.cwd = cwd; const data = await gatherContext(cwd); contextCache.set(ctx.instanceId, data); if (sessionRef) await saveContext(sessionRef.workspacePath, data); broadcast(ctx.instanceId, data); return data; }, }, { name: "get_context", description: "Return the currently assembled developer context as JSON", handler: async (ctx) => { return contextCache.get(ctx.instanceId) || {}; }, }, { name: "get_file_diff", description: "Return the staged, unstaged, or untracked patch for a changed file", inputSchema: { type: "object", properties: { path: { type: "string", minLength: 1, description: "Repository-relative path from the current context", }, code: { type: "string", minLength: 2, maxLength: 2, description: "Optional two-character porcelain status code to disambiguate a path listed more than once", }, }, required: ["path"], additionalProperties: false, }, handler: async (ctx) => { const cwd = await activeCwd(ctx); return await getFileDiff(cwd, ctx.input.path, ctx.input.code || null); }, }, { name: "resume", description: "Send a contextual 'resume' message to the agent with the developer's assembled state", inputSchema: { type: "object", properties: { thread: { type: "string", description: "Optional specific thread/topic to focus on when resuming", }, }, }, handler: async (ctx) => { const thread = ctx.input?.thread || null; const data = contextCache.get(ctx.instanceId) || {}; const commits = data.branchCommits?.length ? data.branchCommits : (data.recentCommits || []); let prompt; if (thread) { prompt = `I was working on ${thread} and got interrupted. Context: worktree=${data.worktreeRoot}, branch=${data.branch}, worktree commits: ${commits.join("; ")}. Help me resume.`; } else { prompt = `Help me resume. Worktree: ${data.worktreeRoot}. Branch: ${data.branch}. Commits: ${commits.join("; ")}. Uncommitted: ${(data.uncommitted || []).join("; ")}.`; } if (sessionRef) await sessionRef.send(prompt); return { sent: true }; }, }, ], open: async (ctx) => { const cwd = await activeCwd(ctx); let entry = servers.get(ctx.instanceId); if (!entry) { entry = await startServer(ctx.instanceId, cwd, sessionRef?.workspacePath); servers.set(ctx.instanceId, entry); } else { entry.cwd = cwd; } const data = await gatherContext(cwd); await saveContext(sessionRef?.workspacePath, data); contextCache.set(ctx.instanceId, data); // Push to any waiting SSE clients setTimeout(() => broadcast(ctx.instanceId, data), 100); return { title: "Where Was I?", url: entry.url }; }, onClose: async (ctx) => { const entry = servers.get(ctx.instanceId); if (entry) { servers.delete(ctx.instanceId); await new Promise((r) => entry.server.close(() => r())); } sseClients.delete(ctx.instanceId); contextCache.delete(ctx.instanceId); }, }), ], }); sessionRef = session;