mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 21:51:27 +00:00
fix: gesture review canvas post-permission flow (#2037)
Load PRs from the active workspace repo instead of the extension directory, surface PR load errors in the canvas UI, and add MediaPipe CDN fallbacks for better runtime reliability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,18 +1,15 @@
|
|||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { dirname } from "node:path";
|
|
||||||
import { createCanvas, joinSession } from "@github/copilot-sdk/extension";
|
import { createCanvas, joinSession } from "@github/copilot-sdk/extension";
|
||||||
|
|
||||||
// This file lives inside the repo worktree, so its directory is a safe cwd for
|
// The extension should query PRs from the active workspace repository.
|
||||||
// git/gh regardless of where the extension host process was launched from.
|
|
||||||
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
// In-memory state
|
// In-memory state
|
||||||
let currentPR = null;
|
let currentPR = null;
|
||||||
let prList = [];
|
let prList = [];
|
||||||
let gestureState = "idle"; // idle | detecting | approved | rejected
|
let gestureState = "idle"; // idle | detecting | approved | rejected
|
||||||
let lastDecision = null;
|
let lastDecision = null;
|
||||||
|
let lastLoadError = null;
|
||||||
const sseClients = new Set();
|
const sseClients = new Set();
|
||||||
let loadPRsPromise = null; // in-flight guard for loadOpenPRs
|
let loadPRsPromise = null; // in-flight guard for loadOpenPRs
|
||||||
let cachedHTML = null; // cached HTML string
|
let cachedHTML = null; // cached HTML string
|
||||||
@@ -23,6 +20,13 @@ function broadcast(event, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeErrorMessage(error) {
|
||||||
|
if (!error) return "Unknown error loading pull requests.";
|
||||||
|
const message = typeof error === "string" ? error : error.message || String(error);
|
||||||
|
const singleLine = message.split(/\r?\n/)[0].trim();
|
||||||
|
return singleLine || "Unknown error loading pull requests.";
|
||||||
|
}
|
||||||
|
|
||||||
// --- Load open PRs from the repo via the gh CLI ---
|
// --- Load open PRs from the repo via the gh CLI ---
|
||||||
function shortDescription(body) {
|
function shortDescription(body) {
|
||||||
if (!body) return "";
|
if (!body) return "";
|
||||||
@@ -40,6 +44,7 @@ function loadOpenPRs() {
|
|||||||
if (loadPRsPromise) return loadPRsPromise;
|
if (loadPRsPromise) return loadPRsPromise;
|
||||||
|
|
||||||
loadPRsPromise = new Promise((resolve) => {
|
loadPRsPromise = new Promise((resolve) => {
|
||||||
|
const repoCwd = process.cwd();
|
||||||
execFile(
|
execFile(
|
||||||
"gh",
|
"gh",
|
||||||
[
|
[
|
||||||
@@ -52,16 +57,22 @@ function loadOpenPRs() {
|
|||||||
"--json",
|
"--json",
|
||||||
"number,title,author,additions,deletions,body",
|
"number,title,author,additions,deletions,body",
|
||||||
],
|
],
|
||||||
{ cwd: extensionDir, maxBuffer: 1024 * 1024 },
|
{ cwd: repoCwd, maxBuffer: 1024 * 1024 },
|
||||||
(err, stdout) => {
|
(err, stdout) => {
|
||||||
loadPRsPromise = null;
|
loadPRsPromise = null;
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("gesture-review: failed to load PRs:", err.message);
|
lastLoadError = normalizeErrorMessage(err);
|
||||||
|
prList = [];
|
||||||
|
currentPR = null;
|
||||||
|
console.error("gesture-review: failed to load PRs:", lastLoadError);
|
||||||
|
broadcast("prlist", prList);
|
||||||
|
broadcast("load_error", { message: lastLoadError });
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const raw = JSON.parse(stdout);
|
const raw = JSON.parse(stdout);
|
||||||
|
lastLoadError = null;
|
||||||
prList = raw.map((pr) => ({
|
prList = raw.map((pr) => ({
|
||||||
title: pr.title,
|
title: pr.title,
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
@@ -76,9 +87,12 @@ function loadOpenPRs() {
|
|||||||
}
|
}
|
||||||
broadcast("prlist", prList);
|
broadcast("prlist", prList);
|
||||||
if (currentPR) broadcast("pr", currentPR);
|
if (currentPR) broadcast("pr", currentPR);
|
||||||
|
broadcast("load_error", null);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("gesture-review: failed to parse PRs:", e.message);
|
lastLoadError = normalizeErrorMessage(e);
|
||||||
|
console.error("gesture-review: failed to parse PRs:", lastLoadError);
|
||||||
|
broadcast("load_error", { message: lastLoadError });
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -112,6 +126,11 @@ const server = http.createServer((req, res) => {
|
|||||||
res.write(`event: pr\ndata: ${JSON.stringify(currentPR)}\n\n`);
|
res.write(`event: pr\ndata: ${JSON.stringify(currentPR)}\n\n`);
|
||||||
}
|
}
|
||||||
res.write(`event: state\ndata: ${JSON.stringify({ state: gestureState })}\n\n`);
|
res.write(`event: state\ndata: ${JSON.stringify({ state: gestureState })}\n\n`);
|
||||||
|
if (lastLoadError) {
|
||||||
|
res.write(
|
||||||
|
`event: load_error\ndata: ${JSON.stringify({ message: lastLoadError })}\n\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
sseClients.add(res);
|
sseClients.add(res);
|
||||||
req.on("close", () => sseClients.delete(res));
|
req.on("close", () => sseClients.delete(res));
|
||||||
return;
|
return;
|
||||||
@@ -635,11 +654,13 @@ function getHTML() {
|
|||||||
let allPRs = [];
|
let allPRs = [];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let decisions = {}; // number -> 'approved' | 'rejected'
|
let decisions = {}; // number -> 'approved' | 'rejected'
|
||||||
|
let prLoadError = null;
|
||||||
|
|
||||||
// --- SSE ---
|
// --- SSE ---
|
||||||
const es = new EventSource('/events');
|
const es = new EventSource('/events');
|
||||||
es.addEventListener('prlist', (e) => {
|
es.addEventListener('prlist', (e) => {
|
||||||
allPRs = JSON.parse(e.data);
|
allPRs = JSON.parse(e.data);
|
||||||
|
if (allPRs.length > 0) prLoadError = null;
|
||||||
// Keep index in range, then show the current PR (or auto-select the first
|
// Keep index in range, then show the current PR (or auto-select the first
|
||||||
// undecided one) so the drawer is usable the moment the canvas loads.
|
// undecided one) so the drawer is usable the moment the canvas loads.
|
||||||
if (currentIndex >= allPRs.length) currentIndex = 0;
|
if (currentIndex >= allPRs.length) currentIndex = 0;
|
||||||
@@ -670,6 +691,11 @@ function getHTML() {
|
|||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
es.addEventListener('load_error', (e) => {
|
||||||
|
const payload = e.data ? JSON.parse(e.data) : null;
|
||||||
|
prLoadError = payload?.message || null;
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
|
||||||
function showPR(pr) {
|
function showPR(pr) {
|
||||||
prEmpty.classList.add('hidden');
|
prEmpty.classList.add('hidden');
|
||||||
@@ -791,14 +817,33 @@ function getHTML() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadScriptWithFallback(sources, timeoutMs = 10000) {
|
||||||
|
let lastErr;
|
||||||
|
for (const src of sources) {
|
||||||
|
try {
|
||||||
|
await loadScript(src, timeoutMs);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr || new Error('Script load failed');
|
||||||
|
}
|
||||||
|
|
||||||
async function initMediaPipe() {
|
async function initMediaPipe() {
|
||||||
const INIT_TIMEOUT = 30000;
|
const INIT_TIMEOUT = 30000;
|
||||||
try {
|
try {
|
||||||
// Load MediaPipe scripts dynamically with timeout
|
// Load MediaPipe scripts dynamically with timeout
|
||||||
setLoadingProgress(40, 'Downloading hand tracking library...');
|
setLoadingProgress(40, 'Downloading hand tracking library...');
|
||||||
await loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js', 15000);
|
await loadScriptWithFallback([
|
||||||
|
'https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js',
|
||||||
|
'https://unpkg.com/@mediapipe/hands/hands.js'
|
||||||
|
], 15000);
|
||||||
setLoadingProgress(60, 'Downloading camera utilities...');
|
setLoadingProgress(60, 'Downloading camera utilities...');
|
||||||
await loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js', 15000);
|
await loadScriptWithFallback([
|
||||||
|
'https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js',
|
||||||
|
'https://unpkg.com/@mediapipe/camera_utils/camera_utils.js'
|
||||||
|
], 15000);
|
||||||
|
|
||||||
setLoadingProgress(70, 'Initializing hand detection model...');
|
setLoadingProgress(70, 'Initializing hand detection model...');
|
||||||
|
|
||||||
@@ -1173,7 +1218,9 @@ function getHTML() {
|
|||||||
function updateUI() {
|
function updateUI() {
|
||||||
if (!decided) {
|
if (!decided) {
|
||||||
cameraWrap.className = 'camera-wrap';
|
cameraWrap.className = 'camera-wrap';
|
||||||
statusBar.textContent = currentPR ? 'Show thumbs up or thumbs down...' : 'Waiting for a PR...';
|
statusBar.textContent = currentPR
|
||||||
|
? 'Show thumbs up or thumbs down...'
|
||||||
|
: (prLoadError ? ('Unable to load PRs: ' + prLoadError) : 'Waiting for a PR...');
|
||||||
statusBar.className = 'status-bar';
|
statusBar.className = 'status-bar';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user