@browserbridge/bbx 1.0.1 → 1.1.0

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 (63) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +116 -41
  5. package/packages/agent-client/src/client.js +29 -4
  6. package/packages/agent-client/src/command-registry.js +3 -0
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers.js +28 -7
  13. package/packages/mcp-server/src/server.js +12 -2
  14. package/packages/native-host/bin/bridge-daemon.js +33 -4
  15. package/packages/native-host/bin/install-manifest.js +24 -2
  16. package/packages/native-host/src/config.js +131 -6
  17. package/packages/native-host/src/daemon-process.js +396 -0
  18. package/packages/native-host/src/daemon.js +217 -68
  19. package/packages/native-host/src/framing.js +131 -11
  20. package/packages/native-host/src/install-manifest.js +121 -7
  21. package/packages/native-host/src/native-host.js +110 -73
  22. package/packages/protocol/src/capabilities.js +3 -0
  23. package/packages/protocol/src/defaults.js +1 -0
  24. package/packages/protocol/src/errors.js +4 -0
  25. package/packages/protocol/src/payload-cost.js +19 -6
  26. package/packages/protocol/src/protocol.js +143 -7
  27. package/packages/protocol/src/registry.js +11 -0
  28. package/packages/protocol/src/summary.js +18 -10
  29. package/packages/protocol/src/types.js +28 -3
  30. package/skills/browser-bridge/SKILL.md +2 -1
  31. package/skills/browser-bridge/references/interaction.md +1 -0
  32. package/skills/browser-bridge/references/protocol.md +2 -1
  33. package/CHANGELOG.md +0 -55
  34. package/assets/banner.jpg +0 -0
  35. package/assets/logo.png +0 -0
  36. package/assets/logo.svg +0 -65
  37. package/docs/api-reference.md +0 -157
  38. package/docs/cli-guide.md +0 -128
  39. package/docs/index.md +0 -25
  40. package/docs/manual-setup.md +0 -140
  41. package/docs/mcp-vs-cli.md +0 -258
  42. package/docs/publishing.md +0 -112
  43. package/docs/quickstart.md +0 -104
  44. package/docs/troubleshooting.md +0 -59
  45. package/docs/unpacked-extension.md +0 -72
  46. package/docs/usage-scenarios.md +0 -136
  47. package/manifest.json +0 -38
  48. package/packages/extension/assets/icon-128.png +0 -0
  49. package/packages/extension/assets/icon-16.png +0 -0
  50. package/packages/extension/assets/icon-32.png +0 -0
  51. package/packages/extension/assets/icon-48.png +0 -0
  52. package/packages/extension/src/background-helpers.js +0 -474
  53. package/packages/extension/src/background-routing.js +0 -89
  54. package/packages/extension/src/background.js +0 -3490
  55. package/packages/extension/src/content-script-helpers.js +0 -282
  56. package/packages/extension/src/content-script.js +0 -2043
  57. package/packages/extension/src/debugger-coordinator.js +0 -188
  58. package/packages/extension/src/sidepanel-helpers.js +0 -104
  59. package/packages/extension/ui/popup.html +0 -35
  60. package/packages/extension/ui/popup.js +0 -298
  61. package/packages/extension/ui/sidepanel.html +0 -102
  62. package/packages/extension/ui/sidepanel.js +0 -1771
  63. package/packages/extension/ui/ui.css +0 -1160
