Plugin / Docs / Remote Tab Control

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”).

v7.10.0
HTTPS & permissions: actions are only possible on allowed hosts (Allow-list). LAN/localhost targets require the double barrier (flags + allow-list).

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>
Allow-list required: open the extension popup and add 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);
Auto-resume: when the user adds the domain via the popup, the pending action automatically resumes.
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);
}
Security banner: in the controlled tab, a thin orange banner (minimizable into a draggable “chip”) indicates remote control. It reappears after navigations.

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 (via listTabs), 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.
Need help writing safe flows?
Try the ChatGPT helper: Remote Tab Opener Copilot.

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 / LocalhostHow 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.