diff --git a/extensions/browser/chrome-extension/background.js b/extensions/browser/chrome-extension/background.js
index 99357b9e64fc..1403d0277279 100644
--- a/extensions/browser/chrome-extension/background.js
+++ b/extensions/browser/chrome-extension/background.js
@@ -632,6 +632,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
paired: Boolean(relayUrl),
state: relayState,
sharedTabCount: shared.length,
+ relayUrl: relayUrl ?? "",
});
return;
}
diff --git a/extensions/browser/chrome-extension/popup.html b/extensions/browser/chrome-extension/popup.html
index 053c4dee498e..9a9d25d6988b 100644
--- a/extensions/browser/chrome-extension/popup.html
+++ b/extensions/browser/chrome-extension/popup.html
@@ -2,119 +2,363 @@
-
-
-
-
-
-
-
+
+ OC
+
+
OPENCLAW
+
Checking status…
-
-
+
+
+
+
+
+
+ Run openclaw browser extension pair on this machine, then paste the pairing
+ string:
+
+
+
+
+
+
+
+ Relay unreachable — is the OpenClaw gateway running?
+
+
+
+
+
SEND THIS PAGE
+
+
+
+
+
+
+
+
+
- Version
+
+
+
+
- Relay
+ - —
+
+
+
+
+ Unpairing disconnects the gateway and forgets the relay token.
+
+
+
diff --git a/extensions/browser/chrome-extension/popup.js b/extensions/browser/chrome-extension/popup.js
index 50efb6a26e07..dcd8a0ee0183 100644
--- a/extensions/browser/chrome-extension/popup.js
+++ b/extensions/browser/chrome-extension/popup.js
@@ -1,8 +1,10 @@
-// Popup: pairing, connection status, and per-tab share toggle.
+// Popup: pairing, connection status, per-tab share toggle, and settings.
const statusDot = document.getElementById("statusDot");
const pairSection = document.getElementById("pairSection");
const connectedSection = document.getElementById("connectedSection");
+const settingsSection = document.getElementById("settingsSection");
+const settingsButton = document.getElementById("settingsButton");
const pairingInput = document.getElementById("pairingString");
const pairButton = document.getElementById("pairButton");
const unpairButton = document.getElementById("unpairButton");
@@ -13,30 +15,52 @@ const errorLine = document.getElementById("error");
const pageNote = document.getElementById("pageNote");
const sendPageButton = document.getElementById("sendPageButton");
const pageShareStatus = document.getElementById("pageShareStatus");
+const versionValue = document.getElementById("versionValue");
+const statusHint = document.getElementById("statusHint");
+const unpairNote = document.getElementById("unpairNote");
+const relayValue = document.getElementById("relayValue");
let sendingPage = false;
+let settingsOpen = false;
const STATE_LABEL = {
- on: "Connected to OpenClaw",
+ on: "Connected",
connecting: "Connecting…",
- error: "Relay unreachable — is the OpenClaw gateway running?",
+ error: "Relay unreachable",
off: "Not connected",
};
+versionValue.textContent = `v${chrome.runtime.getManifest().version}`;
+
async function activeTab() {
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
return tab ?? null;
}
+function relayHost(relayUrl) {
+ try {
+ return new URL(relayUrl).host;
+ } catch {
+ return "—";
+ }
+}
+
async function refresh() {
const status = await chrome.runtime.sendMessage({ type: "getStatus" });
- statusDot.className = `dot ${status.state}`;
- pairSection.classList.toggle("hidden", status.paired);
- connectedSection.classList.toggle("hidden", !status.paired);
+ statusDot.className = `status-dot ${status.state}`;
+ pairSection.classList.toggle("hidden", status.paired || settingsOpen);
+ connectedSection.classList.toggle("hidden", !status.paired || settingsOpen);
+ settingsSection.classList.toggle("hidden", !settingsOpen);
+ settingsButton.classList.toggle("active", settingsOpen);
+ relayValue.textContent = status.paired ? relayHost(status.relayUrl) : "—";
+ unpairButton.classList.toggle("hidden", !status.paired);
+ unpairNote.classList.toggle("hidden", !status.paired);
if (!status.paired) {
+ statusLine.textContent = "Not paired with a gateway";
return;
}
const label = STATE_LABEL[status.state] ?? STATE_LABEL.off;
statusLine.textContent = `${label} · ${status.sharedTabCount} tab${status.sharedTabCount === 1 ? "" : "s"} shared`;
+ statusHint.classList.toggle("hidden", status.state !== "error");
const tab = await activeTab();
if (tab?.id === undefined) {
shareButton.classList.add("hidden");
@@ -102,6 +126,7 @@ async function onPair() {
async function onUnpair() {
await chrome.runtime.sendMessage({ type: "unpair" });
+ settingsOpen = false;
await refresh();
}
@@ -124,6 +149,10 @@ async function onOpenCopilot() {
window.close();
}
+settingsButton.addEventListener("click", () => {
+ settingsOpen = !settingsOpen;
+ void refresh();
+});
pairButton.addEventListener("click", () => void onPair());
unpairButton.addEventListener("click", () => void onUnpair());
shareButton.addEventListener("click", () => void onToggleShare());
diff --git a/extensions/browser/chrome-extension/sidepanel.e2e-support.ts b/extensions/browser/chrome-extension/sidepanel.e2e-support.ts
index 6e4474b0fc62..57e65efb026f 100644
--- a/extensions/browser/chrome-extension/sidepanel.e2e-support.ts
+++ b/extensions/browser/chrome-extension/sidepanel.e2e-support.ts
@@ -138,7 +138,9 @@ export async function waitForLoadedExtensionId(
browserCdp: CDPSession,
extensionPath: string,
): Promise
{
- const expectedPath = path.resolve(extensionPath);
+ // macOS os.tmpdir() is a /var -> /private/var symlink; Chromium reports the
+ // canonical path, so compare realpaths or this never matches on Mac.
+ const expectedPath = await fs.realpath(path.resolve(extensionPath));
const deadline = Date.now() + 10_000;
do {
const result = (await browserCdp.send("Extensions.getExtensions")) as {