@myagentroam/node 0.9.125 → 0.9.127

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.
@@ -0,0 +1,484 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { resolve } from 'node:path';
5
+ import { createInterface } from 'node:readline';
6
+ import { terminateProcessTree } from '../../process-tree.js';
7
+ const START_TIMEOUT_MS = 5_000;
8
+ const STOP_TIMEOUT_MS = 2_000;
9
+ /** A non-activating, pointer-transparent primary-display control marker. */
10
+ export class ComputerUseControlIndicator {
11
+ child;
12
+ directory;
13
+ #closed = false;
14
+ #lostError;
15
+ lost;
16
+ constructor(child, directory) {
17
+ this.child = child;
18
+ this.directory = directory;
19
+ this.lost = new Promise((resolveLost) => {
20
+ child.once('exit', (code, signal) => {
21
+ if (this.#closed)
22
+ return;
23
+ this.#lostError = new Error(`Computer Use control indicator exited (${code ?? signal ?? 'unknown'}).`);
24
+ resolveLost(this.#lostError);
25
+ });
26
+ });
27
+ }
28
+ static async show(runtimeExecutable) {
29
+ const directory = await mkdtemp(resolve(tmpdir(), 'mar-computer-use-indicator-'));
30
+ const script = resolve(directory, 'indicator.py');
31
+ await writeFile(script, PYTHON_INDICATOR_SOURCE, { encoding: 'utf8', mode: 0o600 });
32
+ const child = spawn(runtimeExecutable, ['-u', script], {
33
+ cwd: directory,
34
+ env: indicatorEnvironment(),
35
+ stdio: ['pipe', 'pipe', 'pipe'],
36
+ windowsHide: true,
37
+ detached: process.platform !== 'win32'
38
+ });
39
+ const indicator = new ComputerUseControlIndicator(child, directory);
40
+ try {
41
+ await waitUntilVisible(child);
42
+ return indicator;
43
+ }
44
+ catch (error) {
45
+ await indicator.close();
46
+ throw error;
47
+ }
48
+ }
49
+ async close() {
50
+ if (this.#closed)
51
+ return;
52
+ this.#closed = true;
53
+ this.child.stdin.end();
54
+ if (this.child.exitCode === null && this.child.signalCode === null) {
55
+ await terminateProcessTree(this.child).catch(() => {
56
+ try {
57
+ this.child.kill('SIGTERM');
58
+ }
59
+ catch {
60
+ // The process may have exited between the status check and termination.
61
+ }
62
+ });
63
+ if (!(await waitForExit(this.child, STOP_TIMEOUT_MS))) {
64
+ try {
65
+ this.child.kill('SIGKILL');
66
+ }
67
+ catch {
68
+ // The process may have exited before the fallback signal.
69
+ }
70
+ await waitForExit(this.child, STOP_TIMEOUT_MS);
71
+ }
72
+ }
73
+ await rm(this.directory, { recursive: true, force: true }).catch(() => undefined);
74
+ }
75
+ assertActive() {
76
+ if (this.#lostError !== undefined)
77
+ throw this.#lostError;
78
+ if (this.child.exitCode !== null || this.child.signalCode !== null)
79
+ throw new Error('Computer Use control indicator is no longer active.');
80
+ }
81
+ }
82
+ async function waitForExit(child, timeoutMs) {
83
+ if (child.exitCode !== null || child.signalCode !== null)
84
+ return true;
85
+ return await new Promise((resolveExit) => {
86
+ const timer = setTimeout(() => finish(false), timeoutMs);
87
+ const exit = () => finish(true);
88
+ const finish = (exited) => {
89
+ clearTimeout(timer);
90
+ child.off('exit', exit);
91
+ resolveExit(exited);
92
+ };
93
+ child.once('exit', exit);
94
+ if (child.exitCode !== null || child.signalCode !== null)
95
+ finish(true);
96
+ });
97
+ }
98
+ async function waitUntilVisible(child) {
99
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
100
+ try {
101
+ await new Promise((resolveVisible, reject) => {
102
+ let stderr = '';
103
+ const timer = setTimeout(() => finish(() => reject(new Error('Computer Use control indicator start timed out.'))), START_TIMEOUT_MS);
104
+ const finish = (callback) => {
105
+ clearTimeout(timer);
106
+ lines.off('line', line);
107
+ child.off('exit', exit);
108
+ child.off('error', failed);
109
+ callback();
110
+ };
111
+ const line = (value) => {
112
+ if (value !== 'ready')
113
+ return;
114
+ finish(resolveVisible);
115
+ };
116
+ const exit = () => finish(() => reject(new Error(`Computer Use control indicator exited during startup.${stderr ? ` ${stderr}` : ''}`)));
117
+ const failed = (error) => finish(() => reject(error));
118
+ child.stderr.on('data', (chunk) => {
119
+ stderr = `${stderr}${chunk.toString('utf8')}`.slice(-1_000).trim();
120
+ });
121
+ lines.on('line', line);
122
+ child.once('exit', exit);
123
+ child.once('error', failed);
124
+ });
125
+ }
126
+ finally {
127
+ lines.close();
128
+ }
129
+ }
130
+ function indicatorEnvironment() {
131
+ const environment = {};
132
+ for (const key of [
133
+ 'PATH',
134
+ 'SystemRoot',
135
+ 'WINDIR',
136
+ 'TEMP',
137
+ 'TMP',
138
+ 'TMPDIR',
139
+ 'HOME',
140
+ 'USERPROFILE',
141
+ 'DISPLAY',
142
+ 'XAUTHORITY',
143
+ 'LANG',
144
+ 'LC_ALL'
145
+ ]) {
146
+ const value = process.env[key];
147
+ if (value !== undefined)
148
+ environment[key] = value;
149
+ }
150
+ return environment;
151
+ }
152
+ const PYTHON_INDICATOR_SOURCE = String.raw `
153
+ import sys
154
+
155
+ COLOR = (235, 151, 55)
156
+ LABEL = "AI CONTROL"
157
+ THICKNESS = 3
158
+
159
+ def ready():
160
+ sys.stdout.write("ready\n")
161
+ sys.stdout.flush()
162
+
163
+ if sys.platform == "win32":
164
+ import ctypes
165
+ from ctypes import wintypes
166
+
167
+ user32 = ctypes.windll.user32
168
+ gdi32 = ctypes.windll.gdi32
169
+ WNDPROC = ctypes.WINFUNCTYPE(
170
+ ctypes.c_ssize_t, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM
171
+ )
172
+ WM_PAINT = 0x000F
173
+ WM_DESTROY = 0x0002
174
+ WM_NCHITTEST = 0x0084
175
+ HTTRANSPARENT = -1
176
+ WS_POPUP = 0x80000000
177
+ WS_EX_LAYERED = 0x00080000
178
+ WS_EX_TRANSPARENT = 0x00000020
179
+ WS_EX_TOOLWINDOW = 0x00000080
180
+ WS_EX_NOACTIVATE = 0x08000000
181
+ HWND_TOPMOST = -1
182
+ SWP_NOACTIVATE = 0x0010
183
+ SW_SHOWNA = 8
184
+ RGN_OR = 2
185
+ TRANSPARENT = 1
186
+ DT_CENTER = 0x00000001
187
+ DT_VCENTER = 0x00000004
188
+ DT_SINGLELINE = 0x00000020
189
+
190
+ class WNDCLASS(ctypes.Structure):
191
+ _fields_ = [
192
+ ("style", wintypes.UINT), ("lpfnWndProc", WNDPROC),
193
+ ("cbClsExtra", ctypes.c_int), ("cbWndExtra", ctypes.c_int),
194
+ ("hInstance", wintypes.HINSTANCE), ("hIcon", wintypes.HICON),
195
+ ("hCursor", wintypes.HANDLE), ("hbrBackground", wintypes.HBRUSH),
196
+ ("lpszMenuName", wintypes.LPCWSTR), ("lpszClassName", wintypes.LPCWSTR)
197
+ ]
198
+
199
+ class PAINTSTRUCT(ctypes.Structure):
200
+ _fields_ = [
201
+ ("hdc", wintypes.HDC), ("fErase", wintypes.BOOL),
202
+ ("rcPaint", wintypes.RECT), ("fRestore", wintypes.BOOL),
203
+ ("fIncUpdate", wintypes.BOOL), ("rgbReserved", ctypes.c_byte * 32)
204
+ ]
205
+
206
+ rgb = COLOR[0] | (COLOR[1] << 8) | (COLOR[2] << 16)
207
+ brush = gdi32.CreateSolidBrush(rgb)
208
+ dark_brush = gdi32.CreateSolidBrush(0x00251C12)
209
+
210
+ @WNDPROC
211
+ def window_proc(hwnd, message, wparam, lparam):
212
+ if message == WM_PAINT:
213
+ paint = PAINTSTRUCT()
214
+ dc = user32.BeginPaint(hwnd, ctypes.byref(paint))
215
+ rect = wintypes.RECT()
216
+ user32.GetClientRect(hwnd, ctypes.byref(rect))
217
+ user32.FillRect(dc, ctypes.byref(rect), brush)
218
+ label = wintypes.RECT(rect.right - 130, 8, rect.right - 12, 34)
219
+ user32.FillRect(dc, ctypes.byref(label), dark_brush)
220
+ gdi32.SetBkMode(dc, TRANSPARENT)
221
+ gdi32.SetTextColor(dc, rgb)
222
+ user32.DrawTextW(dc, LABEL, -1, ctypes.byref(label), DT_CENTER | DT_VCENTER | DT_SINGLELINE)
223
+ user32.EndPaint(hwnd, ctypes.byref(paint))
224
+ return 0
225
+ if message == WM_NCHITTEST:
226
+ return HTTRANSPARENT
227
+ if message == WM_DESTROY:
228
+ user32.PostQuitMessage(0)
229
+ return 0
230
+ return user32.DefWindowProcW(hwnd, message, wparam, lparam)
231
+
232
+ try:
233
+ user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
234
+ except Exception:
235
+ try:
236
+ user32.SetProcessDPIAware()
237
+ except Exception:
238
+ pass
239
+ instance = ctypes.windll.kernel32.GetModuleHandleW(None)
240
+ class_name = "MyAgentRoamComputerUseIndicator"
241
+ window_class = WNDCLASS(0, window_proc, 0, 0, instance, None, None, None, None, class_name)
242
+ user32.RegisterClassW(ctypes.byref(window_class))
243
+ width = user32.GetSystemMetrics(0)
244
+ height = user32.GetSystemMetrics(1)
245
+ hwnd = user32.CreateWindowExW(
246
+ WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
247
+ class_name, "", WS_POPUP, 0, 0, width, height, None, None, instance, None
248
+ )
249
+ if not hwnd:
250
+ raise RuntimeError("cannot create the control indicator")
251
+ user32.SetLayeredWindowAttributes(hwnd, 0, 220, 0x00000002)
252
+ region = gdi32.CreateRectRgn(0, 0, width, THICKNESS)
253
+ for x, y, right, bottom in (
254
+ (0, height - THICKNESS, width, height),
255
+ (0, 0, THICKNESS, height),
256
+ (width - THICKNESS, 0, width, height),
257
+ (width - 142, 6, width - 8, 38),
258
+ ):
259
+ part = gdi32.CreateRectRgn(x, y, right, bottom)
260
+ gdi32.CombineRgn(region, region, part, RGN_OR)
261
+ gdi32.DeleteObject(part)
262
+ user32.SetWindowRgn(hwnd, region, True)
263
+ user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, width, height, SWP_NOACTIVATE)
264
+ user32.ShowWindow(hwnd, SW_SHOWNA)
265
+ user32.UpdateWindow(hwnd)
266
+ ready()
267
+ message = wintypes.MSG()
268
+ while user32.GetMessageW(ctypes.byref(message), None, 0, 0) > 0:
269
+ user32.TranslateMessage(ctypes.byref(message))
270
+ user32.DispatchMessageW(ctypes.byref(message))
271
+
272
+ elif sys.platform == "darwin":
273
+ from AppKit import (
274
+ NSApplication, NSApplicationActivationPolicyAccessory, NSBezierPath, NSColor,
275
+ NSFont, NSFontAttributeName, NSForegroundColorAttributeName, NSMakeRect, NSPanel,
276
+ NSScreen, NSStatusWindowLevel, NSView, NSWindowCollectionBehaviorCanJoinAllSpaces,
277
+ NSWindowCollectionBehaviorFullScreenAuxiliary, NSWindowStyleMaskBorderless,
278
+ NSWindowStyleMaskNonactivatingPanel
279
+ )
280
+ from Foundation import NSDictionary, NSString
281
+
282
+ class IndicatorView(NSView):
283
+ def drawRect_(self, rect):
284
+ NSColor.clearColor().set()
285
+ NSBezierPath.fillRect_(rect)
286
+ color = NSColor.colorWithCalibratedRed_green_blue_alpha_(
287
+ COLOR[0] / 255, COLOR[1] / 255, COLOR[2] / 255, 0.88
288
+ )
289
+ color.setStroke()
290
+ path = NSBezierPath.bezierPathWithRect_(NSMakeRect(1.5, 1.5, rect.size.width - 3, rect.size.height - 3))
291
+ path.setLineWidth_(THICKNESS)
292
+ path.stroke()
293
+ attributes = NSDictionary.dictionaryWithObjects_forKeys_(
294
+ [NSFont.boldSystemFontOfSize_(12), color],
295
+ [NSFontAttributeName, NSForegroundColorAttributeName]
296
+ )
297
+ NSString.stringWithString_(LABEL).drawAtPoint_withAttributes_(
298
+ (rect.size.width - 94, rect.size.height - 27), attributes
299
+ )
300
+
301
+ application = NSApplication.sharedApplication()
302
+ application.setActivationPolicy_(NSApplicationActivationPolicyAccessory)
303
+ screen = NSScreen.mainScreen()
304
+ frame = screen.frame()
305
+ panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
306
+ frame, NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel, 2, False
307
+ )
308
+ panel.setOpaque_(False)
309
+ panel.setBackgroundColor_(NSColor.clearColor())
310
+ panel.setHasShadow_(False)
311
+ panel.setIgnoresMouseEvents_(True)
312
+ panel.setLevel_(NSStatusWindowLevel)
313
+ panel.setCollectionBehavior_(
314
+ NSWindowCollectionBehaviorCanJoinAllSpaces |
315
+ NSWindowCollectionBehaviorFullScreenAuxiliary
316
+ )
317
+ content_frame = NSMakeRect(0, 0, frame.size.width, frame.size.height)
318
+ panel.setContentView_(IndicatorView.alloc().initWithFrame_(content_frame))
319
+ panel.orderFrontRegardless()
320
+ panel.displayIfNeeded()
321
+ ready()
322
+ application.run()
323
+
324
+ else:
325
+ import ctypes
326
+ import ctypes.util
327
+ from Xlib import display as xdisplay
328
+
329
+ x11_path = ctypes.util.find_library("X11")
330
+ xext_path = ctypes.util.find_library("Xext")
331
+ if not x11_path or not xext_path:
332
+ raise RuntimeError("the X11 indicator libraries are unavailable")
333
+ x11 = ctypes.cdll.LoadLibrary(x11_path)
334
+ xext = ctypes.cdll.LoadLibrary(xext_path)
335
+ x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
336
+ x11.XOpenDisplay.restype = ctypes.c_void_p
337
+ x11.XDefaultScreen.argtypes = [ctypes.c_void_p]
338
+ x11.XDefaultScreen.restype = ctypes.c_int
339
+ x11.XRootWindow.argtypes = [ctypes.c_void_p, ctypes.c_int]
340
+ x11.XRootWindow.restype = ctypes.c_ulong
341
+ x11.XDefaultColormap.argtypes = [ctypes.c_void_p, ctypes.c_int]
342
+ x11.XDefaultColormap.restype = ctypes.c_ulong
343
+ x11.XDisplayWidth.argtypes = [ctypes.c_void_p, ctypes.c_int]
344
+ x11.XDisplayWidth.restype = ctypes.c_int
345
+ x11.XDisplayHeight.argtypes = [ctypes.c_void_p, ctypes.c_int]
346
+ x11.XDisplayHeight.restype = ctypes.c_int
347
+ x11.XCreateSimpleWindow.argtypes = [
348
+ ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int, ctypes.c_int,
349
+ ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_ulong, ctypes.c_ulong
350
+ ]
351
+ x11.XCreateSimpleWindow.restype = ctypes.c_ulong
352
+ x11.XCreateGC.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p]
353
+ x11.XCreateGC.restype = ctypes.c_void_p
354
+ x11.XAllocColor.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_void_p]
355
+ x11.XChangeWindowAttributes.argtypes = [
356
+ ctypes.c_void_p, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p
357
+ ]
358
+ x11.XMapRaised.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
359
+ x11.XSelectInput.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_long]
360
+ x11.XSetForeground.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong]
361
+ x11.XDrawString.argtypes = [
362
+ ctypes.c_void_p, ctypes.c_ulong, ctypes.c_void_p,
363
+ ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int
364
+ ]
365
+ x11.XFlush.argtypes = [ctypes.c_void_p]
366
+ x11.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int]
367
+ x11.XNextEvent.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
368
+ xext.XShapeQueryExtension.argtypes = [
369
+ ctypes.c_void_p, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)
370
+ ]
371
+ xext.XShapeCombineRectangles.argtypes = [
372
+ ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int, ctypes.c_int, ctypes.c_int,
373
+ ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int
374
+ ]
375
+
376
+ class XColor(ctypes.Structure):
377
+ _fields_ = [
378
+ ("pixel", ctypes.c_ulong), ("red", ctypes.c_ushort),
379
+ ("green", ctypes.c_ushort), ("blue", ctypes.c_ushort),
380
+ ("flags", ctypes.c_byte), ("pad", ctypes.c_byte)
381
+ ]
382
+
383
+ class XEvent(ctypes.Structure):
384
+ _fields_ = [("type", ctypes.c_int), ("pad", ctypes.c_long * 23)]
385
+
386
+ class XSetWindowAttributes(ctypes.Structure):
387
+ _fields_ = [
388
+ ("background_pixmap", ctypes.c_ulong), ("background_pixel", ctypes.c_ulong),
389
+ ("border_pixmap", ctypes.c_ulong), ("border_pixel", ctypes.c_ulong),
390
+ ("bit_gravity", ctypes.c_int), ("win_gravity", ctypes.c_int),
391
+ ("backing_store", ctypes.c_int), ("backing_planes", ctypes.c_ulong),
392
+ ("backing_pixel", ctypes.c_ulong), ("save_under", ctypes.c_int),
393
+ ("event_mask", ctypes.c_long), ("do_not_propagate_mask", ctypes.c_long),
394
+ ("override_redirect", ctypes.c_int), ("colormap", ctypes.c_ulong),
395
+ ("cursor", ctypes.c_ulong)
396
+ ]
397
+
398
+ connection = x11.XOpenDisplay(None)
399
+ if not connection:
400
+ raise RuntimeError("cannot open the X11 display")
401
+ shape_event = ctypes.c_int()
402
+ shape_error = ctypes.c_int()
403
+ if not xext.XShapeQueryExtension(
404
+ connection, ctypes.byref(shape_event), ctypes.byref(shape_error)
405
+ ):
406
+ raise RuntimeError("the X11 Shape extension is unavailable")
407
+ screen = x11.XDefaultScreen(connection)
408
+ root = x11.XRootWindow(connection, screen)
409
+ colormap = x11.XDefaultColormap(connection, screen)
410
+
411
+ def allocate(red, green, blue):
412
+ color = XColor(0, red * 257, green * 257, blue * 257, 0x07, 0)
413
+ if not x11.XAllocColor(connection, colormap, ctypes.byref(color)):
414
+ raise RuntimeError("cannot allocate the control indicator color")
415
+ return color.pixel
416
+
417
+ orange = allocate(*COLOR)
418
+ dark = allocate(18, 28, 37)
419
+ windows = []
420
+
421
+ def create(x, y, width, height, background):
422
+ window = x11.XCreateSimpleWindow(
423
+ connection, root, x, y, width, height, 0, background, background
424
+ )
425
+ if not window:
426
+ raise RuntimeError("cannot create the X11 control indicator")
427
+ attributes = XSetWindowAttributes()
428
+ attributes.override_redirect = 1
429
+ x11.XChangeWindowAttributes(connection, window, 1 << 9, ctypes.byref(attributes))
430
+ xext.XShapeCombineRectangles(
431
+ connection, window, 2, 0, 0, None, 0, 0, 0
432
+ )
433
+ x11.XMapRaised(connection, window)
434
+ windows.append(window)
435
+ return window
436
+
437
+ origin_x = 0
438
+ origin_y = 0
439
+ width = x11.XDisplayWidth(connection, screen)
440
+ height = x11.XDisplayHeight(connection, screen)
441
+ geometry_connection = xdisplay.Display()
442
+ try:
443
+ try:
444
+ geometry_root = geometry_connection.screen().root
445
+ primary = geometry_root.xrandr_get_output_primary().output
446
+ if primary:
447
+ resources = geometry_root.xrandr_get_screen_resources_current()
448
+ output = geometry_connection.xrandr_get_output_info(
449
+ primary, resources.config_timestamp
450
+ )
451
+ if output.crtc:
452
+ crtc = geometry_connection.xrandr_get_crtc_info(
453
+ output.crtc, resources.config_timestamp
454
+ )
455
+ origin_x, origin_y = crtc.x, crtc.y
456
+ width, height = crtc.width, crtc.height
457
+ except Exception:
458
+ pass
459
+ finally:
460
+ geometry_connection.close()
461
+ create(origin_x, origin_y, width, THICKNESS, orange)
462
+ create(origin_x, origin_y + height - THICKNESS, width, THICKNESS, orange)
463
+ create(origin_x, origin_y, THICKNESS, height, orange)
464
+ create(origin_x + width - THICKNESS, origin_y, THICKNESS, height, orange)
465
+ label = create(origin_x + width - 128, origin_y + 10, 116, 26, dark)
466
+ exposure_mask = 1 << 15
467
+ x11.XSelectInput(connection, label, exposure_mask)
468
+ gc = x11.XCreateGC(connection, label, 0, None)
469
+ x11.XSetForeground(connection, gc, orange)
470
+ label_bytes = LABEL.encode("ascii")
471
+
472
+ def draw_label():
473
+ x11.XDrawString(connection, label, gc, 18, 17, label_bytes, len(label_bytes))
474
+ x11.XFlush(connection)
475
+
476
+ draw_label()
477
+ x11.XSync(connection, False)
478
+ ready()
479
+ event = XEvent()
480
+ while True:
481
+ x11.XNextEvent(connection, ctypes.byref(event))
482
+ if event.type == 12:
483
+ draw_label()
484
+ `;
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { resolve } from 'node:path';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { terminateProcessTree } from '../../process-tree.js';
8
+ import { ComputerUseControlIndicator } from './computer-use-indicator.js';
8
9
  const TOOL_NAME = 'mcp__computer__exec';
9
10
  const DEFAULT_TIMEOUT_MS = 30_000;
10
11
  const MAX_TIMEOUT_MS = 120_000;
@@ -42,6 +43,7 @@ export class PyAutoGuiComputerUseProvider {
42
43
  configuration;
43
44
  #lease;
44
45
  #probe;
46
+ #showControlIndicator;
45
47
  #sessions = new Map();
46
48
  #pendingBinds = new Map();
47
49
  #supportKey;
@@ -51,6 +53,7 @@ export class PyAutoGuiComputerUseProvider {
51
53
  this.configuration = configuration;
52
54
  this.#lease = options.lease ?? new ComputerUseDesktopLeaseService();
53
55
  this.#probe = options.probe ?? probeComputerUse;
56
+ this.#showControlIndicator = options.showControlIndicator ?? ComputerUseControlIndicator.show;
54
57
  }
55
58
  support() {
56
59
  const configuration = this.#configuration();
@@ -85,7 +88,7 @@ export class PyAutoGuiComputerUseProvider {
85
88
  }
86
89
  const entry = retained ?? {
87
90
  runtimeExecutable: configuration.runtimeExecutable,
88
- session: new ComputerUseRunSession(configuration.runtimeExecutable, this.#lease, async () => {
91
+ session: new ComputerUseRunSession(configuration.runtimeExecutable, this.#lease, this.#showControlIndicator, async () => {
89
92
  const current = await this.#probe(configuration);
90
93
  if (!current.available)
91
94
  throw new ComputerUseError(current.reasonCode ?? 'COMPUTER_UNAVAILABLE', 'The interactive desktop is no longer available.');
@@ -201,15 +204,17 @@ export const computerUseToolDefinition = {
201
204
  class ComputerUseRunSession {
202
205
  runtimeExecutable;
203
206
  lease;
207
+ showControlIndicator;
204
208
  validateDesktop;
205
209
  #worker;
206
210
  #observationId;
207
211
  #runtimeGeneration = 1;
208
212
  #runId;
209
213
  #closed = false;
210
- constructor(runtimeExecutable, lease, validateDesktop) {
214
+ constructor(runtimeExecutable, lease, showControlIndicator, validateDesktop) {
211
215
  this.runtimeExecutable = runtimeExecutable;
212
216
  this.lease = lease;
217
+ this.showControlIndicator = showControlIndicator;
213
218
  this.validateDesktop = validateDesktop;
214
219
  }
215
220
  attach(runId) {
@@ -241,37 +246,60 @@ class ComputerUseRunSession {
241
246
  throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use Run resources are detached.');
242
247
  this.lease.acquire(runId);
243
248
  await this.validateDesktop();
244
- const worker = await this.#ensureWorker();
249
+ let indicator;
245
250
  try {
246
- const result = await worker.execute(input.code, input.timeoutMs, signal);
247
- this.#observationId = randomUUID();
248
- return {
249
- content: renderResultContent('SUCCEEDED', this.#observationId, result.logs),
250
- data: {
251
- outcome: 'SUCCEEDED',
252
- observationId: this.#observationId,
253
- runtimeGeneration: this.#runtimeGeneration,
254
- logs: result.logs
255
- },
256
- images: result.images
257
- };
251
+ indicator = await this.showControlIndicator(this.runtimeExecutable);
258
252
  }
259
253
  catch (error) {
260
- if (worker.startedRequest) {
261
- await this.#resetWorker();
262
- this.#observationId = undefined;
263
- const message = error instanceof Error ? error.message : 'Computer Use execution ended unexpectedly.';
254
+ throw new ComputerUseError('COMPUTER_CONTROL_INDICATOR_UNAVAILABLE', 'The desktop control indicator could not be displayed.', { cause: error });
255
+ }
256
+ try {
257
+ const worker = await this.#ensureWorker();
258
+ let executionStarted = false;
259
+ try {
260
+ indicator.assertActive?.();
261
+ executionStarted = true;
262
+ const execution = worker.execute(input.code, input.timeoutMs, signal);
263
+ const result = await (indicator.lost === undefined
264
+ ? execution
265
+ : Promise.race([
266
+ execution,
267
+ indicator.lost.then((error) => {
268
+ throw error;
269
+ })
270
+ ]));
271
+ this.#observationId = randomUUID();
264
272
  return {
265
- content: `OUTCOME_UNKNOWN: ${message} Reobserve before continuing; do not replay automatically.`,
273
+ content: renderResultContent('SUCCEEDED', this.#observationId, result.logs),
266
274
  data: {
267
- outcome: 'OUTCOME_UNKNOWN',
268
- observationId: '',
275
+ outcome: 'SUCCEEDED',
276
+ observationId: this.#observationId,
269
277
  runtimeGeneration: this.#runtimeGeneration,
270
- logs: []
271
- }
278
+ logs: result.logs
279
+ },
280
+ images: result.images
272
281
  };
273
282
  }
274
- throw error;
283
+ catch (error) {
284
+ if (executionStarted && worker.startedRequest) {
285
+ await this.#resetWorker();
286
+ this.#observationId = undefined;
287
+ const message = error instanceof Error ? error.message : 'Computer Use execution ended unexpectedly.';
288
+ return {
289
+ content: `OUTCOME_UNKNOWN: ${message} Reobserve before continuing; do not replay automatically.`,
290
+ data: {
291
+ outcome: 'OUTCOME_UNKNOWN',
292
+ observationId: '',
293
+ runtimeGeneration: this.#runtimeGeneration,
294
+ logs: []
295
+ }
296
+ };
297
+ }
298
+ throw error;
299
+ }
300
+ }
301
+ finally {
302
+ await indicator.close();
275
303
  }
276
304
  }
277
305
  async close() {
@@ -370,10 +398,10 @@ class PythonWorker {
370
398
  return worker;
371
399
  }
372
400
  execute(code, timeoutMs, signal) {
401
+ this.#requestStarted = false;
373
402
  if (this.#closed)
374
403
  return Promise.reject(new ComputerUseError('COMPUTER_WORKER_EXITED', 'Computer Use worker is closed.'));
375
404
  const id = randomUUID();
376
- this.#requestStarted = false;
377
405
  return new Promise((resolve, reject) => {
378
406
  let settled = false;
379
407
  const finish = (callback) => {
@@ -287,6 +287,10 @@ export class ContinuousRunCoordinator {
287
287
  ? undefined
288
288
  : await this.options.goalMcp?.(pending);
289
289
  const scheduledMcp = await this.options.scheduledMcp?.(pending);
290
+ const mailMcp = await this.options.mailMcp?.({
291
+ ...pending,
292
+ runId: executionId
293
+ });
290
294
  if (!this.goalCurrent(pending)) {
291
295
  this.dispatchedExecutions.delete(executionId);
292
296
  this.rejectStaleGoal(executionId);
@@ -312,10 +316,14 @@ export class ContinuousRunCoordinator {
312
316
  collaborationMode: pending.collaborationMode,
313
317
  ...(pending.serviceTier === undefined ? {} : { serviceTier: pending.serviceTier }),
314
318
  secretEnvironment: pending.secretEnvironment,
315
- ...(goalMcp === undefined && scheduledMcp === undefined
319
+ ...(goalMcp === undefined && scheduledMcp === undefined && mailMcp === undefined
316
320
  ? {}
317
321
  : {
318
- mcpSecretEnvironment: { ...goalMcp?.secrets, ...scheduledMcp?.secrets }
322
+ mcpSecretEnvironment: {
323
+ ...goalMcp?.secrets,
324
+ ...scheduledMcp?.secrets,
325
+ ...mailMcp?.secrets
326
+ }
319
327
  }),
320
328
  ...(pending.personalInstructions === undefined
321
329
  ? {}
@@ -329,7 +337,8 @@ export class ContinuousRunCoordinator {
329
337
  mcpInstallations: [
330
338
  ...this.options.mcps(pending.workspaceId),
331
339
  ...(goalMcp === undefined ? [] : [goalMcp.installation]),
332
- ...(scheduledMcp === undefined ? [] : [scheduledMcp.installation])
340
+ ...(scheduledMcp === undefined ? [] : [scheduledMcp.installation]),
341
+ ...(mailMcp === undefined ? [] : [mailMcp.installation])
333
342
  ]
334
343
  });
335
344
  }