@browserbridge/bbx 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +116 -41
  5. package/packages/agent-client/src/client.js +29 -4
  6. package/packages/agent-client/src/command-registry.js +3 -0
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers.js +28 -7
  13. package/packages/mcp-server/src/server.js +12 -2
  14. package/packages/native-host/bin/bridge-daemon.js +33 -4
  15. package/packages/native-host/bin/install-manifest.js +24 -2
  16. package/packages/native-host/src/config.js +131 -6
  17. package/packages/native-host/src/daemon-process.js +396 -0
  18. package/packages/native-host/src/daemon.js +217 -68
  19. package/packages/native-host/src/framing.js +131 -11
  20. package/packages/native-host/src/install-manifest.js +121 -7
  21. package/packages/native-host/src/native-host.js +110 -73
  22. package/packages/protocol/src/capabilities.js +3 -0
  23. package/packages/protocol/src/defaults.js +1 -0
  24. package/packages/protocol/src/errors.js +4 -0
  25. package/packages/protocol/src/payload-cost.js +19 -6
  26. package/packages/protocol/src/protocol.js +143 -7
  27. package/packages/protocol/src/registry.js +11 -0
  28. package/packages/protocol/src/summary.js +18 -10
  29. package/packages/protocol/src/types.js +28 -3
  30. package/skills/browser-bridge/SKILL.md +2 -1
  31. package/skills/browser-bridge/references/interaction.md +1 -0
  32. package/skills/browser-bridge/references/protocol.md +2 -1
  33. package/CHANGELOG.md +0 -55
  34. package/assets/banner.jpg +0 -0
  35. package/assets/logo.png +0 -0
  36. package/assets/logo.svg +0 -65
  37. package/docs/api-reference.md +0 -157
  38. package/docs/cli-guide.md +0 -128
  39. package/docs/index.md +0 -25
  40. package/docs/manual-setup.md +0 -140
  41. package/docs/mcp-vs-cli.md +0 -258
  42. package/docs/publishing.md +0 -112
  43. package/docs/quickstart.md +0 -104
  44. package/docs/troubleshooting.md +0 -59
  45. package/docs/unpacked-extension.md +0 -72
  46. package/docs/usage-scenarios.md +0 -136
  47. package/manifest.json +0 -38
  48. package/packages/extension/assets/icon-128.png +0 -0
  49. package/packages/extension/assets/icon-16.png +0 -0
  50. package/packages/extension/assets/icon-32.png +0 -0
  51. package/packages/extension/assets/icon-48.png +0 -0
  52. package/packages/extension/src/background-helpers.js +0 -474
  53. package/packages/extension/src/background-routing.js +0 -89
  54. package/packages/extension/src/background.js +0 -3490
  55. package/packages/extension/src/content-script-helpers.js +0 -282
  56. package/packages/extension/src/content-script.js +0 -2043
  57. package/packages/extension/src/debugger-coordinator.js +0 -188
  58. package/packages/extension/src/sidepanel-helpers.js +0 -104
  59. package/packages/extension/ui/popup.html +0 -35
  60. package/packages/extension/ui/popup.js +0 -298
  61. package/packages/extension/ui/sidepanel.html +0 -102
  62. package/packages/extension/ui/sidepanel.js +0 -1771
  63. package/packages/extension/ui/ui.css +0 -1160
@@ -1,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
- }