feat(rebase): fork base on upstream v0.24.0 native architecture
- Rebased onto upstream/main (v0.24.0, full Rust native) - Renamed package to agent-browser-stealth, version 0.24.0-fork.1 - Preserved fork-specific: abs alias, extensions/tab-group-cdp, .husky hooks - Removed upstream-only: docs/, packages/dashboard, examples/, benchmarks/ - Simplified pnpm workspace to root-only - Added [[bin]] section to keep binary name as "agent-browser" Track 1 of native-stealth migration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
82eadcee41
commit
6addc80aa1
@@ -0,0 +1,425 @@
|
||||
(() => {
|
||||
const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST';
|
||||
const RESPONSE_TYPE = 'AB_TAB_GROUP_RESPONSE';
|
||||
|
||||
const CONTENT_EVENT_TYPE = 'AB_CONTENT_EVENT';
|
||||
const CONTENT_EXECUTE_ACTION = 'AB_CONTENT_EXECUTE_ACTION';
|
||||
const CONTENT_GET_DOM_STATE = 'AB_CONTENT_GET_DOM_STATE';
|
||||
const CONTENT_PING = 'AB_CONTENT_PING';
|
||||
const PAGE_BRIDGE_EVENT = 'AB_PAGE_BRIDGE_EVENT';
|
||||
const STORAGE_OPTIONS_KEY = 'abExtensionOptionsV1';
|
||||
|
||||
const mutationState = {
|
||||
total: 0,
|
||||
recent: [],
|
||||
observerReady: false,
|
||||
};
|
||||
|
||||
function pushMutationSummary(entry) {
|
||||
mutationState.total += 1;
|
||||
mutationState.recent.push({
|
||||
...entry,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (mutationState.recent.length > 40) {
|
||||
mutationState.recent.splice(0, mutationState.recent.length - 40);
|
||||
}
|
||||
}
|
||||
|
||||
function serializeValue(value, depth = 0) {
|
||||
if (value === null || typeof value === 'undefined') return value;
|
||||
if (typeof value === 'string') return value.slice(0, 300);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
||||
if (depth > 2) return '[depth-limit]';
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.slice(0, 10).map((item) => serializeValue(item, depth + 1));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const out = {};
|
||||
for (const [key, entry] of Object.entries(value).slice(0, 15)) {
|
||||
out[key] = serializeValue(entry, depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return String(value).slice(0, 300);
|
||||
}
|
||||
|
||||
function sendRuntimeEvent(kind, payload) {
|
||||
try {
|
||||
chrome.runtime.sendMessage({
|
||||
type: CONTENT_EVENT_TYPE,
|
||||
kind,
|
||||
payload: serializeValue(payload),
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch {
|
||||
// Ignore runtime channel errors.
|
||||
}
|
||||
}
|
||||
|
||||
function getPageBridgeEnabled() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
chrome.storage.local.get([STORAGE_OPTIONS_KEY], (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const rawOptions = result?.[STORAGE_OPTIONS_KEY];
|
||||
resolve(Boolean(rawOptions && typeof rawOptions === 'object' && rawOptions.pageBridgeEnabled === true));
|
||||
});
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function installPageBridge() {
|
||||
// Receives events emitted by the injected page-world hook script.
|
||||
const bridgeListener = (event) => {
|
||||
if (event.source !== window) return;
|
||||
const data = event.data;
|
||||
if (!data || data.type !== PAGE_BRIDGE_EVENT) return;
|
||||
sendRuntimeEvent(data.kind || 'page-event', data.payload || {});
|
||||
};
|
||||
|
||||
window.addEventListener('message', bridgeListener);
|
||||
|
||||
const parent = document.documentElement || document.head || document.body;
|
||||
if (!parent) return;
|
||||
|
||||
if (!(await getPageBridgeEnabled())) {
|
||||
sendRuntimeEvent('lifecycle', {
|
||||
event: 'bridge-disabled-default',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Use external extension script instead of inline text to reduce CSP conflicts.
|
||||
const script = document.createElement('script');
|
||||
script.src = chrome.runtime.getURL('page-bridge.js');
|
||||
script.async = false;
|
||||
script.dataset.abBridgeEvent = PAGE_BRIDGE_EVENT;
|
||||
script.onload = () => script.remove();
|
||||
script.onerror = () => {
|
||||
sendRuntimeEvent('lifecycle', {
|
||||
event: 'bridge-load-failed',
|
||||
host: window.location.hostname,
|
||||
});
|
||||
script.remove();
|
||||
};
|
||||
parent.appendChild(script);
|
||||
}
|
||||
|
||||
function ensureMutationObserver() {
|
||||
if (mutationState.observerReady) return;
|
||||
if (!document.documentElement) return;
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
const summary = {
|
||||
records: records.length,
|
||||
addedNodes: 0,
|
||||
removedNodes: 0,
|
||||
};
|
||||
|
||||
for (const record of records.slice(0, 40)) {
|
||||
summary.addedNodes += record.addedNodes?.length || 0;
|
||||
summary.removedNodes += record.removedNodes?.length || 0;
|
||||
}
|
||||
|
||||
pushMutationSummary(summary);
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'style', 'hidden', 'disabled', 'aria-hidden'],
|
||||
});
|
||||
|
||||
mutationState.observerReady = true;
|
||||
}
|
||||
|
||||
function toSimpleNode(element) {
|
||||
if (!element || typeof element !== 'object') return null;
|
||||
const node = {
|
||||
tag: element.tagName?.toLowerCase() || 'unknown',
|
||||
id: element.id || undefined,
|
||||
className: typeof element.className === 'string' ? element.className.slice(0, 120) : '',
|
||||
role: element.getAttribute?.('role') || undefined,
|
||||
name:
|
||||
element.getAttribute?.('aria-label') ||
|
||||
element.getAttribute?.('name') ||
|
||||
element.getAttribute?.('placeholder') ||
|
||||
'',
|
||||
text: (element.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 160),
|
||||
disabled: element.disabled === true,
|
||||
hidden: element.hidden === true,
|
||||
};
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function collectInteractiveElements(root, limit = 80) {
|
||||
const selector = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'input',
|
||||
'select',
|
||||
'textarea',
|
||||
'summary',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[tabindex]'
|
||||
].join(',');
|
||||
|
||||
const out = [];
|
||||
const nodes = root.querySelectorAll(selector);
|
||||
for (const element of nodes) {
|
||||
if (out.length >= limit) break;
|
||||
out.push(toSimpleNode(element));
|
||||
}
|
||||
return out.filter(Boolean);
|
||||
}
|
||||
|
||||
function collectDomState(options = {}) {
|
||||
const selector = typeof options.selector === 'string' ? options.selector.trim() : '';
|
||||
const root = selector ? document.querySelector(selector) : document.body || document.documentElement;
|
||||
|
||||
if (!root) {
|
||||
return {
|
||||
ok: false,
|
||||
error: selector ? `selector-not-found: ${selector}` : 'root-not-found',
|
||||
};
|
||||
}
|
||||
|
||||
const textPreview = (root.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 1000);
|
||||
const interactiveOnly = options.interactiveOnly === true;
|
||||
const interactiveElements = collectInteractiveElements(root, options.maxNodes || 80);
|
||||
|
||||
const dom = {
|
||||
href: window.location.href,
|
||||
title: document.title,
|
||||
readyState: document.readyState,
|
||||
selector: selector || null,
|
||||
rootTag: root.tagName?.toLowerCase() || 'unknown',
|
||||
textPreview,
|
||||
interactiveCount: interactiveElements.length,
|
||||
interactiveElements,
|
||||
mutation: {
|
||||
total: mutationState.total,
|
||||
recent: mutationState.recent.slice(-10),
|
||||
},
|
||||
capturedAt: Date.now(),
|
||||
};
|
||||
|
||||
if (interactiveOnly) {
|
||||
dom.textPreview = '';
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
state: dom,
|
||||
};
|
||||
}
|
||||
|
||||
function queryElement(selector) {
|
||||
if (typeof selector !== 'string' || selector.trim().length === 0) {
|
||||
throw new Error('selector is required');
|
||||
}
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Element not found: ${selector}`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function focusElement(element) {
|
||||
if (typeof element.focus === 'function') {
|
||||
element.focus({ preventScroll: false });
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchInputEvents(element) {
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
async function executeAction(command, args = {}) {
|
||||
switch (command) {
|
||||
case 'click': {
|
||||
const element = queryElement(args.selector);
|
||||
focusElement(element);
|
||||
element.click();
|
||||
return { ok: true, action: command, selector: args.selector };
|
||||
}
|
||||
case 'fill': {
|
||||
const element = queryElement(args.selector);
|
||||
if (!('value' in element)) {
|
||||
throw new Error(`Element is not fillable: ${args.selector}`);
|
||||
}
|
||||
focusElement(element);
|
||||
element.value = typeof args.value === 'string' ? args.value : String(args.value || '');
|
||||
dispatchInputEvents(element);
|
||||
return { ok: true, action: command, selector: args.selector, valueLength: element.value.length };
|
||||
}
|
||||
case 'press': {
|
||||
const key = typeof args.key === 'string' && args.key.trim().length > 0 ? args.key.trim() : 'Enter';
|
||||
let target;
|
||||
if (typeof args.selector === 'string' && args.selector.trim().length > 0) {
|
||||
target = queryElement(args.selector);
|
||||
focusElement(target);
|
||||
} else {
|
||||
target = document.activeElement || document.body;
|
||||
}
|
||||
|
||||
const down = new KeyboardEvent('keydown', { key, bubbles: true });
|
||||
const up = new KeyboardEvent('keyup', { key, bubbles: true });
|
||||
target.dispatchEvent(down);
|
||||
target.dispatchEvent(up);
|
||||
return { ok: true, action: command, key };
|
||||
}
|
||||
case 'eval': {
|
||||
if (typeof args.expression !== 'string' || args.expression.trim().length === 0) {
|
||||
throw new Error('expression is required');
|
||||
}
|
||||
const fn = new Function(`return (${args.expression});`);
|
||||
const result = fn();
|
||||
return { ok: true, action: command, result: serializeValue(result) };
|
||||
}
|
||||
case 'snapshot': {
|
||||
return {
|
||||
ok: true,
|
||||
action: command,
|
||||
...collectDomState({
|
||||
selector: args.selector,
|
||||
interactiveOnly: args.interactiveOnly === true,
|
||||
maxNodes: args.maxNodes,
|
||||
}),
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown content action: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureMutationObserver();
|
||||
installPageBridge();
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
if (!data || data.type !== REQUEST_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = {
|
||||
type: REQUEST_TYPE,
|
||||
nonce: data.nonce,
|
||||
session: data.session,
|
||||
groupTitle: data.groupTitle,
|
||||
pluginId: data.pluginId,
|
||||
allowedDomains: Array.isArray(data.allowedDomains) ? data.allowedDomains : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
chrome.runtime.sendMessage(request, (response) => {
|
||||
const lastError = chrome.runtime.lastError;
|
||||
if (lastError) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: RESPONSE_TYPE,
|
||||
nonce: request.nonce,
|
||||
ok: false,
|
||||
error: lastError.message,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = response && typeof response === 'object' ? response : { ok: false };
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: RESPONSE_TYPE,
|
||||
nonce: request.nonce,
|
||||
ok: payload.ok === true,
|
||||
extensionId:
|
||||
typeof payload.extensionId === 'string' && payload.extensionId.length > 0
|
||||
? payload.extensionId
|
||||
: chrome.runtime.id,
|
||||
groupId: typeof payload.groupId === 'number' ? payload.groupId : undefined,
|
||||
windowId: typeof payload.windowId === 'number' ? payload.windowId : undefined,
|
||||
color: typeof payload.color === 'string' ? payload.color : undefined,
|
||||
collapsed: payload.collapsed === true,
|
||||
policy:
|
||||
payload.policy && typeof payload.policy === 'object'
|
||||
? {
|
||||
enforced: payload.policy.enforced === true,
|
||||
blocked: payload.policy.blocked === true,
|
||||
reason:
|
||||
typeof payload.policy.reason === 'string' ? payload.policy.reason : undefined,
|
||||
}
|
||||
: undefined,
|
||||
riskHints: Array.isArray(payload.riskHints) ? payload.riskHints : undefined,
|
||||
error: typeof payload.error === 'string' ? payload.error : undefined,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
window.postMessage(
|
||||
{
|
||||
type: RESPONSE_TYPE,
|
||||
nonce: request.nonce,
|
||||
ok: false,
|
||||
error: errorMessage,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (!message || typeof message !== 'object') return;
|
||||
|
||||
if (message.type === CONTENT_PING) {
|
||||
sendResponse({
|
||||
ok: true,
|
||||
href: window.location.href,
|
||||
title: document.title,
|
||||
readyState: document.readyState,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === CONTENT_GET_DOM_STATE) {
|
||||
sendResponse(collectDomState(message.options || {}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === CONTENT_EXECUTE_ACTION) {
|
||||
executeAction(message.command, message.args || {})
|
||||
.then((result) => sendResponse(result))
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
action: message.command,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" rx="32" fill="#1A73E8"/>
|
||||
<rect x="30" y="34" width="68" height="10" rx="2" fill="white"/>
|
||||
<rect x="30" y="54" width="48" height="10" rx="2" fill="white" fill-opacity="0.8"/>
|
||||
<rect x="30" y="74" width="28" height="10" rx="2" fill="white" fill-opacity="0.6"/>
|
||||
<circle cx="94" cy="90" r="10" fill="#34A853" stroke="#1A73E8" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.2.0",
|
||||
"description": "Session-aware tab grouping and coordination for CDP-driven agent-browser workflows.",
|
||||
"icons": {
|
||||
"128": "icons/icon.svg"
|
||||
},
|
||||
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel", "alarms"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "service-worker.js"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "agent-browser-stealth"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "sidepanel.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content-script.js"],
|
||||
"run_at": "document_start",
|
||||
"match_about_blank": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["page-bridge.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
(() => {
|
||||
if (window.__AB_STEALTH_BRIDGE_INSTALLED__) return;
|
||||
window.__AB_STEALTH_BRIDGE_INSTALLED__ = true;
|
||||
|
||||
const currentScript = document.currentScript;
|
||||
const TYPE = currentScript?.dataset?.abBridgeEvent || 'AB_PAGE_BRIDGE_EVENT';
|
||||
|
||||
const post = (kind, payload) => {
|
||||
try {
|
||||
window.postMessage({ type: TYPE, kind, payload, timestamp: Date.now() }, '*');
|
||||
} catch {
|
||||
// Ignore post failures.
|
||||
}
|
||||
};
|
||||
|
||||
const serializeArg = (value, depth = 0) => {
|
||||
if (value === null || typeof value === 'undefined') return value;
|
||||
if (typeof value === 'string') return value.slice(0, 250);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
||||
if (depth > 2) return '[depth-limit]';
|
||||
if (Array.isArray(value)) return value.slice(0, 10).map((item) => serializeArg(item, depth + 1));
|
||||
if (typeof value === 'object') {
|
||||
const out = {};
|
||||
const entries = Object.entries(value).slice(0, 12);
|
||||
for (const [k, v] of entries) {
|
||||
out[k] = serializeArg(v, depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return String(value).slice(0, 250);
|
||||
};
|
||||
|
||||
const patchConsoleMethod = (name) => {
|
||||
const original = console[name];
|
||||
if (typeof original !== 'function') return;
|
||||
console[name] = function patchedConsole(...args) {
|
||||
post('console', {
|
||||
level: name,
|
||||
args: args.map((arg) => serializeArg(arg)),
|
||||
});
|
||||
return original.apply(this, args);
|
||||
};
|
||||
};
|
||||
|
||||
patchConsoleMethod('error');
|
||||
patchConsoleMethod('warn');
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
post('console', {
|
||||
level: 'error',
|
||||
message: event.message,
|
||||
source: event.filename,
|
||||
line: event.lineno,
|
||||
column: event.colno,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
post('console', {
|
||||
level: 'error',
|
||||
message: 'Unhandled rejection',
|
||||
reason: serializeArg(event.reason),
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof window.fetch === 'function') {
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = async (...args) => {
|
||||
const startedAt = Date.now();
|
||||
const requestInfo = args[0];
|
||||
const requestInit = args[1] || {};
|
||||
const method = requestInit.method || 'GET';
|
||||
const url = typeof requestInfo === 'string' ? requestInfo : requestInfo?.url || '';
|
||||
|
||||
try {
|
||||
const response = await originalFetch(...args);
|
||||
post('network', {
|
||||
transport: 'fetch',
|
||||
method,
|
||||
url,
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
post('network', {
|
||||
transport: 'fetch',
|
||||
method,
|
||||
url,
|
||||
error: serializeArg(error),
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.XMLHttpRequest === 'function') {
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) {
|
||||
this.__abRequestMeta = {
|
||||
method: typeof method === 'string' ? method : 'GET',
|
||||
url: typeof url === 'string' ? url : String(url || ''),
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
return originalOpen.call(this, method, url, ...rest);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function patchedSend(...args) {
|
||||
this.addEventListener('loadend', () => {
|
||||
const meta = this.__abRequestMeta || {};
|
||||
post('network', {
|
||||
transport: 'xhr',
|
||||
method: meta.method || 'GET',
|
||||
url: meta.url || '',
|
||||
status: this.status,
|
||||
ok: this.status >= 200 && this.status < 400,
|
||||
durationMs: Date.now() - (meta.startedAt || Date.now()),
|
||||
});
|
||||
});
|
||||
return originalSend.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
post('lifecycle', {
|
||||
event: 'bridge-installed',
|
||||
href: location.href,
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,258 @@
|
||||
:root {
|
||||
--bg: #f3f5f7;
|
||||
--surface: #ffffff;
|
||||
--surface-alt: #f6f8fb;
|
||||
--primary: #1769e0;
|
||||
--primary-hover: #0f58c0;
|
||||
--border: #d8dde4;
|
||||
--text-main: #18212f;
|
||||
--text-secondary: #4a5568;
|
||||
--text-muted: #667287;
|
||||
--success: #117a3d;
|
||||
--warning: #b86d00;
|
||||
--danger: #bd1e24;
|
||||
--radius: 10px;
|
||||
--mono: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--surface);
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--primary);
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.status-line {
|
||||
min-height: 18px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-line.ok {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-line.warn {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.status-line.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row + .row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 7px 9px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-alt);
|
||||
color: var(--text-main);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input.mono,
|
||||
textarea.mono,
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
background: #0f172a;
|
||||
color: #dce6fb;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.item-url {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.event-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 1px 7px;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
background: #f0f4fa;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 14px 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>agent-browser-stealth panel</title>
|
||||
<link rel="stylesheet" href="sidepanel.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>agent-browser-stealth</h1>
|
||||
<div class="actions">
|
||||
<button id="refresh-btn" type="button">Refresh</button>
|
||||
<button id="cleanup-btn" type="button">Clean Empty Groups</button>
|
||||
</div>
|
||||
<div id="status-line" class="status-line"></div>
|
||||
</header>
|
||||
|
||||
<section id="control" class="card"></section>
|
||||
<section id="summary" class="card"></section>
|
||||
<section id="automation" class="card"></section>
|
||||
<section id="developer" class="card"></section>
|
||||
<section id="sessions" class="stack"></section>
|
||||
<section id="downloads" class="card"></section>
|
||||
|
||||
<script src="sidepanel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user