File size: 8,860 Bytes
7a1ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Input blocker utility for browser agent.
 *
 * Injects a transparent overlay that captures all user input events
 * and displays an informational banner during automation.
 *
 * The overlay is PERSISTENT — it stays in the DOM for the entire
 * browser agent session.  To allow CDP tool calls to interact with
 * page elements, we temporarily set `pointer-events: none` on the
 * overlay (via {@link suspendInputBlocker}) which makes it invisible
 * to hit-testing / interactability checks without any DOM mutation
 * or visual change.  After the tool call, {@link resumeInputBlocker}
 * restores `pointer-events: auto`.
 *
 * IMPORTANT: chrome-devtools-mcp's evaluate_script tool expects:
 *   { function: "() => { ... }" }
 * It takes a function declaration string, NOT raw code.
 * The parameter name is "function", not "code" or "expression".
 */

import type { BrowserManager } from './browserManager.js';
import { debugLogger } from '../../utils/debugLogger.js';

/**
 * JavaScript function to inject the input blocker overlay.
 * This blocks all user input events while allowing CDP commands to work normally.
 *
 * Must be a function declaration (NOT an IIFE) because evaluate_script
 * evaluates it via Puppeteer's page.evaluate().
 */
const INPUT_BLOCKER_FUNCTION = `() => {
  // If the blocker already exists, just ensure it's active and return.
  // This makes re-injection after potentially-navigating tools near-free
  // when the page didn't actually navigate (most clicks don't navigate).
  var existing = document.getElementById('__gemini_input_blocker');
  if (existing) {
    existing.style.pointerEvents = 'auto';
    return;
  }

  const blocker = document.createElement('div');
  blocker.id = '__gemini_input_blocker';
  blocker.setAttribute('aria-hidden', 'true');
  blocker.setAttribute('role', 'presentation');
  blocker.style.cssText = [
    'position: fixed',
    'inset: 0',
    'z-index: 2147483646',
    'cursor: not-allowed',
    'background: transparent',
  ].join('; ');

  // Block all input events on the overlay itself
  var blockEvent = function(e) {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
  };

  var events = [
    'click', 'mousedown', 'mouseup', 'keydown', 'keyup',
    'keypress', 'touchstart', 'touchend', 'touchmove', 'wheel',
    'contextmenu', 'dblclick', 'pointerdown', 'pointerup', 'pointermove',
  ];
  for (var i = 0; i < events.length; i++) {
    blocker.addEventListener(events[i], blockEvent, { capture: true });
  }

  // Capsule-shaped floating pill at bottom center
  var pill = document.createElement('div');
  pill.style.cssText = [
    'position: fixed',
    'bottom: 20px',
    'left: 50%',
    'transform: translateX(-50%) translateY(20px)',
    'display: flex',
    'align-items: center',
    'gap: 10px',
    'padding: 10px 20px',
    'background: rgba(24, 24, 27, 0.88)',
    'color: #fff',
    'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
    'font-size: 13px',
    'line-height: 1',
    'border-radius: 999px',
    'z-index: 2147483647',
    'backdrop-filter: blur(16px)',
    '-webkit-backdrop-filter: blur(16px)',
    'border: 1px solid rgba(255, 255, 255, 0.08)',
    'box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05)',
    'opacity: 0',
    'transition: opacity 0.4s ease, transform 0.4s ease',
    'white-space: nowrap',
    'user-select: none',
    'pointer-events: none',
  ].join('; ');

  // Pulsing red dot
  var dot = document.createElement('span');
  dot.style.cssText = [
    'width: 10px',
    'height: 10px',
    'border-radius: 50%',
    'background: #ef4444',
    'display: inline-block',
    'flex-shrink: 0',
    'box-shadow: 0 0 6px rgba(239, 68, 68, 0.6)',
    'animation: __gemini_pulse 2s ease-in-out infinite',
  ].join('; ');

  // Labels
  var label = document.createElement('span');
  label.style.cssText = 'font-weight: 600; letter-spacing: 0.01em;';
  label.textContent = 'Gemini CLI is controlling this browser';

  var sep = document.createElement('span');
  sep.style.cssText = 'width: 1px; height: 14px; background: rgba(255,255,255,0.2); flex-shrink: 0;';

  var sub = document.createElement('span');
  sub.style.cssText = 'color: rgba(255,255,255,0.55); font-size: 12px;';
  sub.textContent = 'Input disabled during automation';

  pill.appendChild(dot);
  pill.appendChild(label);
  pill.appendChild(sep);
  pill.appendChild(sub);

  // Inject @keyframes for the pulse animation
  var styleEl = document.createElement('style');
  styleEl.id = '__gemini_input_blocker_style';
  styleEl.textContent = '@keyframes __gemini_pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.85); } }';
  document.head.appendChild(styleEl);

  blocker.appendChild(pill);
  var target = document.body || document.documentElement;
  if (target) {
    target.appendChild(blocker);
    // Trigger entrance animation
    requestAnimationFrame(function() {
      pill.style.opacity = '1';
      pill.style.transform = 'translateX(-50%) translateY(0)';
    });
  }
}`;

