The popup named by action.default_popup is an HTML page with full extension API access, destroyed whenever it loses focus. Extension pages block inline scripts by default, so its code lives in popup.js.
<!doctype html>
<meta charset="utf-8">
<style>
body { width: 240px; padding: 0 16px; font: 14px system-ui, sans-serif; color: #37474f; }
h1 { font-size: 16px; color: #1565c0; } output { font-size: 32px; font-weight: 700; }
</style>
<h1>Link Counter</h1>
<p id="title">No data for this tab</p>
<output id="total">-</output>
<p id="external"></p>
<script src="popup.js"></script>async function show(tabId) {
const key = `tab-${tabId}`;
const { [key]: data } = await chrome.storage.session.get(key);
if (!data) return;
document.querySelector('#title').textContent = data.title;
document.querySelector('#total').textContent = `${data.total} links`;
document.querySelector('#external').textContent = `${data.external} point to other sites`;
}
chrome.tabs.query({ active: true, currentWindow: true }).then(([tab]) => show(tab.id));
A side panel stays open while the user browses: declare "side_panel": { "default_path": "panel.html" } plus the sidePanel permission (Chrome 114 1 +), or sidebar_action in Firefox 555 . Settings belong on an options page (options_page opens a tab, options_ui embeds it in the extensions page), stored with chrome.storage.sync so they follow the user's signed-in browsers.