@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.
- package/README.md +6 -4
- package/package.json +53 -53
- package/packages/agent-client/src/cli-helpers.js +43 -5
- package/packages/agent-client/src/cli.js +176 -171
- package/packages/agent-client/src/client.js +66 -21
- package/packages/agent-client/src/command-registry.js +104 -69
- package/packages/agent-client/src/detect.js +162 -54
- package/packages/agent-client/src/install.js +34 -28
- package/packages/agent-client/src/mcp-config.js +40 -40
- package/packages/agent-client/src/runtime.js +41 -20
- package/packages/agent-client/src/setup-status.js +23 -30
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +573 -256
- package/packages/mcp-server/src/server.js +568 -257
- package/packages/native-host/bin/bridge-daemon.js +39 -6
- package/packages/native-host/bin/install-manifest.js +26 -4
- package/packages/native-host/bin/postinstall.js +4 -2
- package/packages/native-host/src/config.js +142 -13
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +350 -150
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +194 -29
- package/packages/native-host/src/native-host.js +154 -102
- package/packages/protocol/src/budget.js +3 -7
- package/packages/protocol/src/capabilities.js +6 -3
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +15 -11
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +242 -73
- package/packages/protocol/src/registry.js +311 -45
- package/packages/protocol/src/summary.js +260 -109
- package/packages/protocol/src/types.js +29 -4
- package/skills/browser-bridge/SKILL.md +3 -2
- package/skills/browser-bridge/agents/openai.yaml +3 -3
- package/skills/browser-bridge/references/interaction.md +34 -11
- package/skills/browser-bridge/references/patch-workflow.md +3 -0
- package/skills/browser-bridge/references/protocol.md +127 -71
- package/skills/browser-bridge/references/tailwind.md +12 -11
- package/skills/browser-bridge/references/token-efficiency.md +23 -22
- package/skills/browser-bridge/references/ui-workflows.md +8 -0
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -114
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -52
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -459
- package/packages/extension/src/background-routing.js +0 -91
- package/packages/extension/src/background.js +0 -3227
- package/packages/extension/src/content-script-helpers.js +0 -281
- package/packages/extension/src/content-script.js +0 -1977
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -102
- package/packages/extension/ui/offscreen.html +0 -6
- package/packages/extension/ui/offscreen.js +0 -61
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -279
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1854
- 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
|
-
}
|