/**
 * JavaScript function to remove the input blocker overlay entirely.
 * Used only during final cleanup.
 */
const REMOVE_BLOCKER_FUNCTION = `() => {
  var blocker = document.getElementById('__gemini_input_blocker');
  if (blocker) {
    blocker.remove();
  }
  var style = document.getElementById('__gemini_input_blocker_style');
  if (style) {
    style.remove();
  }
}`;

/**
 * JavaScript to temporarily suspend the input blocker by setting
 * pointer-events to 'none'.  This makes the overlay invisible to
 * hit-testing so chrome-devtools-mcp's interactability checks pass
 * and CDP clicks fall through to page elements.
 *
 * The overlay DOM element stays in place — no visual change, no flickering.
 */
const SUSPEND_BLOCKER_FUNCTION = `() => {
  var blocker = document.getElementById('__gemini_input_blocker');
  if (blocker) {
    blocker.style.pointerEvents = 'none';
  }
}`;

/**
 * JavaScript to resume the input blocker by restoring pointer-events
 * to 'auto'.  User clicks are blocked again.
 */
const RESUME_BLOCKER_FUNCTION = `() => {
  var blocker = document.getElementById('__gemini_input_blocker');
  if (blocker) {
    blocker.style.pointerEvents = 'auto';
  }
}`;

/**
 * Injects the input blocker overlay into the current page.
 *
 * @param browserManager The browser manager to use for script execution
 * @returns Promise that resolves when the blocker is injected
 */
export async function injectInputBlocker(
  browserManager: BrowserManager,
  signal?: AbortSignal,
): Promise<void> {
  try {
    await browserManager.callTool(
      'evaluate_script',
      { function: INPUT_BLOCKER_FUNCTION },
      signal,
      true,
    );
  } catch (error) {
    // Log but don't throw - input blocker is a UX enhancement, not critical functionality
    debugLogger.warn(
      'Failed to inject input blocker: ' +
        (error instanceof Error ? error.message : String(error)),
    );
  }
}

/**
 * Removes the input blocker overlay from the current page entirely.
 * Used only during final cleanup.
 *
 * @param browserManager The browser manager to use for script execution
 * @returns Promise that resolves when the blocker is removed
 */
export async function removeInputBlocker(
  browserManager: BrowserManager,
  signal?: AbortSignal,
): Promise<void> {
  try {
    await browserManager.callTool(
      'evaluate_script',
      { function: REMOVE_BLOCKER_FUNCTION },
      signal,
      true,
    );
  } catch (error) {
    // Log but don't throw - removal failure is not critical
    debugLogger.warn(
      'Failed to remove input blocker: ' +
        (error instanceof Error ? error.message : String(error)),
    );
  }
}

/**
 * Temporarily suspends the input blocker so CDP tool calls can
 * interact with page elements.  The overlay stays in the DOM
 * (no visual change) — only pointer-events is toggled.
 */
export async function suspendInputBlocker(
  browserManager: BrowserManager,
  signal?: AbortSignal,
): Promise<void> {
  try {
    await browserManager.callTool(
      'evaluate_script',
      { function: SUSPEND_BLOCKER_FUNCTION },
      signal,
      true,
    );
  } catch {
    // Non-critical — tool call will still attempt to proceed
  }
}

/**
 * Resumes the input blocker after a tool call completes.
 * Restores pointer-events so user clicks are blocked again.
 */
export async function resumeInputBlocker(
  browserManager: BrowserManager,
  signal?: AbortSignal,
): Promise<void> {
  try {
    await browserManager.callTool(
      'evaluate_script',
      { function: RESUME_BLOCKER_FUNCTION },
      signal,
      true,
    );
  } catch {
    // Non-critical
  }
}