const PANEL_GET_STATE = 'AB_PANEL_GET_STATE'; const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS'; const PANEL_SET_OPTIONS = 'AB_PANEL_SET_OPTIONS'; const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY'; const PANEL_CLOSE_OTHER_TABS = 'AB_PANEL_CLOSE_OTHER_SESSION_TABS'; const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION'; const PANEL_RUN_ACTION = 'AB_PANEL_RUN_ACTION'; const PANEL_CLEAR_ACTIVITY = 'AB_PANEL_CLEAR_ACTIVITY'; const PANEL_START_RECORDING = 'AB_PANEL_START_RECORDING'; const PANEL_STOP_RECORDING = 'AB_PANEL_STOP_RECORDING'; const PANEL_SAVE_RECORDING = 'AB_PANEL_SAVE_RECORDING'; const PANEL_RUN_WORKFLOW = 'AB_PANEL_RUN_WORKFLOW'; const PANEL_DELETE_WORKFLOW = 'AB_PANEL_DELETE_WORKFLOW'; const PANEL_SET_SHORTCUT = 'AB_PANEL_SET_SHORTCUT'; const PANEL_DELETE_SHORTCUT = 'AB_PANEL_DELETE_SHORTCUT'; const PANEL_RUN_SHORTCUT = 'AB_PANEL_RUN_SHORTCUT'; const PANEL_CREATE_SCHEDULE = 'AB_PANEL_CREATE_SCHEDULE'; const PANEL_DELETE_SCHEDULE = 'AB_PANEL_DELETE_SCHEDULE'; const PANEL_TOGGLE_SCHEDULE = 'AB_PANEL_TOGGLE_SCHEDULE'; const summaryEl = document.getElementById('summary'); const controlEl = document.getElementById('control'); const automationEl = document.getElementById('automation'); const developerEl = document.getElementById('developer'); const sessionsEl = document.getElementById('sessions'); const downloadsEl = document.getElementById('downloads'); const statusLineEl = document.getElementById('status-line'); const refreshBtn = document.getElementById('refresh-btn'); const cleanupBtn = document.getElementById('cleanup-btn'); const viewState = { panelState: null, lastDomState: null, }; async function send(message) { return chrome.runtime.sendMessage(message); } function normalizePanelState(rawState) { const state = rawState && typeof rawState === 'object' ? rawState : {}; return { extensionId: typeof state.extensionId === 'string' ? state.extensionId : 'unknown', latestDomState: state.latestDomState && typeof state.latestDomState === 'object' ? state.latestDomState : null, options: state.options && typeof state.options === 'object' ? { strictWindowIsolation: state.options.strictWindowIsolation !== false, suppressCrossWindowActivation: state.options.suppressCrossWindowActivation !== false, autoCleanEmptyGroups: state.options.autoCleanEmptyGroups !== false, pageBridgeEnabled: state.options.pageBridgeEnabled === true, } : { strictWindowIsolation: true, suppressCrossWindowActivation: true, autoCleanEmptyGroups: true, pageBridgeEnabled: false, }, totals: state.totals && typeof state.totals === 'object' ? { sessions: Number.isFinite(state.totals.sessions) ? state.totals.sessions : 0, tabs: Number.isFinite(state.totals.tabs) ? state.totals.tabs : 0, } : { sessions: 0, tabs: 0, }, sessions: Array.isArray(state.sessions) ? state.sessions : [], downloads: Array.isArray(state.downloads) ? state.downloads : [], control: state.control && typeof state.control === 'object' ? { activeTab: state.control.activeTab && typeof state.control.activeTab === 'object' ? state.control.activeTab : null, tabs: Array.isArray(state.control.tabs) ? state.control.tabs : [], } : { activeTab: null, tabs: [], }, activity: state.activity && typeof state.activity === 'object' ? { events: Array.isArray(state.activity.events) ? state.activity.events : [], console: Array.isArray(state.activity.console) ? state.activity.console : [], network: Array.isArray(state.activity.network) ? state.activity.network : [], commandHistory: Array.isArray(state.activity.commandHistory) ? state.activity.commandHistory : [], } : { events: [], console: [], network: [], commandHistory: [], }, automation: state.automation && typeof state.automation === 'object' ? { recording: state.automation.recording && typeof state.automation.recording === 'object' ? state.automation.recording : null, workflows: Array.isArray(state.automation.workflows) ? state.automation.workflows : [], shortcuts: Array.isArray(state.automation.shortcuts) ? state.automation.shortcuts : [], schedules: Array.isArray(state.automation.schedules) ? state.automation.schedules : [], } : { recording: null, workflows: [], shortcuts: [], schedules: [], }, }; } function setStatus(text, tone = 'ok') { statusLineEl.textContent = text || ''; statusLineEl.className = `status-line ${tone}`; } function escapeInline(value) { return String(value || '') .replace(/[\n\r\t]+/g, ' ') .replace(/&/g, '&') .replace(//g, '>') .replace(/\"/g, '"') .replace(/'/g, ''') .slice(0, 240); } function formatTime(ts) { if (!ts) return '-'; try { return new Date(ts).toLocaleString(); } catch { return String(ts); } } function createTag(text) { const span = document.createElement('span'); span.className = 'tag'; span.textContent = text; return span; } function parseTimeInput(timeText) { const match = String(timeText || '').trim().match(/^(\d{1,2}):(\d{2})$/); if (!match) return null; const hour = Number.parseInt(match[1], 10); const minute = Number.parseInt(match[2], 10); if (Number.isNaN(hour) || Number.isNaN(minute)) return null; if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null; return { hour, minute }; } async function runAction(action, args = {}, tabId) { const response = await send({ type: PANEL_RUN_ACTION, action, args, tabId, }); if (!response || response.ok !== true) { setStatus(`Action failed: ${response?.error || 'unknown error'}`, 'error'); return response; } if (response.state) { viewState.lastDomState = response.state; } setStatus(`Action succeeded: ${action}`, 'ok'); return response; } function renderSummary(state) { summaryEl.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Overview'; summaryEl.appendChild(title); const extensionInfo = document.createElement('div'); extensionInfo.className = 'caption'; extensionInfo.textContent = `Extension ID: ${state.extensionId}`; summaryEl.appendChild(extensionInfo); const tags = document.createElement('div'); tags.className = 'tags'; tags.appendChild(createTag(`Sessions: ${state.totals.sessions}`)); tags.appendChild(createTag(`Tabs: ${state.totals.tabs}`)); tags.appendChild( createTag(`Isolation: ${state.options.strictWindowIsolation === false ? 'Off' : 'On'}`) ); tags.appendChild( createTag(`Guard: ${state.options.suppressCrossWindowActivation === false ? 'Off' : 'On'}`) ); tags.appendChild(createTag(`PageBridge: ${state.options.pageBridgeEnabled ? 'On' : 'Off'}`)); tags.appendChild(createTag(`Workflows: ${state.automation.workflows.length}`)); tags.appendChild(createTag(`Schedules: ${state.automation.schedules.length}`)); summaryEl.appendChild(tags); const actions = document.createElement('div'); actions.className = 'row wrap'; const isolationBtn = document.createElement('button'); isolationBtn.textContent = 'Toggle Isolation'; isolationBtn.addEventListener('click', async () => { await send({ type: PANEL_SET_OPTIONS, options: { ...state.options, strictWindowIsolation: state.options.strictWindowIsolation === false, }, }); await refresh(); }); const guardBtn = document.createElement('button'); guardBtn.textContent = 'Toggle Guard'; guardBtn.addEventListener('click', async () => { await send({ type: PANEL_SET_OPTIONS, options: { ...state.options, suppressCrossWindowActivation: state.options.suppressCrossWindowActivation === false, }, }); await refresh(); }); const cleanBtn = document.createElement('button'); cleanBtn.textContent = 'Toggle Auto-Clean'; cleanBtn.addEventListener('click', async () => { await send({ type: PANEL_SET_OPTIONS, options: { ...state.options, autoCleanEmptyGroups: state.options.autoCleanEmptyGroups === false, }, }); await refresh(); }); actions.appendChild(isolationBtn); actions.appendChild(guardBtn); actions.appendChild(cleanBtn); summaryEl.appendChild(actions); } function renderControl(state) { const control = state.control || { activeTab: null, tabs: [] }; controlEl.innerHTML = `

