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