feat(extension): add popup status page (paired/not-paired) for Web Store review
The biggest Web Store rejection risk for a CLI-bridge extension is "non-functional
without external software." Give ab-connect a visible standalone UI: a branded
popup that shows whether the native-messaging link to the local agent-browser CLI
is live (Connected + attached tab count, or Not paired with the install hint),
plus a one-line privacy statement (no tracking, no remote server) and a repo link.
- manifest: action.default_popup = popup.html; bump 0.4.0 -> 0.4.1
- background.js: track hostConnected; respond to {type:'ab-status'} from the popup
and nudge a reconnect on open
- popup.html/popup.js: dark/cyan branded status page (MV3-CSP-safe: external JS,
no inline handlers), with a safety timeout so it never hangs on "Checking…"
- repacked ab-connect.zip/.crx
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,9 @@ const SKIP_URL = /^(chrome|chrome-extension|devtools|chrome-untrusted|edge|about
|
|||||||
|
|
||||||
/** @type {chrome.runtime.Port|null} */
|
/** @type {chrome.runtime.Port|null} */
|
||||||
let port = null
|
let port = null
|
||||||
|
/** Whether the native-messaging host (the local agent-browser CLI) is linked.
|
||||||
|
* Read by the popup status page. */
|
||||||
|
let hostConnected = false
|
||||||
let nextSession = 1
|
let nextSession = 1
|
||||||
/** tabId -> { sessionId, targetId } */
|
/** tabId -> { sessionId, targetId } */
|
||||||
const tabs = new Map()
|
const tabs = new Map()
|
||||||
@@ -92,13 +95,16 @@ function connectHost() {
|
|||||||
if (port) return
|
if (port) return
|
||||||
try {
|
try {
|
||||||
port = chrome.runtime.connectNative(HOST_NAME)
|
port = chrome.runtime.connectNative(HOST_NAME)
|
||||||
|
hostConnected = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
port = null
|
port = null
|
||||||
|
hostConnected = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
port.onMessage.addListener((msg) => void whenReady(() => onHostMessage(msg)))
|
port.onMessage.addListener((msg) => void whenReady(() => onHostMessage(msg)))
|
||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
port = null
|
port = null
|
||||||
|
hostConnected = false
|
||||||
// Sessions are stale once the host is gone; the daemon re-discovers on
|
// Sessions are stale once the host is gone; the daemon re-discovers on
|
||||||
// reconnect. Keep chrome.debugger attached so reconnect is cheap.
|
// reconnect. Keep chrome.debugger attached so reconnect is cheap.
|
||||||
for (const tabId of tabs.keys()) setBadge(tabId, 'connecting')
|
for (const tabId of tabs.keys()) setBadge(tabId, 'connecting')
|
||||||
@@ -343,7 +349,18 @@ chrome.tabs.onRemoved.addListener((tabId) => void whenReady(() => detachTab(tabI
|
|||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => void whenReady(connectHost))
|
chrome.runtime.onInstalled.addListener(() => void whenReady(connectHost))
|
||||||
chrome.runtime.onStartup.addListener(() => void whenReady(connectHost))
|
chrome.runtime.onStartup.addListener(() => void whenReady(connectHost))
|
||||||
chrome.action.onClicked.addListener(() => void whenReady(connectHost))
|
|
||||||
|
// Popup status page asks for the live pairing state. Attempt a (re)connect on
|
||||||
|
// demand so opening the popup also nudges the link awake, then report.
|
||||||
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||||
|
if (msg && msg.type === 'ab-status') {
|
||||||
|
if (!port) {
|
||||||
|
try { connectHost() } catch (e) {}
|
||||||
|
}
|
||||||
|
sendResponse({ connected: hostConnected, tabCount: tabs.size, host: HOST_NAME })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
// MV3 service workers get suspended; an alarm wakes us to keep the host link
|
// MV3 service workers get suspended; an alarm wakes us to keep the host link
|
||||||
// and badges fresh.
|
// and badges fresh.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "agent-browser-stealth",
|
"name": "agent-browser-stealth",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.",
|
"description": "Let agent-browser drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "agent-browser-stealth"
|
"default_title": "agent-browser-stealth",
|
||||||
|
"default_popup": "popup.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0f1115;
|
||||||
|
--panel: #161a21;
|
||||||
|
--fg: #e6edf3;
|
||||||
|
--muted: #8b949e;
|
||||||
|
--cyan: #2ad4ff;
|
||||||
|
--green: #3fb950;
|
||||||
|
--amber: #d29922;
|
||||||
|
--border: #232a33;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; }
|
||||||
|
body {
|
||||||
|
width: 320px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 16px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
header img { width: 32px; height: 32px; border-radius: 7px; }
|
||||||
|
header .title { font-weight: 600; font-size: 14px; }
|
||||||
|
header .ver { color: var(--muted); font-size: 11px; }
|
||||||
|
main { padding: 14px 16px 8px; }
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 9px; height: 9px; border-radius: 50%;
|
||||||
|
background: var(--muted); flex: none;
|
||||||
|
box-shadow: 0 0 0 0 rgba(0,0,0,0);
|
||||||
|
}
|
||||||
|
.dot.on { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
||||||
|
.dot.off { background: var(--amber); box-shadow: 0 0 8px var(--amber); }
|
||||||
|
.status .label { font-weight: 600; }
|
||||||
|
.status .sub { color: var(--muted); font-size: 11px; }
|
||||||
|
.desc { color: var(--muted); margin: 12px 2px 4px; }
|
||||||
|
.hint {
|
||||||
|
margin: 10px 0 2px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
background: #1d1a12;
|
||||||
|
border: 1px solid #3a3014;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e3c878;
|
||||||
|
font-size: 12px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hint code {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #0b0d10;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 11.5px;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 10px 16px 14px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
footer .privacy { color: var(--muted); font-size: 11px; }
|
||||||
|
footer a { color: var(--cyan); text-decoration: none; font-size: 11px; cursor: pointer; }
|
||||||
|
footer a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<img src="icons/icon128.png" alt="" />
|
||||||
|
<div>
|
||||||
|
<div class="title">agent-browser-stealth</div>
|
||||||
|
<div class="ver">local automation bridge</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="status">
|
||||||
|
<span id="dot" class="dot"></span>
|
||||||
|
<div>
|
||||||
|
<div class="label" id="statusLabel">Checking…</div>
|
||||||
|
<div class="sub" id="statusSub">contacting the local CLI</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="desc">
|
||||||
|
Lets your locally-installed <strong>agent-browser</strong> command-line tool
|
||||||
|
drive your own logged-in Chrome tabs — entirely on this machine, only when
|
||||||
|
you run a command. No remote server, no data collection.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hint" id="hint">
|
||||||
|
Not linked yet. Install & pair the CLI, then reopen this popup:
|
||||||
|
<code>agent-browser extension install</code>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span class="privacy">No tracking · no remote server</span>
|
||||||
|
<a id="repo" data-href="https://github.com/leeguooooo/agent-browser-stealth">GitHub ↗</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Popup status page for agent-browser-stealth.
|
||||||
|
// Asks the service worker whether the native-messaging link to the local
|
||||||
|
// agent-browser CLI is live, and renders a paired / not-paired indicator.
|
||||||
|
|
||||||
|
const dot = document.getElementById('dot')
|
||||||
|
const label = document.getElementById('statusLabel')
|
||||||
|
const sub = document.getElementById('statusSub')
|
||||||
|
const hint = document.getElementById('hint')
|
||||||
|
|
||||||
|
let resolved = false
|
||||||
|
|
||||||
|
function render(state) {
|
||||||
|
resolved = true
|
||||||
|
const connected = !!(state && state.connected)
|
||||||
|
dot.classList.remove('on', 'off')
|
||||||
|
if (connected) {
|
||||||
|
dot.classList.add('on')
|
||||||
|
label.textContent = 'Connected'
|
||||||
|
const n = state.tabCount | 0
|
||||||
|
sub.textContent =
|
||||||
|
n > 0
|
||||||
|
? `bridged to the local CLI · ${n} tab${n === 1 ? '' : 's'} attached`
|
||||||
|
: 'bridged to the local CLI · ready'
|
||||||
|
hint.style.display = 'none'
|
||||||
|
} else {
|
||||||
|
dot.classList.add('off')
|
||||||
|
label.textContent = 'Not paired'
|
||||||
|
sub.textContent = 'no local agent-browser CLI linked'
|
||||||
|
hint.style.display = 'block'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryStatus() {
|
||||||
|
try {
|
||||||
|
chrome.runtime.sendMessage({ type: 'ab-status' }, (resp) => {
|
||||||
|
// lastError fires if the service worker can't be reached.
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
render({ connected: false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render(resp)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
render({ connected: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the repo in a real tab (no inline handlers under MV3 CSP).
|
||||||
|
const repo = document.getElementById('repo')
|
||||||
|
if (repo) {
|
||||||
|
repo.addEventListener('click', () => {
|
||||||
|
chrome.tabs.create({ url: repo.dataset.href })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query now, then once more shortly after — opening the popup also nudges the
|
||||||
|
// service worker to (re)connect the host, which may complete a beat later.
|
||||||
|
queryStatus()
|
||||||
|
setTimeout(queryStatus, 700)
|
||||||
|
|
||||||
|
// Never leave the popup stuck on "Checking…" if the worker never answers.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) render({ connected: false })
|
||||||
|
}, 1500)
|
||||||
Reference in New Issue
Block a user