@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.
Files changed (96) hide show
  1. package/dist/bridge/events.d.ts +3 -0
  2. package/dist/bridge/events.js +8 -0
  3. package/dist/bridge/events.js.map +1 -1
  4. package/dist/commands/welcome.d.ts +15 -0
  5. package/dist/commands/welcome.js +44 -2
  6. package/dist/commands/welcome.js.map +1 -1
  7. package/dist/extension/background.js +411 -1
  8. package/dist/extension/background.js.map +1 -1
  9. package/dist/extension/manifest.json +1 -1
  10. package/dist/extension/popup.d.ts +2 -1
  11. package/dist/extension/popup.html +128 -50
  12. package/dist/extension/popup.js +22 -3
  13. package/dist/extension/popup.js.map +1 -1
  14. package/dist/permissions.js +6 -0
  15. package/dist/permissions.js.map +1 -1
  16. package/dist/server.js +42 -36
  17. package/dist/server.js.map +1 -1
  18. package/dist/tools/browser-definitions.js +69 -0
  19. package/dist/tools/browser-definitions.js.map +1 -1
  20. package/dist/tools/browser-dispatch.d.ts +8 -14
  21. package/dist/tools/browser-dispatch.js +35 -0
  22. package/dist/tools/browser-dispatch.js.map +1 -1
  23. package/dist/tools/tab-claims.d.ts +5 -1
  24. package/dist/tools/tab-claims.js +17 -0
  25. package/dist/tools/tab-claims.js.map +1 -1
  26. package/dist/ui/app.js +27 -2
  27. package/dist/ui/app.js.map +1 -1
  28. package/dist/ui/components.d.ts +15 -3
  29. package/dist/ui/components.js +34 -14
  30. package/dist/ui/components.js.map +1 -1
  31. package/dist/ui/primitives/badge.d.ts +11 -0
  32. package/dist/ui/primitives/badge.js +35 -0
  33. package/dist/ui/primitives/badge.js.map +1 -0
  34. package/dist/ui/primitives/check-row.d.ts +11 -0
  35. package/dist/ui/primitives/check-row.js +10 -0
  36. package/dist/ui/primitives/check-row.js.map +1 -0
  37. package/dist/ui/primitives/footer-keys.d.ts +12 -0
  38. package/dist/ui/primitives/footer-keys.js +11 -0
  39. package/dist/ui/primitives/footer-keys.js.map +1 -0
  40. package/dist/ui/primitives/index.d.ts +8 -0
  41. package/dist/ui/primitives/index.js +13 -0
  42. package/dist/ui/primitives/index.js.map +1 -0
  43. package/dist/ui/primitives/inline-spinner.d.ts +7 -0
  44. package/dist/ui/primitives/inline-spinner.js +23 -0
  45. package/dist/ui/primitives/inline-spinner.js.map +1 -0
  46. package/dist/ui/primitives/key-cap.d.ts +6 -0
  47. package/dist/ui/primitives/key-cap.js +6 -0
  48. package/dist/ui/primitives/key-cap.js.map +1 -0
  49. package/dist/ui/primitives/menu-row.d.ts +17 -0
  50. package/dist/ui/primitives/menu-row.js +8 -0
  51. package/dist/ui/primitives/menu-row.js.map +1 -0
  52. package/dist/ui/primitives/section-head.d.ts +11 -0
  53. package/dist/ui/primitives/section-head.js +7 -0
  54. package/dist/ui/primitives/section-head.js.map +1 -0
  55. package/dist/ui/primitives/status-bar.d.ts +13 -0
  56. package/dist/ui/primitives/status-bar.js +17 -0
  57. package/dist/ui/primitives/status-bar.js.map +1 -0
  58. package/dist/ui/screens/about.js +13 -4
  59. package/dist/ui/screens/about.js.map +1 -1
  60. package/dist/ui/screens/agent-instructions.js +53 -47
  61. package/dist/ui/screens/agent-instructions.js.map +1 -1
  62. package/dist/ui/screens/client-picker.js +64 -16
  63. package/dist/ui/screens/client-picker.js.map +1 -1
  64. package/dist/ui/screens/doctor.js +20 -7
  65. package/dist/ui/screens/doctor.js.map +1 -1
  66. package/dist/ui/screens/empty-clients.d.ts +8 -0
  67. package/dist/ui/screens/empty-clients.js +57 -0
  68. package/dist/ui/screens/empty-clients.js.map +1 -0
  69. package/dist/ui/screens/extension.js +25 -8
  70. package/dist/ui/screens/extension.js.map +1 -1
  71. package/dist/ui/screens/free-port.js +48 -33
  72. package/dist/ui/screens/free-port.js.map +1 -1
  73. package/dist/ui/screens/index.d.ts +2 -0
  74. package/dist/ui/screens/index.js +2 -0
  75. package/dist/ui/screens/index.js.map +1 -1
  76. package/dist/ui/screens/install-result.js +29 -6
  77. package/dist/ui/screens/install-result.js.map +1 -1
  78. package/dist/ui/screens/language.js +39 -18
  79. package/dist/ui/screens/language.js.map +1 -1
  80. package/dist/ui/screens/menu.js +99 -43
  81. package/dist/ui/screens/menu.js.map +1 -1
  82. package/dist/ui/screens/multi-agent.js +24 -18
  83. package/dist/ui/screens/multi-agent.js.map +1 -1
  84. package/dist/ui/screens/permissions.js +145 -33
  85. package/dist/ui/screens/permissions.js.map +1 -1
  86. package/dist/ui/screens/port-collision.d.ts +18 -0
  87. package/dist/ui/screens/port-collision.js +95 -0
  88. package/dist/ui/screens/port-collision.js.map +1 -0
  89. package/dist/ui/screens/updates.js +44 -21
  90. package/dist/ui/screens/updates.js.map +1 -1
  91. package/dist/ui/screens/welcome.js +56 -26
  92. package/dist/ui/screens/welcome.js.map +1 -1
  93. package/dist/ui/tokens.d.ts +37 -0
  94. package/dist/ui/tokens.js +58 -0
  95. package/dist/ui/tokens.js.map +1 -0
  96. package/package.json +1 -1
