From 1636db878fadca47fc6e1a9db508e74a00562ab4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 08:23:57 +0000 Subject: [PATCH 01/10] Initial plan From c02484b78af4f2ba4a75395a2cc65bcc58e61f39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 08:29:26 +0000 Subject: [PATCH 02/10] Modernize unclose for current Chrome extensions Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/93b36160-7632-48fe-9bd2-80cc78fb3ad3 Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/background.js | 1 + unclose/bg.js | 56 +++++++++++--------- unclose/init.js | 61 ++++++++++++++------- unclose/manifest.json | 12 +++-- unclose/popup.html | 39 +++++++------- unclose/popup.js | 120 +++++++++++++++++++++++++++++------------- unclose/storage.js | 31 +++++++++++ 7 files changed, 217 insertions(+), 103 deletions(-) create mode 100644 unclose/background.js create mode 100644 unclose/storage.js diff --git a/unclose/background.js b/unclose/background.js new file mode 100644 index 0000000..37f0b05 --- /dev/null +++ b/unclose/background.js @@ -0,0 +1 @@ +importScripts("storage.js", "init.js", "bg.js"); diff --git a/unclose/bg.js b/unclose/bg.js index f2a8d3e..9a3b566 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -1,33 +1,41 @@ -// Replace HTML tags < > -function quote(s) { - var s1 = s.replace("<", "<"); - var s2 = s1.replace(">", ">"); - return s2; -} - -chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { - localStorage["TabList-"+tabId] = tab.url; - localStorage["TabIndex-"+tabId] = tab.index; +chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { + await ensureInitialized(); + var updates = {}; + if (tab.url) + updates["TabList-" + tabId] = tab.url; + updates["TabIndex-" + tabId] = tab.index; if (tab.favIconUrl) - localStorage["TabFavicon-"+tabId] = tab.favIconUrl; + updates["TabFavicon-" + tabId] = tab.favIconUrl; if(tab.title != null) - localStorage["TabTitle-"+tabId] = quote(tab.title); - else - localStorage["TabTitle-"+tabId] = tab.url; + updates["TabTitle-" + tabId] = tab.title; + else if (tab.url) + updates["TabTitle-" + tabId] = tab.url; + await storageSet(updates); }); -chrome.tabs.onRemoved.addListener(function(tabId, info) { +chrome.tabs.onRemoved.addListener(async function(tabId, info) { + await ensureInitialized(); // Should we record this tab? - var url = localStorage["TabList-"+tabId]; + var tabKey = "TabList-" + tabId; + var state = await storageGet({ + closeCount: 0, + actualCount: 0, + [tabKey]: null + }); + var url = state[tabKey]; var re = /^(http:|https:|ftp:|file:)/; if (url && re.test(url)) { - var digital = new Date(); - - localStorage["ClosedTab-"+localStorage["closeCount"]] = tabId; - localStorage["ClosedTabTime-"+localStorage["closeCount"]] = digital.getTime(); - localStorage["closeCount"] ++; - localStorage["actualCount"] ++; - setBadgeText(); + var closeCount = parseInt(state.closeCount, 10) || 0; + var actualCount = (parseInt(state.actualCount, 10) || 0) + 1; + var updates = { + closeCount: closeCount + 1, + actualCount: actualCount + }; + updates["ClosedTab-" + closeCount] = tabId; + updates["ClosedTabTime-" + closeCount] = Date.now(); + await storageSet(updates); + await setBadgeText(); } - else clear(tabId); + else + await clear(tabId); }); diff --git a/unclose/init.js b/unclose/init.js index 0deb60b..794c308 100644 --- a/unclose/init.js +++ b/unclose/init.js @@ -1,28 +1,53 @@ -function clear(tabId) { - delete localStorage["TabList-"+tabId]; - delete localStorage["TabIndex-"+tabId]; - delete localStorage["TabTitle-"+tabId]; - delete localStorage["TabFavicon-"+tabId]; +async function clear(tabId) { + return storageRemove(getTabKeys(tabId)); } -function setBadgeText() { - var n = localStorage["actualCount"]; - if (parseInt(n) > 0) - chrome.browserAction.setBadgeText({text:n}); +async function setBadgeText() { + var n = parseInt(await storageGet("actualCount"), 10) || 0; + if (n > 0) + await chrome.action.setBadgeText({text: String(n)}); else - chrome.browserAction.setBadgeText({text:""}); + await chrome.action.setBadgeText({text:""}); } -function initialize() { - localStorage["closeCount"] = 0; - localStorage["actualCount"] = 0; - setBadgeText(); +async function initialize() { + await storageSet({ + closeCount: 0, + actualCount: 0 + }); + await setBadgeText(); } -function init() +async function ensureInitialized() { - // Deep clean: clear localStorage otherwise we get old history from previous sessions - localStorage.clear(); + var state = await storageGet({ + closeCount: null, + actualCount: null + }); + var updates = {}; - initialize(); + if (state.closeCount === null) + updates.closeCount = 0; + if (state.actualCount === null) + updates.actualCount = 0; + + if (Object.keys(updates).length > 0) + await storageSet(updates); + + await setBadgeText(); } + +async function init() +{ + // Deep clean: clear storage otherwise we get old history from previous sessions + await storageClear(); + await initialize(); +} + +chrome.runtime.onInstalled.addListener(function() { + init(); +}); + +chrome.runtime.onStartup.addListener(function() { + init(); +}); diff --git a/unclose/manifest.json b/unclose/manifest.json index 1e4a4a5..ed8119c 100644 --- a/unclose/manifest.json +++ b/unclose/manifest.json @@ -3,18 +3,22 @@ "version": "4.0", "author": "MK & Wei Hu & Xerios", "description": "Undo your closed tabs", - "background_page": "background.html", + "manifest_version": 3, + "background": { + "service_worker": "background.js" + }, "icons": { "48": "icon.png", - "128": "icon-128.png" - }, + "128": "icon-128.png" + }, "options_page": "popup.html", - "browser_action": { + "action": { "default_icon": "icon.png", "popup": "popup.html", "default_title": "Undo closed tabs" }, "permissions": [ + "storage", "tabs" ] } diff --git a/unclose/popup.html b/unclose/popup.html index 108e37f..048359a 100644 --- a/unclose/popup.html +++ b/unclose/popup.html @@ -1,7 +1,7 @@ - - - - - - -
- - - - - -
- - - - -
+ + + + + + + +
+ + + + + +
+ + + + +
diff --git a/unclose/popup.js b/unclose/popup.js index 7e2c6d0..eb74108 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -8,33 +8,51 @@ function createLink(id, url) { return link; } -function loadText() +function appendTime(parent, label) { - // Don't popup if there's nothing to show - var n = localStorage["actualCount"]; - if (parseInt(n) == 0) + if (!label) + return; + + var textdiv = document.createElement('span'); + var bold = document.createElement('b'); + bold.textContent = label; + textdiv.appendChild(bold); + parent.appendChild(textdiv); +} + +async function loadText() +{ + await ensureInitialized(); + + var state = await storageGetAll(); + var n = parseInt(state.actualCount, 10) || 0; + if (n == 0) { window.close(); + return; + } + + while (pageNo > 0 && n <= pageNo * nItems) + pageNo--; var tabId, tabUrl, tabTime; - - content = document.getElementById("contentDiv"); + var content = document.getElementById("contentDiv"); // Clear while (content.hasChildNodes()) content.removeChild(content.firstChild); // Drop the first (pageNo*nItems) valid items - for (j = 0, i = localStorage["closeCount"] - 1; i>=0 && j=0 && j=0 && j'; else if (hoursDifference < 4) timeTextz= '' + hoursDifference + 'hr ' + minutesDifference + 'm'; else if (hoursDifference < 24) timeTextz='' + hoursDifference + ' hr'; - textdiv2.innerHTML=timeTextz; - text_link.appendChild(textdiv2); + if (timeTextz) + appendTime(text_link, timeTextz.replace(/<\/?b>/g, "")); content.appendChild(text_link); j++; @@ -82,7 +99,7 @@ function loadText() if (pageNo > 0) document.getElementById("prev").style.visibility="visible"; else document.getElementById("prev").style.visibility="hidden"; - if (localStorage["actualCount"] > (pageNo+1) * nItems) + if (n > (pageNo+1) * nItems) document.getElementById("next").style.visibility="visible"; else document.getElementById("next").style.visibility="hidden"; } @@ -93,15 +110,14 @@ function loadFavicon() { imgs[i].src = imgs[i].alt; } -function loadContent() { - loadText(); +async function loadContent() { + await loadText(); // Delay this function a little bit in order not to halt the popup setTimeout(loadFavicon, 500); } function next() { - if (localStorage["actualCount"] > (pageNo+1) * nItems) - pageNo++; + pageNo++; loadContent(); } @@ -112,29 +128,57 @@ function prev() { } // Show |url| in a new tab. -function showUrl(tabId) { - var url = localStorage["TabList-"+tabId]; - var index = parseInt(localStorage["TabIndex-"+tabId]); - chrome.tabs.create({"url": url, "index": index}); - clear(tabId); - localStorage["actualCount"] --; - setBadgeText(); - loadContent(); +async function showUrl(tabId) { + var state = await storageGet({ + actualCount: 0, + ["TabList-" + tabId]: null, + ["TabIndex-" + tabId]: null + }); + var url = state["TabList-" + tabId]; + var index = parseInt(state["TabIndex-" + tabId], 10); + if (!url) + return; + + var createProperties = {"url": url}; + if (!isNaN(index)) + createProperties.index = index; + + await chrome.tabs.create(createProperties); + await clear(tabId); + await storageSet({ + actualCount: Math.max((parseInt(state.actualCount, 10) || 0) - 1, 0) + }); + await setBadgeText(); + await loadContent(); } -function reset() +async function reset() { // Shallow clean: only forgets history about closed tabs - for(i = localStorage["closeCount"]-1; i >= 0; i--) + var state = await storageGetAll(); + var keys = []; + + for(var i = (parseInt(state.closeCount, 10) || 0) - 1; i >= 0; i--) { - tabId = localStorage["ClosedTab-"+i]; - delete localStorage["ClosedTab-"+i]; - delete localStorage["ClosedTabTime-"+i]; - clear(tabId); + var tabId = state["ClosedTab-"+i]; + keys.push("ClosedTab-"+i, "ClosedTabTime-"+i); + if (tabId != null) + keys = keys.concat(getTabKeys(tabId)); } - initialize(); + await storageRemove(keys); + await initialize(); pageNo = 0; window.close(); } + +document.addEventListener("DOMContentLoaded", function() { + document.body.addEventListener("selectstart", function(event) { + event.preventDefault(); + }); + document.getElementById("prev").addEventListener("click", prev); + document.getElementById("next").addEventListener("click", next); + document.getElementById("clear").addEventListener("click", reset); + loadContent(); +}); diff --git a/unclose/storage.js b/unclose/storage.js new file mode 100644 index 0000000..b3ad93c --- /dev/null +++ b/unclose/storage.js @@ -0,0 +1,31 @@ +function getTabKeys(tabId) { + return [ + "TabList-" + tabId, + "TabIndex-" + tabId, + "TabTitle-" + tabId, + "TabFavicon-" + tabId + ]; +} + +async function storageGet(keyOrKeys) { + var data = await chrome.storage.session.get(keyOrKeys); + if (typeof keyOrKeys == "string") + return data[keyOrKeys]; + return data; +} + +async function storageGetAll() { + return chrome.storage.session.get(null); +} + +async function storageSet(items) { + return chrome.storage.session.set(items); +} + +async function storageRemove(keys) { + return chrome.storage.session.remove(keys); +} + +async function storageClear() { + return chrome.storage.session.clear(); +} From e1b6a07b38761ac1b253685efce205506ce6d695 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 08:30:38 +0000 Subject: [PATCH 03/10] Address validation feedback for unclose modernization Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/93b36160-7632-48fe-9bd2-80cc78fb3ad3 Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/init.js | 8 ++++---- unclose/popup.js | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/unclose/init.js b/unclose/init.js index 794c308..7251ea4 100644 --- a/unclose/init.js +++ b/unclose/init.js @@ -44,10 +44,10 @@ async function init() await initialize(); } -chrome.runtime.onInstalled.addListener(function() { - init(); +chrome.runtime.onInstalled.addListener(async function() { + await init(); }); -chrome.runtime.onStartup.addListener(function() { - init(); +chrome.runtime.onStartup.addListener(async function() { + await init(); }); diff --git a/unclose/popup.js b/unclose/popup.js index eb74108..73c90a8 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -69,7 +69,7 @@ async function loadText() text_link.appendChild(img); var textdiv = document.createElement('a'); - textdiv.textContent = " " + (state["TabTitle-"+tabId] || tabUrl); + textdiv.textContent = state["TabTitle-"+tabId] || tabUrl; text_link.appendChild(textdiv); var timeTextz=''; @@ -83,13 +83,13 @@ async function loadText() var secondsDifference = Math.floor(difference/1000); // This next line below looks for entries over a day old - if ( hoursDifference < 1 && minutesDifference < 1 &&secondsDifference < 60) timeTextz = ''+ secondsDifference + ' sec'; - else if (hoursDifference < 1 && minutesDifference < 10) timeTextz = ''+ minutesDifference + ' min'; - else if (hoursDifference < 1) timeTextz = ''+ minutesDifference + ' min'; - else if (hoursDifference < 4) timeTextz= '' + hoursDifference + 'hr ' + minutesDifference + 'm'; - else if (hoursDifference < 24) timeTextz='' + hoursDifference + ' hr'; + if ( hoursDifference < 1 && minutesDifference < 1 &&secondsDifference < 60) timeTextz = secondsDifference + ' sec'; + else if (hoursDifference < 1 && minutesDifference < 10) timeTextz = minutesDifference + ' min'; + else if (hoursDifference < 1) timeTextz = minutesDifference + ' min'; + else if (hoursDifference < 4) timeTextz = hoursDifference + 'hr ' + minutesDifference + 'm'; + else if (hoursDifference < 24) timeTextz = hoursDifference + ' hr'; if (timeTextz) - appendTime(text_link, timeTextz.replace(/<\/?b>/g, "")); + appendTime(text_link, timeTextz); content.appendChild(text_link); j++; @@ -116,8 +116,9 @@ async function loadContent() { setTimeout(loadFavicon, 500); } -function next() { - pageNo++; +async function next() { + if ((parseInt(await storageGet("actualCount"), 10) || 0) > (pageNo+1) * nItems) + pageNo++; loadContent(); } From af73dac319bfa29d8040d562bce7b2ceaacdfc54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 08:31:50 +0000 Subject: [PATCH 04/10] Polish popup follow-up review nits Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/93b36160-7632-48fe-9bd2-80cc78fb3ad3 Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/popup.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/unclose/popup.js b/unclose/popup.js index 73c90a8..ce1d38e 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -69,7 +69,7 @@ async function loadText() text_link.appendChild(img); var textdiv = document.createElement('a'); - textdiv.textContent = state["TabTitle-"+tabId] || tabUrl; + textdiv.textContent = " " + (state["TabTitle-"+tabId] || tabUrl); text_link.appendChild(textdiv); var timeTextz=''; @@ -131,7 +131,6 @@ function prev() { // Show |url| in a new tab. async function showUrl(tabId) { var state = await storageGet({ - actualCount: 0, ["TabList-" + tabId]: null, ["TabIndex-" + tabId]: null }); From 103bfe7988bf8d534165b7643151a6060d9dba95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 08:32:56 +0000 Subject: [PATCH 05/10] Fix popup restore count regression Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/93b36160-7632-48fe-9bd2-80cc78fb3ad3 Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/bg.js | 2 +- unclose/popup.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/unclose/bg.js b/unclose/bg.js index 9a3b566..efb3203 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -32,7 +32,7 @@ chrome.tabs.onRemoved.addListener(async function(tabId, info) { actualCount: actualCount }; updates["ClosedTab-" + closeCount] = tabId; - updates["ClosedTabTime-" + closeCount] = Date.now(); + updates["ClosedTabTime-" + closeCount] = new Date().getTime(); await storageSet(updates); await setBadgeText(); } diff --git a/unclose/popup.js b/unclose/popup.js index ce1d38e..6bdc8a9 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -83,7 +83,7 @@ async function loadText() var secondsDifference = Math.floor(difference/1000); // This next line below looks for entries over a day old - if ( hoursDifference < 1 && minutesDifference < 1 &&secondsDifference < 60) timeTextz = secondsDifference + ' sec'; + if ( hoursDifference < 1 && minutesDifference < 1 && secondsDifference < 60) timeTextz = secondsDifference + ' sec'; else if (hoursDifference < 1 && minutesDifference < 10) timeTextz = minutesDifference + ' min'; else if (hoursDifference < 1) timeTextz = minutesDifference + ' min'; else if (hoursDifference < 4) timeTextz = hoursDifference + 'hr ' + minutesDifference + 'm'; @@ -131,6 +131,7 @@ function prev() { // Show |url| in a new tab. async function showUrl(tabId) { var state = await storageGet({ + actualCount: 0, ["TabList-" + tabId]: null, ["TabIndex-" + tabId]: null }); From 47b3f0cb7912d27987ddf22b1c98eaa4ac01d308 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 08:34:31 +0000 Subject: [PATCH 06/10] Tighten tab update and popup spacing logic Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/93b36160-7632-48fe-9bd2-80cc78fb3ad3 Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/bg.js | 10 ++++++---- unclose/popup.html | 13 +++++++------ unclose/popup.js | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/unclose/bg.js b/unclose/bg.js index efb3203..58405a9 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -1,15 +1,17 @@ chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { await ensureInitialized(); + var tabKey = "TabList-" + tabId; + var currentUrl = tab.url || await storageGet(tabKey); var updates = {}; if (tab.url) - updates["TabList-" + tabId] = tab.url; + updates[tabKey] = tab.url; updates["TabIndex-" + tabId] = tab.index; if (tab.favIconUrl) updates["TabFavicon-" + tabId] = tab.favIconUrl; - if(tab.title != null) + if(tab.title != null && currentUrl) updates["TabTitle-" + tabId] = tab.title; - else if (tab.url) - updates["TabTitle-" + tabId] = tab.url; + else if (currentUrl) + updates["TabTitle-" + tabId] = currentUrl; await storageSet(updates); }); diff --git a/unclose/popup.html b/unclose/popup.html index 048359a..8687335 100644 --- a/unclose/popup.html +++ b/unclose/popup.html @@ -25,12 +25,13 @@ #contentDiv div:hover { background:#E4E7FF; } - #contentDiv div a{ - display:inline-block; - width:330px; - margin: 3px 0 3px 0; - overflow:hidden; - } + #contentDiv div a{ + display:inline-block; + width:330px; + margin: 3px 0 3px 0; + padding-left: 1px; + overflow:hidden; + } #contentDiv div span,b{ padding: 3px 0 0 0; vertical-align:top; diff --git a/unclose/popup.js b/unclose/popup.js index 6bdc8a9..57f830d 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -69,7 +69,7 @@ async function loadText() text_link.appendChild(img); var textdiv = document.createElement('a'); - textdiv.textContent = " " + (state["TabTitle-"+tabId] || tabUrl); + textdiv.textContent = state["TabTitle-"+tabId] || tabUrl; text_link.appendChild(textdiv); var timeTextz=''; @@ -83,7 +83,7 @@ async function loadText() var secondsDifference = Math.floor(difference/1000); // This next line below looks for entries over a day old - if ( hoursDifference < 1 && minutesDifference < 1 && secondsDifference < 60) timeTextz = secondsDifference + ' sec'; + if ( hoursDifference < 1 && minutesDifference < 1) timeTextz = secondsDifference + ' sec'; else if (hoursDifference < 1 && minutesDifference < 10) timeTextz = minutesDifference + ' min'; else if (hoursDifference < 1) timeTextz = minutesDifference + ' min'; else if (hoursDifference < 4) timeTextz = hoursDifference + 'hr ' + minutesDifference + 'm'; From 8c16c5a56a9f88deb3d92f0ba72c9479bb39e3cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 09:09:31 +0000 Subject: [PATCH 07/10] Initial plan From 8ac02105d78b9ee92601f6e9995804b87312f929 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 09:13:52 +0000 Subject: [PATCH 08/10] Fix concurrency bugs: write queue, onInstalled gate, title bug, onStartup cleanup Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/89b171b4-e2a4-4bc6-9b6e-b4407919ab0f Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/bg.js | 91 +++++++++++++++++++++++++++--------------------- unclose/init.js | 9 +++-- unclose/popup.js | 6 ++-- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/unclose/bg.js b/unclose/bg.js index 58405a9..e82e159 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -1,43 +1,56 @@ -chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { - await ensureInitialized(); - var tabKey = "TabList-" + tabId; - var currentUrl = tab.url || await storageGet(tabKey); - var updates = {}; - if (tab.url) - updates[tabKey] = tab.url; - updates["TabIndex-" + tabId] = tab.index; - if (tab.favIconUrl) - updates["TabFavicon-" + tabId] = tab.favIconUrl; - if(tab.title != null && currentUrl) - updates["TabTitle-" + tabId] = tab.title; - else if (currentUrl) - updates["TabTitle-" + tabId] = currentUrl; - await storageSet(updates); +// Serialize all storage writes to prevent counter races on rapid tab events. +var _writeQueue = Promise.resolve(); +function enqueueWrite(asyncFn) { + _writeQueue = _writeQueue.then(asyncFn).catch((err) => { + console.error('[unclose] storage write failed:', err); + }); + return _writeQueue; +} + +chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { + enqueueWrite(async () => { + await ensureInitialized(); + var tabKey = "TabList-" + tabId; + var currentUrl = tab.url || await storageGet(tabKey); + var updates = {}; + if (tab.url) + updates[tabKey] = tab.url; + updates["TabIndex-" + tabId] = tab.index; + if (tab.favIconUrl) + updates["TabFavicon-" + tabId] = tab.favIconUrl; + if (tab.title != null) + updates["TabTitle-" + tabId] = tab.title; + else if (currentUrl) + updates["TabTitle-" + tabId] = currentUrl; + await storageSet(updates); + }); }); -chrome.tabs.onRemoved.addListener(async function(tabId, info) { - await ensureInitialized(); - // Should we record this tab? - var tabKey = "TabList-" + tabId; - var state = await storageGet({ - closeCount: 0, - actualCount: 0, - [tabKey]: null +chrome.tabs.onRemoved.addListener(function(tabId, info) { + enqueueWrite(async () => { + await ensureInitialized(); + // Should we record this tab? + var tabKey = "TabList-" + tabId; + var state = await storageGet({ + closeCount: 0, + actualCount: 0, + [tabKey]: null + }); + var url = state[tabKey]; + var re = /^(http:|https:|ftp:|file:)/; + if (url && re.test(url)) { + var closeCount = parseInt(state.closeCount, 10) || 0; + var actualCount = (parseInt(state.actualCount, 10) || 0) + 1; + var updates = { + closeCount: closeCount + 1, + actualCount: actualCount + }; + updates["ClosedTab-" + closeCount] = tabId; + updates["ClosedTabTime-" + closeCount] = new Date().getTime(); + await storageSet(updates); + await setBadgeText(); + } + else + await clear(tabId); }); - var url = state[tabKey]; - var re = /^(http:|https:|ftp:|file:)/; - if (url && re.test(url)) { - var closeCount = parseInt(state.closeCount, 10) || 0; - var actualCount = (parseInt(state.actualCount, 10) || 0) + 1; - var updates = { - closeCount: closeCount + 1, - actualCount: actualCount - }; - updates["ClosedTab-" + closeCount] = tabId; - updates["ClosedTabTime-" + closeCount] = new Date().getTime(); - await storageSet(updates); - await setBadgeText(); - } - else - await clear(tabId); }); diff --git a/unclose/init.js b/unclose/init.js index 7251ea4..6ddf2d0 100644 --- a/unclose/init.js +++ b/unclose/init.js @@ -44,10 +44,13 @@ async function init() await initialize(); } -chrome.runtime.onInstalled.addListener(async function() { - await init(); +chrome.runtime.onInstalled.addListener(async function(details) { + if (details.reason === 'install') await init(); + else await ensureInitialized(); }); chrome.runtime.onStartup.addListener(async function() { - await init(); + // chrome.storage.session is cleared automatically on browser restart; + // just reset the counters and badge. + await initialize(); }); diff --git a/unclose/popup.js b/unclose/popup.js index 57f830d..7531b27 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -131,7 +131,6 @@ function prev() { // Show |url| in a new tab. async function showUrl(tabId) { var state = await storageGet({ - actualCount: 0, ["TabList-" + tabId]: null, ["TabIndex-" + tabId]: null }); @@ -146,8 +145,11 @@ async function showUrl(tabId) { await chrome.tabs.create(createProperties); await clear(tabId); + // Re-read actualCount right before decrementing to minimise the race window + // with concurrent onRemoved increments. + var currentCount = parseInt(await storageGet("actualCount"), 10) || 0; await storageSet({ - actualCount: Math.max((parseInt(state.actualCount, 10) || 0) - 1, 0) + actualCount: Math.max(currentCount - 1, 0) }); await setBadgeText(); await loadContent(); From 360c75ce15a057d9f4d4e45cef67f128929eabfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 16:31:23 +0000 Subject: [PATCH 09/10] Make tab event listeners async and await enqueueWrite to keep MV3 worker alive Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/88e12e2b-d875-4235-bc17-b22a6f740199 Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/bg.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unclose/bg.js b/unclose/bg.js index e82e159..6362674 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -7,8 +7,8 @@ function enqueueWrite(asyncFn) { return _writeQueue; } -chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { - enqueueWrite(async () => { +chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { + await enqueueWrite(async () => { await ensureInitialized(); var tabKey = "TabList-" + tabId; var currentUrl = tab.url || await storageGet(tabKey); @@ -26,8 +26,8 @@ chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { }); }); -chrome.tabs.onRemoved.addListener(function(tabId, info) { - enqueueWrite(async () => { +chrome.tabs.onRemoved.addListener(async function(tabId, info) { + await enqueueWrite(async () => { await ensureInitialized(); // Should we record this tab? var tabKey = "TabList-" + tabId; From 24269301ffac2068c6a276adcb5eb36a28143b6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 21:42:00 +0000 Subject: [PATCH 10/10] Fix remaining unclose MV3 review races Agent-Logs-Url: https://github.com/wh5a/chrome/sessions/9965d665-d2af-432a-abf7-e8d3f0e536ea Co-authored-by: wh5a <81014+wh5a@users.noreply.github.com> --- unclose/bg.js | 42 ++++++++++++++++++++++++++++++++++++++++++ unclose/init.js | 36 ++++++++++++++++++++++++------------ unclose/popup.js | 28 ++++++---------------------- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/unclose/bg.js b/unclose/bg.js index 6362674..f93d31f 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -7,6 +7,32 @@ function enqueueWrite(asyncFn) { return _writeQueue; } +async function restoreTab(tabId) { + await ensureInitialized(); + + var state = await storageGet({ + actualCount: 0, + ["TabList-" + tabId]: null, + ["TabIndex-" + tabId]: null + }); + var url = state["TabList-" + tabId]; + var index = parseInt(state["TabIndex-" + tabId], 10); + if (!url) + return false; + + var createProperties = {"url": url}; + if (!isNaN(index)) + createProperties.index = index; + + await chrome.tabs.create(createProperties); + await clear(tabId); + await storageSet({ + actualCount: Math.max((parseInt(state.actualCount, 10) || 0) - 1, 0) + }); + await setBadgeText(); + return true; +} + chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { await enqueueWrite(async () => { await ensureInitialized(); @@ -54,3 +80,19 @@ chrome.tabs.onRemoved.addListener(async function(tabId, info) { await clear(tabId); }); }); + +chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { + if (!message || message.method !== "restoreTab") + return false; + + enqueueWrite(async function() { + return restoreTab(message.tabId); + }).then(function(restored) { + sendResponse({ok: restored}); + }).catch(function(error) { + console.error('[unclose] restore failed:', error); + sendResponse({ok: false}); + }); + + return true; +}); diff --git a/unclose/init.js b/unclose/init.js index 6ddf2d0..8016c03 100644 --- a/unclose/init.js +++ b/unclose/init.js @@ -1,3 +1,5 @@ +var _initializationPromise = null; + async function clear(tabId) { return storageRemove(getTabKeys(tabId)); } @@ -15,32 +17,42 @@ async function initialize() { closeCount: 0, actualCount: 0 }); + _initializationPromise = Promise.resolve(); await setBadgeText(); } async function ensureInitialized() { - var state = await storageGet({ - closeCount: null, - actualCount: null - }); - var updates = {}; + if (_initializationPromise) + return _initializationPromise; - if (state.closeCount === null) - updates.closeCount = 0; - if (state.actualCount === null) - updates.actualCount = 0; + _initializationPromise = (async function() { + var state = await storageGet({ + closeCount: null, + actualCount: null + }); + var updates = {}; - if (Object.keys(updates).length > 0) - await storageSet(updates); + if (state.closeCount === null) + updates.closeCount = 0; + if (state.actualCount === null) + updates.actualCount = 0; - await setBadgeText(); + if (Object.keys(updates).length > 0) + await storageSet(updates); + })().catch(function(error) { + _initializationPromise = null; + throw error; + }); + + return _initializationPromise; } async function init() { // Deep clean: clear storage otherwise we get old history from previous sessions await storageClear(); + _initializationPromise = null; await initialize(); } diff --git a/unclose/popup.js b/unclose/popup.js index 7531b27..515ae77 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -106,7 +106,7 @@ async function loadText() function loadFavicon() { var imgs = document.images; - for (i=0; i