feat: polish chrome extension UX
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
const DEFAULT_PORT = 18792
|
const DEFAULT_PORT = 18792
|
||||||
|
|
||||||
const BADGE = {
|
const BADGE = {
|
||||||
on: { text: 'ON', color: '#0B6E4F' },
|
on: { text: 'ON', color: '#FF5A36' },
|
||||||
off: { text: '', color: '#000000' },
|
off: { text: '', color: '#000000' },
|
||||||
connecting: { text: '…', color: '#B45309' },
|
connecting: { text: '…', color: '#F59E0B' },
|
||||||
error: { text: '!', color: '#B91C1C' },
|
error: { text: '!', color: '#B91C1C' },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ function setBadge(tabId, kind) {
|
|||||||
const cfg = BADGE[kind]
|
const cfg = BADGE[kind]
|
||||||
void chrome.action.setBadgeText({ tabId, text: cfg.text })
|
void chrome.action.setBadgeText({ tabId, text: cfg.text })
|
||||||
void chrome.action.setBadgeBackgroundColor({ tabId, color: cfg.color })
|
void chrome.action.setBadgeBackgroundColor({ tabId, color: cfg.color })
|
||||||
|
void chrome.action.setBadgeTextColor({ tabId, color: '#FFFFFF' }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureRelayConnection() {
|
async function ensureRelayConnection() {
|
||||||
@@ -111,6 +112,10 @@ function onRelayClosed(reason) {
|
|||||||
for (const tabId of tabs.keys()) {
|
for (const tabId of tabs.keys()) {
|
||||||
void chrome.debugger.detach({ tabId }).catch(() => {})
|
void chrome.debugger.detach({ tabId }).catch(() => {})
|
||||||
setBadge(tabId, 'connecting')
|
setBadge(tabId, 'connecting')
|
||||||
|
void chrome.action.setTitle({
|
||||||
|
tabId,
|
||||||
|
title: 'Clawdbot Browser Relay: disconnected (click to re-attach)',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
tabs.clear()
|
tabs.clear()
|
||||||
tabBySession.clear()
|
tabBySession.clear()
|
||||||
@@ -125,6 +130,17 @@ function sendToRelay(payload) {
|
|||||||
ws.send(JSON.stringify(payload))
|
ws.send(JSON.stringify(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function maybeOpenHelpOnce() {
|
||||||
|
try {
|
||||||
|
const stored = await chrome.storage.local.get(['helpOnErrorShown'])
|
||||||
|
if (stored.helpOnErrorShown === true) return
|
||||||
|
await chrome.storage.local.set({ helpOnErrorShown: true })
|
||||||
|
await chrome.runtime.openOptionsPage()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function requestFromRelay(command) {
|
function requestFromRelay(command) {
|
||||||
const id = command.id
|
const id = command.id
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -207,6 +223,10 @@ async function attachTab(tabId, opts = {}) {
|
|||||||
|
|
||||||
tabs.set(tabId, { state: 'connected', sessionId, targetId, attachOrder })
|
tabs.set(tabId, { state: 'connected', sessionId, targetId, attachOrder })
|
||||||
tabBySession.set(sessionId, tabId)
|
tabBySession.set(sessionId, tabId)
|
||||||
|
void chrome.action.setTitle({
|
||||||
|
tabId,
|
||||||
|
title: 'Clawdbot Browser Relay: attached (click to detach)',
|
||||||
|
})
|
||||||
|
|
||||||
if (!opts.skipAttachedEvent) {
|
if (!opts.skipAttachedEvent) {
|
||||||
sendToRelay({
|
sendToRelay({
|
||||||
@@ -256,6 +276,10 @@ async function detachTab(tabId, reason) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBadge(tabId, 'off')
|
setBadge(tabId, 'off')
|
||||||
|
void chrome.action.setTitle({
|
||||||
|
tabId,
|
||||||
|
title: 'Clawdbot Browser Relay (click to attach/detach)',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectOrToggleForActiveTab() {
|
async function connectOrToggleForActiveTab() {
|
||||||
@@ -271,6 +295,10 @@ async function connectOrToggleForActiveTab() {
|
|||||||
|
|
||||||
tabs.set(tabId, { state: 'connecting' })
|
tabs.set(tabId, { state: 'connecting' })
|
||||||
setBadge(tabId, 'connecting')
|
setBadge(tabId, 'connecting')
|
||||||
|
void chrome.action.setTitle({
|
||||||
|
tabId,
|
||||||
|
title: 'Clawdbot Browser Relay: connecting to local relay…',
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureRelayConnection()
|
await ensureRelayConnection()
|
||||||
@@ -278,10 +306,13 @@ async function connectOrToggleForActiveTab() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
tabs.delete(tabId)
|
tabs.delete(tabId)
|
||||||
setBadge(tabId, 'error')
|
setBadge(tabId, 'error')
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
void chrome.action.setTitle({
|
||||||
// Service worker: best-effort surface via title.
|
tabId,
|
||||||
void chrome.action.setTitle({ tabId, title: `Clawdbot: ${message}` })
|
title: 'Clawdbot Browser Relay: relay not running (open options for setup)',
|
||||||
|
})
|
||||||
|
void maybeOpenHelpOnce()
|
||||||
// Extra breadcrumbs in chrome://extensions service worker logs.
|
// Extra breadcrumbs in chrome://extensions service worker logs.
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
console.warn('attach failed', message, nowStack())
|
console.warn('attach failed', message, nowStack())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/chrome-extension/icons/icon128.png
Normal file
BIN
assets/chrome-extension/icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 B |
BIN
assets/chrome-extension/icons/icon16.png
Normal file
BIN
assets/chrome-extension/icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 B |
BIN
assets/chrome-extension/icons/icon32.png
Normal file
BIN
assets/chrome-extension/icons/icon32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
BIN
assets/chrome-extension/icons/icon48.png
Normal file
BIN
assets/chrome-extension/icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 370 B |
@@ -3,10 +3,23 @@
|
|||||||
"name": "Clawdbot Browser Relay",
|
"name": "Clawdbot Browser Relay",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Attach Clawdbot to your existing Chrome tab via a local CDP relay server.",
|
"description": "Attach Clawdbot to your existing Chrome tab via a local CDP relay server.",
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
},
|
||||||
"permissions": ["debugger", "tabs", "activeTab", "storage"],
|
"permissions": ["debugger", "tabs", "activeTab", "storage"],
|
||||||
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
||||||
"background": { "service_worker": "background.js", "type": "module" },
|
"background": { "service_worker": "background.js", "type": "module" },
|
||||||
"action": { "default_title": "Attach Clawdbot" },
|
"action": {
|
||||||
|
"default_title": "Clawdbot Browser Relay (click to attach/detach)",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
"options_ui": { "page": "options.html", "open_in_tab": true }
|
"options_ui": { "page": "options.html", "open_in_tab": true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,69 +7,185 @@
|
|||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
font-family: ui-monospace, Menlo, Monaco, Consolas, "SF Mono", monospace;
|
--accent: #ff5a36;
|
||||||
|
--panel: color-mix(in oklab, canvas 92%, canvasText 8%);
|
||||||
|
--border: color-mix(in oklab, canvasText 18%, transparent);
|
||||||
|
--muted: color-mix(in oklab, canvasText 70%, transparent);
|
||||||
|
--shadow: 0 10px 30px color-mix(in oklab, canvasText 18%, transparent);
|
||||||
|
font-family: ui-rounded, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Rounded",
|
||||||
|
"SF Pro Display", "Segoe UI", sans-serif;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 24px;
|
margin: 0;
|
||||||
max-width: 720px;
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(1000px 500px at 10% 0%, color-mix(in oklab, var(--accent) 30%, transparent), transparent 70%),
|
||||||
|
radial-gradient(900px 450px at 90% 0%, color-mix(in oklab, var(--accent) 18%, transparent), transparent 75%),
|
||||||
|
canvas;
|
||||||
|
color: canvasText;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 820px;
|
||||||
|
margin: 36px auto;
|
||||||
|
padding: 0 24px 48px 24px;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: color-mix(in oklab, var(--accent) 18%, transparent);
|
||||||
|
border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.logo img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
margin: 0 0 16px 0;
|
margin: 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
label {
|
.subtitle {
|
||||||
display: block;
|
margin: 2px 0 0 0;
|
||||||
font-size: 12px;
|
color: var(--muted);
|
||||||
opacity: 0.9;
|
font-size: 13px;
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
input {
|
.grid {
|
||||||
width: 140px;
|
display: grid;
|
||||||
padding: 8px 10px;
|
grid-template-columns: 1fr;
|
||||||
border-radius: 10px;
|
gap: 14px;
|
||||||
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
|
|
||||||
background: color-mix(in oklab, currentColor 3%, transparent);
|
|
||||||
}
|
}
|
||||||
button {
|
.card {
|
||||||
margin-left: 8px;
|
background: var(--panel);
|
||||||
padding: 8px 12px;
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
|
padding: 16px;
|
||||||
background: color-mix(in oklab, currentColor 10%, transparent);
|
box-shadow: var(--shadow);
|
||||||
cursor: pointer;
|
}
|
||||||
|
.card h2 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 160px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: color-mix(in oklab, canvas 92%, canvasText 8%);
|
||||||
|
color: canvasText;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
border-color: color-mix(in oklab, var(--accent) 70%, transparent);
|
||||||
|
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 20%, transparent);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--accent) 55%, transparent);
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in oklab, var(--accent) 80%, white 20%),
|
||||||
|
var(--accent)
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
.hint {
|
.hint {
|
||||||
margin-top: 12px;
|
margin-top: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
opacity: 0.8;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-family: inherit;
|
font-family: ui-monospace, Menlo, Monaco, Consolas, "SF Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: color-mix(in oklab, var(--accent) 85%, canvasText 15%);
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: color-mix(in oklab, var(--accent) 70%, canvasText 30%);
|
||||||
|
min-height: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Clawdbot Browser Relay</h1>
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<div class="logo" aria-hidden="true">
|
||||||
|
<img src="icons/icon128.png" alt="" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>Clawdbot Browser Relay</h1>
|
||||||
|
<p class="subtitle">Click the toolbar button on a tab to attach / detach.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div>
|
<div class="grid">
|
||||||
<label for="port">Relay port</label>
|
<div class="card">
|
||||||
<div class="row">
|
<h2>Getting started</h2>
|
||||||
<input id="port" inputmode="numeric" pattern="[0-9]*" />
|
<p>
|
||||||
<button id="save">Save</button>
|
If you see a red <code>!</code> badge on the extension icon, the relay server is not reachable.
|
||||||
|
Start Clawdbot’s browser relay on this machine (Gateway or <code>clawdbot browser serve</code>),
|
||||||
|
then click the toolbar button again.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Full guide (install, remote Gateway, security): <a href="https://docs.clawd.bot/tools/chrome-extension" target="_blank" rel="noreferrer">docs.clawd.bot/tools/chrome-extension</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Relay port</h2>
|
||||||
|
<label for="port">Port</label>
|
||||||
|
<div class="row">
|
||||||
|
<input id="port" inputmode="numeric" pattern="[0-9]*" />
|
||||||
|
<button id="save" type="button">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">
|
||||||
|
Default: <code>18792</code>. Extension connects to: <code id="relay-url">http://127.0.0.1:<port>/</code>.
|
||||||
|
Only change this if your Clawdbot profile uses a different <code>cdpUrl</code> port.
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint">
|
|
||||||
Default: <code>18792</code>. Relay base URL: <code>http://127.0.0.1:<port>/</code>.
|
<script type="module" src="options.js"></script>
|
||||||
</div>
|
|
||||||
<div class="hint" id="status"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="options.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,17 @@ function clampPort(value) {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateRelayUrl(port) {
|
||||||
|
const el = document.getElementById('relay-url')
|
||||||
|
if (!el) return
|
||||||
|
el.textContent = `http://127.0.0.1:${port}/`
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const stored = await chrome.storage.local.get(['relayPort'])
|
const stored = await chrome.storage.local.get(['relayPort'])
|
||||||
const port = clampPort(stored.relayPort)
|
const port = clampPort(stored.relayPort)
|
||||||
document.getElementById('port').value = String(port)
|
document.getElementById('port').value = String(port)
|
||||||
|
updateRelayUrl(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -18,6 +25,7 @@ async function save() {
|
|||||||
const port = clampPort(input.value)
|
const port = clampPort(input.value)
|
||||||
await chrome.storage.local.set({ relayPort: port })
|
await chrome.storage.local.set({ relayPort: port })
|
||||||
input.value = String(port)
|
input.value = String(port)
|
||||||
|
updateRelayUrl(port)
|
||||||
const status = document.getElementById('status')
|
const status = document.getElementById('status')
|
||||||
status.textContent = `Saved. Using http://127.0.0.1:${port}/`
|
status.textContent = `Saved. Using http://127.0.0.1:${port}/`
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -27,4 +35,3 @@ async function save() {
|
|||||||
|
|
||||||
document.getElementById('save').addEventListener('click', () => void save())
|
document.getElementById('save').addEventListener('click', () => void save())
|
||||||
void load()
|
void load()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user