agent-browser-stealth 0.17.0-fork.2 → 0.24.0-fork.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +1256 -240
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/bin/agent-browser.js +13 -2
  8. package/extensions/tab-group-cdp/content-script.js +425 -0
  9. package/extensions/tab-group-cdp/icons/icon.svg +7 -0
  10. package/extensions/tab-group-cdp/manifest.json +34 -0
  11. package/extensions/tab-group-cdp/page-bridge.js +133 -0
  12. package/extensions/tab-group-cdp/service-worker.js +2249 -0
  13. package/extensions/tab-group-cdp/sidepanel.css +258 -0
  14. package/extensions/tab-group-cdp/sidepanel.html +28 -0
  15. package/extensions/tab-group-cdp/sidepanel.js +1225 -0
  16. package/package.json +17 -69
  17. package/scripts/build-all-platforms.sh +6 -0
  18. package/scripts/check-version-sync.js +14 -2
  19. package/scripts/copy-native.js +8 -50
  20. package/scripts/postinstall.js +149 -165
  21. package/scripts/windows-debug/provision.sh +220 -0
  22. package/scripts/windows-debug/run.sh +92 -0
  23. package/scripts/windows-debug/start.sh +43 -0
  24. package/scripts/windows-debug/stop.sh +28 -0
  25. package/scripts/windows-debug/sync.sh +27 -0
  26. package/skills/agent-browser/SKILL.md +256 -159
  27. package/skills/agent-browser/references/authentication.md +101 -0
  28. package/skills/agent-browser/references/commands.md +34 -2
  29. package/skills/agent-browser/references/snapshot-refs.md +25 -0
  30. package/skills/agentcore/SKILL.md +115 -0
  31. package/skills/dogfood/SKILL.md +4 -2
  32. package/skills/electron/SKILL.md +26 -2
  33. package/skills/slack/SKILL.md +0 -9
  34. package/skills/slack/references/slack-tasks.md +2 -8
  35. package/skills/vercel-sandbox/SKILL.md +280 -0
  36. package/bin/agent-browser-local +0 -0
  37. package/bin/agent-browser-stealth +0 -0
  38. package/bin/agent-browser-stealth.d +0 -1
  39. package/dist/action-policy.d.ts +0 -14
  40. package/dist/action-policy.d.ts.map +0 -1
  41. package/dist/action-policy.js +0 -253
  42. package/dist/action-policy.js.map +0 -1
  43. package/dist/actions.d.ts +0 -21
  44. package/dist/actions.d.ts.map +0 -1
  45. package/dist/actions.js +0 -2139
  46. package/dist/actions.js.map +0 -1
  47. package/dist/auth-cli.d.ts +0 -2
  48. package/dist/auth-cli.d.ts.map +0 -1
  49. package/dist/auth-cli.js +0 -97
  50. package/dist/auth-cli.js.map +0 -1
  51. package/dist/auth-vault.d.ts +0 -36
  52. package/dist/auth-vault.d.ts.map +0 -1
  53. package/dist/auth-vault.js +0 -125
  54. package/dist/auth-vault.js.map +0 -1
  55. package/dist/browser.d.ts +0 -665
  56. package/dist/browser.d.ts.map +0 -1
  57. package/dist/browser.js +0 -3210
  58. package/dist/browser.js.map +0 -1
  59. package/dist/confirmation.d.ts +0 -8
  60. package/dist/confirmation.d.ts.map +0 -1
  61. package/dist/confirmation.js +0 -30
  62. package/dist/confirmation.js.map +0 -1
  63. package/dist/daemon.d.ts +0 -78
  64. package/dist/daemon.d.ts.map +0 -1
  65. package/dist/daemon.js +0 -744
  66. package/dist/daemon.js.map +0 -1
  67. package/dist/diff.d.ts +0 -18
  68. package/dist/diff.d.ts.map +0 -1
  69. package/dist/diff.js +0 -271
  70. package/dist/diff.js.map +0 -1
  71. package/dist/domain-filter.d.ts +0 -28
  72. package/dist/domain-filter.d.ts.map +0 -1
  73. package/dist/domain-filter.js +0 -149
  74. package/dist/domain-filter.js.map +0 -1
  75. package/dist/encryption.d.ts +0 -73
  76. package/dist/encryption.d.ts.map +0 -1
  77. package/dist/encryption.js +0 -171
  78. package/dist/encryption.js.map +0 -1
  79. package/dist/ios-actions.d.ts +0 -11
  80. package/dist/ios-actions.d.ts.map +0 -1
  81. package/dist/ios-actions.js +0 -228
  82. package/dist/ios-actions.js.map +0 -1
  83. package/dist/ios-manager.d.ts +0 -266
  84. package/dist/ios-manager.d.ts.map +0 -1
  85. package/dist/ios-manager.js +0 -1073
  86. package/dist/ios-manager.js.map +0 -1
  87. package/dist/protocol.d.ts +0 -26
  88. package/dist/protocol.d.ts.map +0 -1
  89. package/dist/protocol.js +0 -990
  90. package/dist/protocol.js.map +0 -1
  91. package/dist/snapshot.d.ts +0 -67
  92. package/dist/snapshot.d.ts.map +0 -1
  93. package/dist/snapshot.js +0 -514
  94. package/dist/snapshot.js.map +0 -1
  95. package/dist/state-utils.d.ts +0 -77
  96. package/dist/state-utils.d.ts.map +0 -1
  97. package/dist/state-utils.js +0 -178
  98. package/dist/state-utils.js.map +0 -1
  99. package/dist/stealth.d.ts +0 -41
  100. package/dist/stealth.d.ts.map +0 -1
  101. package/dist/stealth.js +0 -1743
  102. package/dist/stealth.js.map +0 -1
  103. package/dist/stream-server.d.ts +0 -117
  104. package/dist/stream-server.d.ts.map +0 -1
  105. package/dist/stream-server.js +0 -309
  106. package/dist/stream-server.js.map +0 -1
  107. package/dist/types.d.ts +0 -973
  108. package/dist/types.d.ts.map +0 -1
  109. package/dist/types.js +0 -2
  110. package/dist/types.js.map +0 -1
  111. package/scripts/check-creepjs-headless.js +0 -137
  112. package/scripts/check-daemon-pid-recovery.js +0 -148
  113. package/scripts/check-sannysoft-webdriver.js +0 -112
  114. package/scripts/check-stealth-regression.js +0 -199
  115. package/scripts/check-turnstile-testkey.ts +0 -125
  116. package/scripts/clawhub-sync.sh +0 -27
  117. package/scripts/sync-upstream.sh +0 -142
  118. package/scripts/verify-bundled-binaries.js +0 -71
  119. package/scripts/verify-native-version.js +0 -48
  120. package/scripts/verify-packed-host-binary.js +0 -88
  121. package/scripts/verify-registry-host-binary.js +0 -120
  122. package/skills/agent-browser-stealth/SKILL.md +0 -127
