@namzu/sdk 48.1.0 → 48.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { resolveProviderCapabilities } from '../../provider/capabilities.js';
3
3
  import { sleep } from '../../utils/backoff.js';
4
4
  import { defineTool } from '../defineTool.js';
5
5
  import { neutralizeEnvelopeDelimiter, wrapUntrusted } from '../untrusted-envelope.js';
6
- import { ScreenshotFrames, assumedDisplay, desktopRectOnImage, pointOnImage, toDisplayPoint, toDisplayRect, toImagePoint, } from './computer-use-coordinates.js';
6
+ import { ScreenshotFrames, assumedDisplay, desktopRectOnImage, pointOnImage, toDisplayPoint, toDisplayRect, toImagePoint, toWindowPoint, } from './computer-use-coordinates.js';
7
7
  import { STANDARD_SCREENSHOT_LIMITS, cropAndFitPng, fitPng, pngSize, } from './computer-use-image.js';
8
8
  export { HIGH_RES_SCREENSHOT_LIMITS, STANDARD_SCREENSHOT_LIMITS, screenshotTargetSize, } from './computer-use-image.js';
9
9
  export const COMPUTER_USE_TOOL_NAME = 'computer_use';
@@ -50,21 +50,36 @@ const mouseClickSchema = z.object({
50
50
  type: z.literal('mouse_click'),
51
51
  at: pointSchema,
52
52
  button: mouseButtonSchema,
53
+ /** Accepted only to return an explicit refusal; pointer foreground is unsafe. */
54
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
53
55
  });
54
56
  const mouseDragSchema = z.object({
55
57
  type: z.literal('mouse_drag'),
56
58
  from: pointSchema,
57
59
  to: pointSchema,
58
60
  button: mouseButtonSchema,
61
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
59
62
  });
60
63
  const scrollSchema = z.object({
61
64
  type: z.literal('scroll'),
62
65
  at: pointSchema,
63
66
  direction: z.enum(['up', 'down', 'left', 'right']),
64
67
  amount: z.number().int().positive(),
68
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
69
+ });
70
+ const typeTextSchema = z.object({
71
+ type: z.literal('type_text'),
72
+ text: z.string(),
73
+ /** Accepted only to refuse an unsafe atomic foreground focus click. */
74
+ at: pointSchema.optional(),
75
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
76
+ });
77
+ const keySchema = z.object({
78
+ type: z.literal('key'),
79
+ keys: z.string(),
80
+ at: pointSchema.optional(),
81
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
65
82
  });
66
- const typeTextSchema = z.object({ type: z.literal('type_text'), text: z.string() });
67
- const keySchema = z.object({ type: z.literal('key'), keys: z.string() });
68
83
  const waitSchema = z.object({ type: z.literal('wait'), ms: z.number().int().nonnegative() });
69
84
  const listWindowsSchema = z.object({ type: z.literal('list_windows') });
