@jobshimo/browser-link 0.8.5 → 0.9.1
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 +3 -0
- package/dist/bridge/events.js +8 -0
- package/dist/bridge/events.js.map +1 -1
- package/dist/commands/welcome.d.ts +15 -0
- package/dist/commands/welcome.js +44 -2
- package/dist/commands/welcome.js.map +1 -1
- package/dist/extension/background.js +411 -1
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/popup.d.ts +2 -1
- package/dist/extension/popup.html +128 -50
- package/dist/extension/popup.js +22 -3
- package/dist/extension/popup.js.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/permissions.js.map +1 -1
- package/dist/server.js +42 -36
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +69 -0
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +8 -14
- package/dist/tools/browser-dispatch.js +35 -0
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/tab-claims.d.ts +5 -1
- package/dist/tools/tab-claims.js +17 -0
- package/dist/tools/tab-claims.js.map +1 -1
- package/dist/ui/app.js +27 -2
- package/dist/ui/app.js.map +1 -1
- package/dist/ui/components.d.ts +15 -3
- package/dist/ui/components.js +34 -14
- package/dist/ui/components.js.map +1 -1
- package/dist/ui/primitives/badge.d.ts +11 -0
- package/dist/ui/primitives/badge.js +35 -0
- package/dist/ui/primitives/badge.js.map +1 -0
- package/dist/ui/primitives/check-row.d.ts +11 -0
- package/dist/ui/primitives/check-row.js +10 -0
- package/dist/ui/primitives/check-row.js.map +1 -0
- package/dist/ui/primitives/footer-keys.d.ts +12 -0
- package/dist/ui/primitives/footer-keys.js +11 -0
- package/dist/ui/primitives/footer-keys.js.map +1 -0
- package/dist/ui/primitives/index.d.ts +8 -0
- package/dist/ui/primitives/index.js +13 -0
- package/dist/ui/primitives/index.js.map +1 -0
- package/dist/ui/primitives/inline-spinner.d.ts +7 -0
- package/dist/ui/primitives/inline-spinner.js +23 -0
- package/dist/ui/primitives/inline-spinner.js.map +1 -0
- package/dist/ui/primitives/key-cap.d.ts +6 -0
- package/dist/ui/primitives/key-cap.js +6 -0
- package/dist/ui/primitives/key-cap.js.map +1 -0
- package/dist/ui/primitives/menu-row.d.ts +17 -0
- package/dist/ui/primitives/menu-row.js +8 -0
- package/dist/ui/primitives/menu-row.js.map +1 -0
- package/dist/ui/primitives/section-head.d.ts +11 -0
- package/dist/ui/primitives/section-head.js +7 -0
- package/dist/ui/primitives/section-head.js.map +1 -0
- package/dist/ui/primitives/status-bar.d.ts +13 -0
- package/dist/ui/primitives/status-bar.js +17 -0
- package/dist/ui/primitives/status-bar.js.map +1 -0
- package/dist/ui/screens/about.js +13 -4
- package/dist/ui/screens/about.js.map +1 -1
- package/dist/ui/screens/agent-instructions.js +53 -47
- package/dist/ui/screens/agent-instructions.js.map +1 -1
- package/dist/ui/screens/client-picker.js +64 -16
- package/dist/ui/screens/client-picker.js.map +1 -1
- package/dist/ui/screens/doctor.js +20 -7
- package/dist/ui/screens/doctor.js.map +1 -1
- package/dist/ui/screens/empty-clients.d.ts +8 -0
- package/dist/ui/screens/empty-clients.js +57 -0
- package/dist/ui/screens/empty-clients.js.map +1 -0
- package/dist/ui/screens/extension.js +25 -8
- package/dist/ui/screens/extension.js.map +1 -1
- package/dist/ui/screens/free-port.js +48 -33
- package/dist/ui/screens/free-port.js.map +1 -1
- package/dist/ui/screens/index.d.ts +2 -0
- package/dist/ui/screens/index.js +2 -0
- package/dist/ui/screens/index.js.map +1 -1
- package/dist/ui/screens/install-result.js +29 -6
- package/dist/ui/screens/install-result.js.map +1 -1
- package/dist/ui/screens/language.js +39 -18
- package/dist/ui/screens/language.js.map +1 -1
- package/dist/ui/screens/menu.js +99 -43
- package/dist/ui/screens/menu.js.map +1 -1
- package/dist/ui/screens/multi-agent.js +24 -18
- package/dist/ui/screens/multi-agent.js.map +1 -1
- package/dist/ui/screens/permissions.js +145 -33
- package/dist/ui/screens/permissions.js.map +1 -1
- package/dist/ui/screens/port-collision.d.ts +18 -0
- package/dist/ui/screens/port-collision.js +95 -0
- package/dist/ui/screens/port-collision.js.map +1 -0
- package/dist/ui/screens/updates.js +44 -21
- package/dist/ui/screens/updates.js.map +1 -1
- package/dist/ui/screens/welcome.js +56 -26
- package/dist/ui/screens/welcome.js.map +1 -1
- package/dist/ui/tokens.d.ts +37 -0
- package/dist/ui/tokens.js +58 -0
- package/dist/ui/tokens.js.map +1 -0
- package/package.json +1 -1
package/dist/bridge/events.d.ts
CHANGED
|
@@ -36,4 +36,7 @@ export declare class BridgeEventLog {
|
|
|
36
36
|
latestId(): number;
|
|
37
37
|
/** Total number of events in the buffer. */
|
|
38
38
|
size(): number;
|
|
39
|
+
/** Drop every event in the buffer and reset the id counter. Returns the
|
|
40
|
+
* count dropped, so callers can surface it (used by `browser.reset`). */
|
|
41
|
+
clear(): number;
|
|
39
42
|
}
|
package/dist/bridge/events.js
CHANGED
|
@@ -44,5 +44,13 @@ export class BridgeEventLog {
|
|
|
44
44
|
size() {
|
|
45
45
|
return this.buffer.length;
|
|
46
46
|
}
|
|
47
|
+
/** Drop every event in the buffer and reset the id counter. Returns the
|
|
48
|
+
* count dropped, so callers can surface it (used by `browser.reset`). */
|
|
49
|
+
clear() {
|
|
50
|
+
const dropped = this.buffer.length;
|
|
51
|
+
this.buffer = [];
|
|
52
|
+
this.nextId = 1;
|
|
53
|
+
return dropped;
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
//# sourceMappingURL=events.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/bridge/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,MAAM,OAAO,cAAc;IACjB,MAAM,GAAkB,EAAE,CAAC;IAC3B,MAAM,GAAG,CAAC,CAAC;IAEnB,6EAA6E;IAC7E,GAAG,CAAC,IAAqB,EAAE,OAAgC,EAAE;QAC3D,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACjB,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,IAAI;YACJ,IAAI;SACL,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,UAAU;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACzD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;oDAEgD;IAChD,MAAM,CAAC,OAA6C,EAAE;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;QAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,MAAM,QAAQ,GACZ,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC;QAClF,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,2DAA2D;IAC3D,QAAQ;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,CAAC;IAED,4CAA4C;IAC5C,IAAI;QACF,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/bridge/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,MAAM,OAAO,cAAc;IACjB,MAAM,GAAkB,EAAE,CAAC;IAC3B,MAAM,GAAG,CAAC,CAAC;IAEnB,6EAA6E;IAC7E,GAAG,CAAC,IAAqB,EAAE,OAAgC,EAAE;QAC3D,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACjB,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,IAAI;YACJ,IAAI;SACL,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,UAAU;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACzD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;oDAEgD;IAChD,MAAM,CAAC,OAA6C,EAAE;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;QAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,MAAM,QAAQ,GACZ,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC;QAClF,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,2DAA2D;IAC3D,QAAQ;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,CAAC;IAED,4CAA4C;IAC5C,IAAI;QACF,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED;6EACyE;IACzE,KAAK;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,OAAO,OAAO,CAAC;IACjB,CAAC;CACF"}
|
|
@@ -15,6 +15,10 @@ export interface WelcomeOptions {
|
|
|
15
15
|
}
|
|
16
16
|
interface I18n {
|
|
17
17
|
title: string;
|
|
18
|
+
/** One-paragraph mission statement for the compact (default) view. */
|
|
19
|
+
shortAbout: string;
|
|
20
|
+
/** One-line critical warning for the compact (default) view. */
|
|
21
|
+
shortWarning: string;
|
|
18
22
|
aboutTitle: string;
|
|
19
23
|
about: string;
|
|
20
24
|
capabilitiesTitle: string;
|
|
@@ -24,12 +28,23 @@ interface I18n {
|
|
|
24
28
|
responsibility: string;
|
|
25
29
|
extensionNote: string;
|
|
26
30
|
prompt: string;
|
|
31
|
+
/** Trailing hint pointing at where the full breakdown lives. */
|
|
32
|
+
detailHint: string;
|
|
27
33
|
options: {
|
|
28
34
|
accept: string;
|
|
29
35
|
dismiss: string;
|
|
30
36
|
swap: string;
|
|
31
37
|
quit: string;
|
|
32
38
|
};
|
|
39
|
+
/** Footer keycap labels — language-stable except for the verb. */
|
|
40
|
+
footerKeys: {
|
|
41
|
+
navigate: string;
|
|
42
|
+
select: string;
|
|
43
|
+
lang: string;
|
|
44
|
+
quit: string;
|
|
45
|
+
info: string;
|
|
46
|
+
hide: string;
|
|
47
|
+
};
|
|
33
48
|
}
|
|
34
49
|
export declare const I18N_WELCOME: Record<Language, I18n>;
|
|
35
50
|
export {};
|
package/dist/commands/welcome.js
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
export const I18N_WELCOME = {
|
|
5
5
|
en: {
|
|
6
6
|
title: 'browser-link',
|
|
7
|
+
shortAbout: [
|
|
8
|
+
'MCP bridge between your editor agent and the Chrome tabs you',
|
|
9
|
+
'explicitly enable through a companion extension.',
|
|
10
|
+
].join('\n'),
|
|
11
|
+
shortWarning: [
|
|
12
|
+
'The agent can read and write whatever is on a connected tab —',
|
|
13
|
+
'sessions, wallets, admin panels. Only connect tabs you would let',
|
|
14
|
+
'a remote junior dev touch.',
|
|
15
|
+
].join('\n'),
|
|
7
16
|
aboutTitle: 'What this is',
|
|
8
17
|
about: [
|
|
9
18
|
'An MCP server that opens a small WebSocket bridge between an MCP',
|
|
@@ -40,15 +49,36 @@ export const I18N_WELCOME = {
|
|
|
40
49
|
responsibility: 'You are responsible for every action the agent performs on the tabs you explicitly enable. The extension stays inert on any tab where you have not pressed "Conectar" yourself.',
|
|
41
50
|
extensionNote: 'The Chrome extension is custom and ships inside this package. The setup menu after this screen will tell you where it lives so you can load it via chrome://extensions → Load unpacked.',
|
|
42
51
|
prompt: 'How do you want to proceed?',
|
|
52
|
+
detailHint: 'Press i for the full breakdown · also available under About.',
|
|
43
53
|
options: {
|
|
44
54
|
accept: 'I understand, continue',
|
|
45
55
|
dismiss: "Accept and don't show again",
|
|
46
|
-
|
|
56
|
+
// Label sits in the destination language so a monolingual user
|
|
57
|
+
// can still parse it — they recognise "Español" universally and
|
|
58
|
+
// the verb in the destination language confirms it is the switch.
|
|
59
|
+
swap: 'Cambiar a Español',
|
|
47
60
|
quit: 'Quit',
|
|
48
61
|
},
|
|
62
|
+
footerKeys: {
|
|
63
|
+
navigate: 'navigate',
|
|
64
|
+
select: 'select',
|
|
65
|
+
lang: 'lang',
|
|
66
|
+
quit: 'quit',
|
|
67
|
+
info: 'info',
|
|
68
|
+
hide: 'hide',
|
|
69
|
+
},
|
|
49
70
|
},
|
|
50
71
|
es: {
|
|
51
72
|
title: 'browser-link',
|
|
73
|
+
shortAbout: [
|
|
74
|
+
'Puente MCP entre el agente de tu editor y las pestañas de Chrome',
|
|
75
|
+
'que vos habilitás explícitamente a través de una extensión.',
|
|
76
|
+
].join('\n'),
|
|
77
|
+
shortWarning: [
|
|
78
|
+
'El agente puede leer y escribir todo lo que esté en una pestaña',
|
|
79
|
+
'conectada — sesiones, wallets, paneles de admin. Solo conectá',
|
|
80
|
+
'pestañas que dejarías tocar a un dev junior remoto.',
|
|
81
|
+
].join('\n'),
|
|
52
82
|
aboutTitle: 'Qué es esto',
|
|
53
83
|
about: [
|
|
54
84
|
'Un servidor MCP que abre un puente WebSocket entre un cliente MCP',
|
|
@@ -87,12 +117,24 @@ export const I18N_WELCOME = {
|
|
|
87
117
|
responsibility: 'Sos responsable de cada acción que el agente haga en las pestañas que habilitás explícitamente. La extensión se mantiene inerte en cualquier pestaña donde no hayas apretado "Conectar" vos mismo.',
|
|
88
118
|
extensionNote: 'La extensión de Chrome es custom y viene incluida en este paquete. El menú que aparece después de esta pantalla te dice exactamente dónde está para que la cargues vía chrome://extensions → Cargar sin empaquetar.',
|
|
89
119
|
prompt: '¿Cómo querés seguir?',
|
|
120
|
+
detailHint: 'Apretá i para ver el detalle completo · también disponible en About.',
|
|
90
121
|
options: {
|
|
91
122
|
accept: 'Entendido, continuar',
|
|
92
123
|
dismiss: 'Aceptar y no volver a mostrar',
|
|
93
|
-
|
|
124
|
+
// El label vive en el idioma destino para que un user monolingüe
|
|
125
|
+
// lo lea sin esfuerzo — "English" se reconoce universalmente y el
|
|
126
|
+
// verbo en el idioma destino confirma que es el switch.
|
|
127
|
+
swap: 'Switch to English',
|
|
94
128
|
quit: 'Salir',
|
|
95
129
|
},
|
|
130
|
+
footerKeys: {
|
|
131
|
+
navigate: 'moverse',
|
|
132
|
+
select: 'elegir',
|
|
133
|
+
lang: 'idioma',
|
|
134
|
+
quit: 'salir',
|
|
135
|
+
info: 'info',
|
|
136
|
+
hide: 'ocultar',
|
|
137
|
+
},
|
|
96
138
|
},
|
|
97
139
|
};
|
|
98
140
|
//# sourceMappingURL=welcome.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"welcome.js","sourceRoot":"","sources":["../../src/commands/welcome.ts"],"names":[],"mappings":"AAAA;;yDAEyD;
|
|
1
|
+
{"version":3,"file":"welcome.js","sourceRoot":"","sources":["../../src/commands/welcome.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,19 @@ const WS_URL = 'ws://127.0.0.1:17529';
|
|
|
2
2
|
const CDP_VERSION = '1.3';
|
|
3
3
|
const CONSOLE_BUFFER_MAX = 200;
|
|
4
4
|
const NETWORK_BUFFER_MAX = 200;
|
|
5
|
+
/* Idle TTL for a tab's WebSocket bridge.
|
|
6
|
+
*
|
|
7
|
+
* After this many milliseconds without a tool.request from the server,
|
|
8
|
+
* the extension closes its side of the WS and detaches the debugger.
|
|
9
|
+
* The popup then shows "Not connected" again and the user explicitly
|
|
10
|
+
* re-presses Connect when they want to reactivate.
|
|
11
|
+
*
|
|
12
|
+
* This replaces the previous behaviour where the bridge implicitly
|
|
13
|
+
* died together with the MCP server subprocess (parent_death_guard,
|
|
14
|
+
* removed in v0.9.0). Lifecycle is now client-side: agent activity
|
|
15
|
+
* keeps the tab warm, silence eventually parks it. */
|
|
16
|
+
const WS_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
17
|
+
const WS_IDLE_SWEEP_MS = 60 * 1000;
|
|
5
18
|
function isRuntimeMessage(msg) {
|
|
6
19
|
if (typeof msg !== 'object' || msg === null)
|
|
7
20
|
return false;
|
|
@@ -25,6 +38,124 @@ function safeParse(raw) {
|
|
|
25
38
|
function cdp(tabId, method, params = {}) {
|
|
26
39
|
return chrome.debugger.sendCommand({ tabId }, method, params);
|
|
27
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Defensive caps applied to every duration that ends up in a setTimeout
|
|
43
|
+
* driven by tool params. The MCP request payload is technically untrusted
|
|
44
|
+
* input, so CodeQL flags unbounded user-controlled timer durations as a
|
|
45
|
+
* resource-exhaustion risk. We clamp at the boundary (drag handler) AND
|
|
46
|
+
* inside `sleep` so the property is enforced even if a future call site
|
|
47
|
+
* forgets to validate.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_DRAG_DURATION_MS = 60_000;
|
|
50
|
+
const MAX_DRAG_HOLD_MS = 10_000;
|
|
51
|
+
function sleep(ms) {
|
|
52
|
+
// Range-branch sanitizer for CodeQL's js/resource-exhaustion: the
|
|
53
|
+
// setTimeout call is INSIDE a literal-bounded range check. Caller-side
|
|
54
|
+
// clamping in the drag handler is the first line of defence; this is
|
|
55
|
+
// the second.
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
if (typeof ms === 'number' && ms > 0 && ms < 60_000) {
|
|
58
|
+
setTimeout(resolve, ms);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
setTimeout(resolve, 0);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wait for ONE CDP event of a given method on a given tab. Adds a temporary
|
|
67
|
+
* chrome.debugger.onEvent listener for the duration of the wait. Resolves
|
|
68
|
+
* with the event params, or null on timeout. Always cleans up its listener.
|
|
69
|
+
*
|
|
70
|
+
* Used by the drag tool to capture Input.dragIntercepted while
|
|
71
|
+
* Input.setInterceptDrags is enabled.
|
|
72
|
+
*/
|
|
73
|
+
function waitForCdpEvent(tabId, method, timeoutMs) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
let settled = false;
|
|
76
|
+
const handler = (source, m, params) => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
if (source.tabId !== tabId)
|
|
80
|
+
return;
|
|
81
|
+
if (m !== method)
|
|
82
|
+
return;
|
|
83
|
+
settled = true;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
chrome.debugger.onEvent.removeListener(handler);
|
|
86
|
+
resolve((params ?? null));
|
|
87
|
+
};
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
if (settled)
|
|
90
|
+
return;
|
|
91
|
+
settled = true;
|
|
92
|
+
chrome.debugger.onEvent.removeListener(handler);
|
|
93
|
+
resolve(null);
|
|
94
|
+
}, timeoutMs);
|
|
95
|
+
chrome.debugger.onEvent.addListener(handler);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build the JS expression that:
|
|
100
|
+
* 1. scrollIntoView(center) for any selector-based endpoint (so both are
|
|
101
|
+
* visible at the same time if possible),
|
|
102
|
+
* 2. detects HTML5 native drag eligibility on the source (`element.draggable`
|
|
103
|
+
* true, or implicit via <img> / <a href>),
|
|
104
|
+
* 3. reads the final centre coords AFTER both scrolls have happened,
|
|
105
|
+
* 4. flags in_viewport so the caller can refuse offscreen drags instead of
|
|
106
|
+
* dispatching cursor events into nowhere.
|
|
107
|
+
*
|
|
108
|
+
* Returns one of:
|
|
109
|
+
* - `{ err: string }` — selector miss or stale element
|
|
110
|
+
* - `{ from, to, draggable }` — ready to drag
|
|
111
|
+
*/
|
|
112
|
+
function buildDragProbeExpr(params) {
|
|
113
|
+
return `
|
|
114
|
+
(() => {
|
|
115
|
+
const P = ${JSON.stringify(params)};
|
|
116
|
+
function scrollAndProbe(sel) {
|
|
117
|
+
const el = document.querySelector(sel);
|
|
118
|
+
if (!el) return { err: 'not_found' };
|
|
119
|
+
el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
120
|
+
return {
|
|
121
|
+
draggable:
|
|
122
|
+
el.draggable === true ||
|
|
123
|
+
el instanceof HTMLImageElement ||
|
|
124
|
+
(el instanceof HTMLAnchorElement && !!el.href),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function readCenter(sel) {
|
|
128
|
+
const el = document.querySelector(sel);
|
|
129
|
+
if (!el) return null;
|
|
130
|
+
const r = el.getBoundingClientRect();
|
|
131
|
+
return {
|
|
132
|
+
x: r.left + r.width / 2,
|
|
133
|
+
y: r.top + r.height / 2,
|
|
134
|
+
in_viewport:
|
|
135
|
+
r.bottom > 0 && r.right > 0 && r.top < innerHeight && r.left < innerWidth,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
let fromHint = null;
|
|
139
|
+
if (P.from_selector) {
|
|
140
|
+
fromHint = scrollAndProbe(P.from_selector);
|
|
141
|
+
if (fromHint.err) return { err: 'from_selector not found: ' + P.from_selector };
|
|
142
|
+
}
|
|
143
|
+
if (P.to_selector) {
|
|
144
|
+
const t = scrollAndProbe(P.to_selector);
|
|
145
|
+
if (t.err) return { err: 'to_selector not found: ' + P.to_selector };
|
|
146
|
+
}
|
|
147
|
+
const from = P.from_selector
|
|
148
|
+
? readCenter(P.from_selector)
|
|
149
|
+
: { x: P.from_x, y: P.from_y, in_viewport: true };
|
|
150
|
+
const to = P.to_selector
|
|
151
|
+
? readCenter(P.to_selector)
|
|
152
|
+
: { x: P.to_x, y: P.to_y, in_viewport: true };
|
|
153
|
+
if (!from) return { err: 'from element disappeared between probes' };
|
|
154
|
+
if (!to) return { err: 'to element disappeared between probes' };
|
|
155
|
+
return { from, to, draggable: fromHint ? fromHint.draggable : false };
|
|
156
|
+
})()
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
28
159
|
// Convert an arbitrary console arg (`unknown`) into something printable
|
|
29
160
|
// without leaking `[object Object]`. Used by the console buffer when CDP
|
|
30
161
|
// hands us values we don't statically know.
|
|
@@ -366,6 +497,262 @@ async function handleTool(state, msg) {
|
|
|
366
497
|
result: { typed: text.length, selector },
|
|
367
498
|
};
|
|
368
499
|
}
|
|
500
|
+
case 'drag': {
|
|
501
|
+
const optNum = (key) => {
|
|
502
|
+
const v = p[key];
|
|
503
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
|
|
504
|
+
};
|
|
505
|
+
const clamp = (v, max) => Math.min(v, max);
|
|
506
|
+
const fromSelector = optStr('from_selector');
|
|
507
|
+
const toSelector = optStr('to_selector');
|
|
508
|
+
const fromXRaw = optNum('from_x');
|
|
509
|
+
const fromYRaw = optNum('from_y');
|
|
510
|
+
const toXRaw = optNum('to_x');
|
|
511
|
+
const toYRaw = optNum('to_y');
|
|
512
|
+
// Cap every duration that ends up in a setTimeout to keep CodeQL's
|
|
513
|
+
// "user-controlled timer duration" check happy and to prevent a
|
|
514
|
+
// misbehaving agent from parking the bridge for hours on a typo.
|
|
515
|
+
const durationMs = clamp(optNum('duration_ms') ?? 1500, MAX_DRAG_DURATION_MS);
|
|
516
|
+
const holdBeforeMoveMs = clamp(optNum('hold_before_move_ms') ?? 0, MAX_DRAG_HOLD_MS);
|
|
517
|
+
const holdBeforeReleaseMs = clamp(optNum('hold_before_release_ms') ?? 0, MAX_DRAG_HOLD_MS);
|
|
518
|
+
const hasFromCoords = fromXRaw !== undefined && fromYRaw !== undefined;
|
|
519
|
+
const hasToCoords = toXRaw !== undefined && toYRaw !== undefined;
|
|
520
|
+
if (!fromSelector && !hasFromCoords) {
|
|
521
|
+
return {
|
|
522
|
+
kind: 'tool.response',
|
|
523
|
+
id: msg.id,
|
|
524
|
+
ok: false,
|
|
525
|
+
error: 'drag: provide from_selector or both from_x and from_y',
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (!toSelector && !hasToCoords) {
|
|
529
|
+
return {
|
|
530
|
+
kind: 'tool.response',
|
|
531
|
+
id: msg.id,
|
|
532
|
+
ok: false,
|
|
533
|
+
error: 'drag: provide to_selector or both to_x and to_y',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const probeExpr = buildDragProbeExpr({
|
|
537
|
+
from_selector: fromSelector,
|
|
538
|
+
to_selector: toSelector,
|
|
539
|
+
from_x: fromXRaw,
|
|
540
|
+
from_y: fromYRaw,
|
|
541
|
+
to_x: toXRaw,
|
|
542
|
+
to_y: toYRaw,
|
|
543
|
+
});
|
|
544
|
+
const probe = await evaluateInTab(tabId, probeExpr);
|
|
545
|
+
if (probe.err) {
|
|
546
|
+
return { kind: 'tool.response', id: msg.id, ok: false, error: `drag: ${probe.err}` };
|
|
547
|
+
}
|
|
548
|
+
if (!probe.from || !probe.to) {
|
|
549
|
+
return {
|
|
550
|
+
kind: 'tool.response',
|
|
551
|
+
id: msg.id,
|
|
552
|
+
ok: false,
|
|
553
|
+
error: 'drag: could not resolve coordinates',
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (!probe.from.in_viewport) {
|
|
557
|
+
return {
|
|
558
|
+
kind: 'tool.response',
|
|
559
|
+
id: msg.id,
|
|
560
|
+
ok: false,
|
|
561
|
+
error: 'drag: source point is offscreen — scroll first or pass viewport coords',
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (!probe.to.in_viewport) {
|
|
565
|
+
return {
|
|
566
|
+
kind: 'tool.response',
|
|
567
|
+
id: msg.id,
|
|
568
|
+
ok: false,
|
|
569
|
+
error: 'drag: destination point is offscreen — scroll first or pass viewport coords',
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const fromX = probe.from.x;
|
|
573
|
+
const fromY = probe.from.y;
|
|
574
|
+
const toX = probe.to.x;
|
|
575
|
+
const toY = probe.to.y;
|
|
576
|
+
const isDraggable = !!probe.draggable;
|
|
577
|
+
// ~30fps interpolation; minimum 2 steps so the path actually has a midpoint.
|
|
578
|
+
const steps = durationMs > 0 ? Math.max(2, Math.round(durationMs / 33)) : 1;
|
|
579
|
+
const stepDelayMs = steps > 0 ? durationMs / steps : 0;
|
|
580
|
+
const eventsFired = [];
|
|
581
|
+
let dragMode = 'pointer';
|
|
582
|
+
const dragStart = Date.now();
|
|
583
|
+
let interceptionEnabled = false;
|
|
584
|
+
const interpolate = (t) => ({
|
|
585
|
+
x: fromX + (toX - fromX) * t,
|
|
586
|
+
y: fromY + (toY - fromY) * t,
|
|
587
|
+
});
|
|
588
|
+
try {
|
|
589
|
+
if (isDraggable) {
|
|
590
|
+
try {
|
|
591
|
+
await cdp(tabId, 'Input.setInterceptDrags', { enabled: true });
|
|
592
|
+
interceptionEnabled = true;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
// setInterceptDrags is experimental — fall back to pointer mode silently.
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (interceptionEnabled) {
|
|
599
|
+
// Arm the listener BEFORE the press+wiggle that may trigger it.
|
|
600
|
+
const interceptPromise = waitForCdpEvent(tabId, 'Input.dragIntercepted', 120);
|
|
601
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
602
|
+
type: 'mouseMoved',
|
|
603
|
+
x: fromX,
|
|
604
|
+
y: fromY,
|
|
605
|
+
});
|
|
606
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
607
|
+
type: 'mousePressed',
|
|
608
|
+
x: fromX,
|
|
609
|
+
y: fromY,
|
|
610
|
+
button: 'left',
|
|
611
|
+
clickCount: 1,
|
|
612
|
+
});
|
|
613
|
+
if (holdBeforeMoveMs > 0)
|
|
614
|
+
await sleep(holdBeforeMoveMs);
|
|
615
|
+
// Small wiggle toward the destination so Chrome's native drag system
|
|
616
|
+
// crosses its activation threshold. Direction matters for some libs.
|
|
617
|
+
const dx = toX - fromX;
|
|
618
|
+
const dy = toY - fromY;
|
|
619
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
620
|
+
const wx = fromX + (dx / len) * 5;
|
|
621
|
+
const wy = fromY + (dy / len) * 5;
|
|
622
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: wx, y: wy });
|
|
623
|
+
const intercepted = await interceptPromise;
|
|
624
|
+
if (intercepted) {
|
|
625
|
+
dragMode = 'html5';
|
|
626
|
+
eventsFired.push('Input.dragIntercepted');
|
|
627
|
+
const dragData = intercepted.data;
|
|
628
|
+
for (let i = 1; i <= steps; i++) {
|
|
629
|
+
const t = i / steps;
|
|
630
|
+
const { x, y } = interpolate(t);
|
|
631
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
632
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
633
|
+
type: 'dragOver',
|
|
634
|
+
x,
|
|
635
|
+
y,
|
|
636
|
+
data: dragData,
|
|
637
|
+
});
|
|
638
|
+
if (i === 1)
|
|
639
|
+
eventsFired.push('dragOver');
|
|
640
|
+
if (stepDelayMs > 0)
|
|
641
|
+
await sleep(stepDelayMs);
|
|
642
|
+
}
|
|
643
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
644
|
+
type: 'dragEnter',
|
|
645
|
+
x: toX,
|
|
646
|
+
y: toY,
|
|
647
|
+
data: dragData,
|
|
648
|
+
});
|
|
649
|
+
eventsFired.push('dragEnter');
|
|
650
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
651
|
+
type: 'dragOver',
|
|
652
|
+
x: toX,
|
|
653
|
+
y: toY,
|
|
654
|
+
data: dragData,
|
|
655
|
+
});
|
|
656
|
+
if (holdBeforeReleaseMs > 0)
|
|
657
|
+
await sleep(holdBeforeReleaseMs);
|
|
658
|
+
await cdp(tabId, 'Input.dispatchDragEvent', {
|
|
659
|
+
type: 'drop',
|
|
660
|
+
x: toX,
|
|
661
|
+
y: toY,
|
|
662
|
+
data: dragData,
|
|
663
|
+
});
|
|
664
|
+
eventsFired.push('drop');
|
|
665
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
666
|
+
type: 'mouseReleased',
|
|
667
|
+
x: toX,
|
|
668
|
+
y: toY,
|
|
669
|
+
button: 'left',
|
|
670
|
+
clickCount: 1,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Element was tagged draggable but no native drag fired — the page
|
|
675
|
+
// either preventDefault'd dragstart or wires its own pointer logic.
|
|
676
|
+
// Continue with pointer-only events from where we already pressed.
|
|
677
|
+
dragMode = 'pointer';
|
|
678
|
+
for (let i = 1; i <= steps; i++) {
|
|
679
|
+
const t = i / steps;
|
|
680
|
+
const { x, y } = interpolate(t);
|
|
681
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
682
|
+
if (stepDelayMs > 0)
|
|
683
|
+
await sleep(stepDelayMs);
|
|
684
|
+
}
|
|
685
|
+
if (holdBeforeReleaseMs > 0)
|
|
686
|
+
await sleep(holdBeforeReleaseMs);
|
|
687
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
688
|
+
type: 'mouseReleased',
|
|
689
|
+
x: toX,
|
|
690
|
+
y: toY,
|
|
691
|
+
button: 'left',
|
|
692
|
+
clickCount: 1,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
// Pointer-only branch: no native drag involvement at all.
|
|
698
|
+
dragMode = 'pointer';
|
|
699
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
700
|
+
type: 'mouseMoved',
|
|
701
|
+
x: fromX,
|
|
702
|
+
y: fromY,
|
|
703
|
+
});
|
|
704
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
705
|
+
type: 'mousePressed',
|
|
706
|
+
x: fromX,
|
|
707
|
+
y: fromY,
|
|
708
|
+
button: 'left',
|
|
709
|
+
clickCount: 1,
|
|
710
|
+
});
|
|
711
|
+
if (holdBeforeMoveMs > 0)
|
|
712
|
+
await sleep(holdBeforeMoveMs);
|
|
713
|
+
for (let i = 1; i <= steps; i++) {
|
|
714
|
+
const t = i / steps;
|
|
715
|
+
const { x, y } = interpolate(t);
|
|
716
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
717
|
+
if (stepDelayMs > 0)
|
|
718
|
+
await sleep(stepDelayMs);
|
|
719
|
+
}
|
|
720
|
+
if (holdBeforeReleaseMs > 0)
|
|
721
|
+
await sleep(holdBeforeReleaseMs);
|
|
722
|
+
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
723
|
+
type: 'mouseReleased',
|
|
724
|
+
x: toX,
|
|
725
|
+
y: toY,
|
|
726
|
+
button: 'left',
|
|
727
|
+
clickCount: 1,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
// Critical: leaving interception on would block the human user from
|
|
733
|
+
// dragging anything in this tab. Best-effort cleanup, swallow errors.
|
|
734
|
+
if (interceptionEnabled) {
|
|
735
|
+
try {
|
|
736
|
+
await cdp(tabId, 'Input.setInterceptDrags', { enabled: false });
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// ignore
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
kind: 'tool.response',
|
|
745
|
+
id: msg.id,
|
|
746
|
+
ok: true,
|
|
747
|
+
result: {
|
|
748
|
+
from: { x: fromX, y: fromY, selector: fromSelector ?? null },
|
|
749
|
+
to: { x: toX, y: toY, selector: toSelector ?? null },
|
|
750
|
+
duration_ms_actual: Date.now() - dragStart,
|
|
751
|
+
drag_mode: dragMode,
|
|
752
|
+
events_fired: eventsFired,
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
369
756
|
case 'evaluate': {
|
|
370
757
|
const expression = str('expression');
|
|
371
758
|
const value = await evaluateInTab(tabId, expression);
|
|
@@ -474,6 +861,7 @@ async function connectTab(tabId) {
|
|
|
474
861
|
consoleBuffer: [],
|
|
475
862
|
networkBuffer: new Map(),
|
|
476
863
|
networkOrder: [],
|
|
864
|
+
lastActivityAt: Date.now(),
|
|
477
865
|
};
|
|
478
866
|
tabStates.set(tabId, state);
|
|
479
867
|
attachDebuggerListener(state);
|
|
@@ -527,7 +915,9 @@ async function connectTab(tabId) {
|
|
|
527
915
|
return;
|
|
528
916
|
}
|
|
529
917
|
// ServerToExtension is the union { tab.registered | tool.request },
|
|
530
|
-
// so by elimination this branch handles the tool.request case.
|
|
918
|
+
// so by elimination this branch handles the tool.request case. Touch
|
|
919
|
+
// the activity timestamp so the WS-idle sweeper keeps this tab warm.
|
|
920
|
+
state.lastActivityAt = Date.now();
|
|
531
921
|
const response = await handleTool(state, msg);
|
|
532
922
|
if (ws.readyState === WebSocket.OPEN)
|
|
533
923
|
send(ws, response);
|
|
@@ -594,5 +984,25 @@ chrome.debugger.onDetach.addListener((source) => {
|
|
|
594
984
|
// Best effort.
|
|
595
985
|
});
|
|
596
986
|
});
|
|
987
|
+
/* WS-idle sweeper.
|
|
988
|
+
*
|
|
989
|
+
* Every WS_IDLE_SWEEP_MS, walk every connected tab. Any tab whose last
|
|
990
|
+
* tool.request landed more than WS_IDLE_TTL_MS ago gets disconnected:
|
|
991
|
+
* the WS closes, the debugger detaches, the popup goes back to "Not
|
|
992
|
+
* connected". The user explicitly re-presses Connect to bring it back.
|
|
993
|
+
*
|
|
994
|
+
* This is the replacement for the parent_death_guard that used to kill
|
|
995
|
+
* the entire MCP server on stdio close — now the server stays alive and
|
|
996
|
+
* the bridge is parked tab-by-tab from the client side. */
|
|
997
|
+
setInterval(() => {
|
|
998
|
+
const now = Date.now();
|
|
999
|
+
for (const [tabId, state] of tabStates) {
|
|
1000
|
+
if (now - state.lastActivityAt < WS_IDLE_TTL_MS)
|
|
1001
|
+
continue;
|
|
1002
|
+
void cleanup(tabId).catch(() => {
|
|
1003
|
+
// Best effort.
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}, WS_IDLE_SWEEP_MS);
|
|
597
1007
|
export {};
|
|
598
1008
|
//# sourceMappingURL=background.js.map
|