feat: complete agent-browser-stealth extension controls
This commit is contained in:
@@ -74,11 +74,12 @@ Install once in Chrome: load unpacked extension from `extensions/tab-group-cdp/`
|
|||||||
### Extension Capabilities (`agent-browser-stealth`)
|
### Extension Capabilities (`agent-browser-stealth`)
|
||||||
|
|
||||||
- Session window isolation: tabs are kept in their session window when possible.
|
- Session window isolation: tabs are kept in their session window when possible.
|
||||||
|
- Configurable isolation controls: side panel can toggle `strictWindowIsolation` and cross-window activation guard.
|
||||||
- Session-aware grouping: deterministic group color, default session expanded, non-default sessions collapsed.
|
- Session-aware grouping: deterministic group color, default session expanded, non-default sessions collapsed.
|
||||||
- Download archive routing: downloads from managed tabs are routed to `agent-browser-stealth/<session>/...`.
|
- Download archive routing: downloads from managed tabs are routed to `agent-browser-stealth/<session>/...`.
|
||||||
- Domain allowlist fallback: when allowlist is configured for a session, extension can force-block out-of-policy tabs to `about:blank`.
|
- Domain allowlist fallback: when allowlist is configured for a session, extension can force-block out-of-policy tabs to `about:blank`.
|
||||||
- Risk hints (debug only): suspicious host/TLD hints are returned via handshake and printed only when `AGENT_BROWSER_DEBUG=1`.
|
- Risk hints (debug only): suspicious host/TLD hints are returned via handshake and printed only when `AGENT_BROWSER_DEBUG=1`.
|
||||||
- Side panel console: view session/tab/group mapping, focus a session, keep only one session, clean empty groups, and edit session allowlist.
|
- Side panel console: view session/tab/group mapping, focus a session, keep only one session, clean empty groups, edit session allowlist, and toggle auto-clean.
|
||||||
|
|
||||||
## Stealth Architecture
|
## Stealth Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ CDP mode uses a browser extension handshake to group tabs.
|
|||||||
- Extension side panel (`agent-browser-stealth`) also provides:
|
- Extension side panel (`agent-browser-stealth`) also provides:
|
||||||
- Session window isolation and deterministic group colors.
|
- Session window isolation and deterministic group colors.
|
||||||
- `Keep Only This`, `Focus`, `Clean Empty Groups` quick actions.
|
- `Keep Only This`, `Focus`, `Clean Empty Groups` quick actions.
|
||||||
|
- Toggle switches for strict isolation / activation guard / auto-clean.
|
||||||
- Session allowlist editing (domain fallback to `about:blank` when violated).
|
- Session allowlist editing (domain fallback to `about:blank` when violated).
|
||||||
- Download routing to `agent-browser-stealth/<session>/...`.
|
- Download routing to `agent-browser-stealth/<session>/...`.
|
||||||
- Use `--tab-group` / `AGENT_BROWSER_TAB_GROUP` for base title.
|
- Use `--tab-group` / `AGENT_BROWSER_TAB_GROUP` for base title.
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ For tab grouping in CDP mode, grouping is best-effort through the extension hand
|
|||||||
extension available => grouped by session; extension missing/unavailable => silent no-op.
|
extension available => grouped by session; extension missing/unavailable => silent no-op.
|
||||||
|
|
||||||
With the `agent-browser-stealth` extension installed, the side panel also exposes
|
With the `agent-browser-stealth` extension installed, the side panel also exposes
|
||||||
session window isolation controls, empty-group cleanup, and per-session allowlist policy editing.
|
session window isolation controls, activation guard toggles, empty-group cleanup, and per-session allowlist policy editing.
|
||||||
|
|
||||||
## Common Configurations
|
## Common Configurations
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"name": "agent-browser-stealth",
|
"name": "agent-browser-stealth",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"description": "Session-aware tab grouping and coordination for CDP-driven agent-browser workflows.",
|
"description": "Session-aware tab grouping and coordination for CDP-driven agent-browser workflows.",
|
||||||
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel"],
|
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel", "alarms"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "service-worker.js"
|
"service_worker": "service-worker.js"
|
||||||
|
|||||||
@@ -4,22 +4,32 @@ const PANEL_CLOSE_OTHER_TABS = 'AB_PANEL_CLOSE_OTHER_SESSION_TABS';
|
|||||||
const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION';
|
const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION';
|
||||||
const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS';
|
const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS';
|
||||||
const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY';
|
const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY';
|
||||||
|
const PANEL_SET_OPTIONS = 'AB_PANEL_SET_OPTIONS';
|
||||||
|
|
||||||
const DEFAULT_GROUP_TITLE = 'Agent Browser Stealth';
|
const DEFAULT_GROUP_TITLE = 'Agent Browser Stealth';
|
||||||
const DOWNLOAD_ARCHIVE_ROOT = 'agent-browser-stealth';
|
const DOWNLOAD_ARCHIVE_ROOT = 'agent-browser-stealth';
|
||||||
const STORAGE_POLICY_KEY = 'abSessionPoliciesV1';
|
const STORAGE_POLICY_KEY = 'abSessionPoliciesV1';
|
||||||
|
const STORAGE_OPTIONS_KEY = 'abExtensionOptionsV1';
|
||||||
|
const CLEANUP_ALARM_NAME = 'ab-clean-empty-groups';
|
||||||
const GROUP_COLORS = ['blue', 'green', 'pink', 'orange', 'purple', 'cyan', 'red', 'yellow'];
|
const GROUP_COLORS = ['blue', 'green', 'pink', 'orange', 'purple', 'cyan', 'red', 'yellow'];
|
||||||
const RISKY_TLDS = new Set(['zip', 'mov', 'click', 'top', 'gq', 'tk', 'country']);
|
const RISKY_TLDS = new Set(['zip', 'mov', 'click', 'top', 'gq', 'tk', 'country']);
|
||||||
const RISKY_HOST_KEYWORDS = ['secure-login', 'account-verify', 'wallet-verify', 'airdrop-claim'];
|
const RISKY_HOST_KEYWORDS = ['secure-login', 'account-verify', 'wallet-verify', 'airdrop-claim'];
|
||||||
|
|
||||||
|
const DEFAULT_EXTENSION_OPTIONS = {
|
||||||
|
strictWindowIsolation: true,
|
||||||
|
suppressCrossWindowActivation: true,
|
||||||
|
autoCleanEmptyGroups: true,
|
||||||
|
};
|
||||||
|
|
||||||
const sessionGroupCache = new Map();
|
const sessionGroupCache = new Map();
|
||||||
const sessionWindowMap = new Map();
|
const sessionWindowMap = new Map();
|
||||||
const tabSessionMap = new Map();
|
const tabSessionMap = new Map();
|
||||||
const tabMetaById = new Map();
|
const tabMetaById = new Map();
|
||||||
const downloadEvents = [];
|
const downloadEvents = [];
|
||||||
const sessionPolicies = new Map();
|
const sessionPolicies = new Map();
|
||||||
|
let extensionOptions = { ...DEFAULT_EXTENSION_OPTIONS };
|
||||||
|
|
||||||
let policyLoadPromise = loadPolicies();
|
let bootstrapPromise = bootstrapState();
|
||||||
|
|
||||||
function normalizeSession(session) {
|
function normalizeSession(session) {
|
||||||
if (typeof session !== 'string') return 'default';
|
if (typeof session !== 'string') return 'default';
|
||||||
@@ -141,6 +151,46 @@ async function loadPolicies() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOptions(raw) {
|
||||||
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
return { ...DEFAULT_EXTENSION_OPTIONS };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
strictWindowIsolation: raw.strictWindowIsolation !== false,
|
||||||
|
suppressCrossWindowActivation: raw.suppressCrossWindowActivation !== false,
|
||||||
|
autoCleanEmptyGroups: raw.autoCleanEmptyGroups !== false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOptions() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get([STORAGE_OPTIONS_KEY]);
|
||||||
|
extensionOptions = normalizeOptions(result?.[STORAGE_OPTIONS_KEY]);
|
||||||
|
} catch {
|
||||||
|
extensionOptions = { ...DEFAULT_EXTENSION_OPTIONS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistOptions() {
|
||||||
|
await chrome.storage.local.set({ [STORAGE_OPTIONS_KEY]: extensionOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setExtensionOptions(nextOptions) {
|
||||||
|
extensionOptions = {
|
||||||
|
...extensionOptions,
|
||||||
|
...normalizeOptions(nextOptions),
|
||||||
|
};
|
||||||
|
await persistOptions();
|
||||||
|
await syncCleanupAlarm();
|
||||||
|
return extensionOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrapState() {
|
||||||
|
await loadPolicies();
|
||||||
|
await loadOptions();
|
||||||
|
await syncCleanupAlarm();
|
||||||
|
}
|
||||||
|
|
||||||
async function persistPolicies() {
|
async function persistPolicies() {
|
||||||
const serialized = {};
|
const serialized = {};
|
||||||
for (const [session, domains] of sessionPolicies.entries()) {
|
for (const [session, domains] of sessionPolicies.entries()) {
|
||||||
@@ -200,6 +250,11 @@ function removeWindowCaches(windowId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSessionWindow(tabId, currentWindowId, session) {
|
async function ensureSessionWindow(tabId, currentWindowId, session) {
|
||||||
|
if (!extensionOptions.strictWindowIsolation) {
|
||||||
|
sessionWindowMap.set(session, currentWindowId);
|
||||||
|
return currentWindowId;
|
||||||
|
}
|
||||||
|
|
||||||
let targetWindowId = sessionWindowMap.get(session);
|
let targetWindowId = sessionWindowMap.get(session);
|
||||||
|
|
||||||
if (typeof targetWindowId === 'number') {
|
if (typeof targetWindowId === 'number') {
|
||||||
@@ -420,6 +475,75 @@ async function cleanEmptyGroups() {
|
|||||||
return { removedGroups, removedWindows };
|
return { removedGroups, removedWindows };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncCleanupAlarm() {
|
||||||
|
try {
|
||||||
|
await chrome.alarms.clear(CLEANUP_ALARM_NAME);
|
||||||
|
if (extensionOptions.autoCleanEmptyGroups) {
|
||||||
|
await chrome.alarms.create(CLEANUP_ALARM_NAME, { periodInMinutes: 1 });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore alarms API failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enforceSessionWindowAffinity(tabId) {
|
||||||
|
if (!extensionOptions.suppressCrossWindowActivation) return { moved: false };
|
||||||
|
|
||||||
|
const session = getManagedSessionForTab(tabId);
|
||||||
|
if (!session) return { moved: false };
|
||||||
|
if (!extensionOptions.strictWindowIsolation) return { moved: false };
|
||||||
|
|
||||||
|
let tab;
|
||||||
|
try {
|
||||||
|
tab = await chrome.tabs.get(tabId);
|
||||||
|
} catch {
|
||||||
|
return { moved: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedWindowId = sessionWindowMap.get(session);
|
||||||
|
if (typeof mappedWindowId !== 'number' || mappedWindowId === tab.windowId) {
|
||||||
|
if (typeof tab.windowId === 'number') {
|
||||||
|
sessionWindowMap.set(session, tab.windowId);
|
||||||
|
}
|
||||||
|
return { moved: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.tabs.move(tabId, { windowId: mappedWindowId, index: -1 });
|
||||||
|
await chrome.tabs.update(tabId, { active: false }).catch(() => {});
|
||||||
|
return { moved: true, toWindowId: mappedWindowId };
|
||||||
|
} catch {
|
||||||
|
return { moved: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRiskBadge(tabId) {
|
||||||
|
let text = '';
|
||||||
|
let title = 'agent-browser-stealth';
|
||||||
|
|
||||||
|
const session = getManagedSessionForTab(tabId);
|
||||||
|
if (session) {
|
||||||
|
let tab;
|
||||||
|
try {
|
||||||
|
tab = await chrome.tabs.get(tabId);
|
||||||
|
} catch {
|
||||||
|
tab = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab) {
|
||||||
|
const hints = collectRiskHints(tab.url, getSessionPolicy(session));
|
||||||
|
if (hints.length > 0) {
|
||||||
|
text = '!';
|
||||||
|
title = `Risk hints (${hints.length}): ${hints.join(', ')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.action.setBadgeText({ text }).catch(() => {});
|
||||||
|
await chrome.action.setBadgeBackgroundColor({ color: '#dc2626' }).catch(() => {});
|
||||||
|
await chrome.action.setTitle({ title }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
async function buildPanelState() {
|
async function buildPanelState() {
|
||||||
const allTabs = await chrome.tabs.query({});
|
const allTabs = await chrome.tabs.query({});
|
||||||
for (const tab of allTabs) {
|
for (const tab of allTabs) {
|
||||||
@@ -492,6 +616,7 @@ async function buildPanelState() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
extensionId: chrome.runtime.id,
|
extensionId: chrome.runtime.id,
|
||||||
|
options: { ...extensionOptions },
|
||||||
totals: {
|
totals: {
|
||||||
sessions: sessions.length,
|
sessions: sessions.length,
|
||||||
tabs: sessions.reduce((sum, session) => sum + session.tabs.length, 0),
|
tabs: sessions.reduce((sum, session) => sum + session.tabs.length, 0),
|
||||||
@@ -502,7 +627,7 @@ async function buildPanelState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleTabGroupRequest(message, sender) {
|
async function handleTabGroupRequest(message, sender) {
|
||||||
await policyLoadPromise;
|
await bootstrapPromise;
|
||||||
|
|
||||||
const tabId = sender.tab?.id;
|
const tabId = sender.tab?.id;
|
||||||
const windowId = sender.tab?.windowId;
|
const windowId = sender.tab?.windowId;
|
||||||
@@ -555,6 +680,11 @@ async function handleTabGroupRequest(message, sender) {
|
|||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
||||||
|
bootstrapPromise = bootstrapState();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
|
bootstrapPromise = bootstrapState();
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
@@ -580,7 +710,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === PANEL_GET_STATE) {
|
if (type === PANEL_GET_STATE) {
|
||||||
buildPanelState()
|
bootstrapPromise
|
||||||
|
.then(() => buildPanelState())
|
||||||
.then((state) => sendResponse({ ok: true, state }))
|
.then((state) => sendResponse({ ok: true, state }))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
@@ -620,7 +751,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === PANEL_SET_POLICY) {
|
if (type === PANEL_SET_POLICY) {
|
||||||
setSessionPolicy(message.session, message.allowedDomains)
|
bootstrapPromise
|
||||||
|
.then(() => setSessionPolicy(message.session, message.allowedDomains))
|
||||||
.then(() => sendResponse({ ok: true }))
|
.then(() => sendResponse({ ok: true }))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
@@ -628,12 +760,28 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === PANEL_SET_OPTIONS) {
|
||||||
|
bootstrapPromise
|
||||||
|
.then(() => setExtensionOptions(message.options))
|
||||||
|
.then((options) => sendResponse({ ok: true, options }))
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
sendResponse({ ok: false, error: errorMessage });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
updateTabMeta(tab);
|
updateTabMeta(tab);
|
||||||
const session = getManagedSessionForTab(tabId);
|
const session = getManagedSessionForTab(tabId);
|
||||||
if (!session) return;
|
if (!session) {
|
||||||
|
if (changeInfo.status === 'complete' && tab.active === true) {
|
||||||
|
updateRiskBadge(tabId).catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof tab.windowId === 'number') {
|
if (typeof tab.windowId === 'number') {
|
||||||
sessionWindowMap.set(session, tab.windowId);
|
sessionWindowMap.set(session, tab.windowId);
|
||||||
@@ -641,7 +789,15 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|||||||
|
|
||||||
if (changeInfo.status === 'complete') {
|
if (changeInfo.status === 'complete') {
|
||||||
applySessionDomainFallback(tabId, session).catch(() => {});
|
applySessionDomainFallback(tabId, session).catch(() => {});
|
||||||
|
if (tab.active === true) {
|
||||||
|
updateRiskBadge(tabId).catch(() => {});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||||
|
enforceSessionWindowAffinity(activeInfo.tabId).catch(() => {});
|
||||||
|
updateRiskBadge(activeInfo.tabId).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
||||||
@@ -659,6 +815,7 @@ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
|||||||
if (remaining.length === 0) {
|
if (remaining.length === 0) {
|
||||||
sessionWindowMap.delete(session);
|
sessionWindowMap.delete(session);
|
||||||
}
|
}
|
||||||
|
cleanEmptyGroups().catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.tabs.onDetached.addListener((tabId) => {
|
chrome.tabs.onDetached.addListener((tabId) => {
|
||||||
@@ -675,6 +832,12 @@ chrome.tabs.onAttached.addListener((tabId, attachInfo) => {
|
|||||||
|
|
||||||
chrome.windows.onRemoved.addListener((windowId) => {
|
chrome.windows.onRemoved.addListener((windowId) => {
|
||||||
removeWindowCaches(windowId);
|
removeWindowCaches(windowId);
|
||||||
|
cleanEmptyGroups().catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
|
if (alarm?.name !== CLEANUP_ALARM_NAME) return;
|
||||||
|
cleanEmptyGroups().catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function createTag(text) {
|
|||||||
|
|
||||||
function renderSummary(state) {
|
function renderSummary(state) {
|
||||||
summaryEl.innerHTML = '';
|
summaryEl.innerHTML = '';
|
||||||
|
const options = state.options || {};
|
||||||
const title = document.createElement('div');
|
const title = document.createElement('div');
|
||||||
title.innerHTML = `<strong>Overview</strong> · extensionId: <code>${state.extensionId}</code>`;
|
title.innerHTML = `<strong>Overview</strong> · extensionId: <code>${state.extensionId}</code>`;
|
||||||
|
|
||||||
@@ -24,9 +25,68 @@ function renderSummary(state) {
|
|||||||
tags.className = 'tags';
|
tags.className = 'tags';
|
||||||
tags.appendChild(createTag(`sessions: ${state.totals.sessions}`));
|
tags.appendChild(createTag(`sessions: ${state.totals.sessions}`));
|
||||||
tags.appendChild(createTag(`tabs: ${state.totals.tabs}`));
|
tags.appendChild(createTag(`tabs: ${state.totals.tabs}`));
|
||||||
|
tags.appendChild(
|
||||||
|
createTag(`isolation: ${options.strictWindowIsolation === false ? 'off' : 'on'}`)
|
||||||
|
);
|
||||||
|
tags.appendChild(
|
||||||
|
createTag(`activation-guard: ${options.suppressCrossWindowActivation === false ? 'off' : 'on'}`)
|
||||||
|
);
|
||||||
|
tags.appendChild(
|
||||||
|
createTag(`auto-clean: ${options.autoCleanEmptyGroups === false ? 'off' : 'on'}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const optionActions = document.createElement('div');
|
||||||
|
optionActions.className = 'row-actions';
|
||||||
|
|
||||||
|
const isolationBtn = document.createElement('button');
|
||||||
|
isolationBtn.type = 'button';
|
||||||
|
isolationBtn.textContent =
|
||||||
|
options.strictWindowIsolation === false ? 'Enable Isolation' : 'Disable Isolation';
|
||||||
|
isolationBtn.addEventListener('click', async () => {
|
||||||
|
await send({
|
||||||
|
type: 'AB_PANEL_SET_OPTIONS',
|
||||||
|
options: { ...options, strictWindowIsolation: options.strictWindowIsolation === false },
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
const guardBtn = document.createElement('button');
|
||||||
|
guardBtn.type = 'button';
|
||||||
|
guardBtn.textContent =
|
||||||
|
options.suppressCrossWindowActivation === false ? 'Enable Guard' : 'Disable Guard';
|
||||||
|
guardBtn.addEventListener('click', async () => {
|
||||||
|
await send({
|
||||||
|
type: 'AB_PANEL_SET_OPTIONS',
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
suppressCrossWindowActivation: options.suppressCrossWindowActivation === false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
const autoCleanBtn = document.createElement('button');
|
||||||
|
autoCleanBtn.type = 'button';
|
||||||
|
autoCleanBtn.textContent =
|
||||||
|
options.autoCleanEmptyGroups === false ? 'Enable Auto-Clean' : 'Disable Auto-Clean';
|
||||||
|
autoCleanBtn.addEventListener('click', async () => {
|
||||||
|
await send({
|
||||||
|
type: 'AB_PANEL_SET_OPTIONS',
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
autoCleanEmptyGroups: options.autoCleanEmptyGroups === false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
optionActions.appendChild(isolationBtn);
|
||||||
|
optionActions.appendChild(guardBtn);
|
||||||
|
optionActions.appendChild(autoCleanBtn);
|
||||||
|
|
||||||
summaryEl.appendChild(title);
|
summaryEl.appendChild(title);
|
||||||
summaryEl.appendChild(tags);
|
summaryEl.appendChild(tags);
|
||||||
|
summaryEl.appendChild(optionActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSessions(state) {
|
function renderSessions(state) {
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ Notes:
|
|||||||
- non-default session: `Agent Browser Stealth • <session>`
|
- non-default session: `Agent Browser Stealth • <session>`
|
||||||
- Additional extension-side capabilities:
|
- Additional extension-side capabilities:
|
||||||
- Session window isolation + deterministic group colors.
|
- Session window isolation + deterministic group colors.
|
||||||
- Side panel controls: Focus / Keep Only This / Clean Empty Groups.
|
- Side panel controls: Focus / Keep Only This / Clean Empty Groups + isolation/auto-clean toggles.
|
||||||
- Session allowlist policy editing and fallback blocking (`about:blank`).
|
- Session allowlist policy editing and fallback blocking (`about:blank`).
|
||||||
- Download auto-routing to `agent-browser-stealth/<session>/...`.
|
- Download auto-routing to `agent-browser-stealth/<session>/...`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user