mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
feat(browser): redesign extension popup with settings panel (#118095)
* feat(browser): redesign extension popup with settings panel * fix(browser): hide unpair action while popup is unpaired * test(browser): compare realpaths when resolving loaded extension id
This commit is contained in:
committed by
GitHub
parent
999f14392b
commit
fb7cb832cc
@@ -632,6 +632,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
paired: Boolean(relayUrl),
|
||||
state: relayState,
|
||||
sharedTabCount: shared.length,
|
||||
relayUrl: relayUrl ?? "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,119 +2,363 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>OpenClaw</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
width: 320px;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
background: #1c1c1e;
|
||||
color: #f2f2f7;
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: "Avenir Next", Avenir, "Segoe UI", sans-serif;
|
||||
--ink: #f4efe8;
|
||||
--muted: #9f978d;
|
||||
--line: #34302c;
|
||||
--panel: #1d1b19;
|
||||
--panel-raised: #25221f;
|
||||
--orange: #ff6437;
|
||||
--orange-soft: #3a2119;
|
||||
--green: #54d18b;
|
||||
--red: #ff6b64;
|
||||
--mono: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||
}
|
||||
h1 {
|
||||
font-size: 15px;
|
||||
margin: 0 0 10px;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 336px;
|
||||
margin: 0;
|
||||
background: #141312;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
button,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 11px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #181614;
|
||||
}
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #6b7280;
|
||||
|
||||
.mark {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #75402f;
|
||||
border-radius: 10px 4px 10px 4px;
|
||||
background: var(--orange-soft);
|
||||
color: #ffad8f;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
.dot.on {
|
||||
background: #34c759;
|
||||
|
||||
.identity {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.dot.connecting {
|
||||
background: #f59e0b;
|
||||
|
||||
.eyebrow {
|
||||
color: var(--orange);
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
.dot.error {
|
||||
background: #ff3b30;
|
||||
}
|
||||
p {
|
||||
|
||||
#statusLine {
|
||||
overflow: hidden;
|
||||
margin-top: 3px;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
color: #98989f;
|
||||
margin: 6px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: #706960;
|
||||
box-shadow: 0 0 0 3px #27231f;
|
||||
}
|
||||
|
||||
.status-dot.on {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 3px #183226;
|
||||
}
|
||||
|
||||
.status-dot.connecting {
|
||||
background: #f5b942;
|
||||
box-shadow: 0 0 0 3px #38301b;
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: var(--red);
|
||||
box-shadow: 0 0 0 3px #381d1b;
|
||||
}
|
||||
|
||||
.gear {
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gear:hover {
|
||||
border-color: var(--line);
|
||||
background: var(--panel-raised);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.gear.active {
|
||||
border-color: #75402f;
|
||||
background: var(--orange-soft);
|
||||
color: #ffad8f;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.rule::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 3px;
|
||||
margin-bottom: 14px;
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.lede code {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: #0f0e0d;
|
||||
color: #c8c0b7;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #2c2c2e;
|
||||
color: #f2f2f7;
|
||||
border: 1px solid #3a3a3c;
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
min-height: 52px;
|
||||
min-height: 56px;
|
||||
padding: 8px 10px;
|
||||
resize: vertical;
|
||||
border: 1px solid #3b3631;
|
||||
border-radius: 5px 13px 13px 13px;
|
||||
background: var(--panel-raised);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
#pageNote {
|
||||
font-family: system-ui, sans-serif;
|
||||
|
||||
textarea:focus {
|
||||
outline: 0;
|
||||
border-color: #86503b;
|
||||
box-shadow: 0 0 0 2px var(--orange-soft);
|
||||
}
|
||||
button {
|
||||
margin-top: 8px;
|
||||
|
||||
textarea::placeholder {
|
||||
color: #797168;
|
||||
}
|
||||
|
||||
#pairingString {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 13px;
|
||||
margin-top: 9px;
|
||||
padding: 9px 13px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
background: var(--panel-raised);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: #ff5a36;
|
||||
color: white;
|
||||
}
|
||||
button.secondary {
|
||||
background: #3a3a3c;
|
||||
color: #f2f2f7;
|
||||
|
||||
.action.primary {
|
||||
border-color: #ff835f;
|
||||
background: var(--orange);
|
||||
color: #1a0d09;
|
||||
font-weight: 750;
|
||||
}
|
||||
button:disabled {
|
||||
|
||||
.action.danger {
|
||||
border-color: #5c2a26;
|
||||
background: #2a1715;
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.action:not(:disabled):hover {
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.action:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
#error {
|
||||
color: #ff453a;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.send-page {
|
||||
border-top: 1px solid #3a3a3c;
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.error {
|
||||
color: #ff453a;
|
||||
|
||||
.send-page .eyebrow {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-note {
|
||||
margin: 8px 2px 0;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.status-note.error,
|
||||
#error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
#error {
|
||||
margin: 8px 2px 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 9px 2px;
|
||||
border-bottom: 1px solid #292622;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.settings-row dt {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.settings-row dd {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><span id="statusDot" class="dot"></span>OpenClaw</h1>
|
||||
<div id="pairSection">
|
||||
<p>
|
||||
Run <code>openclaw browser extension pair</code> on this machine and paste the pairing
|
||||
string:
|
||||
</p>
|
||||
<textarea id="pairingString" placeholder="ws://127.0.0.1:18797/extension#…"></textarea>
|
||||
<p id="error" class="hidden"></p>
|
||||
<button id="pairButton">Pair</button>
|
||||
</div>
|
||||
<div id="connectedSection" class="hidden">
|
||||
<p id="statusLine"></p>
|
||||
<button id="copilotButton" disabled>Open tab copilot</button>
|
||||
<button id="shareButton"></button>
|
||||
<div class="send-page">
|
||||
<textarea
|
||||
id="pageNote"
|
||||
rows="2"
|
||||
maxlength="2000"
|
||||
placeholder="Optional note for the agent…"
|
||||
></textarea>
|
||||
<button id="sendPageButton" disabled>Send page to OpenClaw</button>
|
||||
<p id="pageShareStatus" class="hidden"></p>
|
||||
<header class="topbar">
|
||||
<div class="mark" aria-hidden="true">OC</div>
|
||||
<div class="identity">
|
||||
<div class="eyebrow">OPENCLAW</div>
|
||||
<div id="statusLine">Checking status…</div>
|
||||
</div>
|
||||
<button id="unpairButton" class="secondary">Unpair</button>
|
||||
</div>
|
||||
<span id="statusDot" class="status-dot"></span>
|
||||
<button id="settingsButton" class="gear" type="button" aria-label="Settings" title="Settings">
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<main>
|
||||
<section id="pairSection" class="rule hidden">
|
||||
<p class="lede">
|
||||
Run <code>openclaw browser extension pair</code> on this machine, then paste the pairing
|
||||
string:
|
||||
</p>
|
||||
<textarea id="pairingString" placeholder="ws://127.0.0.1:18797/extension#…"></textarea>
|
||||
<p id="error" class="hidden"></p>
|
||||
<button id="pairButton" class="action primary" type="button">Pair</button>
|
||||
</section>
|
||||
<section id="connectedSection" class="hidden">
|
||||
<p id="statusHint" class="lede hidden" style="margin: 0 2px 4px">
|
||||
Relay unreachable — is the OpenClaw gateway running?
|
||||
</p>
|
||||
<button id="copilotButton" class="action primary" type="button" disabled>
|
||||
Open tab copilot
|
||||
</button>
|
||||
<button id="shareButton" class="action" type="button"></button>
|
||||
<div class="send-page">
|
||||
<div class="eyebrow">SEND THIS PAGE</div>
|
||||
<textarea
|
||||
id="pageNote"
|
||||
rows="2"
|
||||
maxlength="2000"
|
||||
placeholder="Optional note for the agent…"
|
||||
></textarea>
|
||||
<button id="sendPageButton" class="action" type="button" disabled>
|
||||
Send page to OpenClaw
|
||||
</button>
|
||||
<p id="pageShareStatus" class="status-note hidden"></p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="settingsSection" class="rule hidden">
|
||||
<dl>
|
||||
<div class="settings-row">
|
||||
<dt>Version</dt>
|
||||
<dd id="versionValue"></dd>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<dt>Relay</dt>
|
||||
<dd id="relayValue">—</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<button id="unpairButton" class="action danger" type="button">Unpair this browser</button>
|
||||
<p id="unpairNote" class="lede" style="margin: 10px 2px 0">
|
||||
Unpairing disconnects the gateway and forgets the relay token.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
<script src="popup.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -138,7 +138,9 @@ export async function waitForLoadedExtensionId(
|
||||
browserCdp: CDPSession,
|
||||
extensionPath: string,
|
||||
): Promise<string> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user