@@ -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
  }
@@ -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 {};
@@ -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
- swap: 'Switch to español',
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
- swap: 'Cambiar a English',
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;AAkCzD,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,EAAE,EAAE;QACF,KAAK,EAAE,cAAc;QACrB,UAAU,EAAE,cAAc;QAC1B,KAAK,EAAE;YACL,kEAAkE;YAClE,oEAAoE;YACpE,uEAAuE;YACvE,6BAA6B;SAC9B,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,iBAAiB,EAAE,0CAA0C;QAC7D,YAAY,EAAE;YACZ,gCAAgC;YAChC,6CAA6C;YAC7C,oCAAoC;YACpC,oDAAoD;YACpD,EAAE;YACF,oEAAoE;YACpE,uEAAuE;YACvE,kEAAkE;YAClE,4CAA4C;SAC7C,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,YAAY,EAAE,+BAA+B;QAC7C,OAAO,EAAE;YACP,oEAAoE;YACpE,sEAAsE;YACtE,2DAA2D;YAC3D,EAAE;YACF,uEAAuE;YACvE,wEAAwE;YACxE,qEAAqE;YACrE,EAAE;YACF,uEAAuE;YACvE,uEAAuE;YACvE,4BAA4B;SAC7B,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,cAAc,EACZ,iLAAiL;QACnL,aAAa,EACX,yLAAyL;QAC3L,MAAM,EAAE,6BAA6B;QACrC,OAAO,EAAE;YACP,MAAM,EAAE,wBAAwB;YAChC,OAAO,EAAE,6BAA6B;YACtC,IAAI,EAAE,mBAAmB;YACzB,IAAI,EAAE,MAAM;SACb;KACF;IACD,EAAE,EAAE;QACF,KAAK,EAAE,cAAc;QACrB,UAAU,EAAE,aAAa;QACzB,KAAK,EAAE;YACL,mEAAmE;YACnE,uEAAuE;YACvE,wEAAwE;YACxE,6BAA6B;SAC9B,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,iBAAiB,EAAE,oDAAoD;QACvE,YAAY,EAAE;YACZ,uCAAuC;YACvC,+CAA+C;YAC/C,2CAA2C;YAC3C,8DAA8D;YAC9D,EAAE;YACF,qEAAqE;YACrE,uEAAuE;YACvE,yEAAyE;YACzE,iDAAiD;SAClD,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,YAAY,EAAE,0BAA0B;QACxC,OAAO,EAAE;YACP,uEAAuE;YACvE,kEAAkE;YAClE,qEAAqE;YACrE,wCAAwC;YACxC,EAAE;YACF,iEAAiE;YACjE,4DAA4D;YAC5D,qEAAqE;YACrE,oCAAoC;YACpC,EAAE;YACF,oEAAoE;YACpE,uEAAuE;YACvE,4BAA4B;SAC7B,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,cAAc,EACZ,oMAAoM;QACtM,aAAa,EACX,qNAAqN;QACvN,MAAM,EAAE,sBAAsB;QAC9B,OAAO,EAAE;YACP,MAAM,EAAE,sBAAsB;YAC9B,OAAO,EAAE,+BAA+B;YACxC,IAAI,EAAE,mBAAmB;YACzB,IAAI,EAAE,OAAO;SACd;KACF;CACF,CAAC"}
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