import http from "node:http"; import { execFile } from "node:child_process"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; import { createCanvas, joinSession } from "@github/copilot-sdk/extension"; // This file lives inside the repo worktree, so its directory is a safe cwd for // git/gh regardless of where the extension host process was launched from. const extensionDir = dirname(fileURLToPath(import.meta.url)); // In-memory state let currentPR = null; let prList = []; let gestureState = "idle"; // idle | detecting | approved | rejected let lastDecision = null; const sseClients = new Set(); let loadPRsPromise = null; // in-flight guard for loadOpenPRs let cachedHTML = null; // cached HTML string function broadcast(event, data) { for (const res of sseClients) { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } } // --- Load open PRs from the repo via the gh CLI --- function shortDescription(body) { if (!body) return ""; // First non-empty, non-heading line, trimmed to a reasonable length. const line = body .split(/\r?\n/) .map((l) => l.trim()) .find((l) => l && !l.startsWith("#")); if (!line) return ""; return line.length > 140 ? line.slice(0, 137) + "..." : line; } function loadOpenPRs() { // De-dupe: return existing in-flight promise if one is running if (loadPRsPromise) return loadPRsPromise; loadPRsPromise = new Promise((resolve) => { execFile( "gh", [ "pr", "list", "--state", "open", "--limit", "20", "--json", "number,title,author,additions,deletions,body", ], { cwd: extensionDir, maxBuffer: 1024 * 1024 }, (err, stdout) => { loadPRsPromise = null; if (err) { console.error("gesture-review: failed to load PRs:", err.message); resolve(false); return; } try { const raw = JSON.parse(stdout); prList = raw.map((pr) => ({ title: pr.title, number: pr.number, author: pr.author?.login || "unknown", description: shortDescription(pr.body), additions: pr.additions || 0, deletions: pr.deletions || 0, })); // Keep currentPR pointing at a still-open PR if possible. if (currentPR) { currentPR = prList.find((p) => p.number === currentPR.number) || null; } broadcast("prlist", prList); if (currentPR) broadcast("pr", currentPR); resolve(true); } catch (e) { console.error("gesture-review: failed to parse PRs:", e.message); resolve(false); } }, ); }); return loadPRsPromise; } // --- Loopback HTTP server for the iframe --- const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/") { if (!cachedHTML) cachedHTML = getHTML(); res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache", }); res.end(cachedHTML); return; } if (req.method === "GET" && req.url === "/events") { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); // Send current state immediately res.write(`event: prlist\ndata: ${JSON.stringify(prList)}\n\n`); if (currentPR) { res.write(`event: pr\ndata: ${JSON.stringify(currentPR)}\n\n`); } res.write(`event: state\ndata: ${JSON.stringify({ state: gestureState })}\n\n`); sseClients.add(res); req.on("close", () => sseClients.delete(res)); return; } if (req.method === "POST" && req.url === "/select-pr") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { const { number } = JSON.parse(body); const pr = prList.find((p) => p.number === number); if (pr) { currentPR = pr; gestureState = "idle"; broadcast("pr", currentPR); broadcast("state", { state: "idle" }); } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); }); return; } if (req.method === "POST" && req.url === "/gesture-decision") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { const { decision } = JSON.parse(body); gestureState = decision; // "approved" or "rejected" lastDecision = { decision, pr: currentPR, timestamp: Date.now() }; broadcast("state", { state: gestureState }); if (session && currentPR) { const action = decision === "approved" ? "approve" : "reject"; session.send({ prompt: `The user gave a thumbs ${decision === "approved" ? "up" : "down"} gesture to ${action} PR #${currentPR.number} ("${currentPR.title}" by ${currentPR.author}). Please ${action} this pull request accordingly.`, }); } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, decision })); }); return; } if (req.method === "POST" && req.url === "/refresh") { loadOpenPRs().then(() => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, count: prList.length })); }); return; } res.writeHead(404); res.end("Not found"); }); const port = await new Promise((resolve) => { server.listen(0, "127.0.0.1", () => resolve(server.address().port)); }); let session; const canvas = createCanvas({ id: "gesture-review", displayName: "Gesture PR Review", description: "Interactive PR review using hand gestures. Shows a live camera feed and detects thumbs up (approve) or thumbs down (reject) via MediaPipe hand tracking.", actions: [ { name: "show_pr", description: "Display a PR for the user to gesture-review. Shows PR info and activates gesture detection.", inputSchema: { type: "object", properties: { title: { type: "string", description: "PR title" }, number: { type: "number", description: "PR number" }, author: { type: "string", description: "PR author username" }, description: { type: "string", description: "Short PR description", }, additions: { type: "number", description: "Lines added", }, deletions: { type: "number", description: "Lines deleted", }, }, required: ["title", "number", "author"], }, handler({ input }) { currentPR = { title: input.title, number: input.number, author: input.author, description: input.description || "", additions: input.additions || 0, deletions: input.deletions || 0, }; // Add to list if not already there if (!prList.find((p) => p.number === currentPR.number)) { prList.push(currentPR); broadcast("prlist", prList); } gestureState = "idle"; broadcast("pr", currentPR); broadcast("state", { state: "idle" }); return { ok: true, pr: currentPR }; }, }, { name: "get_status", description: "Returns current gesture detection state and last decision made.", inputSchema: { type: "object", properties: {} }, handler() { return { gestureState, currentPR, lastDecision, }; }, }, ], open({ instanceId }) { // Refresh open PRs each time the canvas is opened so the drawer is current. loadOpenPRs(); return { url: `http://127.0.0.1:${port}`, title: "Gesture PR Review", status: "ready", }; }, }); session = await joinSession({ canvases: [canvas] }); // Populate the drawer with open PRs as soon as the extension starts. loadOpenPRs(); function getHTML() { return `
Initializing camera...
👋 Waiting for a PR to review... Ask the agent to show a PR
Initializing camera...
`; }