@browserbridge/bbx 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +11 -13
- package/packages/agent-client/src/cli-helpers.js +33 -0
- package/packages/agent-client/src/cli.js +116 -41
- package/packages/agent-client/src/client.js +29 -4
- package/packages/agent-client/src/command-registry.js +3 -0
- package/packages/agent-client/src/detect.js +159 -48
- package/packages/agent-client/src/install.js +24 -1
- package/packages/agent-client/src/mcp-config.js +29 -10
- package/packages/agent-client/src/setup-status.js +12 -4
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +28 -7
- package/packages/mcp-server/src/server.js +12 -2
- package/packages/native-host/bin/bridge-daemon.js +33 -4
- package/packages/native-host/bin/install-manifest.js +24 -2
- package/packages/native-host/src/config.js +131 -6
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +217 -68
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +121 -7
- package/packages/native-host/src/native-host.js +110 -73
- package/packages/protocol/src/capabilities.js +3 -0
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +4 -0
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +143 -7
- package/packages/protocol/src/registry.js +11 -0
- package/packages/protocol/src/summary.js +18 -10
- package/packages/protocol/src/types.js +28 -3
- package/skills/browser-bridge/SKILL.md +2 -1
- package/skills/browser-bridge/references/interaction.md +1 -0
- package/skills/browser-bridge/references/protocol.md +2 -1
- 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 -112
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/unpacked-extension.md +0 -72
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -38
- 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 -474
- package/packages/extension/src/background-routing.js +0 -89
- package/packages/extension/src/background.js +0 -3490
- package/packages/extension/src/content-script-helpers.js +0 -282
- package/packages/extension/src/content-script.js +0 -2043
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -104
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -298
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1771
- package/packages/extension/ui/ui.css +0 -1160
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
/** @typedef {{ tabId: number }} DebuggerTarget */
|
|
4
|
-
/** @typedef {(target: DebuggerTarget, protocolVersion: string) => Promise<void>} DebuggerAttach */
|
|
5
|
-
/** @typedef {(target: DebuggerTarget) => Promise<void>} DebuggerDetach */
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Serialize Chrome debugger sessions per tab so concurrent bridge requests do
|
|
9
|
-
* not race on `chrome.debugger.attach`.
|
|
10
|
-
*/
|
|
11
|
-
export class TabDebuggerCoordinator {
|
|
12
|
-
/**
|
|
13
|
-
* @param {{
|
|
14
|
-
* attach: DebuggerAttach,
|
|
15
|
-
* detach: DebuggerDetach,
|
|
16
|
-
* protocolVersion?: string,
|
|
17
|
-
* burstIdleMs?: number
|
|
18
|
-
* }} options
|
|
19
|
-
*/
|
|
20
|
-
constructor({ attach, detach, protocolVersion = '1.3', burstIdleMs = 5_000 }) {
|
|
21
|
-
this.attach = attach;
|
|
22
|
-
this.detach = detach;
|
|
23
|
-
this.protocolVersion = protocolVersion;
|
|
24
|
-
this.burstIdleMs = burstIdleMs;
|
|
25
|
-
/** @type {Map<number, Promise<void>>} */
|
|
26
|
-
this.pendingByTab = new Map();
|
|
27
|
-
/** @type {Map<number, number>} */
|
|
28
|
-
this.holdsByTab = new Map();
|
|
29
|
-
/** @type {Map<number, ReturnType<typeof setTimeout>>} */
|
|
30
|
-
this.burstTimers = new Map();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Run one serialized operation for a tab.
|
|
35
|
-
*
|
|
36
|
-
* @template T
|
|
37
|
-
* @param {number} tabId
|
|
38
|
-
* @param {() => Promise<T>} task
|
|
39
|
-
* @returns {Promise<T>}
|
|
40
|
-
*/
|
|
41
|
-
async runExclusive(tabId, task) {
|
|
42
|
-
const previous = this.pendingByTab.get(tabId) ?? Promise.resolve();
|
|
43
|
-
/** @type {(value?: void | PromiseLike<void>) => void} */
|
|
44
|
-
let releaseTurn = () => {};
|
|
45
|
-
const turn = new Promise((resolve) => {
|
|
46
|
-
releaseTurn = resolve;
|
|
47
|
-
});
|
|
48
|
-
const queuedTurn = previous.catch(() => {}).then(() => turn);
|
|
49
|
-
this.pendingByTab.set(tabId, queuedTurn);
|
|
50
|
-
|
|
51
|
-
await previous.catch(() => {});
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
return await task();
|
|
55
|
-
} finally {
|
|
56
|
-
releaseTurn();
|
|
57
|
-
if (this.pendingByTab.get(tabId) === queuedTurn) {
|
|
58
|
-
this.pendingByTab.delete(tabId);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Run one debugger-backed task for a tab once earlier tasks for that tab
|
|
65
|
-
* have finished.
|
|
66
|
-
*
|
|
67
|
-
* @template T
|
|
68
|
-
* @param {number} tabId
|
|
69
|
-
* @param {(target: DebuggerTarget) => Promise<T>} task
|
|
70
|
-
* @returns {Promise<T>}
|
|
71
|
-
*/
|
|
72
|
-
async run(tabId, task) {
|
|
73
|
-
return this.runExclusive(tabId, async () => {
|
|
74
|
-
const target = { tabId };
|
|
75
|
-
const held = (this.holdsByTab.get(tabId) ?? 0) > 0;
|
|
76
|
-
const hasBurst = this.burstTimers.has(tabId);
|
|
77
|
-
/** @type {T | undefined} */
|
|
78
|
-
let result;
|
|
79
|
-
/** @type {unknown} */
|
|
80
|
-
let taskError = null;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
if (!held && !hasBurst) {
|
|
84
|
-
await this.attach(target, this.protocolVersion);
|
|
85
|
-
}
|
|
86
|
-
result = await task(target);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
taskError = error;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Schedule a burst-idle detach instead of detaching immediately.
|
|
92
|
-
if (!held) {
|
|
93
|
-
this._resetBurstTimer(tabId, target);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (taskError) {
|
|
97
|
-
throw taskError;
|
|
98
|
-
}
|
|
99
|
-
return /** @type {T} */ (result);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Reset or start the burst idle timer for a tab. When it fires, detach
|
|
105
|
-
* the debugger if no explicit hold is active.
|
|
106
|
-
*
|
|
107
|
-
* @param {number} tabId
|
|
108
|
-
* @param {DebuggerTarget} target
|
|
109
|
-
* @returns {void}
|
|
110
|
-
*/
|
|
111
|
-
_resetBurstTimer(tabId, target) {
|
|
112
|
-
const existing = this.burstTimers.get(tabId);
|
|
113
|
-
if (existing) clearTimeout(existing);
|
|
114
|
-
const timer = setTimeout(async () => {
|
|
115
|
-
this.burstTimers.delete(tabId);
|
|
116
|
-
if ((this.holdsByTab.get(tabId) ?? 0) > 0) return;
|
|
117
|
-
try {
|
|
118
|
-
await this.detach(target);
|
|
119
|
-
} catch {
|
|
120
|
-
// Already detached or tab closed.
|
|
121
|
-
}
|
|
122
|
-
}, this.burstIdleMs);
|
|
123
|
-
timer.unref?.();
|
|
124
|
-
this.burstTimers.set(tabId, timer);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Attach and keep a debugger session alive across multiple runs for the same
|
|
129
|
-
* tab. Nested holds are reference-counted.
|
|
130
|
-
*
|
|
131
|
-
* @param {number} tabId
|
|
132
|
-
* @param {(target: DebuggerTarget) => Promise<void>} [initialize]
|
|
133
|
-
* @returns {Promise<void>}
|
|
134
|
-
*/
|
|
135
|
-
async acquire(tabId, initialize = async () => {}) {
|
|
136
|
-
await this.runExclusive(tabId, async () => {
|
|
137
|
-
const target = { tabId };
|
|
138
|
-
const holdCount = this.holdsByTab.get(tabId) ?? 0;
|
|
139
|
-
if (holdCount === 0) {
|
|
140
|
-
let attached = false;
|
|
141
|
-
try {
|
|
142
|
-
await this.attach(target, this.protocolVersion);
|
|
143
|
-
attached = true;
|
|
144
|
-
await initialize(target);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (attached) {
|
|
147
|
-
await this.detach(target).catch(() => {});
|
|
148
|
-
}
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
this.holdsByTab.set(tabId, holdCount + 1);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Release one persistent debugger hold for a tab.
|
|
158
|
-
*
|
|
159
|
-
* @param {number} tabId
|
|
160
|
-
* @param {(target: DebuggerTarget) => Promise<void>} [cleanup]
|
|
161
|
-
* @returns {Promise<void>}
|
|
162
|
-
*/
|
|
163
|
-
async release(tabId, cleanup = async () => {}) {
|
|
164
|
-
await this.runExclusive(tabId, async () => {
|
|
165
|
-
const target = { tabId };
|
|
166
|
-
const holdCount = this.holdsByTab.get(tabId) ?? 0;
|
|
167
|
-
if (holdCount === 0) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (holdCount > 1) {
|
|
171
|
-
this.holdsByTab.set(tabId, holdCount - 1);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
this.holdsByTab.delete(tabId);
|
|
176
|
-
let cleanupError = null;
|
|
177
|
-
try {
|
|
178
|
-
await cleanup(target);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
cleanupError = error;
|
|
181
|
-
}
|
|
182
|
-
await this.detach(target);
|
|
183
|
-
if (cleanupError) {
|
|
184
|
-
throw cleanupError;
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {{
|
|
5
|
-
* configured: boolean
|
|
6
|
-
* }} McpClientInstallState
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {{
|
|
11
|
-
* exists: boolean
|
|
12
|
-
* }} SkillInstallState
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {{
|
|
17
|
-
* skills: SkillInstallState[]
|
|
18
|
-
* }} SkillTargetInstallState
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {{
|
|
23
|
-
* mcpClients: McpClientInstallState[],
|
|
24
|
-
* skillTargets: SkillTargetInstallState[]
|
|
25
|
-
* }} SetupStatusInstallState
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {SetupStatusInstallState} setupStatus
|
|
30
|
-
* @returns {{ hasConfiguredMcp: boolean, hasInstalledCliSkill: boolean }}
|
|
31
|
-
*/
|
|
32
|
-
function getSetupInstallState(setupStatus) {
|
|
33
|
-
const hasConfiguredMcp = setupStatus.mcpClients.some((client) => client.configured);
|
|
34
|
-
const hasInstalledCliSkill = setupStatus.skillTargets.some((target) =>
|
|
35
|
-
target.skills.some((skill) => skill.exists)
|
|
36
|
-
);
|
|
37
|
-
return { hasConfiguredMcp, hasInstalledCliSkill };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Auto-expand Host Setup when the panel opens into a completely unconfigured
|
|
42
|
-
* machine: no MCP clients configured and no CLI skill present anywhere.
|
|
43
|
-
*
|
|
44
|
-
* @param {SetupStatusInstallState | null} setupStatus
|
|
45
|
-
* @returns {boolean}
|
|
46
|
-
*/
|
|
47
|
-
export function shouldAutoExpandHostSetup(setupStatus) {
|
|
48
|
-
if (!setupStatus) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const { hasConfiguredMcp, hasInstalledCliSkill } = getSetupInstallState(setupStatus);
|
|
53
|
-
if (hasConfiguredMcp) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
return !hasInstalledCliSkill;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Pick which prompt-example set to show in the side panel.
|
|
61
|
-
*
|
|
62
|
-
* - `mcp`: MCP is installed, CLI skill is not.
|
|
63
|
-
* - `cli`: CLI skill is installed, MCP is not.
|
|
64
|
-
* - `grouped`: neither is installed, or both are installed.
|
|
65
|
-
*
|
|
66
|
-
* @param {SetupStatusInstallState | null} setupStatus
|
|
67
|
-
* @returns {'mcp' | 'cli' | 'grouped'}
|
|
68
|
-
*/
|
|
69
|
-
export function getPromptExamplesMode(setupStatus) {
|
|
70
|
-
if (!setupStatus) {
|
|
71
|
-
return 'grouped';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const { hasConfiguredMcp, hasInstalledCliSkill } = getSetupInstallState(setupStatus);
|
|
75
|
-
if (hasConfiguredMcp && !hasInstalledCliSkill) {
|
|
76
|
-
return 'mcp';
|
|
77
|
-
}
|
|
78
|
-
if (hasInstalledCliSkill && !hasConfiguredMcp) {
|
|
79
|
-
return 'cli';
|
|
80
|
-
}
|
|
81
|
-
return 'grouped';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Pick the activity source tag to display in the side panel. Prefer explicit
|
|
86
|
-
* request metadata, but fall back to setup state when only one host path is
|
|
87
|
-
* configured so older log entries stay understandable.
|
|
88
|
-
*
|
|
89
|
-
* @param {string | null | undefined} source
|
|
90
|
-
* @param {SetupStatusInstallState | null} setupStatus
|
|
91
|
-
* @returns {'' | 'cli' | 'mcp'}
|
|
92
|
-
*/
|
|
93
|
-
export function getActivitySourceTag(source, setupStatus) {
|
|
94
|
-
if (source === 'cli' || source === 'mcp') {
|
|
95
|
-
return source;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const promptMode = getPromptExamplesMode(setupStatus);
|
|
99
|
-
if (promptMode === 'cli' || promptMode === 'mcp') {
|
|
100
|
-
return promptMode;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return '';
|
|
104
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Browser Bridge</title>
|
|
7
|
-
<link rel="stylesheet" href="./ui.css">
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<main class="panel panel-popup">
|
|
11
|
-
<header class="panel-header">
|
|
12
|
-
<h1 class="popup-title">Browser Bridge</h1>
|
|
13
|
-
<div class="control-links">
|
|
14
|
-
<a class="setup-link" href="https://github.com/koltyakov/browser-bridge" target="_blank"
|
|
15
|
-
rel="noopener noreferrer">GitHub ↗</a>
|
|
16
|
-
<a class="setup-link" href="https://github.com/koltyakov/browser-bridge/blob/main/PRIVACY.md"
|
|
17
|
-
target="_blank" rel="noopener noreferrer">Privacy policy ↗</a>
|
|
18
|
-
</div>
|
|
19
|
-
</header>
|
|
20
|
-
<section class="card control-card popup-control-card">
|
|
21
|
-
<div class="control-copy">
|
|
22
|
-
<div id="popup-access-eyebrow" class="eyebrow">Window access</div>
|
|
23
|
-
<p id="popup-access-detail" class="control-detail">
|
|
24
|
-
Enable Browser Bridge to let your connected agent inspect and interact with pages in this Chrome window.
|
|
25
|
-
</p>
|
|
26
|
-
<p id="popup-disclosure" class="control-detail control-disclosure">
|
|
27
|
-
When enabled, Browser Bridge may access page content, styles and layout, console output, network metadata, screenshots, and browser storage when requested.
|
|
28
|
-
</p>
|
|
29
|
-
</div>
|
|
30
|
-
<button id="communication-action" class="popup-action popup-power-button" type="button">Enable Window Access</button>
|
|
31
|
-
</section>
|
|
32
|
-
</main>
|
|
33
|
-
<script type="module" src="./popup.js"></script>
|
|
34
|
-
</body>
|
|
35
|
-
</html>
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {{
|
|
5
|
-
* tabId: number,
|
|
6
|
-
* windowId: number,
|
|
7
|
-
* title: string,
|
|
8
|
-
* url: string,
|
|
9
|
-
* enabled: boolean,
|
|
10
|
-
* accessRequested: boolean,
|
|
11
|
-
* restricted: boolean
|
|
12
|
-
* }} PopupCurrentTab
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {{
|
|
17
|
-
* type: 'state.sync',
|
|
18
|
-
* state: {
|
|
19
|
-
* nativeConnected: boolean,
|
|
20
|
-
* currentTab: PopupCurrentTab | null
|
|
21
|
-
* }
|
|
22
|
-
* }} PopupStateMessage
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const nativeIndicator =
|
|
26
|
-
/** @type {HTMLSpanElement} */ (document.getElementById('native-indicator'));
|
|
27
|
-
const button = /** @type {HTMLButtonElement} */ (document.getElementById('communication-action'));
|
|
28
|
-
const accessEyebrow = /** @type {HTMLDivElement} */ (
|
|
29
|
-
document.getElementById('popup-access-eyebrow')
|
|
30
|
-
);
|
|
31
|
-
const accessDetail = /** @type {HTMLParagraphElement} */ (
|
|
32
|
-
document.getElementById('popup-access-detail')
|
|
33
|
-
);
|
|
34
|
-
const accessDisclosure =
|
|
35
|
-
/** @type {HTMLParagraphElement} */ (document.getElementById('popup-disclosure'));
|
|
36
|
-
const controlCard = /** @type {HTMLElement | null} */ (
|
|
37
|
-
document.querySelector('.popup-control-card')
|
|
38
|
-
);
|
|
39
|
-
const windowedPopup = isWindowedPopup();
|
|
40
|
-
/** @type {number | null} */
|
|
41
|
-
let popupScopeTabId = null;
|
|
42
|
-
|
|
43
|
-
/** @param {PopupStateMessage} message */
|
|
44
|
-
function handlePopupMessage(message) {
|
|
45
|
-
if (message.type === 'state.sync') {
|
|
46
|
-
renderNativeStatus(message.state.nativeConnected);
|
|
47
|
-
renderPopupState(message.state.currentTab);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** @type {chrome.runtime.Port} */
|
|
52
|
-
let port;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @returns {Promise<number | null>}
|
|
56
|
-
*/
|
|
57
|
-
async function resolveInitialScopeTabId() {
|
|
58
|
-
const explicitScopeTabId = readScopedTabId();
|
|
59
|
-
if (explicitScopeTabId) {
|
|
60
|
-
return explicitScopeTabId;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const [activeTab] = await chrome.tabs.query({
|
|
65
|
-
active: true,
|
|
66
|
-
currentWindow: true,
|
|
67
|
-
});
|
|
68
|
-
return typeof activeTab?.id === 'number' ? activeTab.id : null;
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @returns {Promise<void>}
|
|
76
|
-
*/
|
|
77
|
-
async function connectPopupPort() {
|
|
78
|
-
popupScopeTabId = await resolveInitialScopeTabId();
|
|
79
|
-
const nextPort = chrome.runtime.connect({ name: 'ui-popup' });
|
|
80
|
-
nextPort.onMessage.addListener(handlePopupMessage);
|
|
81
|
-
nextPort.postMessage({
|
|
82
|
-
type: 'state.request',
|
|
83
|
-
...(popupScopeTabId ? { scopeTabId: popupScopeTabId } : {}),
|
|
84
|
-
});
|
|
85
|
-
port = nextPort;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
void connectPopupPort();
|
|
89
|
-
/** @type {PopupCurrentTab | null} */
|
|
90
|
-
let currentTabState = null;
|
|
91
|
-
/** @type {number | null} */
|
|
92
|
-
let resizeFrameId = null;
|
|
93
|
-
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
94
|
-
let nativeDiagnosticTimer = null;
|
|
95
|
-
const NATIVE_DIAGNOSTIC_DELAY_MS = 10_000;
|
|
96
|
-
const PUBLISHED_EXTENSION_ID = 'jjjkmmcdkpcgamlopogicbnnhdgebhie';
|
|
97
|
-
|
|
98
|
-
if (windowedPopup) {
|
|
99
|
-
document.documentElement.dataset.windowed = 'true';
|
|
100
|
-
document.body.dataset.windowed = 'true';
|
|
101
|
-
window.addEventListener('load', queueWindowResize);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
button.addEventListener('click', () => {
|
|
105
|
-
if (!currentTabState || button.dataset.pending === 'true') {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
button.dataset.pending = 'true';
|
|
109
|
-
button.textContent = currentTabState.enabled ? 'Disabling\u2026' : 'Enabling\u2026';
|
|
110
|
-
setCommunicationEnabled(!currentTabState.enabled);
|
|
111
|
-
window.close();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @param {PopupCurrentTab | null} currentTab
|
|
116
|
-
* @returns {void}
|
|
117
|
-
*/
|
|
118
|
-
function renderPopupState(currentTab) {
|
|
119
|
-
currentTabState = currentTab;
|
|
120
|
-
|
|
121
|
-
if (!currentTab) {
|
|
122
|
-
accessEyebrow.textContent = 'Window access unavailable';
|
|
123
|
-
accessDetail.textContent =
|
|
124
|
-
'Open a normal web page to manage Browser Bridge for this Chrome window.';
|
|
125
|
-
accessDisclosure.hidden = false;
|
|
126
|
-
button.textContent = 'Enable Window Access';
|
|
127
|
-
button.disabled = true;
|
|
128
|
-
controlCard?.classList.remove('attention');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
accessDisclosure.hidden = currentTab.enabled;
|
|
133
|
-
|
|
134
|
-
if (currentTab.enabled && currentTab.restricted) {
|
|
135
|
-
accessEyebrow.textContent = 'Window access enabled';
|
|
136
|
-
accessDetail.textContent =
|
|
137
|
-
'This page cannot be interacted with. Switch to a normal web page to use Browser Bridge.';
|
|
138
|
-
accessDisclosure.hidden = false;
|
|
139
|
-
} else if (currentTab.enabled) {
|
|
140
|
-
accessEyebrow.textContent = 'Window access enabled';
|
|
141
|
-
accessDetail.textContent =
|
|
142
|
-
'Your connected agent can inspect and interact with pages in this Chrome window.';
|
|
143
|
-
} else if (currentTab.accessRequested) {
|
|
144
|
-
accessEyebrow.textContent = 'Window access requested';
|
|
145
|
-
accessDetail.textContent =
|
|
146
|
-
'An agent requested access for this Chrome window. Enable it to allow page inspection and interaction.';
|
|
147
|
-
} else {
|
|
148
|
-
accessEyebrow.textContent = 'Window access';
|
|
149
|
-
accessDetail.textContent =
|
|
150
|
-
'Enable Browser Bridge to let your connected agent inspect and interact with pages in this Chrome window.';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
button.textContent = currentTab.enabled ? 'Disable Window Access' : 'Enable Window Access';
|
|
154
|
-
button.disabled = !currentTab.url;
|
|
155
|
-
controlCard?.classList.toggle('attention', currentTab.accessRequested && !currentTab.enabled);
|
|
156
|
-
queueWindowResize();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* @param {boolean} connected
|
|
161
|
-
* @returns {void}
|
|
162
|
-
*/
|
|
163
|
-
function renderNativeStatus(connected) {
|
|
164
|
-
if (!nativeIndicator) return;
|
|
165
|
-
const label = connected ? 'Native host connected' : 'Native host disconnected';
|
|
166
|
-
nativeIndicator.dataset.connected = String(connected);
|
|
167
|
-
nativeIndicator.title = label;
|
|
168
|
-
nativeIndicator.setAttribute('aria-label', label);
|
|
169
|
-
|
|
170
|
-
if (connected) {
|
|
171
|
-
if (nativeDiagnosticTimer) {
|
|
172
|
-
clearTimeout(nativeDiagnosticTimer);
|
|
173
|
-
nativeDiagnosticTimer = null;
|
|
174
|
-
}
|
|
175
|
-
hideDiagnostic();
|
|
176
|
-
} else if (!nativeDiagnosticTimer) {
|
|
177
|
-
nativeDiagnosticTimer = setTimeout(() => {
|
|
178
|
-
nativeDiagnosticTimer = null;
|
|
179
|
-
showDiagnostic(
|
|
180
|
-
`Native host unreachable. Run: npm install -g @browserbridge/bbx && ${getInstallCommand()}`
|
|
181
|
-
);
|
|
182
|
-
}, NATIVE_DIAGNOSTIC_DELAY_MS);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* @param {string} message
|
|
188
|
-
* @returns {void}
|
|
189
|
-
*/
|
|
190
|
-
function showDiagnostic(message) {
|
|
191
|
-
let el = document.getElementById('native-diagnostic');
|
|
192
|
-
if (!el) {
|
|
193
|
-
el = document.createElement('div');
|
|
194
|
-
el.id = 'native-diagnostic';
|
|
195
|
-
el.style.cssText =
|
|
196
|
-
'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';
|
|
197
|
-
const container = document.querySelector('.popup-content') || document.body;
|
|
198
|
-
container.prepend(el);
|
|
199
|
-
}
|
|
200
|
-
el.textContent = message;
|
|
201
|
-
el.hidden = false;
|
|
202
|
-
queueWindowResize();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function hideDiagnostic() {
|
|
206
|
-
const el = document.getElementById('native-diagnostic');
|
|
207
|
-
if (el) {
|
|
208
|
-
el.hidden = true;
|
|
209
|
-
queueWindowResize();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @param {boolean} enabled
|
|
215
|
-
* @returns {void}
|
|
216
|
-
*/
|
|
217
|
-
function setCommunicationEnabled(enabled) {
|
|
218
|
-
const scopedTabId = currentTabState?.tabId ?? popupScopeTabId;
|
|
219
|
-
port.postMessage({
|
|
220
|
-
type: 'scope.set_enabled',
|
|
221
|
-
enabled,
|
|
222
|
-
...(scopedTabId ? { tabId: scopedTabId } : {}),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @returns {number | null}
|
|
228
|
-
*/
|
|
229
|
-
function readScopedTabId() {
|
|
230
|
-
const value = new URLSearchParams(window.location.search).get('tabId');
|
|
231
|
-
const tabId = Number(value);
|
|
232
|
-
return Number.isFinite(tabId) && tabId > 0 ? tabId : null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* @returns {boolean}
|
|
237
|
-
*/
|
|
238
|
-
function isWindowedPopup() {
|
|
239
|
-
return new URLSearchParams(window.location.search).get('windowed') === '1';
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* @returns {string}
|
|
244
|
-
*/
|
|
245
|
-
function getInstallCommand() {
|
|
246
|
-
return chrome.runtime.id === PUBLISHED_EXTENSION_ID
|
|
247
|
-
? 'bbx install'
|
|
248
|
-
: `bbx install ${chrome.runtime.id}`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* @returns {void}
|
|
253
|
-
*/
|
|
254
|
-
function queueWindowResize() {
|
|
255
|
-
if (!windowedPopup || resizeFrameId != null) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
resizeFrameId = window.requestAnimationFrame(() => {
|
|
259
|
-
resizeFrameId = null;
|
|
260
|
-
void resizeWindowToContent();
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* @returns {Promise<void>}
|
|
266
|
-
*/
|
|
267
|
-
async function resizeWindowToContent() {
|
|
268
|
-
if (!windowedPopup) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const panel = /** @type {HTMLElement | null} */ (document.querySelector('.panel-popup'));
|
|
273
|
-
const panelRect = panel?.getBoundingClientRect();
|
|
274
|
-
const contentWidth = Math.ceil(panelRect?.width ?? document.body.getBoundingClientRect().width);
|
|
275
|
-
const contentHeight = Math.ceil(
|
|
276
|
-
panelRect?.height ?? document.body.getBoundingClientRect().height
|
|
277
|
-
);
|
|
278
|
-
const frameWidth = Math.max(window.outerWidth - window.innerWidth, 0);
|
|
279
|
-
const frameHeight = Math.max(window.outerHeight - window.innerHeight, 0);
|
|
280
|
-
const targetWidth = Math.min(Math.max(contentWidth + frameWidth + 2, 420), 560);
|
|
281
|
-
const targetHeight = Math.min(Math.max(contentHeight + frameHeight + 2, 180), 520);
|
|
282
|
-
const currentWindow = await chrome.windows.getCurrent();
|
|
283
|
-
if (currentWindow.id == null) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** @type {chrome.windows.UpdateInfo} */
|
|
288
|
-
const updateInfo = {
|
|
289
|
-
width: targetWidth,
|
|
290
|
-
height: targetHeight,
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
if (typeof currentWindow.left === 'number' && typeof currentWindow.width === 'number') {
|
|
294
|
-
updateInfo.left = currentWindow.left + currentWindow.width - targetWidth;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
await chrome.windows.update(currentWindow.id, updateInfo);
|
|
298
|
-
}
|