Remote Tab Control (v7.10.0)
The remote tab is a regular browser tab that the extension opens or selects for you. Your admin page sends messages; the extension acts in that tab and returns structured replies.
You can open, navigate, focus, read URL/title and, as of v7.10.0, drive multiple tabs via tabKey/tabId.
The security banner is visible only inside the controlled tab (can be minimized into a small “chip”).
2) Minimal Request Helper (canonical)
Keep a single helper in your layout. It wraps requestId + timeout and returns { ok, data } or { ok:false, error:{code,message} }.
<script>
const ADMIN_ORIGIN='admin', EXT_ORIGIN='extension';
let _seq=0, _waiters=new Map();
addEventListener('message', (e) => {
const d=e.data; if(!d||typeof d!=='object') return;
if(d.origin!==EXT_ORIGIN) return;
if(d.requestId && _waiters.has(d.requestId)) { _waiters.get(d.requestId)(d); _waiters.delete(d.requestId); }
});
function ask(action, args={}, timeoutMs=12000){
const requestId='r'+(++_seq);
postMessage({ origin:ADMIN_ORIGIN, type:'RTO_REQUEST', requestId, action, args }, '*');
return new Promise((resolve,reject)=>{
const t=setTimeout(()=>{ _waiters.delete(requestId); reject(Object.assign(new Error('timeout'),{ code:'TIMEOUT' })); }, timeoutMs);
_waiters.set(requestId, (resp)=>{ clearTimeout(t); resolve(resp); });
});
}
async function askBackoff(action, args={}, tries=3, base=300){
let n=0;
while(true){
const r = await ask(action, args).catch(e=>({ ok:false, error:{ code:e.code||'TIMEOUT', message:e.message||'timeout' }}));
if (r && r.ok) return r;
if (++n >= tries) return r;
await new Promise(res=>setTimeout(res, base*Math.pow(2,n-1)));
}
}
function showAllowFor(url){
try{ const host=new URL(url).host;
const box=document.getElementById('allow-box');
if(box){ box.querySelector('.domain').textContent=host; box.classList.remove('d-none'); }
}catch(_){}
}
</script>
example.com.
3) Open / Navigate the Remote Tab
openTab creates (or reuses) the target tab. navigate changes the URL. URLs must be absolute https://.
Control the focus with focus:true|false. Tip: there is an internal one-shot to avoid the following auto-focus (handled by the extension).
3.1 Open a page (named tab)
Use tabKey to give the tab a name (e.g., "sales").
const url='https://example.com';
const r = await askBackoff('openTab', { url, focus:false, tabKey:'sales' });
if (!r?.ok && r?.error?.code==='DOMAIN_NOT_ALLOWED') showAllowFor(url);
3.2 Navigate within the same tab
try{
await ask('navigate', { url:'https://example.com/dashboard', tabKey:'sales' });
}catch(_){
showAllowFor('https://example.com/dashboard');
}
3.3 Error handling (standard shape)
const r = await ask('navigate', { url:'https://app.example.com', tabKey:'sales' });
if (!r.ok) {
if (r.error?.code==='DOMAIN_NOT_ALLOWED') showAllowFor('https://app.example.com');
else console.warn('navigate failed:', r.error);
}
4) Focus (bring the tab to the front)
4.1 Focus by named tab
await ask('focus', { tabKey:'sales' });
On some OS/window managers, a second call may be needed if another window was just opened.
4.2 Focus by URL prefix (existing tab)
await ask('focusByPrefix', { prefix:'https://example.com/' });
If multiple tabs match, the extension picks one; you can then navigate to refine.
5) Read the tab URL / Title
5.1 Current URL
const u = await ask('getCurrentUrl', { tabKey:'sales' });
console.log('Remote URL =>', u.ok ? u.data.url : '(none)');
If there is no tab yet, start with openTab/navigate.
5.2 Document title
/* Recommended method: tabInfo (URL + title) */
const info = await ask('tabInfo', { tabKey:'sales' });
console.log('Title =>', info.ok ? info.data?.title : '(unknown)');
/* Possible fallback: runJs (prefer tabInfo when available) */
const t = await ask('runJs', { code:'document.title', tabKey:'sales' });
console.log('Title via runJs =>', t.ok ? t.data : '(unknown)');
5.3 Soft assert: “be on the right page”
const u2 = await ask('getCurrentUrl', { tabKey:'sales' });
if (!u2.ok || !u2.data.url.startsWith('https://example.com/dashboard')) {
await ask('navigate', { url:'https://example.com/dashboard', tabKey:'sales' });
}
6) Detect / Restore a remote tab
6.1 Does it already exist?
async function hasRemote(tabKey){
const r = await ask('getCurrentUrl', tabKey?{tabKey}:{});
return !!(r && r.ok && r.data?.url);
}
if (!(await hasRemote('sales'))) {
await ask('openTab', { url:'https://example.com', tabKey:'sales', focus:false });
}
6.2 Restore after admin reload
/* Save on every navigate */
await ask('navigate', { url:'https://example.com/dashboard', tabKey:'sales' });
sessionStorage.setItem('sales.last', 'https://example.com/dashboard');
/* On admin reload, try to realign */
(async ()=>{
const last=sessionStorage.getItem('sales.last');
if(!last) return;
const r=await ask('getCurrentUrl', { tabKey:'sales' }).catch(_=>null);
if(!r?.ok || !r.data?.url?.startsWith(last)){
await ask('navigate', { url:last, tabKey:'sales' });
}
})();
7) Target an already-open user tab
When the user already has a tab on the right domain, attach to it instead of creating a new one.
7.1 By URL prefix
await ask('focusByPrefix', { prefix:'https://example.com/' });
If multiple tabs match, the extension chooses one. You can then navigate to be specific.
7.2 List, adopt, release (v7.10.0)
const list = await ask('listTabs'); // { ok, data:{ items:[{tabId, tabKey, url, title, focused}, ...] } }
if (list.ok && list.data.items.length){
const first = list.data.items[0];
await ask('adoptTab', { tabId:first.tabId, tabKey:'ops' });
await ask('navigate', { url:'https://example.org/tools', tabKey:'ops' });
await ask('releaseTab', { tabKey:'ops' });
}
8) Multi-Tab: target precisely with tabKey / tabId
tabKey: human-friendly name you choose (simple for beginners). Reuse it in each action.tabId: browser numeric id (vialistTabs), useful to “adopt” an existing tab.- When no target is provided, RTO acts on the default tab (compat mode: single tab).
8.1 Two named tabs
await ask('openTab', { url:'https://example.org/login', tabKey:'auth', focus:true });
await ask('openTab', { url:'https://status.example', tabKey:'monitor', focus:false });
await ask('domType', { selector:'#email', value:'user@example.org', tabKey:'auth' });
await ask('domClick', { selector:'form button[type=submit]', tabKey:'auth' });
const cur = await ask('getCurrentUrl', { tabKey:'monitor' });
console.log('Monitor at:', cur.ok ? cur.data.url : '(unknown)');
8.2 Visual round-robin
for (const key of ['auth','monitor']) {
await ask('focus', { tabKey:key });
await ask('highlight', { selector:'body', ms:500, tabKey:key });
}
9) Timeouts & Retries
9.1 Simple retry
async function askRetry(action, args={}, tries=3, waitMs=400){
for (let i=0;i<tries;i++){
const r = await ask(action, args).catch(_=>null);
if (r?.ok) return r;
if (i===tries-1) return r;
await new Promise(res=>setTimeout(res, waitMs));
}
}
await askRetry('navigate', { url:'https://example.com', tabKey:'sales' });
9.2 Soft assert + fallback
const u = await ask('getCurrentUrl', { tabKey:'sales' }).catch(_=>null);
if (!u?.ok || !u.data?.url?.includes('/dashboard')) {
await ask('navigate', { url:'https://example.com/dashboard', tabKey:'sales' });
}
10) Best Practices (checklist)
- Before showing any “Remote” buttons, call
ask('detect'). - Use absolute HTTPS URLs (
http(s)://only). For LAN/localhost, see double barrier. - Verify the target page (
getCurrentUrl/tabInfo) before DOM actions. - Manage focus sparingly (only when it truly helps the user).
- Make your flows idempotent (safe to relaunch).
- Handle
DOMAIN_NOT_ALLOWED(app-side banner + extension prompt). - Do not hide the security banner; the user can minimize it into a chip.
- Log
requestId, type, status, and duration to help debugging.
Next Pages
| Page | Description |
|---|---|
| Start | Overview, how it works, setup, quick start, and links to all sections. |
| Detect the Extension | Detection contract (ping/pong), install prompts, and graceful fallbacks when the extension isn’t present. |
| Allow-list & Permissions | Deny-by-default model, domain allow-list via popup, and suggested UX to guide users to enable hosts. |
| LAN / Localhost | How to safely enable intranet/localhost (flags + allow-list). |
| Remote Tab Control | Open/navigate/focus the controlled tab, read URL/title, select an existing tab, lifecycle & restore patterns. |
| DOM & Automation | Action catalog (type, setValue, click, select, submit, waitFor) and safe interaction tips. |
| Recipes & Flows | Copy-paste flows: resilient login, dashboard checks, table waits, filters, playlists, and allow-list banners. |
| Events | Event stream reference, subscription helpers, live logger widget, and basic metrics/durations collection. |
| Diagnostics | Inline console, error cookbook, QA self-check, and performance tips for troubleshooting flows quickly. |
| Full API Reference | Canonical message envelopes, request/response schemas, error codes, and return shapes for every action. |
| Compatibility | Supported browsers/versions, permissions nuances, CSP/iframe notes, and known limitations or workarounds. |
| Advanced Patterns | Idempotent flows, retries/backoff, state restore after reload, playlist strategies, and robust selectors. |
| Favorites | Star and group URLs, quick actions (open/focus/navigate), export/import sets, and playlist runs from favorites. |
| Real-World Flows (E2E) | Run end-to-end flows from your admin page: helper setup, modern message contract, copy-paste journeys (login, multi-tab, extract), lightweight assertions, best practices, and troubleshooting. |
Need help writing safe flows?