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} */
|
||||
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
|
||||
/** tabId -> { sessionId, targetId } */
|
||||
const tabs = new Map()
|
||||
@@ -92,13 +95,16 @@ function connectHost() {
|
||||
if (port) return
|
||||
try {
|
||||
port = chrome.runtime.connectNative(HOST_NAME)
|
||||
hostConnected = true
|
||||
} catch (e) {
|
||||
port = null
|
||||
hostConnected = false
|
||||
return
|
||||
}
|
||||
port.onMessage.addListener((msg) => void whenReady(() => onHostMessage(msg)))
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null
|
||||
hostConnected = false
|
||||
// Sessions are stale once the host is gone; the daemon re-discovers on
|
||||
// reconnect. Keep chrome.debugger attached so reconnect is cheap.
|
||||
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.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
|
||||
// and badges fresh.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.4.0",
|
||||
"description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.",
|
||||
"version": "0.4.1",
|
||||
"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",
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
@@ -24,6 +24,7 @@
|
||||
"type": "module"
|
||||
},
|
||||
"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