Browser Control


Tabs

`; const activeTab = control.activeTab; const activeText = activeTab ? `Active: #${activeTab.id} ${escapeInline(activeTab.title)}` : 'No active tab'; controlEl.querySelector('#ctl-active').textContent = activeText; const urlInput = controlEl.querySelector('#ctl-url'); const selectorInput = controlEl.querySelector('#ctl-selector'); const valueInput = controlEl.querySelector('#ctl-value'); const keyInput = controlEl.querySelector('#ctl-key'); const shortcutInput = controlEl.querySelector('#ctl-shortcut'); controlEl.querySelector('#ctl-open').addEventListener('click', async () => { const url = urlInput.value.trim(); if (!url) { setStatus('Please enter a URL', 'warn'); return; } await runAction('open', { url }, activeTab?.id); await refresh(); }); controlEl.querySelector('#ctl-back').addEventListener('click', async () => { await runAction('back', {}, activeTab?.id); await refresh(); }); controlEl.querySelector('#ctl-forward').addEventListener('click', async () => { await runAction('forward', {}, activeTab?.id); await refresh(); }); controlEl.querySelector('#ctl-reload').addEventListener('click', async () => { await runAction('reload', {}, activeTab?.id); await refresh(); }); controlEl.querySelector('#ctl-snapshot').addEventListener('click', async () => { const selector = selectorInput.value.trim(); const response = await runAction( 'snapshot', { selector: selector || undefined, interactiveOnly: true, maxNodes: 80, }, activeTab?.id ); if (response?.state) { viewState.lastDomState = response.state; renderDeveloper(state); } }); controlEl.querySelector('#ctl-dom').addEventListener('click', async () => { const selector = selectorInput.value.trim(); const response = await runAction( 'dom-state', { selector: selector || undefined, interactiveOnly: false, maxNodes: 100, }, activeTab?.id ); if (response?.state) { viewState.lastDomState = response.state; renderDeveloper(state); } }); controlEl.querySelector('#ctl-click').addEventListener('click', async () => { const selector = selectorInput.value.trim(); if (!selector) { setStatus('Selector is required for click', 'warn'); return; } await runAction('click', { selector }, activeTab?.id); await refresh(); }); controlEl.querySelector('#ctl-fill').addEventListener('click', async () => { const selector = selectorInput.value.trim(); if (!selector) { setStatus('Selector is required for fill', 'warn'); return; } await runAction( 'fill', { selector, value: valueInput.value, }, activeTab?.id ); await refresh(); }); controlEl.querySelector('#ctl-press').addEventListener('click', async () => { await runAction( 'press', { selector: selectorInput.value.trim() || undefined, key: keyInput.value.trim() || 'Enter', }, activeTab?.id ); await refresh(); }); controlEl.querySelector('#ctl-run-shortcut').addEventListener('click', async () => { const raw = shortcutInput.value.trim(); if (!raw) { setStatus('Shortcut name is required', 'warn'); return; } const name = raw.startsWith('/') ? raw.slice(1) : raw; const response = await send({ type: PANEL_RUN_SHORTCUT, name, tabId: activeTab?.id, }); if (!response || response.ok !== true) { setStatus(`Shortcut failed: ${response?.error || 'unknown error'}`, 'error'); return; } setStatus(`Shortcut executed: /${name}`, 'ok'); await refresh(); }); const tabsEl = controlEl.querySelector('#ctl-tabs'); if (!Array.isArray(control.tabs) || control.tabs.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No tabs in current window.'; tabsEl.appendChild(empty); } else { for (const tab of control.tabs.slice(0, 20)) { const item = document.createElement('div'); item.className = 'item'; const title = document.createElement('div'); title.className = 'item-title'; title.textContent = `#${tab.id} [${tab.index}] ${escapeInline(tab.title)}`; const url = document.createElement('div'); url.className = 'item-url'; url.textContent = tab.url || 'about:blank'; const row = document.createElement('div'); row.className = 'row wrap'; const switchBtn = document.createElement('button'); switchBtn.textContent = tab.active ? 'Active' : 'Switch'; switchBtn.disabled = tab.active === true; switchBtn.addEventListener('click', async () => { await runAction('tabs:switch', { tabId: tab.id }); await refresh(); }); const closeBtn = document.createElement('button'); closeBtn.className = 'danger'; closeBtn.textContent = 'Close'; closeBtn.addEventListener('click', async () => { await runAction('tabs:close', {}, tab.id); await refresh(); }); row.appendChild(switchBtn); row.appendChild(closeBtn); if (tab.session) { const pill = document.createElement('span'); pill.className = 'event-pill'; pill.textContent = `session:${tab.session}`; row.appendChild(pill); } item.appendChild(title); item.appendChild(url); item.appendChild(row); tabsEl.appendChild(item); } } } function renderSessions(state) { sessionsEl.innerHTML = ''; const heading = document.createElement('h3'); heading.textContent = 'Managed Sessions'; sessionsEl.appendChild(heading); if (!state.sessions || state.sessions.length === 0) { const empty = document.createElement('div'); empty.className = 'card empty'; empty.textContent = 'No active managed sessions found.'; sessionsEl.appendChild(empty); return; } for (const session of state.sessions) { const card = document.createElement('article'); card.className = 'card'; const titleRow = document.createElement('div'); titleRow.className = 'section-title'; const title = document.createElement('h4'); title.textContent = session.session; const actionRow = document.createElement('div'); actionRow.className = 'row wrap'; const focusBtn = document.createElement('button'); focusBtn.textContent = 'Focus'; focusBtn.addEventListener('click', async () => { await send({ type: PANEL_FOCUS_SESSION, session: session.session }); await refresh(); }); const isolateBtn = document.createElement('button'); isolateBtn.textContent = 'Isolate'; isolateBtn.addEventListener('click', async () => { await send({ type: PANEL_CLOSE_OTHER_TABS, session: session.session }); await refresh(); }); const policyBtn = document.createElement('button'); policyBtn.textContent = 'Policy'; policyBtn.addEventListener('click', async () => { const current = (session.allowedDomains || []).join(','); const input = window.prompt('Allowed domains (comma-separated)', current); if (input === null) return; const allowedDomains = input .split(',') .map((item) => item.trim().toLowerCase()) .filter((item) => item.length > 0); await send({ type: PANEL_SET_POLICY, session: session.session, allowedDomains }); await refresh(); }); actionRow.appendChild(focusBtn); actionRow.appendChild(isolateBtn); actionRow.appendChild(policyBtn); titleRow.appendChild(title); titleRow.appendChild(actionRow); const tags = document.createElement('div'); tags.className = 'tags'; tags.appendChild(createTag(`Window ${session.windowId ?? 'N/A'}`)); tags.appendChild(createTag(`${session.tabs.length} tabs`)); if (session.group?.title) { tags.appendChild(createTag(`Group: ${session.group.title}`)); } const list = document.createElement('div'); list.className = 'list'; for (const tab of session.tabs.slice(0, 8)) { const item = document.createElement('div'); item.className = 'item'; const tabTitle = document.createElement('div'); tabTitle.className = 'item-title'; tabTitle.textContent = `${tab.active ? '● ' : ''}#${tab.id} ${escapeInline(tab.title || '(Untitled)')}`; const tabUrl = document.createElement('div'); tabUrl.className = 'item-url'; tabUrl.textContent = tab.url || 'about:blank'; item.appendChild(tabTitle); item.appendChild(tabUrl); list.appendChild(item); } card.appendChild(titleRow); card.appendChild(tags); card.appendChild(list); sessionsEl.appendChild(card); } } function renderDownloads(state) { downloadsEl.innerHTML = '