70
85
  const focusWindowSchema = z.object({
@@ -107,7 +122,7 @@ const batchItemSchema = z.discriminatedUnion('type', [
107
122
  ]);
108
123
  const withFrame = { screenshot_id: screenshotIdSchema };
109
124
  const actionSchema = z.discriminatedUnion('type', [
110
- z.object({ type: z.literal('screenshot') }),
125
+ z.object({ type: z.literal('screenshot'), window_id: z.string().min(1).optional() }),
111
126
  z.object({ type: z.literal('zoom'), region: regionSchema, ...withFrame }),
112
127
  cursorPositionSchema.extend(withFrame),
113
128
  mouseMoveSchema.extend(withFrame),
@@ -240,8 +255,15 @@ function hostActionAvailable(caps, action) {
240
255
  (action !== 'mouse_click' || caps.mouseClickButtons?.length !== 0) &&
241
256
  (action !== 'mouse_drag' || caps.mouseDragButtons?.length !== 0));
242
257
  }
258
+ function windowCaptureAvailable(host, caps) {
259
+ return (caps.windowCapture === true &&
260
+ typeof host.captureWindow === 'function' &&
261
+ typeof host.executeWindow === 'function');
262
+ }
243
263
  function availableActions(host, caps) {
244
- const screenshot = hostActionAvailable(caps, 'screenshot');
264
+ const displayScreenshot = hostActionAvailable(caps, 'screenshot');
265
+ const window = windowCaptureAvailable(host, caps);
266
+ const screenshot = displayScreenshot || window;
245
267
  const windows = caps.windows === true &&
246
268
  typeof host.listWindows === 'function' &&
247
269
  typeof host.focusWindow === 'function';
@@ -250,9 +272,20 @@ function availableActions(host, caps) {
250
272
  typeof host.uiAct === 'function';
251
273
  const available = ALL_ACTIONS.filter((action) => {
252
274
  switch (action) {
275
+ case 'screenshot':
276
+ return screenshot;
253
277
  case 'zoom':
254
278
  case 'wait':
255
279
  return screenshot;
280
+ case 'mouse_click':
281
+ return hostActionAvailable(caps, action) || (window && caps.mouseClickButtons?.length !== 0);
282
+ case 'mouse_drag':
283
+ return hostActionAvailable(caps, action) || (window && caps.mouseDragButtons?.length !== 0);
284
+ case 'type_text':
285
+ case 'key':
286
+ return hostActionAvailable(caps, action) || window;
287
+ case 'scroll':
288
+ return hostActionAvailable(caps, action) || (window && caps.windowScroll !== false);
256
289
  case 'list_windows':
257
290
  case 'focus_window':
258
291
  return windows;
@@ -269,19 +302,21 @@ function availableActions(host, caps) {
269
302
  available.push('batch');
270
303
  return available;
271
304
  }
272
- function unavailableHostActions(caps) {
305
+ function unavailableHostActions(host, caps) {
273
306
  const unavailable = [];
307
+ const window = windowCaptureAvailable(host, caps);
274
308
  if (!caps.screenshot)
275
- unavailable.push('screenshot');
309
+ unavailable.push(window ? 'display screenshot' : 'screenshot');
276
310
  if (!caps.cursorPosition)
277
311
  unavailable.push('cursor_position');
278
312
  if (!caps.mouse)
279
- unavailable.push('mouse');
313
+ unavailable.push(window ? 'display mouse' : 'mouse');
280
314
  if (!caps.keyboard)
281
- unavailable.push('keyboard');
315
+ unavailable.push(window ? 'display keyboard' : 'keyboard');
282
316
  if (caps.supportedActions) {
317
+ const available = new Set(availableActions(host, caps));
283
318
  for (const action of HOST_ACTIONS) {
284
- if (!hostActionAvailable(caps, action) && !unavailable.includes(action))
319
+ if (!available.has(action) && !unavailable.includes(action))
285
320
  unavailable.push(action);
286
321
  }
287
322
  }
@@ -292,11 +327,15 @@ function unavailableHostActions(caps) {
292
327
  // ---------------------------------------------------------------------------
293
328
  function buildDescription(host, caps, settings) {
294
329
  const available = availableActions(host, caps);
295
- const unavailable = unavailableHostActions(caps);
296
- const lines = [
297
- `Controls the user's desktop on a ${caps.displayServer} host: screenshots, mouse and keyboard, for GUI tasks.`,
298
- `Available actions: ${available.join('; ') || 'none'}.`,
299
- ];
330
+ const unavailable = unavailableHostActions(host, caps);
331
+ const intro = hostActionAvailable(caps, 'screenshot')
332
+ ? `Controls the user's desktop on a ${caps.displayServer} host: screenshots, mouse and keyboard, for GUI tasks.`
333
+ : windowCaptureAvailable(host, caps)
334
+ ? `Controls named windows on a ${caps.displayServer} host: window screenshots, mouse and keyboard, for GUI tasks.`
335
+ : available.length > 0
336
+ ? `Offers the available computer controls on a ${caps.displayServer} host.`
337
+ : `Computer control is unavailable on this ${caps.displayServer} host; this tool reports the limitation.`;
338
+ const lines = [intro, `Available actions: ${available.join('; ') || 'none'}.`];
300
339
  if (unavailable.length > 0) {
301
340
  lines.push(caps.unavailableReason
302
341
  ? `Unavailable on this host: ${unavailable.join(', ')} — ${caps.unavailableReason} Do not retry; tell the user.`
@@ -304,7 +343,7 @@ function buildDescription(host, caps, settings) {
304
343
  }
305
344
  if (!available.includes('screenshot'))
306
345
  return finish(lines, caps, available);
307
- lines.push('Coordinates: every x/y you send is a pixel of the most recent screenshot this tool returned — origin at its top-left, x to the right, y down — never a screen pixel. Each screenshot states its id and size (for example "s3: 1456x819"); stay inside it. The tool maps your coordinates onto the display. To aim at an earlier screenshot, pass its id as screenshot_id.', 'Take a screenshot before your first click.');
346
+ lines.push('Coordinates: every x/y you send is a pixel of the most recent screenshot this tool returned — origin at its top-left, x to the right, y down — never a screen pixel. Each screenshot states its id and size (for example "s3: 1456x819"); stay inside it. Display screenshots map onto the display. To aim at an earlier display screenshot, pass its id as screenshot_id.', 'Take a screenshot before your first click.');
308
347
  if (settings.screenshotAfterActions)
309
348
  lines.push(`After any action that changes something, the tool waits ${settings.settleMs} ms and returns a new screenshot — do not call screenshot again after acting.`);
310
349
  lines.push('zoom returns a closer, sharper view of region {x, y, width, height} of the screenshot; your coordinates still refer to the screenshot, not to the zoomed image.', `wait pauses for ms milliseconds (at most ${settings.maxWaitMs} per call, a batch's waits together) and then shows the screen.`);
@@ -316,9 +355,13 @@ function buildDescription(host, caps, settings) {
316
355
  lines.push('To start a Windows program, a shell command (Start-Process notepad, or cmd.exe /c start calc) is surest when you have a shell tool. The Start menu searches by display names in the system language, and ENTER on a name it does not find opens a web search in the browser.');
317
356
  if (available.includes('list_windows'))
318
357
  lines.push('list_windows names the open windows with their ids; focus_window brings one to the front.');
358
+ if (windowCaptureAvailable(host, caps))
359
+ lines.push(`When the user names an application or window, ${available.includes('list_windows') ? 'use list_windows then screenshot {window_id}' : 'use screenshot {window_id} with a host-provided window id'} to capture pixels for that window. Its x/y are pixels of that window image; clicks, drags, typing and keys target that window. Window pointer input uses background delivery only; if a click or drag is refused, inspect a fresh image before any other action. ${caps.windowScroll === false ? `Window pixel scrolling is unavailable on this host; for a browser page use key PAGE_DOWN or ARROW_DOWN with delivery_mode:"foreground"${hostActionAvailable(caps, 'screenshot') ? ', or deliberately take a display screenshot for display scrolling' : ''}. ` : ''}To type into a browser field, first mouse_click its point in the window image, then type_text with delivery_mode:"foreground" if background text is unavailable. This may briefly bring the verified window to the front. For browser shortcuts such as CTRL+L, use key with delivery_mode:"foreground". A stale or moved window is refused; take a fresh window screenshot. ${hostActionAvailable(caps, 'screenshot') ? 'Plain screenshot still shows the display. ' : 'screenshot requires window_id on this host. '}Browser permission bubbles outside the window may be absent. If the window image is blank or shows another window, do not click it; focus the target and capture it again.`);
319
360
  if (available.includes('ui_snapshot'))
320
361
  lines.push(`ui_snapshot {window_id} reads a window's controls (its accessibility tree) as text, each control you can act on with a ref such as e12; without window_id it reads the window in front. ui_act {ref, action, value} acts on one control: invoke (press a button, open a menu item), set_value (replace a field's text with value), toggle, select, expand, collapse. Prefer ui_act to clicking pixels when the control is in the tree: it does not depend on coordinates or on which window is in front, and a batch of ui_act steps (for example pressing several buttons) runs in one call. Refs are valid only until the next ui_snapshot; take a new one after the window changes.`);
321
- lines.push('Typing and keys go to whichever window has focus: confirm on a screenshot that the right window is in front before type_text or key.');
362
+ lines.push(windowCaptureAvailable(host, caps)
363
+ ? 'After a display screenshot, typing and keys go to whichever window has focus: confirm on that screenshot that the right window is in front before type_text or key. After a named-window screenshot, input targets the captured window.'
364
+ : 'Typing and keys go to whichever window has focus: confirm on a screenshot that the right window is in front before type_text or key.');
322
365
  if (available.includes('list_windows'))
323
366
  lines.push('type_text and key are refused while a terminal window is in front — usually the one running this agent, or the user’s own; bring the window you mean to the front first (focus_window, or click it).');
324
367
  return finish(lines, caps, available);
@@ -360,8 +403,8 @@ const ACTION_REQUIREMENTS = {
360
403
  mouse_click: 'mouse_click needs at (button defaults to left)',
361
404
  mouse_drag: 'mouse_drag needs from and to (button defaults to left)',
362
405
  scroll: 'scroll needs at, direction, and amount',
363
- type_text: 'type_text needs text',
364
- key: 'key needs keys',
406
+ type_text: 'type_text needs text; delivery_mode is optional on a window screenshot',
407
+ key: 'key needs keys; delivery_mode is optional on a window screenshot',
365
408
  wait: 'wait needs ms',
366
409
  list_windows: 'list_windows needs no other fields',
367
410
  focus_window: 'focus_window needs window_id',
@@ -369,6 +412,11 @@ const ACTION_REQUIREMENTS = {
369
412
  ui_act: 'ui_act needs ref and action, and value for set_value',
370
413
  batch: 'batch needs actions',
371
414
  };
415
+ function actionRequirement(action, host, caps) {
416
+ return action === 'screenshot' && windowCaptureAvailable(host, caps)
417
+ ? `screenshot ${hostActionAvailable(caps, 'screenshot') ? 'takes an optional' : 'requires'} window_id to capture pixels for that window`
418
+ : ACTION_REQUIREMENTS[action];
419
+ }
372
420
  function hostModelSchema(host, caps, settings) {
373
421
  // An unavailable host remains a diagnostic tool. Avoid invalid empty enums
374
422
  // on provider wires; every execution is still refused before host access.
@@ -401,13 +449,28 @@ function hostModelSchema(host, caps, settings) {
401
449
  type: 'string',
402
450
  description: 'Key or key chord to press, for example ENTER or CTRL+R.',
403
451
  },
452
+ ...(windowCaptureAvailable(host, caps)
453
+ ? {
454
+ delivery_mode: {
455
+ type: 'string',
456
+ enum: ['background', 'foreground'],
457
+ description: 'For type_text or key on a window screenshot only. Foreground may briefly bring that verified window forward when background input is unavailable. Background is the default.',
458
+ },
459
+ }
460
+ : {}),
404
461
  ms: {
405
462
  type: 'integer',
406
463
  description: `Milliseconds to wait, 0 to ${settings.maxWaitMs}.`,
407
464
  },
408
465
  };
409
- if (present.includes('focus_window') || (!forItems && present.includes('ui_snapshot')))
410
- fields.window_id = { type: 'string', description: 'A window id from list_windows.' };
466
+ if (present.includes('focus_window') ||
467
+ (!forItems &&
468
+ (present.includes('ui_snapshot') ||
469
+ (present.includes('screenshot') && windowCaptureAvailable(host, caps)))))
470
+ fields.window_id = {
471
+ type: 'string',
472
+ description: 'A window id from list_windows or supplied by the host.',
473
+ };
411
474
  if (present.includes('ui_act')) {
412
475
  fields.ref = {
413
476
  type: 'string',
@@ -439,7 +502,7 @@ function hostModelSchema(host, caps, settings) {
439
502
  type: {
440
503
  type: 'string',
441
504
  enum: actions,
442
- description: `Desktop action. ${actions.map((action) => ACTION_REQUIREMENTS[action]).join('; ')}.`,
505
+ description: `Desktop action. ${actions.map((action) => actionRequirement(action, host, caps)).join('; ')}.`,
443
506
  },
444
507
  ...fieldSchemas(false),
445
508
  };
@@ -480,6 +543,9 @@ function hostModelSchema(host, caps, settings) {
480
543
  function pointLabel(point) {
481
544
  return `(${point.x}, ${point.y})`;
482
545
  }
546
+ function deliveryLabel(mode) {
547
+ return mode === 'foreground' ? ' in foreground' : '';
548
+ }
483
549
  function quotedText(value) {
484
550
  const oneLine = value.replace(/\s+/g, ' ');
485
551
  const visible = oneLine.length > 64 ? `${oneLine.slice(0, 63)}…` : oneLine;
@@ -517,7 +583,7 @@ function uiActLabel(input, describe) {
517
583
  function itemLabel(input, describe) {
518
584
  switch (input.type) {
519
585
  case 'screenshot':
520
- return 'Capture screenshot';
586
+ return input.window_id ? `Capture window ${input.window_id}` : 'Capture screenshot';
521
587
  case 'zoom':
522
588
  return `Zoom into ${input.region.width}x${input.region.height} at ${pointLabel(input.region)}`;
523
589
  case 'cursor_position':
@@ -531,9 +597,9 @@ function itemLabel(input, describe) {
531
597
  case 'scroll':
532
598
  return `Scroll ${input.direction} ${input.amount} at ${pointLabel(input.at)}`;
533
599
  case 'type_text':
534
- return `Type ${quotedText(input.text)}`;
600
+ return `Type ${quotedText(input.text)}${deliveryLabel(input.delivery_mode)}`;
535
601
  case 'key':
536
- return `Press ${input.keys}`;
602
+ return `Press ${input.keys}${deliveryLabel(input.delivery_mode)}`;
537
603
  case 'wait':
538
604
  return waitLabel(input.ms);
539
605
  case 'list_windows':
@@ -595,6 +661,10 @@ function isOutcomeUnknown(value, action) {
595
661
  function errorText(error) {
596
662
  return error instanceof Error ? error.message : String(error);
597
663
  }
664
+ /** Driver IDs are normally short opaque tokens; never put arbitrary host text in tool instructions. */
665
+ function visibleWindowId(id) {
666
+ return /^[A-Za-z0-9_.:-]{1,64}$/.test(id) ? id : '[window id]';
667
+ }
598
668
  class StepFailure extends Error {
599
669
  unknown;
600
670
  constructor(message, unknown) {
@@ -641,7 +711,7 @@ function uiElementLine(element, ref, frame) {
641
711
  const actions = (element.actions ?? []).filter((action) => UI_ACTIONS.includes(action));
642
712
  if (actions.length > 0)
643
713
  parts.push(`[${actions.join(', ')}]`);
644
- const at = frame && element.bounds ? centreOnImage(frame, element.bounds) : undefined;
714
+ const at = frame && !frame.window && element.bounds ? centreOnImage(frame, element.bounds) : undefined;
645
715
  if (at)
646
716
  parts.push(`@${pointLabel(at)}`);
647
717
  }
@@ -691,6 +761,7 @@ export function createComputerUseTool(host, options = {}) {
691
761
  windows: false,
692
762
  regionCapture: false,
693
763
  uiTree: false,
764
+ windowCapture: false,
694
765
  unavailableReason: options.unavailableReason,
695
766
  }
696
767
  : host.capabilities;
@@ -702,12 +773,16 @@ export function createComputerUseTool(host, options = {}) {
702
773
  let uiRefs = new Map();
703
774
  let uiSnapshots = 0;
704
775
  let uiRefCount = 0;
776
+ let uiSnapshotWindowId;
777
+ let uiSnapshotFrame;
705
778
  const describeUiRef = (ref) => {
706
779
  const entry = uiRefs.get(ref);
707
780
  return entry ? `${uiElementText(entry.element)} (${ref})` : undefined;
708
781
  };
709
782
  const label = (input) => itemLabel(input, describeUiRef);
710
783
  const refusal = (type) => {
784
+ if (available.has(type))
785
+ return null;
711
786
  const required = requiredCapability(type);
712
787
  const why = caps.unavailableReason
713
788
  ? ` ${caps.unavailableReason} Do not retry; tell the user.`
@@ -718,8 +793,19 @@ export function createComputerUseTool(host, options = {}) {
718
793
  return `computer_use: action "${type}" is not supported on this host.${why}`;
719
794
  return null;
720
795
  };
721
- /** Capture the display, fit it, and number it. */
722
- const capture = async () => {
796
+ /** Capture the display or one explicitly named window, fit it, and number it. */
797
+ const capture = async (windowId) => {
798
+ if (windowId !== undefined) {
799
+ if (!windowCaptureAvailable(host, caps))
800
+ throw new StepFailure('this host cannot capture a named window; no desktop screenshot was taken');
801
+ const shot = await host.captureWindow(windowId);
802
+ const image = await fitPng(shot.data, settings.limits);
803
+ if (image.sourceWidth !== shot.width || image.sourceHeight !== shot.height)
804
+ throw new StepFailure('the window PNG dimensions changed during capture; take a new screenshot');
805
+ return { frame: frames.recordWindow(image, shot), image };
806
+ }
807
+ if (!hostActionAvailable(caps, 'screenshot'))
808
+ throw new StepFailure('this host requires window_id for screenshots; no display screenshot was taken');
723
809
  const result = await host.execute({ type: 'screenshot' });
724
810
  if (result.type !== 'screenshot')
725
811
  throw new Error(`computer_use: the host answered a screenshot with "${result.type}"`);
@@ -733,6 +819,8 @@ export function createComputerUseTool(host, options = {}) {
733
819
  const frame = frames.get(id);
734
820
  if (!frame)
735
821
  throw new StepFailure(`there is no screenshot ${id} (the latest is ${frames.latest()?.id ?? 'none'}); take a screenshot and use its coordinates`);
822
+ if (frame.window && frames.latest() !== frame)
823
+ throw new StepFailure(`window screenshot ${id} is stale; take a new window screenshot before acting`);
736
824
  return frame;
737
825
  }
738
826
  const latest = frames.latest();
@@ -752,20 +840,32 @@ export function createComputerUseTool(host, options = {}) {
752
840
  const mapPoint = (frame, point, field) => {
753
841
  if (!pointOnImage(frame, point))
754
842
  throw new StepFailure(`${field} ${pointLabel(point)} is outside screenshot ${frame.id}, which is ${frame.imageWidth}x${frame.imageHeight} (x 0–${frame.imageWidth - 1}, y 0–${frame.imageHeight - 1}); coordinates are pixels of the screenshot, not of the screen`);
755
- return toDisplayPoint(frame, point);
843
+ return frame.window ? toWindowPoint(frame, point) : toDisplayPoint(frame, point);
756
844
  };
757
- const runHost = async (action) => {
845
+ const runHost = async (action, frame) => {
758
846
  try {
847
+ if (frame?.window) {
848
+ if (action.type === 'screenshot' ||
849
+ action.type === 'cursor_position' ||
850
+ action.type === 'mouse_move')
851
+ throw new StepFailure(`${action.type} has no window-scoped input path; use a display screenshot`);
852
+ if (frames.latest() !== frame)
853
+ throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`);
854
+ await host.executeWindow(frame.window.captureId, action);
855
+ return 'done';
856
+ }
759
857
  const result = await host.execute(action);
760
858
  if (result.type === 'cursor_position') {
761
- const frame = frames.latest();
762
- if (!frame)
859
+ const selected = frame ?? frames.latest();
860
+ if (!selected)
763
861
  return `at display pixel ${pointLabel(result.point)}`;
764
- return `at ${pointLabel(toImagePoint(frame, result.point))} in ${frame.id}`;
862
+ return `at ${pointLabel(toImagePoint(selected, result.point))} in ${selected.id}`;
765
863
  }
766
864
  return 'done';
767
865
  }
768
866
  catch (error) {
867
+ if (error instanceof StepFailure)
868
+ throw error;
769
869
  if (isOutcomeUnknown(error, action.type))
770
870
  throw new StepFailure(error.message, error);
771
871
  throw new StepFailure(errorText(error));
@@ -783,43 +883,82 @@ export function createComputerUseTool(host, options = {}) {
783
883
  run,
784
884
  });
785
885
  switch (item.type) {
786
- case 'cursor_position':
787
- frameFor(frameId);
788
- return step(() => runHost({ type: 'cursor_position' }));
886
+ case 'cursor_position': {
887
+ const frame = frameFor(frameId);
888
+ if (frame.window)
889
+ throw new StepFailure('cursor_position is display-relative; take a display screenshot first');
890
+ return step(() => runHost({ type: 'cursor_position' }, frame));
891
+ }
789
892
  case 'mouse_move': {
790
893
  const frame = frameFor(frameId);
894
+ if (frame.window)
895
+ throw new StepFailure('mouse_move cannot move the OS pointer in window scope; use a click or a display screenshot');
791
896
  const to = mapPoint(frame, item.to, 'to');
792
897
  return step(() => runHost({ type: 'mouse_move', to }));
793
898
  }
794
899
  case 'mouse_click': {
900
+ if (item.delivery_mode)
901
+ throw new StepFailure('window pointer delivery_mode is unavailable; foreground input cannot verify which window is under the cursor');
795
902
  const buttons = caps.mouseClickButtons;
796
903
  if (buttons && !buttons.includes(item.button))
797
904
  throw new StepFailure(`computer_use: action "mouse_click" does not support button "${item.button}" on this host.`);
798
905
  const frame = frameFor(frameId);
799
906
  const at = mapPoint(frame, item.at, 'at');
800
- return step(() => runHost({ type: 'mouse_click', at, button: item.button }));
907
+ return step(() => runHost({ type: 'mouse_click', at, button: item.button }, frame));
801
908
  }
802
909
  case 'mouse_drag': {
910
+ if (item.delivery_mode)
911
+ throw new StepFailure('window pointer delivery_mode is unavailable; foreground input cannot verify which window is under the cursor');
803
912
  const buttons = caps.mouseDragButtons;
804
913
  if (buttons && !buttons.includes(item.button))
805
914
  throw new StepFailure(`computer_use: action "mouse_drag" does not support button "${item.button}" on this host.`);
806
915
  const frame = frameFor(frameId);
807
916
  const from = mapPoint(frame, item.from, 'from');
808
917
  const to = mapPoint(frame, item.to, 'to');
809
- return step(() => runHost({ type: 'mouse_drag', from, to, button: item.button }));
918
+ return step(() => runHost({ type: 'mouse_drag', from, to, button: item.button }, frame));
810
919
  }
811
920
  case 'scroll': {
812
921
  const frame = frameFor(frameId);
922
+ if (item.delivery_mode)
923
+ throw new StepFailure('window pointer delivery_mode is unavailable; foreground scrolling cannot verify which window receives the wheel');
924
+ if (frame.window && caps.windowScroll === false)
925
+ throw new StepFailure('window pixel scrolling is unavailable on this host; use foreground PAGE_DOWN or ARROW_DOWN in this window, or take a deliberate display screenshot before display scrolling');
813
926
  const at = mapPoint(frame, item.at, 'at');
814
- return step(() => runHost({ type: 'scroll', at, direction: item.direction, amount: item.amount }));
927
+ return step(() => runHost({ type: 'scroll', at, direction: item.direction, amount: item.amount }, frame));
815
928
  }
816
- case 'type_text':
929
+ case 'type_text': {
930
+ if (item.at)
931
+ throw new StepFailure('type_text at is unavailable: a foreground focus click cannot verify which window is under the point; click the field first, then type');
817
932
  requireLook();
818
- return step(() => runHost({ type: 'type_text', text: item.text }));
819
- case 'key':
933
+ const frame = frameFor(frameId);
934
+ if (item.delivery_mode && !frame.window)
935
+ throw new StepFailure('type_text delivery_mode requires a window screenshot');
936
+ if (frame.window && TERMINAL_APPS.test(frame.window.window.app))
937
+ throw new StepFailure('computer_use never types into a terminal window');
938
+ return step(() => runHost({
939
+ type: 'type_text',
940
+ text: item.text,
941
+ ...(item.delivery_mode ? { delivery_mode: item.delivery_mode } : {}),
942
+ }, frame));
943
+ }
944
+ case 'key': {
945
+ if (item.at)
946
+ throw new StepFailure('key at is unavailable: a foreground focus click cannot verify which window is under the point; click the field first, then send the key');
820
947
  requireLook();
821
- return step(() => runHost({ type: 'key', keys: item.keys }));
948
+ const frame = frameFor(frameId);
949
+ if (item.delivery_mode && !frame.window)
950
+ throw new StepFailure('key delivery_mode requires a window screenshot; take one with screenshot {window_id}');
951
+ if (frame.window && TERMINAL_APPS.test(frame.window.window.app))
952
+ throw new StepFailure('computer_use never sends keys to a terminal window');
953
+ return step(() => runHost({
954
+ type: 'key',
955
+ keys: item.keys,
956
+ ...(item.delivery_mode ? { delivery_mode: item.delivery_mode } : {}),
957
+ }, frame));
958
+ }
822
959
  case 'wait':
960
+ if (!hostActionAvailable(caps, 'screenshot') && !frames.latest() && !uiSnapshotWindowId)
961
+ throw new StepFailure('this host has no display screenshot; capture or inspect a named window before waiting');
823
962
  return step(async (signal) => {
824
963
  await sleep(item.ms, signal);
825
964
  return 'done';
@@ -877,7 +1016,8 @@ export function createComputerUseTool(host, options = {}) {
877
1016
  uiSnapshots += 1;
878
1017
  const id = `u${uiSnapshots}`;
879
1018
  const refs = new Map();
880
- const frame = frames.latest();
1019
+ const latest = frames.latest();
1020
+ const frame = latest?.window ? undefined : latest;
881
1021
  const lines = [];
882
1022
  let chars = 0;
883
1023
  let shown = 0;
@@ -911,10 +1051,12 @@ export function createComputerUseTool(host, options = {}) {
911
1051
  };
912
1052
  visit(snapshot.root, 0);
913
1053
  uiRefs = refs;
1054
+ uiSnapshotWindowId = snapshot.windowId ?? input.window_id;
1055
+ uiSnapshotFrame = frames.latest();
914
1056
  const window = snapshot.windowId && /^[\w.:-]{1,64}$/.test(snapshot.windowId) ? snapshot.windowId : undefined;
915
1057
  const header = [
916
1058
  `UI snapshot ${id}${window ? ` of window ${window}` : ''}: ${shown} controls shown, ${refs.size} with a ref you can pass to ui_act. Refs are valid until the next ui_snapshot.`,
917
- ...(frame
1059
+ ...(frame && !frame.window
918
1060
  ? [
919
1061
  `@(x, y) is a control's centre on screenshot ${frame.id}, for a click when ui_act cannot reach it.`,
920
1062
  ]
@@ -993,7 +1135,8 @@ export function createComputerUseTool(host, options = {}) {
993
1135
  }
994
1136
  if (windows.length === 0)
995
1137
  return 'no windows are open';
996
- const frame = frames.latest();
1138
+ const latest = frames.latest();
1139
+ const frame = latest?.window ? undefined : latest;
997
1140
  const lines = windows.slice(0, MAX_LISTED_WINDOWS).map((window) => {
998
1141
  const where = frame
999
1142
  ? (() => {
@@ -1026,6 +1169,16 @@ export function createComputerUseTool(host, options = {}) {
1026
1169
  }, [...lines, ...more].join('\n'))}`;
1027
1170
  };
1028
1171
  const describeFrame = (frame) => {
1172
+ if (frame.window) {
1173
+ const { window } = frame.window;
1174
+ const focusWarning = window.focused
1175
+ ? ''
1176
+ : ' This window was not first in the host window list when captured. The driver may have used a screen-region fallback containing another window; focus this window and recapture before pixel actions if the image looks covered.';
1177
+ const coverage = frame.window.captureCoverage === 'not_observable_in_window_scope'
1178
+ ? ' Browser permission bubbles outside the window may be absent.'
1179
+ : '';
1180
+ return `Screenshot ${frame.id}: ${frame.imageWidth}x${frame.imageHeight} pixels captured for window ${visibleWindowId(window.id)}. Send coordinates in this image's pixels (x 0–${frame.imageWidth - 1}, y 0–${frame.imageHeight - 1}); actions target this window. Take a new window screenshot if it moves or changes size. If the image is blank or shows another window, do not click it; focus the target and capture it again.${focusWarning}${coverage}`;
1181
+ }
1029
1182
  const { display } = frame;
1030
1183
  const scaled = frame.imageWidth !== display.width || frame.imageHeight !== display.height;
1031
1184
  return scaled
@@ -1036,14 +1189,23 @@ export function createComputerUseTool(host, options = {}) {
1036
1189
  id: frame.id,
1037
1190
  width: frame.imageWidth,
1038
1191
  height: frame.imageHeight,
1039
- display: frame.display,
1192
+ ...(frame.window
1193
+ ? {
1194
+ window: {
1195
+ id: visibleWindowId(frame.window.window.id),
1196
+ pid: frame.window.window.pid,
1197
+ bounds: frame.window.window.bounds,
1198
+ },
1199
+ captureCoverage: frame.window.captureCoverage,
1200
+ }
1201
+ : { display: frame.display }),
1040
1202
  mimeType: 'image/png',
1041
1203
  encoding: 'base64',
1042
1204
  });
1043
1205
  const pin = (frame) => [
1044
1206
  {
1045
1207
  key: 'computer_use.screenshot',
1046
- text: `computer_use coordinates are pixels of screenshot ${frame.id} (${frame.imageWidth}x${frame.imageHeight}), origin top-left.`,
1208
+ text: `computer_use coordinates are pixels of screenshot ${frame.id} (${frame.imageWidth}x${frame.imageHeight}), origin top-left${frame.window ? `, bound to window ${visibleWindowId(frame.window.window.id)}` : ''}.`,
1047
1209
  },
1048
1210
  ];
1049
1211
  const imageBlock = (image) => ({
@@ -1061,7 +1223,9 @@ export function createComputerUseTool(host, options = {}) {
1061
1223
  : [];
1062
1224
  const screenshotResult = (shot) => ({
1063
1225
  success: true,
1064
- output: `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of the ${shot.frame.display.width}x${shot.frame.display.height} display).`,
1226
+ output: shot.frame.window
1227
+ ? `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of window ${visibleWindowId(shot.frame.window.window.id)}).`
1228
+ : `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of the ${shot.frame.display.width}x${shot.frame.display.height} display).`,
1065
1229
  content: [
1066
1230
  { type: 'text', text: [describeFrame(shot.frame), ...firstLookHint(shot.frame)].join('\n') },
1067
1231
  imageBlock(shot.image),
@@ -1082,7 +1246,12 @@ export function createComputerUseTool(host, options = {}) {
1082
1246
  throw new StepFailure(`region ${region.width}x${region.height} at ${pointLabel(region)} is outside screenshot ${frame.id} (${frame.imageWidth}x${frame.imageHeight})`);
1083
1247
  const rect = toDisplayRect(frame, onImage);
1084
1248
  let fitted;
1085
- if (caps.regionCapture === true && typeof host.captureRegion === 'function') {
1249
+ if (frame.window) {
1250
+ if (!frame.window.data)
1251
+ throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`);
1252
+ fitted = await cropAndFitPng(frame.window.data, rect, settings.limits);
1253
+ }
1254
+ else if (caps.regionCapture === true && typeof host.captureRegion === 'function') {
1086
1255
  const piece = await host.captureRegion(rect);
1087
1256
  fitted = await fitPng(piece.data, settings.limits);
1088
1257
  }
@@ -1146,6 +1315,20 @@ export function createComputerUseTool(host, options = {}) {
1146
1315
  // the screenshot in step 5 should not arrive after steps 1–4 changed
1147
1316
  // the desktop.
1148
1317
  const planned = [];
1318
+ const actionFrame = frameId ? frames.get(frameId) : frames.latest();
1319
+ const explicitDisplayFrame = frameId !== undefined && actionFrame !== undefined && !actionFrame.window;
1320
+ if (actionFrame?.window &&
1321
+ items.some((item, index) => item.type === 'focus_window' &&
1322
+ items
1323
+ .slice(index + 1)
1324
+ .some((later) => ['mouse_click', 'mouse_drag', 'scroll', 'type_text', 'key'].includes(later.type))))
1325
+ return {
1326
+ success: false,
1327
+ output: '',
1328
+ error: 'computer_use: a window-scoped batch cannot act after focus_window changes its target; focus first, then take a new window screenshot. Nothing was run.',
1329
+ };
1330
+ let postWindowId = actionFrame?.window?.window.id ??
1331
+ (!hostActionAvailable(caps, 'screenshot') ? uiSnapshotWindowId : undefined);
1149
1332
  for (const [index, item] of items.entries()) {
1150
1333
  try {
1151
1334
  planned.push(plan(item, frameId));
@@ -1176,13 +1359,30 @@ export function createComputerUseTool(host, options = {}) {
1176
1359
  }
1177
1360
  try {
1178
1361
  const keyboard = step.item.type === 'type_text' || step.item.type === 'key';
1179
- if (keyboard && !frontChecked) {
1362
+ if (keyboard && !actionFrame?.window && !frontChecked) {
1180
1363
  await refuseTerminalInFront();
1181
1364
  frontChecked = true;
1182
1365
  }
1183
1366
  if (step.item.type !== 'type_text')
1184
1367
  frontChecked = false;
1185
1368
  const note = await step.run(signal);
1369
+ if (step.item.type === 'focus_window' && windowCaptureAvailable(host, caps))
1370
+ postWindowId = step.item.window_id;
1371
+ else if (step.item.type === 'ui_act' &&
1372
+ uiSnapshotWindowId &&
1373
+ !explicitDisplayFrame &&
1374
+ windowCaptureAvailable(host, caps))
1375
+ postWindowId = uiSnapshotWindowId;
1376
+ else if (step.item.type === 'wait' &&
1377
+ !changed &&
1378
+ uiSnapshotWindowId &&
1379
+ uiSnapshotFrame === frames.latest() &&
1380
+ frameId === undefined &&
1381
+ windowCaptureAvailable(host, caps))
1382
+ postWindowId = uiSnapshotWindowId;
1383
+ else if (actionFrame?.window &&
1384
+ ['mouse_click', 'mouse_drag', 'scroll', 'type_text', 'key'].includes(step.item.type))
1385
+ postWindowId = actionFrame.window.window.id;
1186
1386
  records.push({ label: step.label, status: 'done', note });
1187
1387
  if (step.mutating)
1188
1388
  changed = true;
@@ -1208,6 +1408,7 @@ export function createComputerUseTool(host, options = {}) {
1208
1408
  // and after a wait, whose whole point is to look again.
1209
1409
  const looks = settings.screenshotAfterActions &&
1210
1410
  available.has('screenshot') &&
1411
+ (hostActionAvailable(caps, 'screenshot') || postWindowId !== undefined) &&
1211
1412
  (changed || planned.some((step) => step.item.type === 'wait')) &&
1212
1413
  !signal?.aborted;
1213
1414
  let shot;
@@ -1215,7 +1416,7 @@ export function createComputerUseTool(host, options = {}) {
1215
1416
  if (looks) {
1216
1417
  try {
1217
1418
  await sleep(settings.settleMs, signal);
1218
- shot = await capture();
1419
+ shot = await capture(postWindowId);
1219
1420
  }
1220
1421
  catch (error) {
1221
1422
  shotError = signal?.aborted ? 'cancelled' : errorText(error);
@@ -1298,7 +1499,7 @@ export function createComputerUseTool(host, options = {}) {
1298
1499
  description: buildDescription(host, caps, settings),
1299
1500
  inputSchema: actionSchema,
1300
1501
  modelInputSchema: hostModelSchema(host, caps, settings),
1301
- validationErrorHint: `Action requirements: ${ALL_ACTIONS.map((action) => ACTION_REQUIREMENTS[action]).join('; ')}. A batch is {"type":"batch","actions":[...]} with at most ${settings.maxBatchActions} actions and no screenshot, zoom, ui_snapshot or batch inside.`,
1502
+ validationErrorHint: `Action requirements: ${ALL_ACTIONS.map((action) => actionRequirement(action, host, caps)).join('; ')}. A batch is {"type":"batch","actions":[...]} with at most ${settings.maxBatchActions} actions and no screenshot, zoom, ui_snapshot or batch inside.`,
1302
1503
  category: 'custom',
1303
1504
  permissions: [],
1304
1505
  readOnly: (input) => isReadOnlyInput(input),
@@ -1324,7 +1525,7 @@ export function createComputerUseTool(host, options = {}) {
1324
1525
  try {
1325
1526
  switch (input.type) {
1326
1527
  case 'screenshot':
1327
- return screenshotResult(await capture());
1528
+ return screenshotResult(await capture(input.window_id));
1328
1529
  case 'zoom':
1329
1530
  return await zoom(input);
1330
1531
  case 'ui_snapshot':