From ebc341ee7e95acca03af8e8301301a3a95f544bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Mon, 22 Jun 2026 09:25:11 +0200 Subject: [PATCH 1/3] fix: allow video playback from GitHub user content in CSP --- src/browser/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/index.html b/src/browser/index.html index b97746a..1602a27 100644 --- a/src/browser/index.html +++ b/src/browser/index.html @@ -36,6 +36,7 @@ default-src 'self'; connect-src 'self' https://api.github.com; img-src 'self' https://github.com https://avatars.githubusercontent.com https://private-user-images.githubusercontent.com https://camo.githubusercontent.com data:; + media-src 'self' https://private-user-images.githubusercontent.com; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self' data:; From 62b50a2af522238beb7f7bdd736d56b4a0710e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Mon, 22 Jun 2026 09:59:28 +0200 Subject: [PATCH 2/3] fix: catch network errors in PR polling to prevent unhandled rejections The checkPRs polling loop in AppShell called getPREnrichment and fetchInvolvedPRs without error handling. When the GitHub API was unreachable, the promise rejection was unhandled (fire-and-forget), producing an 'Uncaught (in promise) HttpError: NetworkError'. Wrapped the polling body in try/catch so transient network failures are silently ignored and polling continues on the next interval. --- src/browser/components/app-shell.tsx | 162 ++++++++++++++------------- 1 file changed, 83 insertions(+), 79 deletions(-) diff --git a/src/browser/components/app-shell.tsx b/src/browser/components/app-shell.tsx index 2b9d444..73cb371 100644 --- a/src/browser/components/app-shell.tsx +++ b/src/browser/components/app-shell.tsx @@ -181,96 +181,100 @@ export function AppShell() { }; const checkPRs = async () => { - const prTabs = tabs.filter( - (t): t is Tab & { owner: string; repo: string; number: number } => - t.type === "pr-review" && - t.owner !== undefined && - t.repo !== undefined && - t.number !== undefined - ); - - const involvedPRs = await githubStore.fetchInvolvedPRs(); - - const prSet = new Map< - string, - { owner: string; repo: string; number: number } - >(); - const addPr = (owner: string, repo: string, number: number) => { - prSet.set(`${owner}/${repo}/${number}`, { owner, repo, number }); - }; - for (const tab of prTabs) addPr(tab.owner, tab.repo, tab.number); - for (const pr of involvedPRs) { - const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/); - if (match && pr.number) addPr(match[1], match[2], pr.number); - } - if (prSet.size === 0) return; - - const enrichmentMap = await githubStore.getPREnrichment([ - ...prSet.values(), - ]); - - const tabPrIds = new Set( - prTabs.map((t) => `${t.owner}/${t.repo}#${t.number}`) - ); - const notifiedThisCycle = new Set(); - - for (const tab of prTabs) { - const key = `${tab.owner}/${tab.repo}/${tab.number}`; - const prId = `${tab.owner}/${tab.repo}#${tab.number}`; - const enrichment = enrichmentMap.get(key); - if (!enrichment) continue; - const viewerLastViewedAt = getLastViewed(prId); - const baseline = getBaseline(viewerLastViewedAt, enrichment); - if (baseline && enrichment.updatedAt > baseline) { - markTabUpdated(tab.id); + try { + const prTabs = tabs.filter( + (t): t is Tab & { owner: string; repo: string; number: number } => + t.type === "pr-review" && + t.owner !== undefined && + t.repo !== undefined && + t.number !== undefined + ); + + const involvedPRs = await githubStore.fetchInvolvedPRs(); + + const prSet = new Map< + string, + { owner: string; repo: string; number: number } + >(); + const addPr = (owner: string, repo: string, number: number) => { + prSet.set(`${owner}/${repo}/${number}`, { owner, repo, number }); + }; + for (const tab of prTabs) addPr(tab.owner, tab.repo, tab.number); + for (const pr of involvedPRs) { + const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/); + if (match && pr.number) addPr(match[1], match[2], pr.number); + } + if (prSet.size === 0) return; + + const enrichmentMap = await githubStore.getPREnrichment([ + ...prSet.values(), + ]); + + const tabPrIds = new Set( + prTabs.map((t) => `${t.owner}/${t.repo}#${t.number}`) + ); + const notifiedThisCycle = new Set(); + + for (const tab of prTabs) { + const key = `${tab.owner}/${tab.repo}/${tab.number}`; + const prId = `${tab.owner}/${tab.repo}#${tab.number}`; + const enrichment = enrichmentMap.get(key); + if (!enrichment) continue; + const viewerLastViewedAt = getLastViewed(prId); + const baseline = getBaseline(viewerLastViewedAt, enrichment); + if (baseline && enrichment.updatedAt > baseline) { + markTabUpdated(tab.id); + if ( + notifsEnabled() && + !notifiedThisCycle.has(prId) && + enrichment.updatedAt > (getNotifiedAt(prId) ?? "") + ) { + notifiedThisCycle.add(prId); + const prUrl = `/${tab.owner}/${tab.repo}/pull/${tab.number}`; + sendNotification( + `New activity on ${tab.owner}/${tab.repo} PR #${tab.number}`, + tab.prTitle || `#${tab.number}`, + prUrl, + `https://avatars.githubusercontent.com/${tab.owner}` + ); + setNotifiedAt(prId, enrichment.updatedAt); + } + } + } + + for (const pr of involvedPRs) { + const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/); + if (!match || !pr.number) continue; + const owner = match[1]; + const repo = match[2]; + const number = pr.number; + const prId = `${owner}/${repo}#${number}`; + const prKey = `${owner}/${repo}/${number}`; + if (tabPrIds.has(prId)) continue; + + const enrichment = enrichmentMap.get(prKey); + if (!enrichment) continue; + const viewerLastViewedAt = getLastViewed(prId); + const baseline = getBaseline(viewerLastViewedAt, enrichment); if ( notifsEnabled() && !notifiedThisCycle.has(prId) && - enrichment.updatedAt > (getNotifiedAt(prId) ?? "") + enrichment.updatedAt > (getNotifiedAt(prId) ?? "") && + (!baseline || enrichment.updatedAt > baseline) ) { notifiedThisCycle.add(prId); - const prUrl = `/${tab.owner}/${tab.repo}/pull/${tab.number}`; + const prUrl = `/${owner}/${repo}/pull/${number}`; sendNotification( - `New activity on ${tab.owner}/${tab.repo} PR #${tab.number}`, - tab.prTitle || `#${tab.number}`, + `New activity on ${owner}/${repo} PR #${number}`, + pr.title, prUrl, - `https://avatars.githubusercontent.com/${tab.owner}` + `https://avatars.githubusercontent.com/${owner}` ); setNotifiedAt(prId, enrichment.updatedAt); } } - } - - for (const pr of involvedPRs) { - const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/); - if (!match || !pr.number) continue; - const owner = match[1]; - const repo = match[2]; - const number = pr.number; - const prId = `${owner}/${repo}#${number}`; - const prKey = `${owner}/${repo}/${number}`; - if (tabPrIds.has(prId)) continue; - - const enrichment = enrichmentMap.get(prKey); - if (!enrichment) continue; - const viewerLastViewedAt = getLastViewed(prId); - const baseline = getBaseline(viewerLastViewedAt, enrichment); - if ( - notifsEnabled() && - !notifiedThisCycle.has(prId) && - enrichment.updatedAt > (getNotifiedAt(prId) ?? "") && - (!baseline || enrichment.updatedAt > baseline) - ) { - notifiedThisCycle.add(prId); - const prUrl = `/${owner}/${repo}/pull/${number}`; - sendNotification( - `New activity on ${owner}/${repo} PR #${number}`, - pr.title, - prUrl, - `https://avatars.githubusercontent.com/${owner}` - ); - setNotifiedAt(prId, enrichment.updatedAt); - } + } catch { + // Ignore transient network errors during polling } }; From f9a1167bbc9e3d2b6047566e61e4e1b1c44af8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Mon, 22 Jun 2026 00:11:29 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20correct=20tab=20updated=20behavior?= =?UTF-8?q?=20and=20style=20=E2=9F=B3=20as=20orange=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed in app-shell.tsx: 1. handleTabSelect called markTabUpdated instead of clearTabUpdated when clicking a tab (despite the comment saying 'Clear updated flag'). This caused the ⟳ to flash briefly when selecting a tab. 2. The updated indicator was a solid orange dot (bg-orange-500). Changed to orange ⟳ with transparent background. --- src/browser/components/app-shell.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/browser/components/app-shell.tsx b/src/browser/components/app-shell.tsx index 73cb371..7cbc700 100644 --- a/src/browser/components/app-shell.tsx +++ b/src/browser/components/app-shell.tsx @@ -100,7 +100,7 @@ export function AppShell() { tab.repo && tab.number !== undefined ) { - markTabUpdated(tab.id); + clearTabUpdated(tab.id); setLastViewed(`${tab.owner}/${tab.repo}#${tab.number}`); } if (tab.type === "home") { @@ -114,7 +114,7 @@ export function AppShell() { navigate(`/${tab.owner}/${tab.repo}/pull/${tab.number}`); } }, - [navigate, markTabUpdated] + [navigate, clearTabUpdated] ); // Close a tab and navigate to the next active tab if needed @@ -532,13 +532,15 @@ function TabStatusIndicator({ status }: { status?: TabStatus }) { ); } - // Updated while in background gets an orange dot + // Updated while in background shows a refresh arrow if (status.updated) { return ( + > + ⟳ + ); }