@@ -1,1771 +0,0 @@
1
- // @ts-check
2
-
3
- import {
4
- getActivitySourceTag,
5
- getPromptExamplesMode,
6
- shouldAutoExpandHostSetup,
7
- } from '../src/sidepanel-helpers.js';
8
-
9
- /**
10
- * @typedef {{
11
- * key: string,
12
- * label: string,
13
- * detected: boolean,
14
- * configPath: string,
15
- * configExists: boolean,
16
- * configured: boolean
17
- * }} McpClientStatus
18
- */
19
-
20
- /**
21
- * @typedef {{
22
- * name: string,
23
- * path: string,
24
- * exists: boolean,
25
- * managed: boolean,
26
- * version: string | null
27
- * }} SkillInstallationStatus
28
- */
29
-
30
- /**
31
- * @typedef {{
32
- * key: string,
33
- * label: string,
34
- * detected: boolean,
35
- * basePath: string,
36
- * installed: boolean,
37
- * managed: boolean,
38
- * installedVersion: string | null,
39
- * currentVersion: string | null,
40
- * updateAvailable: boolean,
41
- * skills: SkillInstallationStatus[]
42
- * }} SkillTargetStatus
43
- */
44
-
45
- /**
46
- * @typedef {{
47
- * scope: 'global' | 'local',
48
- * mcpClients: McpClientStatus[],
49
- * skillTargets: SkillTargetStatus[]
50
- * }} SetupStatus
51
- */
52
-
53
- /**
54
- * @typedef {{
55
- * key: string,
56
- * label: string,
57
- * mcpClient: McpClientStatus | null,
58
- * skillTarget: SkillTargetStatus | null
59
- * }} SetupMatrixRow
60
- */
61
-
62
- /**
63
- * @typedef {{
64
- * tabId: number,
65
- * windowId: number,
66
- * title: string,
67
- * url: string,
68
- * enabled: boolean,
69
- * accessRequested: boolean,
70
- * restricted: boolean
71
- * }} SidePanelCurrentTab
72
- */
73
-
74
- /**
75
- * @typedef {{
76
- * id: string,
77
- * at: number,
78
- * method: string,
79
- * source: string,
80
- * tabId: number | null,
81
- * url: string,
82
- * ok: boolean,
83
- * summary: string,
84
- * responseBytes: number,
85
- * approxTokens: number,
86
- * imageApproxTokens: number,
87
- * costClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
88
- * imageBytes: number,
89
- * summaryBytes: number,
90
- * summaryTokens: number,
91
- * summaryCostClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
92
- * debuggerBacked: boolean,
93
- * overBudget: boolean,
94
- * hasScreenshot: boolean,
95
- * nodeCount: number | null,
96
- * continuationHint: string | null
97
- * }} ActionLogEntry
98
- */
99
-
100
- /**
101
- * @typedef {{
102
- * nativeConnected: boolean,
103
- * currentTab: SidePanelCurrentTab | null,
104
- * setupStatus: SetupStatus | null,
105
- * setupStatusPending: boolean,
106
- * setupStatusError: string | null,
107
- * setupInstallPendingKey: string | null,
108
- * setupInstallError: string | null,
109
- * actionLog: ActionLogEntry[]
110
- * }} UiSnapshot
111
- */
112
-
113
- /**
114
- * @typedef {{
115
- * type: 'native.status',
116
- * connected: boolean,
117
- * error?: string
118
- * } | {
119
- * type: 'state.sync',
120
- * state: UiSnapshot
121
- * } | {
122
- * type: 'toggle.error',
123
- * error: string
124
- * }} SidePanelMessage
125
- */
126
-
127
- /**
128
- * @typedef {{
129
- * kind: 'mcp' | 'skill',
130
- * target: string,
131
- * copyLabel: string,
132
- * copyText: string,
133
- * reinstallLabel?: string,
134
- * uninstallLabel?: string
135
- * }} SetupContextAction
136
- */
137
-
138
- const PUBLISHED_EXTENSION_ID = 'jjjkmmcdkpcgamlopogicbnnhdgebhie';
139
- const SETUP_STATUS_POLL_MS = 15_000;
140
- const SETUP_MATRIX_ORDER = /** @type {const} */ ([
141
- 'codex',
142
- 'claude',
143
- 'cursor',
144
- 'copilot',
145
- 'opencode',
146
- 'antigravity',
147
- 'windsurf',
148
- 'agents',
149
- ]);
150
- /** @type {Map<string, number>} */
151
- const SETUP_MATRIX_RANK = new Map(SETUP_MATRIX_ORDER.map((key, index) => [key, index]));
152
- const SETUP_MATRIX_BETA_KEYS = new Set(['antigravity', 'windsurf', 'agents']);
153
-
154
- const nativeIndicator =
155
- /** @type {HTMLSpanElement} */ (document.getElementById('native-indicator'));
156
- const toggleButton = /** @type {HTMLButtonElement} */ (document.getElementById('bridge-toggle'));
157
- const actionLog = /** @type {HTMLDivElement} */ (document.getElementById('action-log'));
158
- const setupSection = /** @type {HTMLElement} */ (document.getElementById('native-setup'));
159
- const setupInstallCmd = /** @type {HTMLElement} */ (document.getElementById('setup-install-cmd'));
160
- const setupSkillCmd = /** @type {HTMLElement} */ (document.getElementById('setup-skill-cmd'));
161
- const setupMcpCmd = /** @type {HTMLElement} */ (document.getElementById('setup-mcp-cmd'));
162
- const controlSection = /** @type {HTMLElement} */ (document.getElementById('control-section'));
163
- const installationSection = /** @type {HTMLDetailsElement} */ (
164
- document.getElementById('installation-section')
165
- );
166
- const setupStatusNote =
167
- /** @type {HTMLParagraphElement} */ (document.getElementById('setup-status-note'));
168
- const setupStatusSummaryNote = /** @type {HTMLSpanElement} */ (
169
- document.getElementById('setup-status-summary-note')
170
- );
171
- const setupStatusMatrix = /** @type {HTMLDivElement} */ (
172
- document.getElementById('setup-status-matrix')
173
- );
174
- const activitySection = /** @type {HTMLElement} */ (document.getElementById('activity-section'));
175
- const activityHistogram =
176
- /** @type {HTMLDivElement} */ (document.getElementById('activity-histogram'));
177
- const activityHistogramBars = /** @type {HTMLDivElement} */ (
178
- document.getElementById('activity-histogram-bars')
179
- );
180
- const activityHistogramRange = /** @type {HTMLSpanElement} */ (
181
- document.getElementById('activity-histogram-range')
182
- );
183
- const activitySummaryTokens = /** @type {HTMLSpanElement} */ (
184
- document.getElementById('activity-summary-tokens')
185
- );
186
- const agentStatus = /** @type {HTMLDivElement} */ (document.getElementById('agent-status'));
187
- const agentStatusDetail = /** @type {HTMLParagraphElement} */ (
188
- document.getElementById('agent-status-detail')
189
- );
190
- const agentDisclosure =
191
- /** @type {HTMLParagraphElement} */ (document.getElementById('agent-disclosure'));
192
- const examplesSection =
193
- /** @type {HTMLDetailsElement} */ (document.getElementById('examples-section'));
194
- const examplesContent = /** @type {HTMLDivElement} */ (document.getElementById('examples-content'));
195
- /** @type {SidePanelCurrentTab | null} */
196
- let currentTabState = null;
197
- /** @type {ActionLogEntry[]} */
198
- let currentActionLog = [];
199
- /** @type {ReturnType<typeof setInterval> | null} */
200
- let setupStatusPollTimer = null;
201
- let hasAutoExpandedHostSetup = false;
202
- /** @type {ReturnType<typeof setTimeout> | null} */
203
- let nativeDiagnosticTimer = null;
204
- /** @type {ReturnType<typeof setTimeout> | null} */
205
- let pendingToggleTimer = null;
206
- /** @type {ReturnType<typeof setTimeout> | null} */
207
- let toggleErrorTimer = null;
208
- const NATIVE_DIAGNOSTIC_DELAY_MS = 10_000;
209
- const TOGGLE_PENDING_TIMEOUT_MS = 10_000;
210
- const TOGGLE_ERROR_DISPLAY_MS = 6_000;
211
-
212
- const toggleErrorEl = document.createElement('p');
213
- toggleErrorEl.className = 'toggle-error';
214
- toggleErrorEl.hidden = true;
215
- toggleButton.insertAdjacentElement('afterend', toggleErrorEl);
216
- const setupContextMenu = document.createElement('div');
217
- setupContextMenu.className = 'setup-context-menu';
218
- setupContextMenu.hidden = true;
219
- document.body.append(setupContextMenu);
220
-
221
- const CLI_PROMPT_EXAMPLES = Object.freeze([
222
- '$bbx check why the button looks broken',
223
- '$bbx inspect the layout and fix spacing issues',
224
- '$bbx verify the form validation works correctly',
225
- '$bbx check console errors on this page',
226
- ]);
227
-
228
- const MCP_PROMPT_EXAMPLES = Object.freeze([
229
- 'Use BB MCP to inspect why the button looks broken.',
230
- 'Use BB MCP to compare the live layout to the design and fix spacing issues.',
231
- 'Use BB MCP to verify the form validation flow works correctly.',
232
- 'Use BB MCP to inspect console and network errors on this page.',
233
- ]);
234
- const ACTIVITY_HISTOGRAM_WINDOW_MS = 10 * 60 * 1000;
235
- const ACTIVITY_HISTOGRAM_BUCKET_MS = 30 * 1000;
236
- const ACTIVITY_HISTOGRAM_BARS = Math.floor(
237
- ACTIVITY_HISTOGRAM_WINDOW_MS / ACTIVITY_HISTOGRAM_BUCKET_MS
238
- );
239
- const ACTIVITY_HISTOGRAM_TICK_MS = 5 * 1000;
240
- const HISTOGRAM_METHOD_FAMILIES = /** @type {const} */ ([
241
- 'dom',
242
- 'page',
243
- 'layout',
244
- 'style',
245
- 'input',
246
- 'patch',
247
- 'capture',
248
- 'other',
249
- ]);
250
- for (const cmd of /** @type {NodeListOf<HTMLElement>} */ (
251
- document.querySelectorAll('.setup-cmd')
252
- )) {
253
- if (cmd.classList.contains('example-cmd')) {
254
- continue;
255
- }
256
- cmd.addEventListener('click', () => {
257
- copySetupText(cmd, cmd.textContent?.trim() ?? '');
258
- });
259
- }
260
-
261
- /**
262
- * @param {HTMLElement} target
263
- * @param {string} text
264
- * @returns {void}
265
- */
266
- function copySetupText(target, text) {
267
- if (!text) {
268
- return;
269
- }
270
- navigator.clipboard
271
- .writeText(text)
272
- .then(() => {
273
- target.classList.add('copied');
274
- const copyButton = target.querySelector('.example-copy-button');
275
- const resetLabel = copyButton instanceof HTMLButtonElement ? copyButton.textContent : null;
276
- if (copyButton instanceof HTMLButtonElement) {
277
- copyButton.textContent = '✓';
278
- }
279
- setTimeout(() => {
280
- target.classList.remove('copied');
281
- if (copyButton instanceof HTMLButtonElement) {
282
- copyButton.textContent = resetLabel || '⧉';
283
- }
284
- }, 1500);
285
- })
286
- .catch(() => {
287
- // Ignore clipboard failures; prompt chips expose a dedicated copy button.
288
- });
289
- }
290
-
291
- /** @param {SidePanelMessage} message */
292
- function handlePortMessage(message) {
293
- if (message.type === 'native.status') {
294
- renderNativeStatus(message.connected, message.error);
295
- }
296
-
297
- if (message.type === 'state.sync') {
298
- renderState(message.state);
299
- }
300
-
301
- if (message.type === 'toggle.error') {
302
- renderToggleError(message.error);
303
- }
304
- }
305
-
306
- /** @type {chrome.runtime.Port} */
307
- let port;
308
-
309
- /**
310
- * @returns {number | null}
311
- */
312
- function readRequestedTabId() {
313
- const value = new URLSearchParams(window.location.search).get('tabId');
314
- const tabId = Number(value);
315
- return Number.isFinite(tabId) && tabId > 0 ? tabId : null;
316
- }
317
-
318
- /** @type {number | null} */
319
- const requestedTabId = readRequestedTabId();
320
-
321
- /**
322
- * @returns {Promise<void>}
323
- */
324
- async function connectSidepanelPort() {
325
- const nextPort = chrome.runtime.connect({ name: 'ui-sidepanel' });
326
- nextPort.onMessage.addListener(handlePortMessage);
327
- nextPort.onDisconnect.addListener(() => {
328
- setTimeout(connectSidepanelPort, 500);
329
- });
330
- nextPort.postMessage({ type: 'state.request' });
331
- port = nextPort;
332
- }
333
-
334
- void connectSidepanelPort();
335
- const activityHistogramTimer = setInterval(() => {
336
- updateActivityVisualizations();
337
- }, ACTIVITY_HISTOGRAM_TICK_MS);
338
-
339
- toggleButton.addEventListener('click', () => {
340
- if (!currentTabState || toggleButton.dataset.pending === 'true') {
341
- return;
342
- }
343
-
344
- const pendingEnabled = !currentTabState.enabled;
345
- toggleButton.dataset.pending = 'true';
346
- toggleButton.textContent = pendingEnabled ? 'Enabling\u2026' : 'Disabling\u2026';
347
-
348
- if (pendingToggleTimer) {
349
- clearTimeout(pendingToggleTimer);
350
- }
351
- pendingToggleTimer = setTimeout(() => {
352
- toggleButton.dataset.pending = 'false';
353
- if (currentTabState) {
354
- toggleButton.textContent = currentTabState.enabled
355
- ? 'Disable Window Access'
356
- : 'Enable Window Access';
357
- }
358
- pendingToggleTimer = null;
359
- }, TOGGLE_PENDING_TIMEOUT_MS);
360
-
361
- port.postMessage({
362
- type: 'scope.set_enabled',
363
- enabled: pendingEnabled,
364
- });
365
- });
366
-
367
- installationSection.addEventListener('toggle', () => {
368
- syncExclusiveDetailsSections(installationSection, examplesSection);
369
- syncConnectedSectionsVisibility();
370
- syncSetupStatusPolling();
371
- });
372
-
373
- examplesSection.addEventListener('toggle', () => {
374
- syncExclusiveDetailsSections(examplesSection, installationSection);
375
- syncConnectedSectionsVisibility();
376
- syncSetupStatusPolling();
377
- });
378
-
379
- setupStatusMatrix.addEventListener('contextmenu', (event) => {
380
- const target = event.target;
381
- if (!(target instanceof HTMLElement)) {
382
- return;
383
- }
384
- const actionTarget = target.closest('[data-context-kind][data-context-target]');
385
- if (!(actionTarget instanceof HTMLElement)) {
386
- hideSetupContextMenu();
387
- return;
388
- }
389
-
390
- const kind = actionTarget.dataset.contextKind;
391
- const targetKey = actionTarget.dataset.contextTarget;
392
- const copyLabel = actionTarget.dataset.contextCopyLabel;
393
- const copyText = actionTarget.dataset.contextCopyText;
394
- const reinstallLabel = actionTarget.dataset.contextReinstallLabel;
395
- const uninstallLabel = actionTarget.dataset.contextUninstallLabel;
396
- if ((kind !== 'mcp' && kind !== 'skill') || !targetKey || !copyLabel || !copyText) {
397
- hideSetupContextMenu();
398
- return;
399
- }
400
-
401
- event.preventDefault();
402
- showSetupContextMenu(event.clientX, event.clientY, {
403
- kind,
404
- target: targetKey,
405
- copyLabel,
406
- copyText,
407
- reinstallLabel: reinstallLabel || undefined,
408
- uninstallLabel: uninstallLabel || undefined,
409
- });
410
- });
411
-
412
- document.addEventListener('click', () => {
413
- hideSetupContextMenu();
414
- });
415
-
416
- document.addEventListener('keydown', (event) => {
417
- if (event.key === 'Escape') {
418
- hideSetupContextMenu();
419
- }
420
- });
421
-
422
- window.addEventListener('beforeunload', () => {
423
- clearInterval(activityHistogramTimer);
424
- });
425
-
426
- /**
427
- * @param {UiSnapshot} state
428
- * @returns {void}
429
- */
430
- function renderState(state) {
431
- hideSetupContextMenu();
432
- renderNativeStatus(state.nativeConnected);
433
- renderCurrentTab(state.currentTab);
434
- renderAgentStatus(state);
435
- renderPromptExamples(state.setupStatus);
436
- renderSetupStatus(
437
- state.setupStatus,
438
- state.setupStatusPending,
439
- state.setupStatusError,
440
- state.setupInstallPendingKey,
441
- state.setupInstallError
442
- );
443
-
444
- actionLog.replaceChildren(
445
- ...state.actionLog.map((entry, index, entries) =>
446
- renderActionLogEntry(entry, state.setupStatus, entries, index)
447
- )
448
- );
449
- currentActionLog = state.actionLog;
450
- updateActivityVisualizations();
451
-
452
- if (!state.actionLog.length) {
453
- actionLog.textContent = 'No recent agent actions.';
454
- }
455
-
456
- // Auto-collapse examples when there is activity
457
- if (state.actionLog.length) {
458
- examplesSection.removeAttribute('open');
459
- }
460
- syncConnectedSectionsVisibility();
461
- syncSetupStatusPolling();
462
- }
463
-
464
- /**
465
- * @returns {void}
466
- */
467
- function updateActivityVisualizations() {
468
- const histogram = buildActivityHistogram(currentActionLog);
469
- renderActivityHistogram(histogram);
470
- renderActivitySummary(histogram.totalTokens);
471
- }
472
-
473
- /**
474
- * @param {SidePanelCurrentTab | null} currentTab
475
- * @returns {void}
476
- */
477
- function renderCurrentTab(currentTab) {
478
- currentTabState = currentTab;
479
- toggleButton.dataset.pending = 'false';
480
- toggleErrorEl.hidden = true;
481
-
482
- if (pendingToggleTimer) {
483
- clearTimeout(pendingToggleTimer);
484
- pendingToggleTimer = null;
485
- }
486
-
487
- if (toggleErrorTimer) {
488
- clearTimeout(toggleErrorTimer);
489
- toggleErrorTimer = null;
490
- }
491
-
492
- if (!currentTab) {
493
- toggleButton.textContent = 'Window Access Unavailable';
494
- toggleButton.disabled = true;
495
- toggleButton.dataset.enabled = 'false';
496
- controlSection.classList.remove('attention');
497
- return;
498
- }
499
-
500
- if (currentTab.restricted && currentTab.enabled) {
501
- toggleButton.textContent = 'Disable Window Access';
502
- toggleButton.disabled = false;
503
- toggleButton.dataset.enabled = String(currentTab.enabled);
504
- toggleErrorEl.textContent =
505
- 'This page cannot be interacted with. Switch to a normal web page to inspect and interact.';
506
- toggleErrorEl.hidden = false;
507
- controlSection.classList.remove('attention');
508
- return;
509
- }
510
-
511
- toggleButton.textContent = currentTab.enabled ? 'Disable Window Access' : 'Enable Window Access';
512
- toggleButton.disabled = !currentTab.url;
513
- toggleButton.dataset.enabled = String(currentTab.enabled);
514
- controlSection.classList.toggle('attention', currentTab.accessRequested && !currentTab.enabled);
515
- }
516
-
517
- /**
518
- * @param {string} errorMessage
519
- * @returns {void}
520
- */
521
- function renderToggleError(errorMessage) {
522
- toggleButton.dataset.pending = 'false';
523
-
524
- if (pendingToggleTimer) {
525
- clearTimeout(pendingToggleTimer);
526
- pendingToggleTimer = null;
527
- }
528
-
529
- if (currentTabState) {
530
- toggleButton.textContent = currentTabState.enabled
531
- ? 'Disable Window Access'
532
- : 'Enable Window Access';
533
- }
534
-
535
- const friendly = errorMessage.replace(/^CONTENT_SCRIPT_UNAVAILABLE:\s*/i, '');
536
- toggleErrorEl.textContent = friendly;
537
- toggleErrorEl.hidden = false;
538
-
539
- if (toggleErrorTimer) {
540
- clearTimeout(toggleErrorTimer);
541
- }
542
- toggleErrorTimer = setTimeout(() => {
543
- toggleErrorEl.hidden = true;
544
- toggleErrorTimer = null;
545
- }, TOGGLE_ERROR_DISPLAY_MS);
546
- }
547
-
548
- /**
549
- * @param {UiSnapshot} state
550
- * @returns {void}
551
- */
552
- function renderAgentStatus(state) {
553
- const currentTab = state.currentTab;
554
-
555
- if (!currentTab) {
556
- agentStatus.textContent = 'Window access unavailable';
557
- agentStatusDetail.textContent =
558
- 'Open a normal web page in this Chrome window to enable Browser Bridge.';
559
- agentDisclosure.hidden = false;
560
- return;
561
- }
562
-
563
- agentDisclosure.hidden = currentTab.enabled;
564
-
565
- if (currentTab.enabled && currentTab.restricted) {
566
- agentStatus.textContent = 'Window access enabled';
567
- agentStatusDetail.textContent =
568
- 'This page cannot be interacted with. Switch to a normal web page to use Browser Bridge.';
569
- agentDisclosure.hidden = false;
570
- return;
571
- }
572
-
573
- if (currentTab.enabled) {
574
- agentStatus.textContent = 'Window access enabled';
575
- agentStatusDetail.textContent =
576
- 'Browser Bridge is enabled for this Chrome window. Requests default to the active tab, or can target another tab in this window explicitly.';
577
- return;
578
- }
579
-
580
- if (currentTab.accessRequested) {
581
- agentStatus.textContent = 'Window access requested';
582
- agentStatusDetail.textContent =
583
- 'An agent requested access for this Chrome window. Enable it to allow page inspection and interaction.';
584
- return;
585
- }
586
-
587
- agentStatus.textContent = 'Window access';
588
- agentStatusDetail.textContent =
589
- 'Enable Browser Bridge to let your connected agent inspect and interact with pages in this Chrome window.';
590
- }
591
-
592
- /**
593
- * @param {boolean} connected
594
- * @param {string | undefined} [error]
595
- * @returns {void}
596
- */
597
- function renderNativeStatus(connected, error) {
598
- const label = connected ? 'Native host connected' : error || 'Native host disconnected';
599
- nativeIndicator.dataset.connected = String(connected);
600
- nativeIndicator.title = label;
601
- nativeIndicator.setAttribute('aria-label', label);
602
-
603
- setupSection.hidden = connected;
604
- controlSection.hidden = !connected;
605
- installationSection.hidden = !connected;
606
- if (!connected) {
607
- const extId = chrome.runtime.id;
608
- setupInstallCmd.textContent =
609
- extId === PUBLISHED_EXTENSION_ID ? 'bbx install' : `bbx install ${extId}`;
610
- setupSkillCmd.textContent = 'bbx install-skill';
611
- setupMcpCmd.textContent = 'bbx install-mcp';
612
- hideSetupContextMenu();
613
- }
614
- syncConnectedSectionsVisibility();
615
-
616
- if (connected) {
617
- if (nativeDiagnosticTimer) {
618
- clearTimeout(nativeDiagnosticTimer);
619
- nativeDiagnosticTimer = null;
620
- }
621
- hideSidepanelDiagnostic();
622
- } else if (!nativeDiagnosticTimer) {
623
- nativeDiagnosticTimer = setTimeout(() => {
624
- nativeDiagnosticTimer = null;
625
- showSidepanelDiagnostic(
626
- `Native host unreachable for 10s. Run: npm install -g @browserbridge/bbx && ${setupInstallCmd.textContent || 'bbx install'}`
627
- );
628
- }, NATIVE_DIAGNOSTIC_DELAY_MS);
629
- }
630
- }
631
-
632
- /**
633
- * @param {string} message
634
- * @returns {void}
635
- */
636
- function showSidepanelDiagnostic(message) {
637
- let el = document.getElementById('native-diagnostic');
638
- if (!el) {
639
- el = document.createElement('div');
640
- el.id = 'native-diagnostic';
641
- el.style.cssText =
642
- 'padding:8px 12px;margin:8px 0;background:var(--status-badge-bg,#fef3cd);color:var(--text-primary,#856404);border-radius:6px;font-size:12px;line-height:1.4';
643
- setupSection.after(el);
644
- }
645
- el.textContent = message;
646
- el.hidden = false;
647
- }
648
-
649
- function hideSidepanelDiagnostic() {
650
- const el = document.getElementById('native-diagnostic');
651
- if (el) {
652
- el.remove();
653
- }
654
- }
655
-
656
- /**
657
- * @returns {void}
658
- */
659
- function syncConnectedSectionsVisibility() {
660
- const connected = nativeIndicator.dataset.connected === 'true';
661
- if (!connected) {
662
- examplesSection.hidden = true;
663
- activitySection.hidden = true;
664
- return;
665
- }
666
- examplesSection.hidden = false;
667
- activitySection.hidden = false;
668
- }
669
-
670
- /**
671
- * @returns {void}
672
- */
673
- function syncSetupStatusPolling() {
674
- const shouldPoll =
675
- nativeIndicator.dataset.connected === 'true' &&
676
- !installationSection.hidden &&
677
- installationSection.open;
678
-
679
- if (!shouldPoll) {
680
- stopSetupStatusPolling();
681
- return;
682
- }
683
-
684
- if (setupStatusPollTimer) {
685
- return;
686
- }
687
-
688
- // Only kick off an immediate refresh when polling starts. Doing this on every
689
- // render causes a state.sync -> refresh -> state.sync loop that can peg the
690
- // extension renderer while Host Setup is open.
691
- requestSetupStatusRefresh();
692
- setupStatusPollTimer = setInterval(() => {
693
- requestSetupStatusRefresh();
694
- }, SETUP_STATUS_POLL_MS);
695
- }
696
-
697
- /**
698
- * @returns {void}
699
- */
700
- function stopSetupStatusPolling() {
701
- if (!setupStatusPollTimer) {
702
- return;
703
- }
704
- clearInterval(setupStatusPollTimer);
705
- setupStatusPollTimer = null;
706
- }
707
-
708
- /**
709
- * @returns {void}
710
- */
711
- function requestSetupStatusRefresh() {
712
- port.postMessage({ type: 'setup.status.refresh' });
713
- }
714
-
715
- /**
716
- * @param {SetupStatus | null} setupStatus
717
- * @returns {void}
718
- */
719
- function renderPromptExamples(setupStatus) {
720
- const mode = getPromptExamplesMode(setupStatus);
721
- if (mode === 'cli') {
722
- examplesContent.replaceChildren(createExamplesList(CLI_PROMPT_EXAMPLES));
723
- return;
724
- }
725
- if (mode === 'mcp') {
726
- examplesContent.replaceChildren(createExamplesList(MCP_PROMPT_EXAMPLES));
727
- return;
728
- }
729
-
730
- examplesContent.replaceChildren(
731
- createExamplesGroup('CLI skill', CLI_PROMPT_EXAMPLES),
732
- createExamplesGroup('MCP', MCP_PROMPT_EXAMPLES)
733
- );
734
- }
735
-
736
- /**
737
- * @param {string} title
738
- * @param {readonly string[]} prompts
739
- * @returns {HTMLElement}
740
- */
741
- function createExamplesGroup(title, prompts) {
742
- const section = document.createElement('section');
743
- section.className = 'examples-group';
744
-
745
- const heading = document.createElement('h3');
746
- heading.className = 'examples-group-title';
747
- heading.textContent = title;
748
-
749
- section.append(heading, createExamplesList(prompts));
750
- return section;
751
- }
752
-
753
- /**
754
- * @param {readonly string[]} prompts
755
- * @returns {HTMLElement}
756
- */
757
- function createExamplesList(prompts) {
758
- const list = document.createElement('div');
759
- list.className = 'examples-list';
760
-
761
- for (const prompt of prompts) {
762
- const row = document.createElement('div');
763
- row.className = 'setup-cmd example-cmd';
764
-
765
- const text = document.createElement('code');
766
- text.className = 'example-cmd-text';
767
- text.textContent = prompt;
768
-
769
- const button = document.createElement('button');
770
- button.type = 'button';
771
- button.className = 'example-copy-button';
772
- button.setAttribute('aria-label', `Copy prompt: ${prompt}`);
773
- button.title = 'Copy prompt';
774
- button.textContent = '⧉';
775
- button.addEventListener('click', (event) => {
776
- event.stopPropagation();
777
- copySetupText(row, prompt);
778
- });
779
-
780
- row.append(text, button);
781
- list.append(row);
782
- }
783
-
784
- return list;
785
- }
786
-
787
- window.addEventListener('beforeunload', () => {
788
- stopSetupStatusPolling();
789
- hideSetupContextMenu();
790
- });
791
-
792
- /**
793
- * @param {HTMLDetailsElement} source
794
- * @param {HTMLDetailsElement} other
795
- * @returns {void}
796
- */
797
- function syncExclusiveDetailsSections(source, other) {
798
- if (!source.open || !other.open) {
799
- return;
800
- }
801
- other.open = false;
802
- }
803
-
804
- /**
805
- * @param {SetupStatus | null} setupStatus
806
- * @param {boolean} pending
807
- * @param {string | null} error
808
- * @param {string | null} installPendingKey
809
- * @param {string | null} installError
810
- * @returns {void}
811
- */
812
- function renderSetupStatus(setupStatus, pending, error, installPendingKey, installError) {
813
- if (!setupStatus && pending) {
814
- setupStatusSummaryNote.hidden = true;
815
- setupStatusSummaryNote.textContent = '';
816
- setupStatusNote.textContent = 'Checking global host setup…';
817
- setupStatusNote.hidden = false;
818
- setupStatusMatrix.replaceChildren(createStatusPlaceholder('Checking detected clients…'));
819
- return;
820
- }
821
-
822
- if (!setupStatus) {
823
- setupStatusSummaryNote.hidden = true;
824
- setupStatusSummaryNote.textContent = '';
825
- setupStatusNote.textContent = installError || error || 'Global host setup is unavailable.';
826
- setupStatusNote.hidden = false;
827
- setupStatusMatrix.replaceChildren(createStatusPlaceholder('No host setup status yet.'));
828
- return;
829
- }
830
-
831
- const scopeNote =
832
- setupStatus.scope === 'global' ? 'Global installs only' : 'Project installs only';
833
- setupStatusSummaryNote.textContent = `* ${scopeNote}`;
834
- setupStatusSummaryNote.hidden = false;
835
-
836
- if (!hasAutoExpandedHostSetup) {
837
- hasAutoExpandedHostSetup = true;
838
- const autoExpandHostSetup = shouldAutoExpandHostSetup(setupStatus);
839
- installationSection.open = autoExpandHostSetup;
840
- if (autoExpandHostSetup) {
841
- examplesSection.open = false;
842
- }
843
- }
844
-
845
- const noteParts = [];
846
- if (installError) {
847
- noteParts.push(`Last install failed: ${installError}`);
848
- } else if (error) {
849
- noteParts.push(error);
850
- }
851
- setupStatusNote.textContent = noteParts.join(' ');
852
- setupStatusNote.hidden = noteParts.length === 0;
853
-
854
- const rows = buildSetupMatrixRows(setupStatus);
855
- if (!rows.length) {
856
- setupStatusMatrix.replaceChildren(
857
- createStatusPlaceholder('No supported clients or agents were detected.')
858
- );
859
- return;
860
- }
861
- setupStatusMatrix.replaceChildren(renderSetupMatrix(rows, installPendingKey));
862
- }
863
-
864
- /**
865
- * @param {SetupStatus} setupStatus
866
- * @returns {SetupMatrixRow[]}
867
- */
868
- function buildSetupMatrixRows(setupStatus) {
869
- /** @type {Map<string, SetupMatrixRow>} */
870
- const rowsByKey = new Map();
871
-
872
- for (const entry of setupStatus.mcpClients) {
873
- if (!shouldRenderMcpClientRow(entry)) {
874
- continue;
875
- }
876
- if (!rowsByKey.has(entry.key)) {
877
- rowsByKey.set(entry.key, {
878
- key: entry.key,
879
- label: entry.label,
880
- mcpClient: entry,
881
- skillTarget: null,
882
- });
883
- } else {
884
- const row = rowsByKey.get(entry.key);
885
- if (row) {
886
- row.mcpClient = entry;
887
- }
888
- }
889
- }
890
-
891
- for (const entry of setupStatus.skillTargets) {
892
- if (!shouldRenderSkillTargetRow(entry)) {
893
- continue;
894
- }
895
- if (!rowsByKey.has(entry.key)) {
896
- rowsByKey.set(entry.key, {
897
- key: entry.key,
898
- label: entry.label,
899
- mcpClient: null,
900
- skillTarget: entry,
901
- });
902
- } else {
903
- const row = rowsByKey.get(entry.key);
904
- if (row) {
905
- row.skillTarget = entry;
906
- }
907
- }
908
- }
909
-
910
- return [...rowsByKey.entries()]
911
- .sort(([leftKey], [rightKey]) => compareSetupMatrixKeys(leftKey, rightKey))
912
- .map(([, row]) => row);
913
- }
914
-
915
- /**
916
- * @param {string} leftKey
917
- * @param {string} rightKey
918
- * @returns {number}
919
- */
920
- function compareSetupMatrixKeys(leftKey, rightKey) {
921
- const leftRank = SETUP_MATRIX_RANK.get(leftKey);
922
- const rightRank = SETUP_MATRIX_RANK.get(rightKey);
923
-
924
- if (leftRank !== undefined && rightRank !== undefined) {
925
- return leftRank - rightRank;
926
- }
927
- if (leftRank !== undefined) {
928
- return -1;
929
- }
930
- if (rightRank !== undefined) {
931
- return 1;
932
- }
933
- return leftKey.localeCompare(rightKey);
934
- }
935
-
936
- /**
937
- * @param {McpClientStatus} entry
938
- * @returns {boolean}
939
- */
940
- function shouldRenderMcpClientRow(entry) {
941
- return entry.detected || entry.configured || entry.key === 'agents';
942
- }
943
-
944
- /**
945
- * @param {SkillTargetStatus} entry
946
- * @returns {boolean}
947
- */
948
- function shouldRenderSkillTargetRow(entry) {
949
- return entry.detected || entry.installed || entry.skills.some((skill) => skill.exists);
950
- }
951
-
952
- /**
953
- * @param {SetupMatrixRow[]} rows
954
- * @param {string | null} installPendingKey
955
- * @returns {HTMLElement}
956
- */
957
- function renderSetupMatrix(rows, installPendingKey) {
958
- const table = document.createElement('table');
959
- table.className = 'setup-status-matrix';
960
-
961
- const thead = document.createElement('thead');
962
- const headerRow = document.createElement('tr');
963
- for (const heading of ['Client', 'MCP', 'CLI']) {
964
- const th = document.createElement('th');
965
- th.textContent = heading;
966
- headerRow.append(th);
967
- }
968
- thead.append(headerRow);
969
-
970
- const tbody = document.createElement('tbody');
971
- for (const row of rows) {
972
- const tr = document.createElement('tr');
973
-
974
- const labelCell = document.createElement('td');
975
- labelCell.className = 'setup-matrix-client';
976
- labelCell.append(createSetupMatrixClientLabel(row));
977
-
978
- const mcpCell = document.createElement('td');
979
- mcpCell.append(renderMcpMatrixCell(row, installPendingKey));
980
-
981
- const skillCell = document.createElement('td');
982
- skillCell.append(renderSkillMatrixCell(row, installPendingKey));
983
-
984
- tr.append(labelCell, mcpCell, skillCell);
985
- tbody.append(tr);
986
- }
987
-
988
- table.append(thead, tbody);
989
- return table;
990
- }
991
-
992
- /**
993
- * @param {SetupMatrixRow} row
994
- * @returns {DocumentFragment}
995
- */
996
- function createSetupMatrixClientLabel(row) {
997
- const fragment = document.createDocumentFragment();
998
- fragment.append(row.label);
999
-
1000
- if (SETUP_MATRIX_BETA_KEYS.has(row.key)) {
1001
- const beta = document.createElement('span');
1002
- beta.className = 'setup-matrix-beta';
1003
- beta.textContent = ' (beta)';
1004
- fragment.append(beta);
1005
- }
1006
-
1007
- return fragment;
1008
- }
1009
-
1010
- /**
1011
- * @param {SetupMatrixRow} row
1012
- * @param {string | null} installPendingKey
1013
- * @returns {HTMLElement}
1014
- */
1015
- function renderMcpMatrixCell(row, installPendingKey) {
1016
- const entry = row.mcpClient;
1017
- if (!entry) {
1018
- if (row.key === 'agents') {
1019
- const button = createSetupActionButton(
1020
- 'mcp',
1021
- row.key,
1022
- installPendingKey === getInstallKey('mcp', row.key),
1023
- 'install',
1024
- 'Install',
1025
- '...'
1026
- );
1027
- button.title = 'Install Browser Bridge MCP for generic agents';
1028
- return button;
1029
- }
1030
- return createMatrixMutedValue('\u2014');
1031
- }
1032
- if (entry.configured) {
1033
- return createMatrixBadge('Installed', true, entry.configPath, {
1034
- kind: 'mcp',
1035
- target: row.key,
1036
- copyLabel: 'Copy MCP config path',
1037
- copyText: entry.configPath,
1038
- reinstallLabel: 'Re-install MCP',
1039
- uninstallLabel: 'Uninstall MCP',
1040
- });
1041
- }
1042
- const button = createSetupActionButton(
1043
- 'mcp',
1044
- row.key,
1045
- installPendingKey === getInstallKey('mcp', row.key),
1046
- 'install',
1047
- 'Install',
1048
- '...'
1049
- );
1050
- button.title = entry.configPath;
1051
- return button;
1052
- }
1053
-
1054
- /**
1055
- * @param {SetupMatrixRow} row
1056
- * @param {string | null} installPendingKey
1057
- * @returns {HTMLElement}
1058
- */
1059
- function renderSkillMatrixCell(row, installPendingKey) {
1060
- const entry = row.skillTarget;
1061
- if (!entry) {
1062
- return createMatrixMutedValue('\u2014');
1063
- }
1064
- const reinstallLabel = getSkillReinstallLabel(entry);
1065
- const uninstallLabel = getSkillUninstallLabel(entry);
1066
-
1067
- const installable = entry.skills.every((skill) => !skill.exists || skill.managed);
1068
- if (!installable) {
1069
- return createMatrixBadge('Custom', false, entry.basePath, {
1070
- kind: 'skill',
1071
- target: row.key,
1072
- copyLabel: 'Copy CLI skill folder path',
1073
- copyText: entry.basePath,
1074
- });
1075
- }
1076
- if (entry.installed && entry.managed && !entry.updateAvailable) {
1077
- return createMatrixBadge('Installed', true, createSkillCellTitle(entry), {
1078
- kind: 'skill',
1079
- target: row.key,
1080
- copyLabel: 'Copy CLI skill folder path',
1081
- copyText: entry.basePath,
1082
- reinstallLabel,
1083
- uninstallLabel,
1084
- });
1085
- }
1086
- if (entry.installed && entry.managed && entry.updateAvailable) {
1087
- const button = createSetupActionButton(
1088
- 'skill',
1089
- row.key,
1090
- installPendingKey === getInstallKey('skill', row.key),
1091
- 'update',
1092
- 'Update',
1093
- 'Updating…'
1094
- );
1095
- button.title = createSkillCellTitle(entry);
1096
- assignSetupContext(button, {
1097
- kind: 'skill',
1098
- target: row.key,
1099
- copyLabel: 'Copy CLI skill folder path',
1100
- copyText: entry.basePath,
1101
- reinstallLabel,
1102
- uninstallLabel,
1103
- });
1104
- return button;
1105
- }
1106
-
1107
- const button = createSetupActionButton(
1108
- 'skill',
1109
- row.key,
1110
- installPendingKey === getInstallKey('skill', row.key),
1111
- 'install',
1112
- 'Install',
1113
- '...'
1114
- );
1115
- button.title = entry.basePath;
1116
- return button;
1117
- }
1118
-
1119
- /**
1120
- * @param {'mcp' | 'skill'} kind
1121
- * @param {string} target
1122
- * @param {boolean} pending
1123
- * @param {'install' | 'update'} variant
1124
- * @param {string} label
1125
- * @param {string} pendingLabel
1126
- * @returns {HTMLButtonElement}
1127
- */
1128
- function createSetupActionButton(kind, target, pending, variant, label, pendingLabel) {
1129
- const button = document.createElement('button');
1130
- button.type = 'button';
1131
- button.className = 'setup-install-button';
1132
- button.dataset.action = 'setup-install';
1133
- button.dataset.kind = kind;
1134
- button.dataset.target = target;
1135
- button.dataset.variant = variant;
1136
- button.disabled = pending;
1137
- button.textContent = pending ? pendingLabel : label;
1138
- button.addEventListener('click', () => {
1139
- if (button.disabled) {
1140
- return;
1141
- }
1142
- hideSetupContextMenu();
1143
- button.disabled = true;
1144
- button.textContent = pendingLabel;
1145
- port.postMessage({
1146
- type: 'setup.install',
1147
- action: 'install',
1148
- kind,
1149
- target,
1150
- });
1151
- });
1152
- return button;
1153
- }
1154
-
1155
- /**
1156
- * @param {SkillTargetStatus} entry
1157
- * @returns {string}
1158
- */
1159
- function createSkillCellTitle(entry) {
1160
- if (entry.installedVersion && entry.currentVersion) {
1161
- return `${entry.basePath}\nInstalled with bbx ${entry.installedVersion}\nCurrent bbx ${entry.currentVersion}`;
1162
- }
1163
- if (entry.currentVersion) {
1164
- return `${entry.basePath}\nCurrent bbx ${entry.currentVersion}`;
1165
- }
1166
- return entry.basePath;
1167
- }
1168
-
1169
- /**
1170
- * @param {SkillTargetStatus} _entry
1171
- * @returns {string}
1172
- */
1173
- function getSkillUninstallLabel(_entry) {
1174
- return 'Uninstall CLI skill';
1175
- }
1176
-
1177
- /**
1178
- * @param {SkillTargetStatus} _entry
1179
- * @returns {string}
1180
- */
1181
- function getSkillReinstallLabel(_entry) {
1182
- return 'Re-install CLI skill';
1183
- }
1184
-
1185
- /**
1186
- * @param {string} label
1187
- * @param {boolean} ok
1188
- * @param {string} title
1189
- * @param {SetupContextAction | null} [contextAction=null]
1190
- * @returns {HTMLElement}
1191
- */
1192
- function createMatrixBadge(label, ok, title, contextAction = null) {
1193
- const badge = document.createElement('span');
1194
- badge.className = 'setup-status-badge';
1195
- badge.dataset.ok = String(ok);
1196
- badge.textContent = label;
1197
- badge.title = title;
1198
- if (contextAction) {
1199
- assignSetupContext(badge, contextAction);
1200
- }
1201
- return badge;
1202
- }
1203
-
1204
- /**
1205
- * @param {string} text
1206
- * @returns {HTMLElement}
1207
- */
1208
- function createMatrixMutedValue(text) {
1209
- const value = document.createElement('span');
1210
- value.className = 'setup-matrix-muted';
1211
- value.textContent = text;
1212
- return value;
1213
- }
1214
-
1215
- /**
1216
- * @param {'mcp' | 'skill'} kind
1217
- * @param {string} target
1218
- * @returns {string}
1219
- */
1220
- function getInstallKey(kind, target) {
1221
- return `${kind}:${target}`;
1222
- }
1223
-
1224
- /**
1225
- * @param {HTMLElement} element
1226
- * @param {SetupContextAction} contextAction
1227
- * @returns {void}
1228
- */
1229
- function assignSetupContext(element, contextAction) {
1230
- element.dataset.contextKind = contextAction.kind;
1231
- element.dataset.contextTarget = contextAction.target;
1232
- element.dataset.contextCopyLabel = contextAction.copyLabel;
1233
- element.dataset.contextCopyText = contextAction.copyText;
1234
- if (contextAction.reinstallLabel) {
1235
- element.dataset.contextReinstallLabel = contextAction.reinstallLabel;
1236
- } else {
1237
- delete element.dataset.contextReinstallLabel;
1238
- }
1239
- if (contextAction.uninstallLabel) {
1240
- element.dataset.contextUninstallLabel = contextAction.uninstallLabel;
1241
- } else {
1242
- delete element.dataset.contextUninstallLabel;
1243
- }
1244
- }
1245
-
1246
- /**
1247
- * @param {number} clientX
1248
- * @param {number} clientY
1249
- * @param {SetupContextAction} contextAction
1250
- * @returns {void}
1251
- */
1252
- function showSetupContextMenu(clientX, clientY, contextAction) {
1253
- setupContextMenu.replaceChildren();
1254
-
1255
- const copyButton = document.createElement('button');
1256
- copyButton.type = 'button';
1257
- copyButton.className = 'setup-context-menu-item';
1258
- copyButton.textContent = contextAction.copyLabel;
1259
- copyButton.addEventListener(
1260
- 'click',
1261
- () => {
1262
- navigator.clipboard.writeText(contextAction.copyText).catch(() => {
1263
- // Ignore clipboard failures; the menu still exposes the path in the title.
1264
- });
1265
- hideSetupContextMenu();
1266
- },
1267
- { once: true }
1268
- );
1269
- setupContextMenu.append(copyButton);
1270
-
1271
- if (contextAction.reinstallLabel) {
1272
- const reinstallButton = document.createElement('button');
1273
- reinstallButton.type = 'button';
1274
- reinstallButton.className = 'setup-context-menu-item';
1275
- reinstallButton.textContent = contextAction.reinstallLabel;
1276
- reinstallButton.addEventListener(
1277
- 'click',
1278
- () => {
1279
- port.postMessage({
1280
- type: 'setup.install',
1281
- action: 'install',
1282
- kind: contextAction.kind,
1283
- target: contextAction.target,
1284
- });
1285
- hideSetupContextMenu();
1286
- },
1287
- { once: true }
1288
- );
1289
- setupContextMenu.append(reinstallButton);
1290
- }
1291
-
1292
- if (contextAction.uninstallLabel) {
1293
- const uninstallButton = document.createElement('button');
1294
- uninstallButton.type = 'button';
1295
- uninstallButton.className = 'setup-context-menu-item';
1296
- uninstallButton.textContent = contextAction.uninstallLabel;
1297
- uninstallButton.addEventListener(
1298
- 'click',
1299
- () => {
1300
- port.postMessage({
1301
- type: 'setup.install',
1302
- action: 'uninstall',
1303
- kind: contextAction.kind,
1304
- target: contextAction.target,
1305
- });
1306
- hideSetupContextMenu();
1307
- },
1308
- { once: true }
1309
- );
1310
- setupContextMenu.append(uninstallButton);
1311
- }
1312
-
1313
- setupContextMenu.hidden = false;
1314
- setupContextMenu.style.left = `${clientX}px`;
1315
- setupContextMenu.style.top = `${clientY}px`;
1316
-
1317
- const rect = setupContextMenu.getBoundingClientRect();
1318
- const left = Math.min(clientX, window.innerWidth - rect.width - 12);
1319
- const top = Math.min(clientY, window.innerHeight - rect.height - 12);
1320
- setupContextMenu.style.left = `${Math.max(8, left)}px`;
1321
- setupContextMenu.style.top = `${Math.max(8, top)}px`;
1322
- }
1323
-
1324
- /**
1325
- * @returns {void}
1326
- */
1327
- function hideSetupContextMenu() {
1328
- setupContextMenu.hidden = true;
1329
- setupContextMenu.replaceChildren();
1330
- }
1331
-
1332
- /**
1333
- * @param {string} text
1334
- * @returns {HTMLElement}
1335
- */
1336
- function createStatusPlaceholder(text) {
1337
- const row = document.createElement('div');
1338
- row.className = 'setup-status-placeholder';
1339
- row.textContent = text;
1340
- return row;
1341
- }
1342
-
1343
- /**
1344
- * @param {ActionLogEntry} entry
1345
- * @param {SetupStatus | null} setupStatus
1346
- * @param {ActionLogEntry[]} entries
1347
- * @param {number} index
1348
- * @returns {HTMLElement}
1349
- */
1350
- function renderActionLogEntry(entry, setupStatus, entries, index) {
1351
- const container = document.createElement('article');
1352
- container.className = 'card activity-card';
1353
-
1354
- const header = document.createElement('div');
1355
- header.className = 'activity-header';
1356
-
1357
- const title = document.createElement('h3');
1358
- title.className = 'card-title activity-title';
1359
- const methodLabel = document.createElement('span');
1360
- methodLabel.className = 'activity-method';
1361
- methodLabel.textContent = entry.method;
1362
- methodLabel.title = entry.method;
1363
- title.append(methodLabel);
1364
- const activitySourceTag = getActivitySourceTag(entry.source, setupStatus);
1365
- if (activitySourceTag) {
1366
- const sourceTag = document.createElement('span');
1367
- sourceTag.className = 'activity-source-tag';
1368
- sourceTag.textContent = activitySourceTag.toUpperCase();
1369
- title.append(sourceTag);
1370
- }
1371
-
1372
- const timestamp = document.createElement('span');
1373
- timestamp.className = 'muted activity-time';
1374
- timestamp.textContent = new Date(entry.at).toLocaleTimeString();
1375
-
1376
- header.append(title, timestamp);
1377
-
1378
- const footer = document.createElement('div');
1379
- footer.className = 'activity-footer';
1380
-
1381
- const summary = document.createElement('span');
1382
- summary.className = 'activity-summary';
1383
- if (!entry.ok) summary.classList.add('activity-summary-error');
1384
- const dot = document.createElement('span');
1385
- dot.className = 'activity-status-dot';
1386
- dot.dataset.ok = String(entry.ok);
1387
- const summaryText = document.createElement('span');
1388
- summaryText.textContent = entry.summary;
1389
- summary.append(dot, summaryText);
1390
- footer.append(summary);
1391
-
1392
- const badges = document.createElement('span');
1393
- badges.className = 'activity-badges';
1394
-
1395
- const showScope = !(requestedTabId != null && requestedTabId > 0) && entry.url;
1396
- if (showScope) {
1397
- const scopeLink = document.createElement('a');
1398
- scopeLink.className = 'activity-scope-link';
1399
- scopeLink.href = entry.url;
1400
- scopeLink.target = '_blank';
1401
- scopeLink.rel = 'noopener';
1402
- scopeLink.title = entry.url;
1403
- scopeLink.innerHTML =
1404
- '<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4.5 1.5H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8"/><path d="M7 1h4v4"/><path d="M5 7L11 1"/></svg>';
1405
- badges.append(scopeLink);
1406
- }
1407
-
1408
- const transportTokens = getEntryTransportTokens(entry);
1409
- if (transportTokens > 0 || entry.imageBytes > 0) {
1410
- const tokenLine = document.createElement('span');
1411
- tokenLine.className = 'muted activity-tokens';
1412
- /** @type {string[]} */
1413
- const parts = [];
1414
- if (transportTokens > 0) {
1415
- parts.push(`\u2248${transportTokens.toLocaleString()} tok`);
1416
- }
1417
- if (entry.nodeCount != null) {
1418
- parts.push(`${entry.nodeCount}n`);
1419
- }
1420
- if (entry.imageBytes > 0) {
1421
- parts.push(`${formatByteCount(entry.imageBytes)} img`);
1422
- }
1423
- if (entry.debuggerBacked) {
1424
- parts.push('cdp');
1425
- }
1426
- tokenLine.textContent = parts.join(' \u00b7 ');
1427
- tokenLine.dataset.costClass = entry.costClass;
1428
- badges.append(tokenLine);
1429
- }
1430
-
1431
- if (entry.debuggerBacked) {
1432
- badges.append(createActivityBadge('Debugger', 'activity-badge-warn'));
1433
- }
1434
-
1435
- if (entry.overBudget) {
1436
- badges.append(createActivityBadge('Truncated', 'activity-badge-warn'));
1437
- }
1438
-
1439
- if (entry.hasScreenshot) {
1440
- badges.append(createActivityBadge('Image', 'activity-badge-neutral'));
1441
- }
1442
-
1443
- if (countRecentExpensiveRepeats(entries, index) >= 2) {
1444
- badges.append(createActivityBadge('Repeat', 'activity-badge-warn'));
1445
- }
1446
-
1447
- if (badges.childElementCount) footer.append(badges);
1448
-
1449
- if (entry.continuationHint) {
1450
- const hint = document.createElement('p');
1451
- hint.className = 'muted activity-hint';
1452
- hint.textContent = entry.continuationHint;
1453
- container.append(header, footer, hint);
1454
- return container;
1455
- }
1456
-
1457
- container.append(header, footer);
1458
- return container;
1459
- }
1460
-
1461
- /**
1462
- * @param {string} label
1463
- * @param {string} className
1464
- * @returns {HTMLElement}
1465
- */
1466
- function createActivityBadge(label, className) {
1467
- const badge = document.createElement('span');
1468
- badge.className = `activity-badge ${className}`;
1469
- badge.textContent = label;
1470
- return badge;
1471
- }
1472
-
1473
- /**
1474
- * @param {number} totalTokens
1475
- * @returns {void}
1476
- */
1477
- function renderActivitySummary(totalTokens) {
1478
- if (totalTokens <= 0) {
1479
- activitySummaryTokens.hidden = true;
1480
- activitySummaryTokens.textContent = '';
1481
- activitySummaryTokens.removeAttribute('data-cost-class');
1482
- return;
1483
- }
1484
-
1485
- activitySummaryTokens.hidden = false;
1486
- activitySummaryTokens.textContent = `≈${formatCompactCount(totalTokens)} tok`;
1487
- activitySummaryTokens.dataset.costClass = getAggregateCostClass(totalTokens);
1488
- }
1489
-
1490
- /**
1491
- * @param {number} value
1492
- * @returns {string}
1493
- */
1494
- function formatByteCount(value) {
1495
- if (value < 1024) {
1496
- return `${value} B`;
1497
- }
1498
- if (value < 10 * 1024) {
1499
- return `${Math.round((value / 1024) * 10) / 10} KB`;
1500
- }
1501
- if (value < 1024 * 1024) {
1502
- return `${Math.round(value / 1024)} KB`;
1503
- }
1504
- return `${Math.round((value / (1024 * 1024)) * 10) / 10} MB`;
1505
- }
1506
-
1507
- /**
1508
- * @param {ActionLogEntry} entry
1509
- * @returns {number}
1510
- */
1511
- function getEntryTransportTokens(entry) {
1512
- return Math.max(0, entry.approxTokens + entry.imageApproxTokens);
1513
- }
1514
-
1515
- /**
1516
- * Snap the histogram window to the active bucket boundary so bars only shift
1517
- * when the 30s bin rolls over instead of drifting on every refresh tick.
1518
- *
1519
- * @param {number} value
1520
- * @returns {number}
1521
- */
1522
- function alignHistogramEndAt(value) {
1523
- if (!Number.isFinite(value) || value <= 0) {
1524
- return ACTIVITY_HISTOGRAM_BUCKET_MS;
1525
- }
1526
- return Math.ceil(value / ACTIVITY_HISTOGRAM_BUCKET_MS) * ACTIVITY_HISTOGRAM_BUCKET_MS;
1527
- }
1528
-
1529
- /**
1530
- * @typedef {{
1531
- * startAt: number,
1532
- * endAt: number,
1533
- * totalTokens: number,
1534
- * buckets: Array<{
1535
- * bucketStart: number,
1536
- * totalTokens: number,
1537
- * segments: Array<{ family: string, tokens: number }>
1538
- * }>
1539
- * }} ActivityHistogramModel
1540
- */
1541
-
1542
- /**
1543
- * @param {ActionLogEntry[]} entries
1544
- * @returns {ActivityHistogramModel}
1545
- */
1546
- function buildActivityHistogram(entries) {
1547
- const latestAt = entries.reduce(
1548
- (maxAt, entry) => Math.max(maxAt, Number.isFinite(entry.at) ? entry.at : 0),
1549
- 0
1550
- );
1551
- const endAt = alignHistogramEndAt(Math.max(Date.now(), latestAt));
1552
- const startAt = endAt - ACTIVITY_HISTOGRAM_WINDOW_MS;
1553
- const buckets = Array.from({ length: ACTIVITY_HISTOGRAM_BARS }, (_, index) => ({
1554
- bucketStart: startAt + index * ACTIVITY_HISTOGRAM_BUCKET_MS,
1555
- totalTokens: 0,
1556
- segments: [],
1557
- }));
1558
-
1559
- /** @type {Map<number, Map<string, number>>} */
1560
- const bucketFamilies = new Map(buckets.map((_, index) => [index, new Map()]));
1561
- let totalTokens = 0;
1562
-
1563
- for (const entry of entries) {
1564
- const transportTokens = getEntryTransportTokens(entry);
1565
- if (!Number.isFinite(transportTokens) || transportTokens <= 0) {
1566
- continue;
1567
- }
1568
- if (!Number.isFinite(entry.at) || entry.at < startAt || entry.at > endAt) {
1569
- continue;
1570
- }
1571
-
1572
- const offset = Math.min(
1573
- ACTIVITY_HISTOGRAM_BARS - 1,
1574
- Math.max(0, Math.floor((entry.at - startAt) / ACTIVITY_HISTOGRAM_BUCKET_MS))
1575
- );
1576
- const family = getHistogramMethodFamily(entry.method);
1577
- const familyTotals = /** @type {Map<string, number>} */ (bucketFamilies.get(offset));
1578
- familyTotals.set(family, (familyTotals.get(family) ?? 0) + transportTokens);
1579
- buckets[offset].totalTokens += transportTokens;
1580
- totalTokens += transportTokens;
1581
- }
1582
-
1583
- for (let index = 0; index < buckets.length; index += 1) {
1584
- const familyTotals = /** @type {Map<string, number>} */ (bucketFamilies.get(index));
1585
- buckets[index].segments =
1586
- /** @type {typeof buckets[number]['segments']} */ (
1587
- HISTOGRAM_METHOD_FAMILIES.map((family) => ({
1588
- family,
1589
- tokens: familyTotals.get(family) ?? 0,
1590
- })).filter((segment) => segment.tokens > 0)
1591
- );
1592
- }
1593
-
1594
- return {
1595
- startAt,
1596
- endAt,
1597
- totalTokens,
1598
- buckets,
1599
- };
1600
- }
1601
-
1602
- /**
1603
- * @param {ActivityHistogramModel} histogram
1604
- * @returns {void}
1605
- */
1606
- function renderActivityHistogram(histogram) {
1607
- const activeBuckets = histogram.buckets.filter((bucket) => bucket.totalTokens > 0);
1608
- if (!activeBuckets.length) {
1609
- activityHistogram.hidden = true;
1610
- activityHistogramBars.replaceChildren();
1611
- activityHistogramRange.textContent = '';
1612
- return;
1613
- }
1614
-
1615
- const maxTokens = Math.max(...histogram.buckets.map((bucket) => bucket.totalTokens), 1);
1616
- activityHistogram.hidden = false;
1617
- activityHistogramRange.textContent = '10m window · 30s bins';
1618
- activityHistogramBars.replaceChildren(
1619
- ...histogram.buckets.map((bucket) => createHistogramBar(bucket, maxTokens))
1620
- );
1621
- }
1622
-
1623
- /**
1624
- * @param {number} value
1625
- * @returns {string}
1626
- */
1627
- function formatCompactCount(value) {
1628
- if (value >= 1000) {
1629
- const compact = value >= 10000 ? Math.round(value / 1000) : Math.round(value / 100) / 10;
1630
- return `${compact}k`;
1631
- }
1632
- return String(value);
1633
- }
1634
-
1635
- /**
1636
- * @param {number} approxTokens
1637
- * @returns {'cheap' | 'moderate' | 'heavy' | 'extreme'}
1638
- */
1639
- function getAggregateCostClass(approxTokens) {
1640
- if (approxTokens <= 250) {
1641
- return 'cheap';
1642
- }
1643
- if (approxTokens <= 1000) {
1644
- return 'moderate';
1645
- }
1646
- if (approxTokens <= 3000) {
1647
- return 'heavy';
1648
- }
1649
- return 'extreme';
1650
- }
1651
-
1652
- /**
1653
- * @param {ActivityHistogramModel['buckets'][number]} bucket
1654
- * @param {number} maxTokens
1655
- * @returns {HTMLElement}
1656
- */
1657
- function createHistogramBar(bucket, maxTokens) {
1658
- const bar = document.createElement('span');
1659
- const ratio = Math.log1p(bucket.totalTokens) / Math.log1p(maxTokens);
1660
- const height = bucket.totalTokens > 0 ? Math.max(14, Math.round(ratio * 100)) : 6;
1661
- bar.className = 'activity-histogram-bar';
1662
- bar.style.height = `${height}%`;
1663
-
1664
- if (!bucket.totalTokens) {
1665
- bar.dataset.empty = 'true';
1666
- bar.title = `${new Date(bucket.bucketStart).toLocaleTimeString()} · no activity`;
1667
- bar.setAttribute(
1668
- 'aria-label',
1669
- `${new Date(bucket.bucketStart).toLocaleTimeString()}, no token activity`
1670
- );
1671
- return bar;
1672
- }
1673
-
1674
- const tooltipParts = bucket.segments.map(
1675
- (segment) => `${segment.family}: ${segment.tokens.toLocaleString()} tok`
1676
- );
1677
- bar.title = `${new Date(bucket.bucketStart).toLocaleTimeString()} · ${bucket.totalTokens.toLocaleString()} tok\n${tooltipParts.join('\n')}`;
1678
- bar.setAttribute(
1679
- 'aria-label',
1680
- `${new Date(bucket.bucketStart).toLocaleTimeString()}, approximately ${bucket.totalTokens.toLocaleString()} tokens`
1681
- );
1682
- bar.append(
1683
- ...bucket.segments.map((segment) => createHistogramSegment(segment, bucket.totalTokens))
1684
- );
1685
- return bar;
1686
- }
1687
-
1688
- /**
1689
- * @param {{ family: string, tokens: number }} segment
1690
- * @param {number} totalTokens
1691
- * @returns {HTMLElement}
1692
- */
1693
- function createHistogramSegment(segment, totalTokens) {
1694
- const el = document.createElement('span');
1695
- el.className = 'activity-histogram-segment';
1696
- el.dataset.family = segment.family;
1697
- el.style.height = `${(segment.tokens / totalTokens) * 100}%`;
1698
- return el;
1699
- }
1700
-
1701
- /**
1702
- * @param {string} method
1703
- * @returns {string}
1704
- */
1705
- function getHistogramMethodFamily(method) {
1706
- if (method.startsWith('dom.')) {
1707
- return 'dom';
1708
- }
1709
- if (method.startsWith('page.')) {
1710
- return 'page';
1711
- }
1712
- if (method.startsWith('layout.')) {
1713
- return 'layout';
1714
- }
1715
- if (method.startsWith('styles.')) {
1716
- return 'style';
1717
- }
1718
- if (
1719
- method.startsWith('input.') ||
1720
- method.startsWith('navigation.') ||
1721
- method.startsWith('viewport.')
1722
- ) {
1723
- return 'input';
1724
- }
1725
- if (method.startsWith('patch.')) {
1726
- return 'patch';
1727
- }
1728
- if (
1729
- method.startsWith('screenshot.') ||
1730
- method.startsWith('cdp.') ||
1731
- method.startsWith('performance.')
1732
- ) {
1733
- return 'capture';
1734
- }
1735
- return 'other';
1736
- }
1737
-
1738
- /**
1739
- * @param {ActionLogEntry[]} entries
1740
- * @param {number} index
1741
- * @returns {number}
1742
- */
1743
- function countRecentExpensiveRepeats(entries, index) {
1744
- const current = entries[index];
1745
- const currentCostClass =
1746
- current?.summaryTokens > 0 ? current.summaryCostClass : current?.costClass;
1747
- if (
1748
- !current ||
1749
- (!current.debuggerBacked && currentCostClass !== 'heavy' && currentCostClass !== 'extreme')
1750
- ) {
1751
- return 0;
1752
- }
1753
-
1754
- let count = 0;
1755
- for (let cursor = index + 1; cursor < Math.min(entries.length, index + 4); cursor += 1) {
1756
- const candidate = entries[cursor];
1757
- if (!candidate || candidate.method !== current.method) {
1758
- break;
1759
- }
1760
- const candidateCostClass =
1761
- candidate.summaryTokens > 0 ? candidate.summaryCostClass : candidate.costClass;
1762
- if (
1763
- candidate.debuggerBacked ||
1764
- candidateCostClass === 'heavy' ||
1765
- candidateCostClass === 'extreme'
1766
- ) {
1767
- count += 1;
1768
- }
1769
- }
1770
- return count;
1771
- }