@@ -0,0 +1,1225 @@
1
+ const PANEL_GET_STATE = 'AB_PANEL_GET_STATE';
2
+ const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS';
3
+ const PANEL_SET_OPTIONS = 'AB_PANEL_SET_OPTIONS';
4
+ const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY';
5
+ const PANEL_CLOSE_OTHER_TABS = 'AB_PANEL_CLOSE_OTHER_SESSION_TABS';
6
+ const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION';
7
+ const PANEL_RUN_ACTION = 'AB_PANEL_RUN_ACTION';
8
+ const PANEL_CLEAR_ACTIVITY = 'AB_PANEL_CLEAR_ACTIVITY';
9
+ const PANEL_START_RECORDING = 'AB_PANEL_START_RECORDING';
10
+ const PANEL_STOP_RECORDING = 'AB_PANEL_STOP_RECORDING';
11
+ const PANEL_SAVE_RECORDING = 'AB_PANEL_SAVE_RECORDING';
12
+ const PANEL_RUN_WORKFLOW = 'AB_PANEL_RUN_WORKFLOW';
13
+ const PANEL_DELETE_WORKFLOW = 'AB_PANEL_DELETE_WORKFLOW';
14
+ const PANEL_SET_SHORTCUT = 'AB_PANEL_SET_SHORTCUT';
15
+ const PANEL_DELETE_SHORTCUT = 'AB_PANEL_DELETE_SHORTCUT';
16
+ const PANEL_RUN_SHORTCUT = 'AB_PANEL_RUN_SHORTCUT';
17
+ const PANEL_CREATE_SCHEDULE = 'AB_PANEL_CREATE_SCHEDULE';
18
+ const PANEL_DELETE_SCHEDULE = 'AB_PANEL_DELETE_SCHEDULE';
19
+ const PANEL_TOGGLE_SCHEDULE = 'AB_PANEL_TOGGLE_SCHEDULE';
20
+
21
+ const summaryEl = document.getElementById('summary');
22
+ const controlEl = document.getElementById('control');
23
+ const automationEl = document.getElementById('automation');
24
+ const developerEl = document.getElementById('developer');
25
+ const sessionsEl = document.getElementById('sessions');
26
+ const downloadsEl = document.getElementById('downloads');
27
+ const statusLineEl = document.getElementById('status-line');
28
+ const refreshBtn = document.getElementById('refresh-btn');
29
+ const cleanupBtn = document.getElementById('cleanup-btn');
30
+
31
+ const viewState = {
32
+ panelState: null,
33
+ lastDomState: null,
34
+ };
35
+
36
+ async function send(message) {
37
+ return chrome.runtime.sendMessage(message);
38
+ }
39
+
40
+ function normalizePanelState(rawState) {
41
+ const state = rawState && typeof rawState === 'object' ? rawState : {};
42
+
43
+ return {
44
+ extensionId: typeof state.extensionId === 'string' ? state.extensionId : 'unknown',
45
+ latestDomState:
46
+ state.latestDomState && typeof state.latestDomState === 'object' ? state.latestDomState : null,
47
+ options:
48
+ state.options && typeof state.options === 'object'
49
+ ? {
50
+ strictWindowIsolation: state.options.strictWindowIsolation !== false,
51
+ suppressCrossWindowActivation: state.options.suppressCrossWindowActivation !== false,
52
+ autoCleanEmptyGroups: state.options.autoCleanEmptyGroups !== false,
53
+ pageBridgeEnabled: state.options.pageBridgeEnabled === true,
54
+ }
55
+ : {
56
+ strictWindowIsolation: true,
57
+ suppressCrossWindowActivation: true,
58
+ autoCleanEmptyGroups: true,
59
+ pageBridgeEnabled: false,
60
+ },
61
+ totals:
62
+ state.totals && typeof state.totals === 'object'
63
+ ? {
64
+ sessions: Number.isFinite(state.totals.sessions) ? state.totals.sessions : 0,
65
+ tabs: Number.isFinite(state.totals.tabs) ? state.totals.tabs : 0,
66
+ }
67
+ : {
68
+ sessions: 0,
69
+ tabs: 0,
70
+ },
71
+ sessions: Array.isArray(state.sessions) ? state.sessions : [],
72
+ downloads: Array.isArray(state.downloads) ? state.downloads : [],
73
+ control:
74
+ state.control && typeof state.control === 'object'
75
+ ? {
76
+ activeTab:
77
+ state.control.activeTab && typeof state.control.activeTab === 'object'
78
+ ? state.control.activeTab
79
+ : null,
80
+ tabs: Array.isArray(state.control.tabs) ? state.control.tabs : [],
81
+ }
82
+ : {
83
+ activeTab: null,
84
+ tabs: [],
85
+ },
86
+ activity:
87
+ state.activity && typeof state.activity === 'object'
88
+ ? {
89
+ events: Array.isArray(state.activity.events) ? state.activity.events : [],
90
+ console: Array.isArray(state.activity.console) ? state.activity.console : [],
91
+ network: Array.isArray(state.activity.network) ? state.activity.network : [],
92
+ commandHistory: Array.isArray(state.activity.commandHistory)
93
+ ? state.activity.commandHistory
94
+ : [],
95
+ }
96
+ : {
97
+ events: [],
98
+ console: [],
99
+ network: [],
100
+ commandHistory: [],
101
+ },
102
+ automation:
103
+ state.automation && typeof state.automation === 'object'
104
+ ? {
105
+ recording:
106
+ state.automation.recording && typeof state.automation.recording === 'object'
107
+ ? state.automation.recording
108
+ : null,
109
+ workflows: Array.isArray(state.automation.workflows) ? state.automation.workflows : [],
110
+ shortcuts: Array.isArray(state.automation.shortcuts) ? state.automation.shortcuts : [],
111
+ schedules: Array.isArray(state.automation.schedules) ? state.automation.schedules : [],
112
+ }
113
+ : {
114
+ recording: null,
115
+ workflows: [],
116
+ shortcuts: [],
117
+ schedules: [],
118
+ },
119
+ };
120
+ }
121
+
122
+ function setStatus(text, tone = 'ok') {
123
+ statusLineEl.textContent = text || '';
124
+ statusLineEl.className = `status-line ${tone}`;
125
+ }
126
+
127
+ function escapeInline(value) {
128
+ return String(value || '')
129
+ .replace(/[\n\r\t]+/g, ' ')
130
+ .replace(/&/g, '&')
131
+ .replace(/</g, '&lt;')
132
+ .replace(/>/g, '&gt;')
133
+ .replace(/\"/g, '&quot;')
134
+ .replace(/'/g, '&#39;')
135
+ .slice(0, 240);
136
+ }
137
+
138
+ function formatTime(ts) {
139
+ if (!ts) return '-';
140
+ try {
141
+ return new Date(ts).toLocaleString();
142
+ } catch {
143
+ return String(ts);
144
+ }
145
+ }
146
+
147
+ function createTag(text) {
148
+ const span = document.createElement('span');
149
+ span.className = 'tag';
150
+ span.textContent = text;
151
+ return span;
152
+ }
153
+
154
+ function parseTimeInput(timeText) {
155
+ const match = String(timeText || '').trim().match(/^(\d{1,2}):(\d{2})$/);
156
+ if (!match) return null;
157
+ const hour = Number.parseInt(match[1], 10);
158
+ const minute = Number.parseInt(match[2], 10);
159
+ if (Number.isNaN(hour) || Number.isNaN(minute)) return null;
160
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
161
+ return { hour, minute };
162
+ }
163
+
164
+ async function runAction(action, args = {}, tabId) {
165
+ const response = await send({
166
+ type: PANEL_RUN_ACTION,
167
+ action,
168
+ args,
169
+ tabId,
170
+ });
171
+
172
+ if (!response || response.ok !== true) {
173
+ setStatus(`Action failed: ${response?.error || 'unknown error'}`, 'error');
174
+ return response;
175
+ }
176
+
177
+ if (response.state) {
178
+ viewState.lastDomState = response.state;
179
+ }
180
+
181
+ setStatus(`Action succeeded: ${action}`, 'ok');
182
+ return response;
183
+ }
184
+
185
+ function renderSummary(state) {
186
+ summaryEl.innerHTML = '';
187
+
188
+ const title = document.createElement('h3');
189
+ title.textContent = 'Overview';
190
+ summaryEl.appendChild(title);
191
+
192
+ const extensionInfo = document.createElement('div');
193
+ extensionInfo.className = 'caption';
194
+ extensionInfo.textContent = `Extension ID: ${state.extensionId}`;
195
+ summaryEl.appendChild(extensionInfo);
196
+
197
+ const tags = document.createElement('div');
198
+ tags.className = 'tags';
199
+ tags.appendChild(createTag(`Sessions: ${state.totals.sessions}`));
200
+ tags.appendChild(createTag(`Tabs: ${state.totals.tabs}`));
201
+ tags.appendChild(
202
+ createTag(`Isolation: ${state.options.strictWindowIsolation === false ? 'Off' : 'On'}`)
203
+ );
204
+ tags.appendChild(
205
+ createTag(`Guard: ${state.options.suppressCrossWindowActivation === false ? 'Off' : 'On'}`)
206
+ );
207
+ tags.appendChild(createTag(`PageBridge: ${state.options.pageBridgeEnabled ? 'On' : 'Off'}`));
208
+ tags.appendChild(createTag(`Workflows: ${state.automation.workflows.length}`));
209
+ tags.appendChild(createTag(`Schedules: ${state.automation.schedules.length}`));
210
+ summaryEl.appendChild(tags);
211
+
212
+ const actions = document.createElement('div');
213
+ actions.className = 'row wrap';
214
+
215
+ const isolationBtn = document.createElement('button');
216
+ isolationBtn.textContent = 'Toggle Isolation';
217
+ isolationBtn.addEventListener('click', async () => {
218
+ await send({
219
+ type: PANEL_SET_OPTIONS,
220
+ options: {
221
+ ...state.options,
222
+ strictWindowIsolation: state.options.strictWindowIsolation === false,
223
+ },
224
+ });
225
+ await refresh();
226
+ });
227
+
228
+ const guardBtn = document.createElement('button');
229
+ guardBtn.textContent = 'Toggle Guard';
230
+ guardBtn.addEventListener('click', async () => {
231
+ await send({
232
+ type: PANEL_SET_OPTIONS,
233
+ options: {
234
+ ...state.options,
235
+ suppressCrossWindowActivation: state.options.suppressCrossWindowActivation === false,
236
+ },
237
+ });
238
+ await refresh();
239
+ });
240
+
241
+ const cleanBtn = document.createElement('button');
242
+ cleanBtn.textContent = 'Toggle Auto-Clean';
243
+ cleanBtn.addEventListener('click', async () => {
244
+ await send({
245
+ type: PANEL_SET_OPTIONS,
246
+ options: {
247
+ ...state.options,
248
+ autoCleanEmptyGroups: state.options.autoCleanEmptyGroups === false,
249
+ },
250
+ });
251
+ await refresh();
252
+ });
253
+
254
+ actions.appendChild(isolationBtn);
255
+ actions.appendChild(guardBtn);
256
+ actions.appendChild(cleanBtn);
257
+ summaryEl.appendChild(actions);
258
+ }
259
+
260
+ function renderControl(state) {
261
+ const control = state.control || { activeTab: null, tabs: [] };
262
+
263
+ controlEl.innerHTML = `
264
+ <h3>Browser Control</h3>
265
+ <div class="row">
266
+ <input id="ctl-url" placeholder="Open URL (example.com)" class="mono" />
267
+ <button id="ctl-open" class="primary">Open</button>
268
+ </div>
269
+ <div class="row wrap">
270
+ <button id="ctl-back">Back</button>
271
+ <button id="ctl-forward">Forward</button>
272
+ <button id="ctl-reload">Reload</button>
273
+ <button id="ctl-snapshot">Snapshot</button>
274
+ <button id="ctl-dom">DOM State</button>
275
+ </div>
276
+ <div class="row">
277
+ <input id="ctl-selector" placeholder="CSS selector" class="mono" />
278
+ <input id="ctl-value" placeholder="Value / text" />
279
+ </div>
280
+ <div class="row wrap">
281
+ <button id="ctl-click">Click</button>
282
+ <button id="ctl-fill">Fill</button>
283
+ <input id="ctl-key" placeholder="Key (Enter)" style="max-width:120px" />
284
+ <button id="ctl-press">Press</button>
285
+ </div>
286
+ <div class="row">
287
+ <input id="ctl-shortcut" placeholder="Shortcut name (/login or login)" class="mono" />
288
+ <button id="ctl-run-shortcut">Run Shortcut</button>
289
+ </div>
290
+ <hr />
291
+ <div class="section-title">
292
+ <h4>Tabs</h4>
293
+ <span class="caption" id="ctl-active"></span>
294
+ </div>
295
+ <div id="ctl-tabs" class="list"></div>
296
+ `;
297
+
298
+ const activeTab = control.activeTab;
299
+ const activeText = activeTab
300
+ ? `Active: #${activeTab.id} ${escapeInline(activeTab.title)}`
301
+ : 'No active tab';
302
+ controlEl.querySelector('#ctl-active').textContent = activeText;
303
+
304
+ const urlInput = controlEl.querySelector('#ctl-url');
305
+ const selectorInput = controlEl.querySelector('#ctl-selector');
306
+ const valueInput = controlEl.querySelector('#ctl-value');
307
+ const keyInput = controlEl.querySelector('#ctl-key');
308
+ const shortcutInput = controlEl.querySelector('#ctl-shortcut');
309
+
310
+ controlEl.querySelector('#ctl-open').addEventListener('click', async () => {
311
+ const url = urlInput.value.trim();
312
+ if (!url) {
313
+ setStatus('Please enter a URL', 'warn');
314
+ return;
315
+ }
316
+ await runAction('open', { url }, activeTab?.id);
317
+ await refresh();
318
+ });
319
+
320
+ controlEl.querySelector('#ctl-back').addEventListener('click', async () => {
321
+ await runAction('back', {}, activeTab?.id);
322
+ await refresh();
323
+ });
324
+
325
+ controlEl.querySelector('#ctl-forward').addEventListener('click', async () => {
326
+ await runAction('forward', {}, activeTab?.id);
327
+ await refresh();
328
+ });
329
+
330
+ controlEl.querySelector('#ctl-reload').addEventListener('click', async () => {
331
+ await runAction('reload', {}, activeTab?.id);
332
+ await refresh();
333
+ });
334
+
335
+ controlEl.querySelector('#ctl-snapshot').addEventListener('click', async () => {
336
+ const selector = selectorInput.value.trim();
337
+ const response = await runAction(
338
+ 'snapshot',
339
+ {
340
+ selector: selector || undefined,
341
+ interactiveOnly: true,
342
+ maxNodes: 80,
343
+ },
344
+ activeTab?.id
345
+ );
346
+ if (response?.state) {
347
+ viewState.lastDomState = response.state;
348
+ renderDeveloper(state);
349
+ }
350
+ });
351
+
352
+ controlEl.querySelector('#ctl-dom').addEventListener('click', async () => {
353
+ const selector = selectorInput.value.trim();
354
+ const response = await runAction(
355
+ 'dom-state',
356
+ {
357
+ selector: selector || undefined,
358
+ interactiveOnly: false,
359
+ maxNodes: 100,
360
+ },
361
+ activeTab?.id
362
+ );
363
+ if (response?.state) {
364
+ viewState.lastDomState = response.state;
365
+ renderDeveloper(state);
366
+ }
367
+ });
368
+
369
+ controlEl.querySelector('#ctl-click').addEventListener('click', async () => {
370
+ const selector = selectorInput.value.trim();
371
+ if (!selector) {
372
+ setStatus('Selector is required for click', 'warn');
373
+ return;
374
+ }
375
+ await runAction('click', { selector }, activeTab?.id);
376
+ await refresh();
377
+ });
378
+
379
+ controlEl.querySelector('#ctl-fill').addEventListener('click', async () => {
380
+ const selector = selectorInput.value.trim();
381
+ if (!selector) {
382
+ setStatus('Selector is required for fill', 'warn');
383
+ return;
384
+ }
385
+ await runAction(
386
+ 'fill',
387
+ {
388
+ selector,
389
+ value: valueInput.value,
390
+ },
391
+ activeTab?.id
392
+ );
393
+ await refresh();
394
+ });
395
+
396
+ controlEl.querySelector('#ctl-press').addEventListener('click', async () => {
397
+ await runAction(
398
+ 'press',
399
+ {
400
+ selector: selectorInput.value.trim() || undefined,
401
+ key: keyInput.value.trim() || 'Enter',
402
+ },
403
+ activeTab?.id
404
+ );
405
+ await refresh();
406
+ });
407
+
408
+ controlEl.querySelector('#ctl-run-shortcut').addEventListener('click', async () => {
409
+ const raw = shortcutInput.value.trim();
410
+ if (!raw) {
411
+ setStatus('Shortcut name is required', 'warn');
412
+ return;
413
+ }
414
+
415
+ const name = raw.startsWith('/') ? raw.slice(1) : raw;
416
+ const response = await send({
417
+ type: PANEL_RUN_SHORTCUT,
418
+ name,
419
+ tabId: activeTab?.id,
420
+ });
421
+
422
+ if (!response || response.ok !== true) {
423
+ setStatus(`Shortcut failed: ${response?.error || 'unknown error'}`, 'error');
424
+ return;
425
+ }
426
+
427
+ setStatus(`Shortcut executed: /${name}`, 'ok');
428
+ await refresh();
429
+ });
430
+
431
+ const tabsEl = controlEl.querySelector('#ctl-tabs');
432
+ if (!Array.isArray(control.tabs) || control.tabs.length === 0) {
433
+ const empty = document.createElement('div');
434
+ empty.className = 'empty';
435
+ empty.textContent = 'No tabs in current window.';
436
+ tabsEl.appendChild(empty);
437
+ } else {
438
+ for (const tab of control.tabs.slice(0, 20)) {
439
+ const item = document.createElement('div');
440
+ item.className = 'item';
441
+
442
+ const title = document.createElement('div');
443
+ title.className = 'item-title';
444
+ title.textContent = `#${tab.id} [${tab.index}] ${escapeInline(tab.title)}`;
445
+
446
+ const url = document.createElement('div');
447
+ url.className = 'item-url';
448
+ url.textContent = tab.url || 'about:blank';
449
+
450
+ const row = document.createElement('div');
451
+ row.className = 'row wrap';
452
+
453
+ const switchBtn = document.createElement('button');
454
+ switchBtn.textContent = tab.active ? 'Active' : 'Switch';
455
+ switchBtn.disabled = tab.active === true;
456
+ switchBtn.addEventListener('click', async () => {
457
+ await runAction('tabs:switch', { tabId: tab.id });
458
+ await refresh();
459
+ });
460
+
461
+ const closeBtn = document.createElement('button');
462
+ closeBtn.className = 'danger';
463
+ closeBtn.textContent = 'Close';
464
+ closeBtn.addEventListener('click', async () => {
465
+ await runAction('tabs:close', {}, tab.id);
466
+ await refresh();
467
+ });
468
+
469
+ row.appendChild(switchBtn);
470
+ row.appendChild(closeBtn);
471
+
472
+ if (tab.session) {
473
+ const pill = document.createElement('span');
474
+ pill.className = 'event-pill';
475
+ pill.textContent = `session:${tab.session}`;
476
+ row.appendChild(pill);
477
+ }
478
+
479
+ item.appendChild(title);
480
+ item.appendChild(url);
481
+ item.appendChild(row);
482
+ tabsEl.appendChild(item);
483
+ }
484
+ }
485
+ }
486
+
487
+ function renderSessions(state) {
488
+ sessionsEl.innerHTML = '';
489
+
490
+ const heading = document.createElement('h3');
491
+ heading.textContent = 'Managed Sessions';
492
+ sessionsEl.appendChild(heading);
493
+
494
+ if (!state.sessions || state.sessions.length === 0) {
495
+ const empty = document.createElement('div');
496
+ empty.className = 'card empty';
497
+ empty.textContent = 'No active managed sessions found.';
498
+ sessionsEl.appendChild(empty);
499
+ return;
500
+ }
501
+
502
+ for (const session of state.sessions) {
503
+ const card = document.createElement('article');
504
+ card.className = 'card';
505
+
506
+ const titleRow = document.createElement('div');
507
+ titleRow.className = 'section-title';
508
+
509
+ const title = document.createElement('h4');
510
+ title.textContent = session.session;
511
+
512
+ const actionRow = document.createElement('div');
513
+ actionRow.className = 'row wrap';
514
+
515
+ const focusBtn = document.createElement('button');
516
+ focusBtn.textContent = 'Focus';
517
+ focusBtn.addEventListener('click', async () => {
518
+ await send({ type: PANEL_FOCUS_SESSION, session: session.session });
519
+ await refresh();
520
+ });
521
+
522
+ const isolateBtn = document.createElement('button');
523
+ isolateBtn.textContent = 'Isolate';
524
+ isolateBtn.addEventListener('click', async () => {
525
+ await send({ type: PANEL_CLOSE_OTHER_TABS, session: session.session });
526
+ await refresh();
527
+ });
528
+
529
+ const policyBtn = document.createElement('button');
530
+ policyBtn.textContent = 'Policy';
531
+ policyBtn.addEventListener('click', async () => {
532
+ const current = (session.allowedDomains || []).join(',');
533
+ const input = window.prompt('Allowed domains (comma-separated)', current);
534
+ if (input === null) return;
535
+ const allowedDomains = input
536
+ .split(',')
537
+ .map((item) => item.trim().toLowerCase())
538
+ .filter((item) => item.length > 0);
539
+ await send({ type: PANEL_SET_POLICY, session: session.session, allowedDomains });
540
+ await refresh();
541
+ });
542
+
543
+ actionRow.appendChild(focusBtn);
544
+ actionRow.appendChild(isolateBtn);
545
+ actionRow.appendChild(policyBtn);
546
+
547
+ titleRow.appendChild(title);
548
+ titleRow.appendChild(actionRow);
549
+
550
+ const tags = document.createElement('div');
551
+ tags.className = 'tags';
552
+ tags.appendChild(createTag(`Window ${session.windowId ?? 'N/A'}`));
553
+ tags.appendChild(createTag(`${session.tabs.length} tabs`));
554
+
555
+ if (session.group?.title) {
556
+ tags.appendChild(createTag(`Group: ${session.group.title}`));
557
+ }
558
+
559
+ const list = document.createElement('div');
560
+ list.className = 'list';
561
+
562
+ for (const tab of session.tabs.slice(0, 8)) {
563
+ const item = document.createElement('div');
564
+ item.className = 'item';
565
+
566
+ const tabTitle = document.createElement('div');
567
+ tabTitle.className = 'item-title';
568
+ tabTitle.textContent = `${tab.active ? '● ' : ''}#${tab.id} ${escapeInline(tab.title || '(Untitled)')}`;
569
+
570
+ const tabUrl = document.createElement('div');
571
+ tabUrl.className = 'item-url';
572
+ tabUrl.textContent = tab.url || 'about:blank';
573
+
574
+ item.appendChild(tabTitle);
575
+ item.appendChild(tabUrl);
576
+ list.appendChild(item);
577
+ }
578
+
579
+ card.appendChild(titleRow);
580
+ card.appendChild(tags);
581
+ card.appendChild(list);
582
+ sessionsEl.appendChild(card);
583
+ }
584
+ }
585
+
586
+ function renderDownloads(state) {
587
+ downloadsEl.innerHTML = '<h3>Recent Downloads</h3>';
588
+
589
+ const list = document.createElement('div');
590
+ list.className = 'list';
591
+
592
+ const entries = state.downloads || [];
593
+ if (entries.length === 0) {
594
+ const empty = document.createElement('div');
595
+ empty.className = 'empty';
596
+ empty.textContent = 'No download events yet.';
597
+ list.appendChild(empty);
598
+ } else {
599
+ for (const entry of entries.slice(0, 10)) {
600
+ const item = document.createElement('div');
601
+ item.className = 'item';
602
+
603
+ const title = document.createElement('div');
604
+ title.className = 'item-title';
605
+ title.textContent = `#${entry.id} · ${entry.state || 'updated'}`;
606
+
607
+ const filename = document.createElement('div');
608
+ filename.className = 'item-url';
609
+ filename.textContent = entry.filename || '(no filename)';
610
+
611
+ const meta = document.createElement('div');
612
+ meta.className = 'caption';
613
+ meta.textContent = formatTime(entry.timestamp);
614
+
615
+ item.appendChild(title);
616
+ item.appendChild(filename);
617
+ item.appendChild(meta);
618
+ list.appendChild(item);
619
+ }
620
+ }
621
+
622
+ downloadsEl.appendChild(list);
623
+ }
624
+
625
+ async function promptAndCreateSchedule(workflowId, workflowName) {
626
+ const cadenceKind = (window.prompt('Cadence: daily | weekly | monthly | yearly', 'daily') || '')
627
+ .trim()
628
+ .toLowerCase();
629
+
630
+ if (!cadenceKind) return;
631
+ if (!['daily', 'weekly', 'monthly', 'yearly'].includes(cadenceKind)) {
632
+ setStatus('Invalid cadence. Use daily/weekly/monthly/yearly.', 'warn');
633
+ return;
634
+ }
635
+
636
+ const timeInput = parseTimeInput(window.prompt('Time (HH:MM, 24h)', '09:00'));
637
+ if (!timeInput) {
638
+ setStatus('Invalid time format.', 'warn');
639
+ return;
640
+ }
641
+
642
+ const cadence = {
643
+ kind: cadenceKind,
644
+ hour: timeInput.hour,
645
+ minute: timeInput.minute,
646
+ };
647
+
648
+ if (cadenceKind === 'weekly') {
649
+ const weekday = Number.parseInt(
650
+ window.prompt('Weekday (0=Sun .. 6=Sat)', String(new Date().getDay())) || '',
651
+ 10
652
+ );
653
+ if (Number.isNaN(weekday) || weekday < 0 || weekday > 6) {
654
+ setStatus('Invalid weekday.', 'warn');
655
+ return;
656
+ }
657
+ cadence.weekdays = [weekday];
658
+ }
659
+
660
+ if (cadenceKind === 'monthly') {
661
+ const day = Number.parseInt(window.prompt('Day of month (1-31)', '1') || '', 10);
662
+ if (Number.isNaN(day) || day < 1 || day > 31) {
663
+ setStatus('Invalid day of month.', 'warn');
664
+ return;
665
+ }
666
+ cadence.dayOfMonth = day;
667
+ }
668
+
669
+ if (cadenceKind === 'yearly') {
670
+ const month = Number.parseInt(window.prompt('Month (1-12)', '1') || '', 10);
671
+ const day = Number.parseInt(window.prompt('Day of month (1-31)', '1') || '', 10);
672
+ if (Number.isNaN(month) || month < 1 || month > 12 || Number.isNaN(day) || day < 1 || day > 31) {
673
+ setStatus('Invalid month/day.', 'warn');
674
+ return;
675
+ }
676
+ cadence.month = month;
677
+ cadence.dayOfMonth = day;
678
+ }
679
+
680
+ const response = await send({
681
+ type: PANEL_CREATE_SCHEDULE,
682
+ schedule: {
683
+ name: `Schedule ${workflowName}`,
684
+ workflowId,
685
+ cadence,
686
+ enabled: true,
687
+ },
688
+ });
689
+
690
+ if (!response || response.ok !== true) {
691
+ setStatus(`Create schedule failed: ${response?.error || 'unknown error'}`, 'error');
692
+ return;
693
+ }
694
+
695
+ setStatus('Schedule created.', 'ok');
696
+ await refresh();
697
+ }
698
+
699
+ function renderAutomation(state) {
700
+ const automation = state.automation;
701
+ const activeTabId = state.control?.activeTab?.id;
702
+
703
+ automationEl.innerHTML = `
704
+ <h3>Automation</h3>
705
+ <div class="section-title">
706
+ <h4>Recording</h4>
707
+ <span class="caption" id="recording-state"></span>
708
+ </div>
709
+ <div class="row">
710
+ <input id="record-name" placeholder="Workflow name" />
711
+ <button id="record-start">Start</button>
712
+ <button id="record-stop">Stop</button>
713
+ <button id="record-save" class="primary">Save</button>
714
+ </div>
715
+ <div id="recording-steps" class="list"></div>
716
+ <hr />
717
+ <div class="section-title"><h4>Workflows</h4></div>
718
+ <div id="workflow-list" class="list"></div>
719
+ <hr />
720
+ <div class="grid-2">
721
+ <div>
722
+ <div class="section-title"><h4>Shortcuts</h4></div>
723
+ <div id="shortcut-list" class="list"></div>
724
+ </div>
725
+ <div>
726
+ <div class="section-title"><h4>Schedules</h4></div>
727
+ <div id="schedule-list" class="list"></div>
728
+ </div>
729
+ </div>
730
+ `;
731
+
732
+ const recordingStateEl = automationEl.querySelector('#recording-state');
733
+ const recordingStepsEl = automationEl.querySelector('#recording-steps');
734
+ const recordNameInput = automationEl.querySelector('#record-name');
735
+ const workflowListEl = automationEl.querySelector('#workflow-list');
736
+ const shortcutListEl = automationEl.querySelector('#shortcut-list');
737
+ const scheduleListEl = automationEl.querySelector('#schedule-list');
738
+
739
+ if (automation.recording) {
740
+ recordingStateEl.textContent = `ON · ${automation.recording.stepCount} steps`;
741
+ recordNameInput.value = automation.recording.name || '';
742
+
743
+ for (const step of automation.recording.steps) {
744
+ const item = document.createElement('div');
745
+ item.className = 'item';
746
+ item.innerHTML = `<div class="item-title">${escapeInline(step.action)}</div><div class="item-url">${escapeInline(JSON.stringify(step.args || {}))}</div>`;
747
+ recordingStepsEl.appendChild(item);
748
+ }
749
+
750
+ if (automation.recording.steps.length === 0) {
751
+ const empty = document.createElement('div');
752
+ empty.className = 'caption';
753
+ empty.textContent = 'Recording is active. Perform actions from Browser Control.';
754
+ recordingStepsEl.appendChild(empty);
755
+ }
756
+ } else {
757
+ recordingStateEl.textContent = 'OFF';
758
+ const empty = document.createElement('div');
759
+ empty.className = 'caption';
760
+ empty.textContent = 'Start recording to capture actions into a reusable workflow.';
761
+ recordingStepsEl.appendChild(empty);
762
+ }
763
+
764
+ automationEl.querySelector('#record-start').addEventListener('click', async () => {
765
+ const result = await send({
766
+ type: PANEL_START_RECORDING,
767
+ name: recordNameInput.value.trim() || 'Recorded Workflow',
768
+ });
769
+
770
+ if (!result || result.ok !== true) {
771
+ setStatus(`Start recording failed: ${result?.error || 'unknown error'}`, 'error');
772
+ return;
773
+ }
774
+
775
+ setStatus('Recording started.', 'ok');
776
+ await refresh();
777
+ });
778
+
779
+ automationEl.querySelector('#record-stop').addEventListener('click', async () => {
780
+ const result = await send({ type: PANEL_STOP_RECORDING });
781
+ if (!result || result.ok !== true) {
782
+ setStatus(`Stop recording failed: ${result?.error || 'unknown error'}`, 'error');
783
+ return;
784
+ }
785
+
786
+ setStatus('Recording stopped.', 'ok');
787
+ await refresh();
788
+ });
789
+
790
+ automationEl.querySelector('#record-save').addEventListener('click', async () => {
791
+ const result = await send({
792
+ type: PANEL_SAVE_RECORDING,
793
+ name: recordNameInput.value.trim() || undefined,
794
+ });
795
+
796
+ if (!result || result.ok !== true) {
797
+ setStatus(`Save recording failed: ${result?.error || 'unknown error'}`, 'error');
798
+ return;
799
+ }
800
+
801
+ setStatus(`Workflow saved: ${result.workflow?.name || ''}`, 'ok');
802
+ await refresh();
803
+ });
804
+
805
+ if (!automation.workflows || automation.workflows.length === 0) {
806
+ const empty = document.createElement('div');
807
+ empty.className = 'empty';
808
+ empty.textContent = 'No workflows saved yet.';
809
+ workflowListEl.appendChild(empty);
810
+ } else {
811
+ for (const workflow of automation.workflows) {
812
+ const item = document.createElement('div');
813
+ item.className = 'item';
814
+
815
+ const title = document.createElement('div');
816
+ title.className = 'item-title';
817
+ title.textContent = `${workflow.name} (${workflow.stepCount} steps)`;
818
+
819
+ const meta = document.createElement('div');
820
+ meta.className = 'caption';
821
+ meta.textContent = `Updated: ${formatTime(workflow.updatedAt)}`;
822
+
823
+ const row = document.createElement('div');
824
+ row.className = 'row wrap';
825
+
826
+ const runBtn = document.createElement('button');
827
+ runBtn.textContent = 'Run';
828
+ runBtn.addEventListener('click', async () => {
829
+ const response = await send({
830
+ type: PANEL_RUN_WORKFLOW,
831
+ workflowId: workflow.id,
832
+ tabId: activeTabId,
833
+ });
834
+
835
+ if (!response || response.ok !== true) {
836
+ setStatus(`Workflow failed: ${response?.error || 'unknown error'}`, 'error');
837
+ return;
838
+ }
839
+
840
+ setStatus(`Workflow executed: ${workflow.name}`, 'ok');
841
+ await refresh();
842
+ });
843
+
844
+ const shortcutBtn = document.createElement('button');
845
+ shortcutBtn.textContent = 'Set Shortcut';
846
+ shortcutBtn.addEventListener('click', async () => {
847
+ const defaultName = workflow.name
848
+ .toLowerCase()
849
+ .replace(/[^a-z0-9]+/g, '-')
850
+ .replace(/^-+|-+$/g, '')
851
+ .slice(0, 32);
852
+ const input = window.prompt('Shortcut name (without /)', defaultName || 'workflow');
853
+ if (!input) return;
854
+
855
+ const result = await send({
856
+ type: PANEL_SET_SHORTCUT,
857
+ name: input,
858
+ workflowId: workflow.id,
859
+ });
860
+
861
+ if (!result || result.ok !== true) {
862
+ setStatus(`Set shortcut failed: ${result?.error || 'unknown error'}`, 'error');
863
+ return;
864
+ }
865
+
866
+ setStatus(`Shortcut saved: /${result.shortcut.name}`, 'ok');
867
+ await refresh();
868
+ });
869
+
870
+ const scheduleBtn = document.createElement('button');
871
+ scheduleBtn.textContent = 'Schedule';
872
+ scheduleBtn.addEventListener('click', async () => {
873
+ await promptAndCreateSchedule(workflow.id, workflow.name);
874
+ });
875
+
876
+ const deleteBtn = document.createElement('button');
877
+ deleteBtn.className = 'danger';
878
+ deleteBtn.textContent = 'Delete';
879
+ deleteBtn.addEventListener('click', async () => {
880
+ if (!window.confirm(`Delete workflow \"${workflow.name}\"?`)) return;
881
+ const result = await send({ type: PANEL_DELETE_WORKFLOW, workflowId: workflow.id });
882
+ if (!result || result.ok !== true) {
883
+ setStatus(`Delete workflow failed: ${result?.error || 'unknown error'}`, 'error');
884
+ return;
885
+ }
886
+ setStatus('Workflow deleted.', 'ok');
887
+ await refresh();
888
+ });
889
+
890
+ row.appendChild(runBtn);
891
+ row.appendChild(shortcutBtn);
892
+ row.appendChild(scheduleBtn);
893
+ row.appendChild(deleteBtn);
894
+
895
+ item.appendChild(title);
896
+ item.appendChild(meta);
897
+ item.appendChild(row);
898
+ workflowListEl.appendChild(item);
899
+ }
900
+ }
901
+
902
+ if (!automation.shortcuts || automation.shortcuts.length === 0) {
903
+ const empty = document.createElement('div');
904
+ empty.className = 'empty';
905
+ empty.textContent = 'No shortcuts configured.';
906
+ shortcutListEl.appendChild(empty);
907
+ } else {
908
+ for (const shortcut of automation.shortcuts) {
909
+ const item = document.createElement('div');
910
+ item.className = 'item';
911
+
912
+ const title = document.createElement('div');
913
+ title.className = 'item-title';
914
+ title.textContent = `/${shortcut.name}`;
915
+
916
+ const desc = document.createElement('div');
917
+ desc.className = 'item-url';
918
+ desc.textContent = shortcut.workflowName;
919
+
920
+ const row = document.createElement('div');
921
+ row.className = 'row wrap';
922
+
923
+ const runBtn = document.createElement('button');
924
+ runBtn.textContent = 'Run';
925
+ runBtn.addEventListener('click', async () => {
926
+ const response = await send({
927
+ type: PANEL_RUN_SHORTCUT,
928
+ name: shortcut.name,
929
+ tabId: activeTabId,
930
+ });
931
+
932
+ if (!response || response.ok !== true) {
933
+ setStatus(`Shortcut failed: ${response?.error || 'unknown error'}`, 'error');
934
+ return;
935
+ }
936
+
937
+ setStatus(`Shortcut executed: /${shortcut.name}`, 'ok');
938
+ await refresh();
939
+ });
940
+
941
+ const deleteBtn = document.createElement('button');
942
+ deleteBtn.className = 'danger';
943
+ deleteBtn.textContent = 'Delete';
944
+ deleteBtn.addEventListener('click', async () => {
945
+ const result = await send({ type: PANEL_DELETE_SHORTCUT, name: shortcut.name });
946
+ if (!result || result.ok !== true) {
947
+ setStatus(`Delete shortcut failed: ${result?.error || 'unknown error'}`, 'error');
948
+ return;
949
+ }
950
+ setStatus('Shortcut deleted.', 'ok');
951
+ await refresh();
952
+ });
953
+
954
+ row.appendChild(runBtn);
955
+ row.appendChild(deleteBtn);
956
+
957
+ item.appendChild(title);
958
+ item.appendChild(desc);
959
+ item.appendChild(row);
960
+ shortcutListEl.appendChild(item);
961
+ }
962
+ }
963
+
964
+ if (!automation.schedules || automation.schedules.length === 0) {
965
+ const empty = document.createElement('div');
966
+ empty.className = 'empty';
967
+ empty.textContent = 'No schedules configured.';
968
+ scheduleListEl.appendChild(empty);
969
+ } else {
970
+ for (const schedule of automation.schedules) {
971
+ const item = document.createElement('div');
972
+ item.className = 'item';
973
+
974
+ const title = document.createElement('div');
975
+ title.className = 'item-title';
976
+ title.textContent = schedule.name;
977
+
978
+ const desc = document.createElement('div');
979
+ desc.className = 'item-url';
980
+ desc.textContent = `${schedule.workflowName} · ${schedule.cadence.kind}`;
981
+
982
+ const time = document.createElement('div');
983
+ time.className = 'caption';
984
+ time.textContent = `Next: ${formatTime(schedule.nextRunAt)}`;
985
+
986
+ const row = document.createElement('div');
987
+ row.className = 'row wrap';
988
+
989
+ const toggleBtn = document.createElement('button');
990
+ toggleBtn.textContent = schedule.enabled ? 'Disable' : 'Enable';
991
+ toggleBtn.addEventListener('click', async () => {
992
+ const result = await send({
993
+ type: PANEL_TOGGLE_SCHEDULE,
994
+ scheduleId: schedule.id,
995
+ enabled: !schedule.enabled,
996
+ });
997
+
998
+ if (!result || result.ok !== true) {
999
+ setStatus(`Toggle schedule failed: ${result?.error || 'unknown error'}`, 'error');
1000
+ return;
1001
+ }
1002
+
1003
+ setStatus('Schedule updated.', 'ok');
1004
+ await refresh();
1005
+ });
1006
+
1007
+ const deleteBtn = document.createElement('button');
1008
+ deleteBtn.className = 'danger';
1009
+ deleteBtn.textContent = 'Delete';
1010
+ deleteBtn.addEventListener('click', async () => {
1011
+ const result = await send({
1012
+ type: PANEL_DELETE_SCHEDULE,
1013
+ scheduleId: schedule.id,
1014
+ });
1015
+
1016
+ if (!result || result.ok !== true) {
1017
+ setStatus(`Delete schedule failed: ${result?.error || 'unknown error'}`, 'error');
1018
+ return;
1019
+ }
1020
+
1021
+ setStatus('Schedule deleted.', 'ok');
1022
+ await refresh();
1023
+ });
1024
+
1025
+ row.appendChild(toggleBtn);
1026
+ row.appendChild(deleteBtn);
1027
+
1028
+ item.appendChild(title);
1029
+ item.appendChild(desc);
1030
+ item.appendChild(time);
1031
+ item.appendChild(row);
1032
+ scheduleListEl.appendChild(item);
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ function renderDeveloper(state) {
1038
+ const activity = state.activity;
1039
+ const activeTabId = state.control?.activeTab?.id;
1040
+ const domState = viewState.lastDomState || state.latestDomState || null;
1041
+
1042
+ developerEl.innerHTML = `
1043
+ <h3>Developer Signals</h3>
1044
+ <div class="row wrap">
1045
+ <button id="dev-refresh-dom">Refresh DOM</button>
1046
+ <button id="dev-clear-activity">Clear Events</button>
1047
+ <button id="dev-toggle-bridge">${state.options.pageBridgeEnabled ? 'Disable' : 'Enable'} Page Bridge</button>
1048
+ <span class="caption">Console: ${activity.console.length} · Network: ${activity.network.length}</span>
1049
+ <span class="caption">Bridge: ${state.options.pageBridgeEnabled ? 'ON' : 'OFF (high-risk default)'}</span>
1050
+ </div>
1051
+ <div class="row">
1052
+ <input id="dev-selector" placeholder="DOM selector for capture (optional)" class="mono" />
1053
+ <label class="caption" style="display:flex;align-items:center;gap:4px;white-space:nowrap;">
1054
+ <input type="checkbox" id="dev-interactive-only" style="width:auto" />
1055
+ interactive-only
1056
+ </label>
1057
+ </div>
1058
+ <div class="grid-2">
1059
+ <div>
1060
+ <div class="section-title"><h4>DOM State</h4></div>
1061
+ <pre id="dev-dom-json"></pre>
1062
+ </div>
1063
+ <div>
1064
+ <div class="section-title"><h4>Recent Commands</h4></div>
1065
+ <div id="dev-command-list" class="list"></div>
1066
+ </div>
1067
+ </div>
1068
+ <hr />
1069
+ <div class="grid-2">
1070
+ <div>
1071
+ <div class="section-title"><h4>Console Events</h4></div>
1072
+ <div id="dev-console-list" class="list"></div>
1073
+ </div>
1074
+ <div>
1075
+ <div class="section-title"><h4>Network Events</h4></div>
1076
+ <div id="dev-network-list" class="list"></div>
1077
+ </div>
1078
+ </div>
1079
+ `;
1080
+
1081
+ const domPre = developerEl.querySelector('#dev-dom-json');
1082
+ domPre.textContent = domState
1083
+ ? JSON.stringify(domState, null, 2)
1084
+ : 'No DOM state captured yet. Use Snapshot or DOM State in Browser Control.';
1085
+
1086
+ const commandList = developerEl.querySelector('#dev-command-list');
1087
+ if (!activity.commandHistory || activity.commandHistory.length === 0) {
1088
+ const empty = document.createElement('div');
1089
+ empty.className = 'empty';
1090
+ empty.textContent = 'No commands yet.';
1091
+ commandList.appendChild(empty);
1092
+ } else {
1093
+ for (const entry of activity.commandHistory.slice(0, 8)) {
1094
+ const item = document.createElement('div');
1095
+ item.className = 'item';
1096
+ item.innerHTML = `<div class="item-title">${entry.ok ? 'OK' : 'FAIL'} · ${escapeInline(entry.action)}</div><div class="caption">${formatTime(entry.timestamp)}${entry.error ? ` · ${escapeInline(entry.error)}` : ''}</div>`;
1097
+ commandList.appendChild(item);
1098
+ }
1099
+ }
1100
+
1101
+ const consoleList = developerEl.querySelector('#dev-console-list');
1102
+ if (!activity.console || activity.console.length === 0) {
1103
+ const empty = document.createElement('div');
1104
+ empty.className = 'empty';
1105
+ empty.textContent = 'No console events.';
1106
+ consoleList.appendChild(empty);
1107
+ } else {
1108
+ for (const event of activity.console.slice(0, 12)) {
1109
+ const payload = event.payload || {};
1110
+ const level = payload.level || 'log';
1111
+ const text = payload.message || (Array.isArray(payload.args) ? payload.args.join(' ') : JSON.stringify(payload));
1112
+ const item = document.createElement('div');
1113
+ item.className = 'item';
1114
+ item.innerHTML = `<div class="item-title">${escapeInline(level.toUpperCase())}</div><div class="item-url">${escapeInline(text)}</div><div class="caption">${formatTime(event.timestamp)}</div>`;
1115
+ consoleList.appendChild(item);
1116
+ }
1117
+ }
1118
+
1119
+ const networkList = developerEl.querySelector('#dev-network-list');
1120
+ if (!activity.network || activity.network.length === 0) {
1121
+ const empty = document.createElement('div');
1122
+ empty.className = 'empty';
1123
+ empty.textContent = 'No network events.';
1124
+ networkList.appendChild(empty);
1125
+ } else {
1126
+ for (const event of activity.network.slice(0, 12)) {
1127
+ const payload = event.payload || {};
1128
+ const item = document.createElement('div');
1129
+ item.className = 'item';
1130
+ const line1 = `${payload.transport || 'net'} ${payload.method || ''} ${payload.status || ''}`.trim();
1131
+ const line2 = payload.url || payload.error || '(unknown)';
1132
+ const line3 = payload.durationMs ? `${payload.durationMs} ms` : '';
1133
+ item.innerHTML = `<div class="item-title">${escapeInline(line1)}</div><div class="item-url">${escapeInline(line2)}</div><div class="caption">${escapeInline(line3)} · ${formatTime(event.timestamp)}</div>`;
1134
+ networkList.appendChild(item);
1135
+ }
1136
+ }
1137
+
1138
+ developerEl.querySelector('#dev-refresh-dom').addEventListener('click', async () => {
1139
+ const selector = developerEl.querySelector('#dev-selector').value.trim();
1140
+ const interactiveOnly = developerEl.querySelector('#dev-interactive-only').checked;
1141
+
1142
+ const response = await runAction(
1143
+ 'dom-state',
1144
+ {
1145
+ selector: selector || undefined,
1146
+ interactiveOnly,
1147
+ maxNodes: 120,
1148
+ },
1149
+ activeTabId
1150
+ );
1151
+
1152
+ if (response?.state) {
1153
+ viewState.lastDomState = response.state;
1154
+ renderDeveloper(state);
1155
+ }
1156
+ });
1157
+
1158
+ developerEl.querySelector('#dev-clear-activity').addEventListener('click', async () => {
1159
+ await send({ type: PANEL_CLEAR_ACTIVITY });
1160
+ setStatus('Activity events cleared.', 'ok');
1161
+ await refresh();
1162
+ });
1163
+
1164
+ developerEl.querySelector('#dev-toggle-bridge').addEventListener('click', async () => {
1165
+ const result = await send({
1166
+ type: PANEL_SET_OPTIONS,
1167
+ options: {
1168
+ ...state.options,
1169
+ pageBridgeEnabled: !state.options.pageBridgeEnabled,
1170
+ },
1171
+ });
1172
+ if (!result || result.ok !== true) {
1173
+ setStatus(`Toggle Page Bridge failed: ${result?.error || 'unknown error'}`, 'error');
1174
+ return;
1175
+ }
1176
+ setStatus(
1177
+ `Page Bridge ${!state.options.pageBridgeEnabled ? 'enabled' : 'disabled'} (reload page to apply).`,
1178
+ 'ok'
1179
+ );
1180
+ await refresh();
1181
+ });
1182
+ }
1183
+
1184
+ async function refresh() {
1185
+ const response = await send({ type: PANEL_GET_STATE });
1186
+ if (!response || response.ok !== true || !response.state) {
1187
+ setStatus(response?.error || 'Failed to load extension state.', 'error');
1188
+ return;
1189
+ }
1190
+
1191
+ const normalizedState = normalizePanelState(response.state);
1192
+ viewState.panelState = normalizedState;
1193
+ if (normalizedState.latestDomState) {
1194
+ viewState.lastDomState = normalizedState.latestDomState;
1195
+ }
1196
+ renderControl(normalizedState);
1197
+ renderSummary(normalizedState);
1198
+ renderAutomation(normalizedState);
1199
+ renderDeveloper(normalizedState);
1200
+ renderSessions(normalizedState);
1201
+ renderDownloads(normalizedState);
1202
+ }
1203
+
1204
+ refreshBtn.addEventListener('click', async () => {
1205
+ await refresh();
1206
+ setStatus('Panel refreshed.', 'ok');
1207
+ });
1208
+
1209
+ cleanupBtn.addEventListener('click', async () => {
1210
+ const response = await send({ type: PANEL_CLEAN_EMPTY_GROUPS });
1211
+ if (!response || response.ok !== true) {
1212
+ setStatus(`Clean failed: ${response?.error || 'unknown error'}`, 'error');
1213
+ return;
1214
+ }
1215
+ setStatus('Empty groups cleaned.', 'ok');
1216
+ await refresh();
1217
+ });
1218
+
1219
+ refresh().catch((error) => {
1220
+ setStatus(error instanceof Error ? error.message : String(error), 'error');
1221
+ });
1222
+
1223
+ setInterval(() => {
1224
+ refresh().catch(() => {});
1225
+ }, 5000);