11import http from "node:http" ;
22import { execFile } from "node:child_process" ;
3- import { fileURLToPath } from "node:url" ;
4- import { dirname } from "node:path" ;
53import { createCanvas , joinSession } from "@github/copilot-sdk/extension" ;
64
7- // This file lives inside the repo worktree, so its directory is a safe cwd for
8- // git/gh regardless of where the extension host process was launched from.
9- const extensionDir = dirname ( fileURLToPath ( import . meta. url ) ) ;
5+ // The extension should query PRs from the active workspace repository.
106
117// In-memory state
128let currentPR = null ;
139let prList = [ ] ;
1410let gestureState = "idle" ; // idle | detecting | approved | rejected
1511let lastDecision = null ;
12+ let lastLoadError = null ;
1613const sseClients = new Set ( ) ;
1714let loadPRsPromise = null ; // in-flight guard for loadOpenPRs
1815let cachedHTML = null ; // cached HTML string
@@ -23,6 +20,13 @@ function broadcast(event, data) {
2320 }
2421}
2522
23+ function normalizeErrorMessage ( error ) {
24+ if ( ! error ) return "Unknown error loading pull requests." ;
25+ const message = typeof error === "string" ? error : error . message || String ( error ) ;
26+ const singleLine = message . split ( / \r ? \n / ) [ 0 ] . trim ( ) ;
27+ return singleLine || "Unknown error loading pull requests." ;
28+ }
29+
2630// --- Load open PRs from the repo via the gh CLI ---
2731function shortDescription ( body ) {
2832 if ( ! body ) return "" ;
@@ -40,6 +44,7 @@ function loadOpenPRs() {
4044 if ( loadPRsPromise ) return loadPRsPromise ;
4145
4246 loadPRsPromise = new Promise ( ( resolve ) => {
47+ const repoCwd = process . cwd ( ) ;
4348 execFile (
4449 "gh" ,
4550 [
@@ -52,16 +57,22 @@ function loadOpenPRs() {
5257 "--json" ,
5358 "number,title,author,additions,deletions,body" ,
5459 ] ,
55- { cwd : extensionDir , maxBuffer : 1024 * 1024 } ,
60+ { cwd : repoCwd , maxBuffer : 1024 * 1024 } ,
5661 ( err , stdout ) => {
5762 loadPRsPromise = null ;
5863 if ( err ) {
59- console . error ( "gesture-review: failed to load PRs:" , err . message ) ;
64+ lastLoadError = normalizeErrorMessage ( err ) ;
65+ prList = [ ] ;
66+ currentPR = null ;
67+ console . error ( "gesture-review: failed to load PRs:" , lastLoadError ) ;
68+ broadcast ( "prlist" , prList ) ;
69+ broadcast ( "load_error" , { message : lastLoadError } ) ;
6070 resolve ( false ) ;
6171 return ;
6272 }
6373 try {
6474 const raw = JSON . parse ( stdout ) ;
75+ lastLoadError = null ;
6576 prList = raw . map ( ( pr ) => ( {
6677 title : pr . title ,
6778 number : pr . number ,
@@ -76,9 +87,12 @@ function loadOpenPRs() {
7687 }
7788 broadcast ( "prlist" , prList ) ;
7889 if ( currentPR ) broadcast ( "pr" , currentPR ) ;
90+ broadcast ( "load_error" , null ) ;
7991 resolve ( true ) ;
8092 } catch ( e ) {
81- console . error ( "gesture-review: failed to parse PRs:" , e . message ) ;
93+ lastLoadError = normalizeErrorMessage ( e ) ;
94+ console . error ( "gesture-review: failed to parse PRs:" , lastLoadError ) ;
95+ broadcast ( "load_error" , { message : lastLoadError } ) ;
8296 resolve ( false ) ;
8397 }
8498 } ,
@@ -112,6 +126,11 @@ const server = http.createServer((req, res) => {
112126 res . write ( `event: pr\ndata: ${ JSON . stringify ( currentPR ) } \n\n` ) ;
113127 }
114128 res . write ( `event: state\ndata: ${ JSON . stringify ( { state : gestureState } ) } \n\n` ) ;
129+ if ( lastLoadError ) {
130+ res . write (
131+ `event: load_error\ndata: ${ JSON . stringify ( { message : lastLoadError } ) } \n\n` ,
132+ ) ;
133+ }
115134 sseClients . add ( res ) ;
116135 req . on ( "close" , ( ) => sseClients . delete ( res ) ) ;
117136 return ;
@@ -635,11 +654,13 @@ function getHTML() {
635654 let allPRs = [];
636655 let currentIndex = 0;
637656 let decisions = {}; // number -> 'approved' | 'rejected'
657+ let prLoadError = null;
638658
639659 // --- SSE ---
640660 const es = new EventSource('/events');
641661 es.addEventListener('prlist', (e) => {
642662 allPRs = JSON.parse(e.data);
663+ if (allPRs.length > 0) prLoadError = null;
643664 // Keep index in range, then show the current PR (or auto-select the first
644665 // undecided one) so the drawer is usable the moment the canvas loads.
645666 if (currentIndex >= allPRs.length) currentIndex = 0;
@@ -670,6 +691,11 @@ function getHTML() {
670691 updateUI();
671692 }
672693 });
694+ es.addEventListener('load_error', (e) => {
695+ const payload = e.data ? JSON.parse(e.data) : null;
696+ prLoadError = payload?.message || null;
697+ updateUI();
698+ });
673699
674700 function showPR(pr) {
675701 prEmpty.classList.add('hidden');
@@ -791,14 +817,33 @@ function getHTML() {
791817 });
792818 }
793819
820+ async function loadScriptWithFallback(sources, timeoutMs = 10000) {
821+ let lastErr;
822+ for (const src of sources) {
823+ try {
824+ await loadScript(src, timeoutMs);
825+ return;
826+ } catch (err) {
827+ lastErr = err;
828+ }
829+ }
830+ throw lastErr || new Error('Script load failed');
831+ }
832+
794833 async function initMediaPipe() {
795834 const INIT_TIMEOUT = 30000;
796835 try {
797836 // Load MediaPipe scripts dynamically with timeout
798837 setLoadingProgress(40, 'Downloading hand tracking library...');
799- await loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js', 15000);
838+ await loadScriptWithFallback([
839+ 'https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js',
840+ 'https://unpkg.com/@mediapipe/hands/hands.js'
841+ ], 15000);
800842 setLoadingProgress(60, 'Downloading camera utilities...');
801- await loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js', 15000);
843+ await loadScriptWithFallback([
844+ 'https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js',
845+ 'https://unpkg.com/@mediapipe/camera_utils/camera_utils.js'
846+ ], 15000);
802847
803848 setLoadingProgress(70, 'Initializing hand detection model...');
804849
@@ -1173,7 +1218,9 @@ function getHTML() {
11731218 function updateUI() {
11741219 if (!decided) {
11751220 cameraWrap.className = 'camera-wrap';
1176- statusBar.textContent = currentPR ? 'Show thumbs up or thumbs down...' : 'Waiting for a PR...';
1221+ statusBar.textContent = currentPR
1222+ ? 'Show thumbs up or thumbs down...'
1223+ : (prLoadError ? ('Unable to load PRs: ' + prLoadError) : 'Waiting for a PR...');
11771224 statusBar.className = 'status-bar';
11781225 }
11791226 }
0 commit comments