Recent Downloads

'; const list = document.createElement('div'); list.className = 'list'; const entries = state.downloads || []; if (entries.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No download events yet.'; list.appendChild(empty); } else { for (const entry of entries.slice(0, 10)) { const item = document.createElement('div'); item.className = 'item'; const title = document.createElement('div'); title.className = 'item-title'; title.textContent = `#${entry.id} · ${entry.state || 'updated'}`; const filename = document.createElement('div'); filename.className = 'item-url'; filename.textContent = entry.filename || '(no filename)'; const meta = document.createElement('div'); meta.className = 'caption'; meta.textContent = formatTime(entry.timestamp); item.appendChild(title); item.appendChild(filename); item.appendChild(meta); list.appendChild(item); } } downloadsEl.appendChild(list); } async function promptAndCreateSchedule(workflowId, workflowName) { const cadenceKind = (window.prompt('Cadence: daily | weekly | monthly | yearly', 'daily') || '') .trim() .toLowerCase(); if (!cadenceKind) return; if (!['daily', 'weekly', 'monthly', 'yearly'].includes(cadenceKind)) { setStatus('Invalid cadence. Use daily/weekly/monthly/yearly.', 'warn'); return; } const timeInput = parseTimeInput(window.prompt('Time (HH:MM, 24h)', '09:00')); if (!timeInput) { setStatus('Invalid time format.', 'warn'); return; } const cadence = { kind: cadenceKind, hour: timeInput.hour, minute: timeInput.minute, }; if (cadenceKind === 'weekly') { const weekday = Number.parseInt( window.prompt('Weekday (0=Sun .. 6=Sat)', String(new Date().getDay())) || '', 10 ); if (Number.isNaN(weekday) || weekday < 0 || weekday > 6) { setStatus('Invalid weekday.', 'warn'); return; } cadence.weekdays = [weekday]; } if (cadenceKind === 'monthly') { const day = Number.parseInt(window.prompt('Day of month (1-31)', '1') || '', 10); if (Number.isNaN(day) || day < 1 || day > 31) { setStatus('Invalid day of month.', 'warn'); return; } cadence.dayOfMonth = day; } if (cadenceKind === 'yearly') { const month = Number.parseInt(window.prompt('Month (1-12)', '1') || '', 10); const day = Number.parseInt(window.prompt('Day of month (1-31)', '1') || '', 10); if (Number.isNaN(month) || month < 1 || month > 12 || Number.isNaN(day) || day < 1 || day > 31) { setStatus('Invalid month/day.', 'warn'); return; } cadence.month = month; cadence.dayOfMonth = day; } const response = await send({ type: PANEL_CREATE_SCHEDULE, schedule: { name: `Schedule ${workflowName}`, workflowId, cadence, enabled: true, }, }); if (!response || response.ok !== true) { setStatus(`Create schedule failed: ${response?.error || 'unknown error'}`, 'error'); return; } setStatus('Schedule created.', 'ok'); await refresh(); } function renderAutomation(state) { const automation = state.automation; const activeTabId = state.control?.activeTab?.id; automationEl.innerHTML = `

Automation

Recording


Workflows


Shortcuts

Schedules

`; const recordingStateEl = automationEl.querySelector('#recording-state'); const recordingStepsEl = automationEl.querySelector('#recording-steps'); const recordNameInput = automationEl.querySelector('#record-name'); const workflowListEl = automationEl.querySelector('#workflow-list'); const shortcutListEl = automationEl.querySelector('#shortcut-list'); const scheduleListEl = automationEl.querySelector('#schedule-list'); if (automation.recording) { recordingStateEl.textContent = `ON · ${automation.recording.stepCount} steps`; recordNameInput.value = automation.recording.name || ''; for (const step of automation.recording.steps) { const item = document.createElement('div'); item.className = 'item'; item.innerHTML = `
${escapeInline(step.action)}
${escapeInline(JSON.stringify(step.args || {}))}
`; recordingStepsEl.appendChild(item); } if (automation.recording.steps.length === 0) { const empty = document.createElement('div'); empty.className = 'caption'; empty.textContent = 'Recording is active. Perform actions from Browser Control.'; recordingStepsEl.appendChild(empty); } } else { recordingStateEl.textContent = 'OFF'; const empty = document.createElement('div'); empty.className = 'caption'; empty.textContent = 'Start recording to capture actions into a reusable workflow.'; recordingStepsEl.appendChild(empty); } automationEl.querySelector('#record-start').addEventListener('click', async () => { const result = await send({ type: PANEL_START_RECORDING, name: recordNameInput.value.trim() || 'Recorded Workflow', }); if (!result || result.ok !== true) { setStatus(`Start recording failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus('Recording started.', 'ok'); await refresh(); }); automationEl.querySelector('#record-stop').addEventListener('click', async () => { const result = await send({ type: PANEL_STOP_RECORDING }); if (!result || result.ok !== true) { setStatus(`Stop recording failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus('Recording stopped.', 'ok'); await refresh(); }); automationEl.querySelector('#record-save').addEventListener('click', async () => { const result = await send({ type: PANEL_SAVE_RECORDING, name: recordNameInput.value.trim() || undefined, }); if (!result || result.ok !== true) { setStatus(`Save recording failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus(`Workflow saved: ${result.workflow?.name || ''}`, 'ok'); await refresh(); }); if (!automation.workflows || automation.workflows.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No workflows saved yet.'; workflowListEl.appendChild(empty); } else { for (const workflow of automation.workflows) { const item = document.createElement('div'); item.className = 'item'; const title = document.createElement('div'); title.className = 'item-title'; title.textContent = `${workflow.name} (${workflow.stepCount} steps)`; const meta = document.createElement('div'); meta.className = 'caption'; meta.textContent = `Updated: ${formatTime(workflow.updatedAt)}`; const row = document.createElement('div'); row.className = 'row wrap'; const runBtn = document.createElement('button'); runBtn.textContent = 'Run'; runBtn.addEventListener('click', async () => { const response = await send({ type: PANEL_RUN_WORKFLOW, workflowId: workflow.id, tabId: activeTabId, }); if (!response || response.ok !== true) { setStatus(`Workflow failed: ${response?.error || 'unknown error'}`, 'error'); return; } setStatus(`Workflow executed: ${workflow.name}`, 'ok'); await refresh(); }); const shortcutBtn = document.createElement('button'); shortcutBtn.textContent = 'Set Shortcut'; shortcutBtn.addEventListener('click', async () => { const defaultName = workflow.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 32); const input = window.prompt('Shortcut name (without /)', defaultName || 'workflow'); if (!input) return; const result = await send({ type: PANEL_SET_SHORTCUT, name: input, workflowId: workflow.id, }); if (!result || result.ok !== true) { setStatus(`Set shortcut failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus(`Shortcut saved: /${result.shortcut.name}`, 'ok'); await refresh(); }); const scheduleBtn = document.createElement('button'); scheduleBtn.textContent = 'Schedule'; scheduleBtn.addEventListener('click', async () => { await promptAndCreateSchedule(workflow.id, workflow.name); }); const deleteBtn = document.createElement('button'); deleteBtn.className = 'danger'; deleteBtn.textContent = 'Delete'; deleteBtn.addEventListener('click', async () => { if (!window.confirm(`Delete workflow \"${workflow.name}\"?`)) return; const result = await send({ type: PANEL_DELETE_WORKFLOW, workflowId: workflow.id }); if (!result || result.ok !== true) { setStatus(`Delete workflow failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus('Workflow deleted.', 'ok'); await refresh(); }); row.appendChild(runBtn); row.appendChild(shortcutBtn); row.appendChild(scheduleBtn); row.appendChild(deleteBtn); item.appendChild(title); item.appendChild(meta); item.appendChild(row); workflowListEl.appendChild(item); } } if (!automation.shortcuts || automation.shortcuts.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No shortcuts configured.'; shortcutListEl.appendChild(empty); } else { for (const shortcut of automation.shortcuts) { const item = document.createElement('div'); item.className = 'item'; const title = document.createElement('div'); title.className = 'item-title'; title.textContent = `/${shortcut.name}`; const desc = document.createElement('div'); desc.className = 'item-url'; desc.textContent = shortcut.workflowName; const row = document.createElement('div'); row.className = 'row wrap'; const runBtn = document.createElement('button'); runBtn.textContent = 'Run'; runBtn.addEventListener('click', async () => { const response = await send({ type: PANEL_RUN_SHORTCUT, name: shortcut.name, tabId: activeTabId, }); if (!response || response.ok !== true) { setStatus(`Shortcut failed: ${response?.error || 'unknown error'}`, 'error'); return; } setStatus(`Shortcut executed: /${shortcut.name}`, 'ok'); await refresh(); }); const deleteBtn = document.createElement('button'); deleteBtn.className = 'danger'; deleteBtn.textContent = 'Delete'; deleteBtn.addEventListener('click', async () => { const result = await send({ type: PANEL_DELETE_SHORTCUT, name: shortcut.name }); if (!result || result.ok !== true) { setStatus(`Delete shortcut failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus('Shortcut deleted.', 'ok'); await refresh(); }); row.appendChild(runBtn); row.appendChild(deleteBtn); item.appendChild(title); item.appendChild(desc); item.appendChild(row); shortcutListEl.appendChild(item); } } if (!automation.schedules || automation.schedules.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No schedules configured.'; scheduleListEl.appendChild(empty); } else { for (const schedule of automation.schedules) { const item = document.createElement('div'); item.className = 'item'; const title = document.createElement('div'); title.className = 'item-title'; title.textContent = schedule.name; const desc = document.createElement('div'); desc.className = 'item-url'; desc.textContent = `${schedule.workflowName} · ${schedule.cadence.kind}`; const time = document.createElement('div'); time.className = 'caption'; time.textContent = `Next: ${formatTime(schedule.nextRunAt)}`; const row = document.createElement('div'); row.className = 'row wrap'; const toggleBtn = document.createElement('button'); toggleBtn.textContent = schedule.enabled ? 'Disable' : 'Enable'; toggleBtn.addEventListener('click', async () => { const result = await send({ type: PANEL_TOGGLE_SCHEDULE, scheduleId: schedule.id, enabled: !schedule.enabled, }); if (!result || result.ok !== true) { setStatus(`Toggle schedule failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus('Schedule updated.', 'ok'); await refresh(); }); const deleteBtn = document.createElement('button'); deleteBtn.className = 'danger'; deleteBtn.textContent = 'Delete'; deleteBtn.addEventListener('click', async () => { const result = await send({ type: PANEL_DELETE_SCHEDULE, scheduleId: schedule.id, }); if (!result || result.ok !== true) { setStatus(`Delete schedule failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus('Schedule deleted.', 'ok'); await refresh(); }); row.appendChild(toggleBtn); row.appendChild(deleteBtn); item.appendChild(title); item.appendChild(desc); item.appendChild(time); item.appendChild(row); scheduleListEl.appendChild(item); } } } function renderDeveloper(state) { const activity = state.activity; const activeTabId = state.control?.activeTab?.id; const domState = viewState.lastDomState || state.latestDomState || null; developerEl.innerHTML = `

Developer Signals

Console: ${activity.console.length} · Network: ${activity.network.length} Bridge: ${state.options.pageBridgeEnabled ? 'ON' : 'OFF (high-risk default)'}

DOM State


      

Recent Commands


Console Events

Network Events

`; const domPre = developerEl.querySelector('#dev-dom-json'); domPre.textContent = domState ? JSON.stringify(domState, null, 2) : 'No DOM state captured yet. Use Snapshot or DOM State in Browser Control.'; const commandList = developerEl.querySelector('#dev-command-list'); if (!activity.commandHistory || activity.commandHistory.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No commands yet.'; commandList.appendChild(empty); } else { for (const entry of activity.commandHistory.slice(0, 8)) { const item = document.createElement('div'); item.className = 'item'; item.innerHTML = `
${entry.ok ? 'OK' : 'FAIL'} · ${escapeInline(entry.action)}
${formatTime(entry.timestamp)}${entry.error ? ` · ${escapeInline(entry.error)}` : ''}
`; commandList.appendChild(item); } } const consoleList = developerEl.querySelector('#dev-console-list'); if (!activity.console || activity.console.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No console events.'; consoleList.appendChild(empty); } else { for (const event of activity.console.slice(0, 12)) { const payload = event.payload || {}; const level = payload.level || 'log'; const text = payload.message || (Array.isArray(payload.args) ? payload.args.join(' ') : JSON.stringify(payload)); const item = document.createElement('div'); item.className = 'item'; item.innerHTML = `
${escapeInline(level.toUpperCase())}
${escapeInline(text)}
${formatTime(event.timestamp)}
`; consoleList.appendChild(item); } } const networkList = developerEl.querySelector('#dev-network-list'); if (!activity.network || activity.network.length === 0) { const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No network events.'; networkList.appendChild(empty); } else { for (const event of activity.network.slice(0, 12)) { const payload = event.payload || {}; const item = document.createElement('div'); item.className = 'item'; const line1 = `${payload.transport || 'net'} ${payload.method || ''} ${payload.status || ''}`.trim(); const line2 = payload.url || payload.error || '(unknown)'; const line3 = payload.durationMs ? `${payload.durationMs} ms` : ''; item.innerHTML = `
${escapeInline(line1)}
${escapeInline(line2)}
${escapeInline(line3)} · ${formatTime(event.timestamp)}
`; networkList.appendChild(item); } } developerEl.querySelector('#dev-refresh-dom').addEventListener('click', async () => { const selector = developerEl.querySelector('#dev-selector').value.trim(); const interactiveOnly = developerEl.querySelector('#dev-interactive-only').checked; const response = await runAction( 'dom-state', { selector: selector || undefined, interactiveOnly, maxNodes: 120, }, activeTabId ); if (response?.state) { viewState.lastDomState = response.state; renderDeveloper(state); } }); developerEl.querySelector('#dev-clear-activity').addEventListener('click', async () => { await send({ type: PANEL_CLEAR_ACTIVITY }); setStatus('Activity events cleared.', 'ok'); await refresh(); }); developerEl.querySelector('#dev-toggle-bridge').addEventListener('click', async () => { const result = await send({ type: PANEL_SET_OPTIONS, options: { ...state.options, pageBridgeEnabled: !state.options.pageBridgeEnabled, }, }); if (!result || result.ok !== true) { setStatus(`Toggle Page Bridge failed: ${result?.error || 'unknown error'}`, 'error'); return; } setStatus( `Page Bridge ${!state.options.pageBridgeEnabled ? 'enabled' : 'disabled'} (reload page to apply).`, 'ok' ); await refresh(); }); } async function refresh() { const response = await send({ type: PANEL_GET_STATE }); if (!response || response.ok !== true || !response.state) { setStatus(response?.error || 'Failed to load extension state.', 'error'); return; } const normalizedState = normalizePanelState(response.state); viewState.panelState = normalizedState; if (normalizedState.latestDomState) { viewState.lastDomState = normalizedState.latestDomState; } renderControl(normalizedState); renderSummary(normalizedState); renderAutomation(normalizedState); renderDeveloper(normalizedState); renderSessions(normalizedState); renderDownloads(normalizedState); } refreshBtn.addEventListener('click', async () => { await refresh(); setStatus('Panel refreshed.', 'ok'); }); cleanupBtn.addEventListener('click', async () => { const response = await send({ type: PANEL_CLEAN_EMPTY_GROUPS }); if (!response || response.ok !== true) { setStatus(`Clean failed: ${response?.error || 'unknown error'}`, 'error'); return; } setStatus('Empty groups cleaned.', 'ok'); await refresh(); }); refresh().catch((error) => { setStatus(error instanceof Error ? error.message : String(error), 'error'); }); setInterval(() => { refresh().catch(() => {}); }, 5000);