@jobshimo/browser-link 0.11.0 → 0.12.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/dist/bridge/events.d.ts +25 -2
- package/dist/bridge/events.js +60 -1
- package/dist/bridge/events.js.map +1 -1
- package/dist/bridge/ws-bridge.d.ts +1 -1
- package/dist/bridge/ws-bridge.js +22 -1
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/extension/background.js +294 -1
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/manifest.json +3 -2
- package/dist/extension/popup.d.ts +19 -0
- package/dist/extension/popup.html +155 -0
- package/dist/extension/popup.js +93 -0
- package/dist/extension/popup.js.map +1 -1
- package/dist/messages.d.ts +25 -1
- package/dist/permissions.js +48 -0
- package/dist/permissions.js.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +121 -0
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +9 -1
- package/dist/tools/browser-dispatch.js +118 -0
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge/events.d.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* `primary-elected` event as the fundamental marker — older history is
|
|
11
11
|
* intentionally lost (the agent has nothing useful to do with it anyway).
|
|
12
12
|
*/
|
|
13
|
-
export type BridgeEventKind = 'primary-elected' | 'tab-registered' | 'tab-disconnected' | 'tab-renamed' | 'tab-claimed' | 'tab-released' | 'tab-claim-rejected';
|
|
13
|
+
export type BridgeEventKind = 'primary-elected' | 'tab-registered' | 'tab-disconnected' | 'tab-renamed' | 'tab-claimed' | 'tab-released' | 'tab-claim-rejected' | 'dialog-opening' | 'dialog-closed' | 'tab-created';
|
|
14
|
+
export declare function isExtensionEventKind(kind: string): kind is BridgeEventKind;
|
|
14
15
|
export interface BridgeEvent {
|
|
15
16
|
/** Monotonic id assigned by addEvent. */
|
|
16
17
|
id: number;
|
|
@@ -20,11 +21,33 @@ export interface BridgeEvent {
|
|
|
20
21
|
/** Kind-specific payload. Documented per-kind in the JSDoc below. */
|
|
21
22
|
data: Record<string, unknown>;
|
|
22
23
|
}
|
|
24
|
+
export type BridgeEventListener = (event: BridgeEvent) => void;
|
|
25
|
+
export interface SubscribeOptions {
|
|
26
|
+
/** If > 0, the listener is invoked synchronously with every event in the
|
|
27
|
+
* buffer that is at most `replayWithinMs` old, BEFORE subscribe returns.
|
|
28
|
+
* Lets a caller racing the source of an event still see it: the agent
|
|
29
|
+
* fires an action and `wait_for_tab` in the same MCP batch, the action
|
|
30
|
+
* lands first, wait_for_tab subscribes with replayWithinMs=1500 and the
|
|
31
|
+
* fresh `tab-created` event reaches its listener. Events older than the
|
|
32
|
+
* window are NOT replayed — those belong to a previous flow. */
|
|
33
|
+
replayWithinMs?: number;
|
|
34
|
+
}
|
|
23
35
|
export declare class BridgeEventLog {
|
|
24
36
|
private buffer;
|
|
25
37
|
private nextId;
|
|
26
|
-
|
|
38
|
+
private listeners;
|
|
39
|
+
/** Append an event and return it. Drops the oldest if the buffer is full.
|
|
40
|
+
* Notifies every active subscriber synchronously AFTER the buffer is
|
|
41
|
+
* updated — so a listener that calls `recent()` sees the new entry. */
|
|
27
42
|
add(kind: BridgeEventKind, data?: Record<string, unknown>): BridgeEvent;
|
|
43
|
+
/** Subscribe to every event ADDED after this call returns. Returns an
|
|
44
|
+
* unsubscribe function the caller MUST invoke (on completion / timeout /
|
|
45
|
+
* error) to avoid leaks.
|
|
46
|
+
*
|
|
47
|
+
* If `options.replayWithinMs` is set, recent events from the buffer are
|
|
48
|
+
* replayed synchronously to the listener BEFORE subscribe returns —
|
|
49
|
+
* see `SubscribeOptions.replayWithinMs` for the rationale. */
|
|
50
|
+
subscribe(fn: BridgeEventListener, options?: SubscribeOptions): () => void;
|
|
28
51
|
/** Return up to `limit` recent events, optionally filtered to those
|
|
29
52
|
* with id > sinceId. Cursor pattern — pass the previous-call's last id
|
|
30
53
|
* back in as sinceId to get only what's new. */
|
package/dist/bridge/events.js
CHANGED
|
@@ -10,11 +10,25 @@
|
|
|
10
10
|
* `primary-elected` event as the fundamental marker — older history is
|
|
11
11
|
* intentionally lost (the agent has nothing useful to do with it anyway).
|
|
12
12
|
*/
|
|
13
|
+
/** Kinds the extension is permitted to push via `bridge.event`. Lifecycle
|
|
14
|
+
* events (primary-elected, tab-registered, etc.) are produced server-side
|
|
15
|
+
* and must NOT be spoofable from the extension side. */
|
|
16
|
+
const EXTENSION_EVENT_KINDS = new Set([
|
|
17
|
+
'dialog-opening',
|
|
18
|
+
'dialog-closed',
|
|
19
|
+
'tab-created',
|
|
20
|
+
]);
|
|
21
|
+
export function isExtensionEventKind(kind) {
|
|
22
|
+
return EXTENSION_EVENT_KINDS.has(kind);
|
|
23
|
+
}
|
|
13
24
|
const MAX_EVENTS = 200;
|
|
14
25
|
export class BridgeEventLog {
|
|
15
26
|
buffer = [];
|
|
16
27
|
nextId = 1;
|
|
17
|
-
|
|
28
|
+
listeners = new Set();
|
|
29
|
+
/** Append an event and return it. Drops the oldest if the buffer is full.
|
|
30
|
+
* Notifies every active subscriber synchronously AFTER the buffer is
|
|
31
|
+
* updated — so a listener that calls `recent()` sees the new entry. */
|
|
18
32
|
add(kind, data = {}) {
|
|
19
33
|
const event = {
|
|
20
34
|
id: this.nextId++,
|
|
@@ -25,8 +39,53 @@ export class BridgeEventLog {
|
|
|
25
39
|
this.buffer.push(event);
|
|
26
40
|
if (this.buffer.length > MAX_EVENTS)
|
|
27
41
|
this.buffer.shift();
|
|
42
|
+
// Iterate a snapshot of the set so a listener that unsubscribes itself
|
|
43
|
+
// mid-fire doesn't skip its peers. Catch per-listener errors so one
|
|
44
|
+
// bad listener can't take the whole notification chain down.
|
|
45
|
+
for (const fn of [...this.listeners]) {
|
|
46
|
+
try {
|
|
47
|
+
fn(event);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Best-effort delivery — listener failures stay local.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
28
53
|
return event;
|
|
29
54
|
}
|
|
55
|
+
/** Subscribe to every event ADDED after this call returns. Returns an
|
|
56
|
+
* unsubscribe function the caller MUST invoke (on completion / timeout /
|
|
57
|
+
* error) to avoid leaks.
|
|
58
|
+
*
|
|
59
|
+
* If `options.replayWithinMs` is set, recent events from the buffer are
|
|
60
|
+
* replayed synchronously to the listener BEFORE subscribe returns —
|
|
61
|
+
* see `SubscribeOptions.replayWithinMs` for the rationale. */
|
|
62
|
+
subscribe(fn, options = {}) {
|
|
63
|
+
// Register first so a listener that opts to unsubscribe itself during
|
|
64
|
+
// replay does so against a live registration (the unsubscribe path
|
|
65
|
+
// calls `this.listeners.delete(fn)` and the replay loop breaks when
|
|
66
|
+
// membership drops).
|
|
67
|
+
this.listeners.add(fn);
|
|
68
|
+
const replayMs = options.replayWithinMs;
|
|
69
|
+
if (typeof replayMs === 'number' && replayMs > 0) {
|
|
70
|
+
const cutoff = Date.now() - replayMs;
|
|
71
|
+
for (const e of this.buffer) {
|
|
72
|
+
if (!this.listeners.has(fn))
|
|
73
|
+
break;
|
|
74
|
+
const at = Date.parse(e.at);
|
|
75
|
+
if (!Number.isFinite(at) || at < cutoff)
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
fn(e);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Best-effort replay.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return () => {
|
|
86
|
+
this.listeners.delete(fn);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
30
89
|
/** Return up to `limit` recent events, optionally filtered to those
|
|
31
90
|
* with id > sinceId. Cursor pattern — pass the previous-call's last id
|
|
32
91
|
* back in as sinceId to get only what's new. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/bridge/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/bridge/events.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
-
import type
|
|
2
|
+
import { type BridgeEventLog } from './events.js';
|
|
3
3
|
export declare const WS_HOST = "127.0.0.1";
|
|
4
4
|
export declare const WS_PORT = 17529;
|
|
5
5
|
export interface TabSession {
|
package/dist/bridge/ws-bridge.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
2
|
import { isAllowedBrowser } from '../auth/allowlist.js';
|
|
3
3
|
import { lookupPeerProcess } from '../auth/process-identity.js';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
import { isExtensionEventKind } from './events.js';
|
|
4
6
|
export const WS_HOST = '127.0.0.1';
|
|
5
7
|
export const WS_PORT = 17529;
|
|
6
8
|
function log(msg) {
|
|
@@ -142,7 +144,10 @@ export function startWsBridge(tabs, pendingRequests, events) {
|
|
|
142
144
|
title: msg.payload.title,
|
|
143
145
|
ws,
|
|
144
146
|
});
|
|
145
|
-
send(ws, {
|
|
147
|
+
send(ws, {
|
|
148
|
+
kind: 'tab.registered',
|
|
149
|
+
payload: { tabId: assignedTabId, serverVersion: VERSION },
|
|
150
|
+
});
|
|
146
151
|
log(`Tab registered: ${assignedTabId} -> ${msg.payload.url}`);
|
|
147
152
|
if (previousTabId && previousTabId !== assignedTabId) {
|
|
148
153
|
events.add('tab-renamed', {
|
|
@@ -172,6 +177,22 @@ export function startWsBridge(tabs, pendingRequests, events) {
|
|
|
172
177
|
pending.resolve(msg.result);
|
|
173
178
|
else
|
|
174
179
|
pending.reject(new Error(msg.error));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (msg.kind === 'bridge.event') {
|
|
183
|
+
// Closed allowlist — the extension can only push the kinds tied to
|
|
184
|
+
// popup awareness (dialogs / new tabs). Lifecycle kinds (primary-elected,
|
|
185
|
+
// tab-registered, etc.) stay server-owned so they can't be spoofed
|
|
186
|
+
// from the renderer side.
|
|
187
|
+
if (!isExtensionEventKind(msg.eventKind)) {
|
|
188
|
+
log(`Ignoring bridge.event with unknown/forbidden kind: ${msg.eventKind}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const data = { ...msg.data };
|
|
192
|
+
if (msg.tabId)
|
|
193
|
+
data.tab_id = msg.tabId;
|
|
194
|
+
events.add(msg.eventKind, data);
|
|
195
|
+
return;
|
|
175
196
|
}
|
|
176
197
|
});
|
|
177
198
|
ws.on('close', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-bridge.js","sourceRoot":"","sources":["../../src/bridge/ws-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,eAAe,EAAE,MAAM,IAAI,CAAC;AAEhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"ws-bridge.js","sourceRoot":"","sources":["../../src/bridge/ws-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,9 +19,15 @@ function isRuntimeMessage(msg) {
|
|
|
19
19
|
if (typeof msg !== 'object' || msg === null)
|
|
20
20
|
return false;
|
|
21
21
|
const m = msg;
|
|
22
|
+
if (m.action === 'versionCheck')
|
|
23
|
+
return true;
|
|
22
24
|
if (typeof m.tabId !== 'number')
|
|
23
25
|
return false;
|
|
24
|
-
return m.action === 'connect' ||
|
|
26
|
+
return (m.action === 'connect' ||
|
|
27
|
+
m.action === 'disconnect' ||
|
|
28
|
+
m.action === 'status' ||
|
|
29
|
+
m.action === 'pendingDialog' ||
|
|
30
|
+
m.action === 'respondDialog');
|
|
25
31
|
}
|
|
26
32
|
const tabStates = new Map();
|
|
27
33
|
function send(ws, msg) {
|
|
@@ -38,6 +44,43 @@ function safeParse(raw) {
|
|
|
38
44
|
function cdp(tabId, method, params = {}) {
|
|
39
45
|
return chrome.debugger.sendCommand({ tabId }, method, params);
|
|
40
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Mapping from the CDP permission name (the contract `browser.set_permission`
|
|
49
|
+
* accepts on the wire) to the `chrome.contentSettings` setting key. Only the
|
|
50
|
+
* names that actually have a contentSettings surface in MV3 are listed —
|
|
51
|
+
* `Browser.setPermission` covers more, but that domain is not reachable from
|
|
52
|
+
* `chrome.debugger` attached to a tab.
|
|
53
|
+
*/
|
|
54
|
+
const CONTENT_SETTING_BY_PERMISSION = {
|
|
55
|
+
geolocation: 'location',
|
|
56
|
+
notifications: 'notifications',
|
|
57
|
+
camera: 'camera',
|
|
58
|
+
microphone: 'microphone',
|
|
59
|
+
clipboardReadWrite: 'clipboard',
|
|
60
|
+
clipboardSanitizedWrite: 'clipboard',
|
|
61
|
+
sensors: 'sensors',
|
|
62
|
+
};
|
|
63
|
+
const STATE_TO_CONTENT_SETTING = {
|
|
64
|
+
granted: 'allow',
|
|
65
|
+
denied: 'block',
|
|
66
|
+
prompt: 'ask',
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Apply a `chrome.contentSettings.<setting>.set({ primaryPattern, setting })`
|
|
70
|
+
* call without statically referencing each key — the `contentSettings`
|
|
71
|
+
* surface is dynamic at the API level. We narrow `unknown` carefully so the
|
|
72
|
+
* cast is the smallest possible.
|
|
73
|
+
*/
|
|
74
|
+
async function applyContentSetting(settingKey, primaryPattern, setting) {
|
|
75
|
+
const root = chrome.contentSettings;
|
|
76
|
+
if (!root)
|
|
77
|
+
throw new Error('chrome.contentSettings not available in this build');
|
|
78
|
+
const setter = root[settingKey];
|
|
79
|
+
if (!setter || typeof setter.set !== 'function') {
|
|
80
|
+
throw new Error(`chrome.contentSettings.${settingKey} is not exposed in this Chrome build`);
|
|
81
|
+
}
|
|
82
|
+
await setter.set({ primaryPattern, setting });
|
|
83
|
+
}
|
|
41
84
|
/**
|
|
42
85
|
* Defensive caps applied to every duration that ends up in a setTimeout
|
|
43
86
|
* driven by tool params. The MCP request payload is technically untrusted
|
|
@@ -283,6 +326,56 @@ function attachDebuggerListener(state) {
|
|
|
283
326
|
entry.failed = p.errorText ?? 'failed';
|
|
284
327
|
return;
|
|
285
328
|
}
|
|
329
|
+
if (method === 'Page.javascriptDialogOpening') {
|
|
330
|
+
// The page JS thread is now frozen until something calls
|
|
331
|
+
// Page.handleJavaScriptDialog. We do NOT respond automatically —
|
|
332
|
+
// the agent reads dialog-opening from browser.events and decides
|
|
333
|
+
// via browser.dialog_respond. The popup also exposes accept/dismiss
|
|
334
|
+
// buttons as a manual escape hatch.
|
|
335
|
+
const p = params;
|
|
336
|
+
const info = {
|
|
337
|
+
type: p.type ?? 'alert',
|
|
338
|
+
message: p.message ?? '',
|
|
339
|
+
};
|
|
340
|
+
if (typeof p.defaultPrompt === 'string')
|
|
341
|
+
info.default_prompt = p.defaultPrompt;
|
|
342
|
+
if (typeof p.url === 'string')
|
|
343
|
+
info.url = p.url;
|
|
344
|
+
state.pendingDialog = info;
|
|
345
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN && state.serverTabId) {
|
|
346
|
+
const data = {
|
|
347
|
+
type: info.type,
|
|
348
|
+
message: info.message,
|
|
349
|
+
};
|
|
350
|
+
if (info.default_prompt !== undefined)
|
|
351
|
+
data.default_prompt = info.default_prompt;
|
|
352
|
+
if (info.url !== undefined)
|
|
353
|
+
data.url = info.url;
|
|
354
|
+
send(state.ws, {
|
|
355
|
+
kind: 'bridge.event',
|
|
356
|
+
eventKind: 'dialog-opening',
|
|
357
|
+
tabId: state.serverTabId,
|
|
358
|
+
data,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (method === 'Page.javascriptDialogClosed') {
|
|
364
|
+
const p = params;
|
|
365
|
+
state.pendingDialog = undefined;
|
|
366
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN && state.serverTabId) {
|
|
367
|
+
const data = { accept: p.result === true };
|
|
368
|
+
if (typeof p.userInput === 'string')
|
|
369
|
+
data.user_input = p.userInput;
|
|
370
|
+
send(state.ws, {
|
|
371
|
+
kind: 'bridge.event',
|
|
372
|
+
eventKind: 'dialog-closed',
|
|
373
|
+
tabId: state.serverTabId,
|
|
374
|
+
data,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
286
379
|
};
|
|
287
380
|
state.debuggerListener = listener;
|
|
288
381
|
}
|
|
@@ -901,6 +994,73 @@ async function handleTool(state, msg) {
|
|
|
901
994
|
: { matched: false, elapsed_ms: elapsedMs, checks, reason: 'timeout' },
|
|
902
995
|
};
|
|
903
996
|
}
|
|
997
|
+
case 'dialog_respond': {
|
|
998
|
+
const accept = p.accept === true;
|
|
999
|
+
const promptText = optStr('prompt_text');
|
|
1000
|
+
// No probing — if there is no dialog open, CDP returns an error.
|
|
1001
|
+
// We propagate it; the caller decides if "no dialog to respond to"
|
|
1002
|
+
// is a problem or a race they can ignore.
|
|
1003
|
+
const params = { accept };
|
|
1004
|
+
if (promptText !== undefined)
|
|
1005
|
+
params.promptText = promptText;
|
|
1006
|
+
await cdp(tabId, 'Page.handleJavaScriptDialog', params);
|
|
1007
|
+
return {
|
|
1008
|
+
kind: 'tool.response',
|
|
1009
|
+
id: msg.id,
|
|
1010
|
+
ok: true,
|
|
1011
|
+
result: { accepted: accept },
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
case 'set_permission': {
|
|
1015
|
+
const origin = str('origin');
|
|
1016
|
+
const name = str('name');
|
|
1017
|
+
const state_ = str('state');
|
|
1018
|
+
// CDP `Browser.setPermission` requires a browser-level target,
|
|
1019
|
+
// which `chrome.debugger.attach({ tabId })` does NOT give us in
|
|
1020
|
+
// MV3. We use `chrome.contentSettings` instead — that surface IS
|
|
1021
|
+
// available to extensions and covers the realistic permissions
|
|
1022
|
+
// an agent needs to pre-set (geolocation, notifications, camera,
|
|
1023
|
+
// microphone, clipboard, sensors).
|
|
1024
|
+
const settingKey = CONTENT_SETTING_BY_PERMISSION[name];
|
|
1025
|
+
if (!settingKey) {
|
|
1026
|
+
return {
|
|
1027
|
+
kind: 'tool.response',
|
|
1028
|
+
id: msg.id,
|
|
1029
|
+
ok: false,
|
|
1030
|
+
error: `set_permission: '${name}' is not supported by chrome.contentSettings in MV3. Supported names: ${Object.keys(CONTENT_SETTING_BY_PERMISSION).join(', ')}.`,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const setting = STATE_TO_CONTENT_SETTING[state_];
|
|
1034
|
+
if (!setting) {
|
|
1035
|
+
return {
|
|
1036
|
+
kind: 'tool.response',
|
|
1037
|
+
id: msg.id,
|
|
1038
|
+
ok: false,
|
|
1039
|
+
error: `set_permission: unknown state '${state_}' (expected granted | denied | prompt).`,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
// `chrome.contentSettings.<name>.set({ primaryPattern, setting })`
|
|
1043
|
+
// requires a URL pattern, not a bare origin. Append `/*` so the
|
|
1044
|
+
// pattern covers every path on the origin.
|
|
1045
|
+
const primaryPattern = origin.endsWith('/*') ? origin : `${origin}/*`;
|
|
1046
|
+
try {
|
|
1047
|
+
await applyContentSetting(settingKey, primaryPattern, setting);
|
|
1048
|
+
}
|
|
1049
|
+
catch (err) {
|
|
1050
|
+
return {
|
|
1051
|
+
kind: 'tool.response',
|
|
1052
|
+
id: msg.id,
|
|
1053
|
+
ok: false,
|
|
1054
|
+
error: `set_permission: ${err instanceof Error ? err.message : String(err)}`,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
kind: 'tool.response',
|
|
1059
|
+
id: msg.id,
|
|
1060
|
+
ok: true,
|
|
1061
|
+
result: { origin, name, state: state_, applied_as: settingKey },
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
904
1064
|
default:
|
|
905
1065
|
return { kind: 'tool.response', id: msg.id, ok: false, error: `Unknown tool: ${msg.tool}` };
|
|
906
1066
|
}
|
|
@@ -1050,6 +1210,11 @@ async function connectTab(tabId) {
|
|
|
1050
1210
|
return;
|
|
1051
1211
|
if (msg.kind === 'tab.registered') {
|
|
1052
1212
|
state.serverTabId = msg.payload.tabId;
|
|
1213
|
+
// serverVersion is optional on the wire so older servers that
|
|
1214
|
+
// predate the field still parse — narrow on `typeof string`.
|
|
1215
|
+
if (typeof msg.payload.serverVersion === 'string') {
|
|
1216
|
+
state.serverVersion = msg.payload.serverVersion;
|
|
1217
|
+
}
|
|
1053
1218
|
// Remember this id so the next reconnect (after a primary swap)
|
|
1054
1219
|
// asks the new primary to honour it. The primary emits
|
|
1055
1220
|
// `tab-renamed` in the bridge event log if it can't.
|
|
@@ -1094,6 +1259,40 @@ function getTabStatus(tabId) {
|
|
|
1094
1259
|
return { connected: false };
|
|
1095
1260
|
return { connected: true, serverTabId: state.serverTabId };
|
|
1096
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Compute the version-mismatch summary for the popup. Picks the
|
|
1264
|
+
* `serverVersion` reported by ANY currently-connected tab — they all come
|
|
1265
|
+
* from the same primary, so the first one is enough. When no tab has
|
|
1266
|
+
* registered yet (e.g. user just installed and hasn't pressed Connect),
|
|
1267
|
+
* `server` stays null and the popup hides the banner.
|
|
1268
|
+
*/
|
|
1269
|
+
function getVersionCheck() {
|
|
1270
|
+
const extension = chrome.runtime.getManifest().version;
|
|
1271
|
+
let server = null;
|
|
1272
|
+
for (const s of tabStates.values()) {
|
|
1273
|
+
if (typeof s.serverVersion === 'string' && s.serverVersion.length > 0) {
|
|
1274
|
+
server = s.serverVersion;
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
extension,
|
|
1280
|
+
server,
|
|
1281
|
+
aligned: server === null ? null : server === extension,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
async function respondToDialogFromPopup(tabId, accept, promptText) {
|
|
1285
|
+
try {
|
|
1286
|
+
const params = { accept };
|
|
1287
|
+
if (promptText !== undefined)
|
|
1288
|
+
params.promptText = promptText;
|
|
1289
|
+
await cdp(tabId, 'Page.handleJavaScriptDialog', params);
|
|
1290
|
+
return { ok: true };
|
|
1291
|
+
}
|
|
1292
|
+
catch (err) {
|
|
1293
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1097
1296
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
1098
1297
|
if (!isRuntimeMessage(msg))
|
|
1099
1298
|
return false;
|
|
@@ -1105,6 +1304,19 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
1105
1304
|
void disconnectTab(msg.tabId).then(sendResponse);
|
|
1106
1305
|
return true;
|
|
1107
1306
|
}
|
|
1307
|
+
if (msg.action === 'pendingDialog') {
|
|
1308
|
+
const state = tabStates.get(msg.tabId);
|
|
1309
|
+
sendResponse({ pending: state?.pendingDialog ?? null });
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
if (msg.action === 'respondDialog') {
|
|
1313
|
+
void respondToDialogFromPopup(msg.tabId, msg.accept, msg.promptText).then(sendResponse);
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
if (msg.action === 'versionCheck') {
|
|
1317
|
+
sendResponse(getVersionCheck());
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1108
1320
|
// 'status'
|
|
1109
1321
|
sendResponse(getTabStatus(msg.tabId));
|
|
1110
1322
|
return false;
|
|
@@ -1116,6 +1328,87 @@ chrome.tabs.onRemoved.addListener((tabId) => {
|
|
|
1116
1328
|
// The Chrome tab is gone — drop any cached browser-link id for it.
|
|
1117
1329
|
void forgetTabId(tabId);
|
|
1118
1330
|
});
|
|
1331
|
+
/* Auto-connect tabs spawned by a connected tab.
|
|
1332
|
+
*
|
|
1333
|
+
* When a tab the user has connected via the popup opens a new tab
|
|
1334
|
+
* (window.open, target="_blank", a click handler doing window.open, etc.),
|
|
1335
|
+
* Chrome reports it via tabs.onCreated with `openerTabId` pointing at the
|
|
1336
|
+
* source. We use that link to:
|
|
1337
|
+
* 1. Wait for the new tab to settle on a real URL (the first onCreated
|
|
1338
|
+
* callback often reports `about:blank` even when a destination is set).
|
|
1339
|
+
* 2. Auto-connect the new tab through the same flow as the popup
|
|
1340
|
+
* "Connect" button — attach debugger, register with the server.
|
|
1341
|
+
* 3. Emit a `tab-created` bridge.event tagged with `opened_from = the
|
|
1342
|
+
* opener's server tab id`. The MCP `browser.wait_for_tab` tool reads
|
|
1343
|
+
* that event and auto-claims the new tab for the waiting agent.
|
|
1344
|
+
*
|
|
1345
|
+
* If the opener is not connected (regular browsing), we do nothing —
|
|
1346
|
+
* background tabs the user opens for themselves stay out of the bridge.
|
|
1347
|
+
*/
|
|
1348
|
+
chrome.tabs.onCreated.addListener((tab) => {
|
|
1349
|
+
const newTabId = tab.id;
|
|
1350
|
+
const openerTabId = tab.openerTabId;
|
|
1351
|
+
if (typeof newTabId !== 'number')
|
|
1352
|
+
return;
|
|
1353
|
+
if (typeof openerTabId !== 'number')
|
|
1354
|
+
return;
|
|
1355
|
+
const openerState = tabStates.get(openerTabId);
|
|
1356
|
+
if (!openerState)
|
|
1357
|
+
return;
|
|
1358
|
+
const openedFrom = openerState.serverTabId;
|
|
1359
|
+
if (!openedFrom)
|
|
1360
|
+
return;
|
|
1361
|
+
void (async () => {
|
|
1362
|
+
// Wait until chrome.tabs.get returns a real `url`. `pendingUrl` alone
|
|
1363
|
+
// is NOT enough — connectTab's guard short-circuits on `!tab.url`.
|
|
1364
|
+
// Bound at 5 s so a never-navigating tab doesn't park us forever.
|
|
1365
|
+
const deadline = Date.now() + 5_000;
|
|
1366
|
+
let ready = false;
|
|
1367
|
+
while (Date.now() < deadline) {
|
|
1368
|
+
try {
|
|
1369
|
+
const t = await chrome.tabs.get(newTabId);
|
|
1370
|
+
if (t.url && t.url.length > 0) {
|
|
1371
|
+
ready = true;
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
await sleep(50);
|
|
1379
|
+
}
|
|
1380
|
+
if (!ready)
|
|
1381
|
+
return;
|
|
1382
|
+
try {
|
|
1383
|
+
const result = await connectTab(newTabId);
|
|
1384
|
+
if (!result.ok || !result.serverTabId)
|
|
1385
|
+
return;
|
|
1386
|
+
const newState = tabStates.get(newTabId);
|
|
1387
|
+
if (!newState?.ws || newState.ws.readyState !== WebSocket.OPEN)
|
|
1388
|
+
return;
|
|
1389
|
+
let resolvedUrl = '';
|
|
1390
|
+
try {
|
|
1391
|
+
const settled = await chrome.tabs.get(newTabId);
|
|
1392
|
+
resolvedUrl = settled.url ?? settled.pendingUrl ?? '';
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
// Tab vanished between attach and read.
|
|
1396
|
+
}
|
|
1397
|
+
send(newState.ws, {
|
|
1398
|
+
kind: 'bridge.event',
|
|
1399
|
+
eventKind: 'tab-created',
|
|
1400
|
+
tabId: newState.serverTabId,
|
|
1401
|
+
data: {
|
|
1402
|
+
opened_from: openedFrom,
|
|
1403
|
+
url: resolvedUrl,
|
|
1404
|
+
},
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
catch {
|
|
1408
|
+
// Auto-connect is best-effort.
|
|
1409
|
+
}
|
|
1410
|
+
})();
|
|
1411
|
+
});
|
|
1119
1412
|
chrome.debugger.onDetach.addListener((source) => {
|
|
1120
1413
|
if (source.tabId === undefined)
|
|
1121
1414
|
return;
|