@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.
@@ -13,6 +13,7 @@ import type {
13
13
  UiElementAction,
14
14
  UiSnapshot,
15
15
  WindowInfo,
16
+ WindowInputAction,
16
17
  } from '../../types/computer-use/index.js'
17
18
  import type { ToolResultBlock } from '../../types/message/index.js'
18
19
  import type { LLMProvider } from '../../types/provider/index.js'
@@ -29,6 +30,7 @@ import {
29
30
  toDisplayPoint,
30
31
  toDisplayRect,
31
32
  toImagePoint,
33
+ toWindowPoint,
32
34
  } from './computer-use-coordinates.js'
33
35
  import {
34
36
  type FittedImage,
@@ -136,21 +138,36 @@ const mouseClickSchema = z.object({
136
138
  type: z.literal('mouse_click'),
137
139
  at: pointSchema,
138
140
  button: mouseButtonSchema,
141
+ /** Accepted only to return an explicit refusal; pointer foreground is unsafe. */
142
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
139
143
  })
140
144
  const mouseDragSchema = z.object({
141
145
  type: z.literal('mouse_drag'),
142
146
  from: pointSchema,
143
147
  to: pointSchema,
144
148
  button: mouseButtonSchema,
149
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
145
150
  })
146
151
  const scrollSchema = z.object({
147
152
  type: z.literal('scroll'),
148
153
  at: pointSchema,
149
154
  direction: z.enum(['up', 'down', 'left', 'right']),
150
155
  amount: z.number().int().positive(),
156
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
157
+ })
158
+ const typeTextSchema = z.object({
159
+ type: z.literal('type_text'),
160
+ text: z.string(),
161
+ /** Accepted only to refuse an unsafe atomic foreground focus click. */
162
+ at: pointSchema.optional(),
163
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
164
+ })
165
+ const keySchema = z.object({
166
+ type: z.literal('key'),
167
+ keys: z.string(),
168
+ at: pointSchema.optional(),
169
+ delivery_mode: z.enum(['background', 'foreground']).optional(),
151
170
  })
152
- const typeTextSchema = z.object({ type: z.literal('type_text'), text: z.string() })
153
- const keySchema = z.object({ type: z.literal('key'), keys: z.string() })
154
171
  const waitSchema = z.object({ type: z.literal('wait'), ms: z.number().int().nonnegative() })
155
172
  const listWindowsSchema = z.object({ type: z.literal('list_windows') })
156
173
  const focusWindowSchema = z.object({
@@ -196,7 +213,7 @@ const batchItemSchema = z.discriminatedUnion('type', [
196
213
  const withFrame = { screenshot_id: screenshotIdSchema }
197
214
 
198
215
  const actionSchema = z.discriminatedUnion('type', [
199
- z.object({ type: z.literal('screenshot') }),
216
+ z.object({ type: z.literal('screenshot'), window_id: z.string().min(1).optional() }),
200
217
  z.object({ type: z.literal('zoom'), region: regionSchema, ...withFrame }),
201
218
  cursorPositionSchema.extend(withFrame),
202
219
  mouseMoveSchema.extend(withFrame),
@@ -365,8 +382,18 @@ function hostActionAvailable(
365
382
  )
366
383
  }
367
384
 
385
+ function windowCaptureAvailable(host: ComputerUseHost, caps: ComputerUseCapabilities): boolean {
386
+ return (
387
+ caps.windowCapture === true &&
388
+ typeof host.captureWindow === 'function' &&
389
+ typeof host.executeWindow === 'function'
390
+ )
391
+ }
392
+
368
393
  function availableActions(host: ComputerUseHost, caps: ComputerUseCapabilities): ToolActionType[] {
369
- const screenshot = hostActionAvailable(caps, 'screenshot')
394
+ const displayScreenshot = hostActionAvailable(caps, 'screenshot')
395
+ const window = windowCaptureAvailable(host, caps)
396
+ const screenshot = displayScreenshot || window
370
397
  const windows =
371
398
  caps.windows === true &&
372
399
  typeof host.listWindows === 'function' &&
@@ -377,9 +404,20 @@ function availableActions(host: ComputerUseHost, caps: ComputerUseCapabilities):
377
404
  typeof host.uiAct === 'function'
378
405
  const available = ALL_ACTIONS.filter((action) => {
379
406
  switch (action) {
407
+ case 'screenshot':
408
+ return screenshot
380
409
  case 'zoom':
381
410
  case 'wait':
382
411
  return screenshot
412
+ case 'mouse_click':
413
+ return hostActionAvailable(caps, action) || (window && caps.mouseClickButtons?.length !== 0)
414
+ case 'mouse_drag':
415
+ return hostActionAvailable(caps, action) || (window && caps.mouseDragButtons?.length !== 0)
416
+ case 'type_text':
417
+ case 'key':
418
+ return hostActionAvailable(caps, action) || window
419
+ case 'scroll':
420
+ return hostActionAvailable(caps, action) || (window && caps.windowScroll !== false)
383
421
  case 'list_windows':
384
422
  case 'focus_window':
385
423
  return windows
@@ -396,16 +434,17 @@ function availableActions(host: ComputerUseHost, caps: ComputerUseCapabilities):
396
434
  return available
397
435
  }
398
436
 
399
- function unavailableHostActions(caps: ComputerUseCapabilities): string[] {
437
+ function unavailableHostActions(host: ComputerUseHost, caps: ComputerUseCapabilities): string[] {
400
438
  const unavailable: string[] = []
401
- if (!caps.screenshot) unavailable.push('screenshot')
439
+ const window = windowCaptureAvailable(host, caps)
440
+ if (!caps.screenshot) unavailable.push(window ? 'display screenshot' : 'screenshot')
402
441
  if (!caps.cursorPosition) unavailable.push('cursor_position')
403
- if (!caps.mouse) unavailable.push('mouse')
404
- if (!caps.keyboard) unavailable.push('keyboard')
442
+ if (!caps.mouse) unavailable.push(window ? 'display mouse' : 'mouse')
443
+ if (!caps.keyboard) unavailable.push(window ? 'display keyboard' : 'keyboard')
405
444
  if (caps.supportedActions) {
445
+ const available = new Set(availableActions(host, caps))
406
446
  for (const action of HOST_ACTIONS) {
407
- if (!hostActionAvailable(caps, action) && !unavailable.includes(action))
408
- unavailable.push(action)
447
+ if (!available.has(action) && !unavailable.includes(action)) unavailable.push(action)
409
448
  }
410
449
  }
411
450
  return unavailable
@@ -421,11 +460,15 @@ function buildDescription(
421
460
  settings: ResolvedSettings,
422
461
  ): string {
423
462
  const available = availableActions(host, caps)
424
- const unavailable = unavailableHostActions(caps)
425
- const lines = [
426
- `Controls the user's desktop on a ${caps.displayServer} host: screenshots, mouse and keyboard, for GUI tasks.`,
427
- `Available actions: ${available.join('; ') || 'none'}.`,
428
- ]
463
+ const unavailable = unavailableHostActions(host, caps)
464
+ const intro = hostActionAvailable(caps, 'screenshot')
465
+ ? `Controls the user's desktop on a ${caps.displayServer} host: screenshots, mouse and keyboard, for GUI tasks.`
466
+ : windowCaptureAvailable(host, caps)
467
+ ? `Controls named windows on a ${caps.displayServer} host: window screenshots, mouse and keyboard, for GUI tasks.`
468
+ : available.length > 0
469
+ ? `Offers the available computer controls on a ${caps.displayServer} host.`
470
+ : `Computer control is unavailable on this ${caps.displayServer} host; this tool reports the limitation.`
471
+ const lines = [intro, `Available actions: ${available.join('; ') || 'none'}.`]
429
472
  if (unavailable.length > 0) {
430
473
  lines.push(
431
474
  caps.unavailableReason
@@ -435,7 +478,7 @@ function buildDescription(
435
478
  }
436
479
  if (!available.includes('screenshot')) return finish(lines, caps, available)
437
480
  lines.push(
438
- '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.',
481
+ '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.',
439
482
  'Take a screenshot before your first click.',
440
483
  )
441
484
  if (settings.screenshotAfterActions)
@@ -462,12 +505,18 @@ function buildDescription(
462
505
  lines.push(
463
506
  'list_windows names the open windows with their ids; focus_window brings one to the front.',
464
507
  )
508
+ if (windowCaptureAvailable(host, caps))
509
+ lines.push(
510
+ `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.`,
511
+ )
465
512
  if (available.includes('ui_snapshot'))
466
513
  lines.push(
467
514
  `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.`,
468
515
  )
469
516
  lines.push(
470
- '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.',
517
+ windowCaptureAvailable(host, caps)
518
+ ? '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.'
519
+ : '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.',
471
520
  )
472
521
  if (available.includes('list_windows'))
473
522
  lines.push(
@@ -519,8 +568,8 @@ const ACTION_REQUIREMENTS: Readonly<Record<ToolActionType, string>> = {
519
568
  mouse_click: 'mouse_click needs at (button defaults to left)',
520
569
  mouse_drag: 'mouse_drag needs from and to (button defaults to left)',
521
570
  scroll: 'scroll needs at, direction, and amount',
522
- type_text: 'type_text needs text',
523
- key: 'key needs keys',
571
+ type_text: 'type_text needs text; delivery_mode is optional on a window screenshot',
572
+ key: 'key needs keys; delivery_mode is optional on a window screenshot',
524
573
  wait: 'wait needs ms',
525
574
  list_windows: 'list_windows needs no other fields',
526
575
  focus_window: 'focus_window needs window_id',
@@ -529,6 +578,16 @@ const ACTION_REQUIREMENTS: Readonly<Record<ToolActionType, string>> = {
529
578
  batch: 'batch needs actions',
530
579
  }
531
580
 
581
+ function actionRequirement(
582
+ action: ToolActionType,
583
+ host: ComputerUseHost,
584
+ caps: ComputerUseCapabilities,
585
+ ): string {
586
+ return action === 'screenshot' && windowCaptureAvailable(host, caps)
587
+ ? `screenshot ${hostActionAvailable(caps, 'screenshot') ? 'takes an optional' : 'requires'} window_id to capture pixels for that window`
588
+ : ACTION_REQUIREMENTS[action]
589
+ }
590
+
532
591
  function hostModelSchema(
533
592
  host: ComputerUseHost,
534
593
  caps: ComputerUseCapabilities,
@@ -566,13 +625,31 @@ function hostModelSchema(
566
625
  type: 'string',
567
626
  description: 'Key or key chord to press, for example ENTER or CTRL+R.',
568
627
  },
628
+ ...(windowCaptureAvailable(host, caps)
629
+ ? {
630
+ delivery_mode: {
631
+ type: 'string',
632
+ enum: ['background', 'foreground'],
633
+ description:
634
+ '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.',
635
+ },
636
+ }
637
+ : {}),
569
638
  ms: {
570
639
  type: 'integer',
571
640
  description: `Milliseconds to wait, 0 to ${settings.maxWaitMs}.`,
572
641
  },
573
642
  }
574
- if (present.includes('focus_window') || (!forItems && present.includes('ui_snapshot')))
575
- fields.window_id = { type: 'string', description: 'A window id from list_windows.' }
643
+ if (
644
+ present.includes('focus_window') ||
645
+ (!forItems &&
646
+ (present.includes('ui_snapshot') ||
647
+ (present.includes('screenshot') && windowCaptureAvailable(host, caps))))
648
+ )
649
+ fields.window_id = {
650
+ type: 'string',
651
+ description: 'A window id from list_windows or supplied by the host.',
652
+ }
576
653
  if (present.includes('ui_act')) {
577
654
  fields.ref = {
578
655
  type: 'string',
@@ -605,7 +682,7 @@ function hostModelSchema(
605
682
  type: {
606
683
  type: 'string',
607
684
  enum: actions,
608
- description: `Desktop action. ${actions.map((action) => ACTION_REQUIREMENTS[action]).join('; ')}.`,
685
+ description: `Desktop action. ${actions.map((action) => actionRequirement(action, host, caps)).join('; ')}.`,
609
686
  },
610
687
  ...fieldSchemas(false),
611
688
  }
@@ -650,6 +727,10 @@ function pointLabel(point: { readonly x: number; readonly y: number }): string {
650
727
  return `(${point.x}, ${point.y})`
651
728
  }
652
729
 
730
+ function deliveryLabel(mode: 'background' | 'foreground' | undefined): string {
731
+ return mode === 'foreground' ? ' in foreground' : ''
732
+ }
733
+
653
734
  function quotedText(value: string): string {
654
735
  const oneLine = value.replace(/\s+/g, ' ')
655
736
  const visible = oneLine.length > 64 ? `${oneLine.slice(0, 63)}…` : oneLine
@@ -696,7 +777,7 @@ function itemLabel(
696
777
  ): string {
697
778
  switch (input.type) {
698
779
  case 'screenshot':
699
- return 'Capture screenshot'
780
+ return input.window_id ? `Capture window ${input.window_id}` : 'Capture screenshot'
700
781
  case 'zoom':
701
782
  return `Zoom into ${input.region.width}x${input.region.height} at ${pointLabel(input.region)}`
702
783
  case 'cursor_position':
@@ -710,9 +791,9 @@ function itemLabel(
710
791
  case 'scroll':
711
792
  return `Scroll ${input.direction} ${input.amount} at ${pointLabel(input.at)}`
712
793
  case 'type_text':
713
- return `Type ${quotedText(input.text)}`
794
+ return `Type ${quotedText(input.text)}${deliveryLabel(input.delivery_mode)}`
714
795
  case 'key':
715
- return `Press ${input.keys}`
796
+ return `Press ${input.keys}${deliveryLabel(input.delivery_mode)}`
716
797
  case 'wait':
717
798
  return waitLabel(input.ms)
718
799
  case 'list_windows':
@@ -806,6 +887,11 @@ function errorText(error: unknown): string {
806
887
  return error instanceof Error ? error.message : String(error)
807
888
  }
808
889
 
890
+ /** Driver IDs are normally short opaque tokens; never put arbitrary host text in tool instructions. */
891
+ function visibleWindowId(id: string): string {
892
+ return /^[A-Za-z0-9_.:-]{1,64}$/.test(id) ? id : '[window id]'
893
+ }
894
+
809
895
  /** One planned step: validated, coordinates already mapped, ready to run. */
810
896
  interface PlannedStep {
811
897
  readonly item: BatchItem
@@ -903,7 +989,8 @@ function uiElementLine(
903
989
  (UI_ACTIONS as readonly string[]).includes(action),
904
990
  )
905
991
  if (actions.length > 0) parts.push(`[${actions.join(', ')}]`)
906
- const at = frame && element.bounds ? centreOnImage(frame, element.bounds) : undefined
992
+ const at =
993
+ frame && !frame.window && element.bounds ? centreOnImage(frame, element.bounds) : undefined
907
994
  if (at) parts.push(`@${pointLabel(at)}`)
908
995
  }
909
996
  return neutralizeEnvelopeDelimiter(parts.join(' '))
@@ -956,6 +1043,7 @@ export function createComputerUseTool(
956
1043
  windows: false,
957
1044
  regionCapture: false,
958
1045
  uiTree: false,
1046
+ windowCapture: false,
959
1047
  unavailableReason: options.unavailableReason,
960
1048
  }
961
1049
  : host.capabilities
@@ -968,6 +1056,8 @@ export function createComputerUseTool(
968
1056
  let uiRefs = new Map<string, UiRef>()
969
1057
  let uiSnapshots = 0
970
1058
  let uiRefCount = 0
1059
+ let uiSnapshotWindowId: string | undefined
1060
+ let uiSnapshotFrame: ScreenshotFrame | undefined
971
1061
  const describeUiRef = (ref: string): string | undefined => {
972
1062
  const entry = uiRefs.get(ref)
973
1063
  return entry ? `${uiElementText(entry.element)} (${ref})` : undefined
@@ -975,6 +1065,7 @@ export function createComputerUseTool(
975
1065
  const label = (input: BatchItem | ActionInput): string => itemLabel(input, describeUiRef)
976
1066
 
977
1067
  const refusal = (type: string): string | null => {
1068
+ if (available.has(type)) return null
978
1069
  const required = requiredCapability(type)
979
1070
  const why = caps.unavailableReason
980
1071
  ? ` ${caps.unavailableReason} Do not retry; tell the user.`
@@ -986,8 +1077,27 @@ export function createComputerUseTool(
986
1077
  return null
987
1078
  }
988
1079
 
989
- /** Capture the display, fit it, and number it. */
990
- const capture = async (): Promise<Capture> => {
1080
+ /** Capture the display or one explicitly named window, fit it, and number it. */
1081
+ const capture = async (windowId?: string): Promise<Capture> => {
1082
+ if (windowId !== undefined) {
1083
+ if (!windowCaptureAvailable(host, caps))
1084
+ throw new StepFailure(
1085
+ 'this host cannot capture a named window; no desktop screenshot was taken',
1086
+ )
1087
+ const shot = await (host.captureWindow as NonNullable<ComputerUseHost['captureWindow']>)(
1088
+ windowId,
1089
+ )
1090
+ const image = await fitPng(shot.data, settings.limits)
1091
+ if (image.sourceWidth !== shot.width || image.sourceHeight !== shot.height)
1092
+ throw new StepFailure(
1093
+ 'the window PNG dimensions changed during capture; take a new screenshot',
1094
+ )
1095
+ return { frame: frames.recordWindow(image, shot), image }
1096
+ }
1097
+ if (!hostActionAvailable(caps, 'screenshot'))
1098
+ throw new StepFailure(
1099
+ 'this host requires window_id for screenshots; no display screenshot was taken',
1100
+ )
991
1101
  const result = await host.execute({ type: 'screenshot' })
992
1102
  if (result.type !== 'screenshot')
993
1103
  throw new Error(`computer_use: the host answered a screenshot with "${result.type}"`)
@@ -1004,6 +1114,10 @@ export function createComputerUseTool(
1004
1114
  throw new StepFailure(
1005
1115
  `there is no screenshot ${id} (the latest is ${frames.latest()?.id ?? 'none'}); take a screenshot and use its coordinates`,
1006
1116
  )
1117
+ if (frame.window && frames.latest() !== frame)
1118
+ throw new StepFailure(
1119
+ `window screenshot ${id} is stale; take a new window screenshot before acting`,
1120
+ )
1007
1121
  return frame
1008
1122
  }
1009
1123
  const latest = frames.latest()
@@ -1031,19 +1145,40 @@ export function createComputerUseTool(
1031
1145
  throw new StepFailure(
1032
1146
  `${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`,
1033
1147
  )
1034
- return toDisplayPoint(frame, point)
1148
+ return frame.window ? toWindowPoint(frame, point) : toDisplayPoint(frame, point)
1035
1149
  }
1036
1150
 
1037
- const runHost = async (action: ComputerUseAction): Promise<string> => {
1151
+ const runHost = async (
1152
+ action: ComputerUseAction | WindowInputAction,
1153
+ frame?: ScreenshotFrame,
1154
+ ): Promise<string> => {
1038
1155
  try {
1156
+ if (frame?.window) {
1157
+ if (
1158
+ action.type === 'screenshot' ||
1159
+ action.type === 'cursor_position' ||
1160
+ action.type === 'mouse_move'
1161
+ )
1162
+ throw new StepFailure(
1163
+ `${action.type} has no window-scoped input path; use a display screenshot`,
1164
+ )
1165
+ if (frames.latest() !== frame)
1166
+ throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`)
1167
+ await (host.executeWindow as NonNullable<ComputerUseHost['executeWindow']>)(
1168
+ frame.window.captureId,
1169
+ action as WindowInputAction,
1170
+ )
1171
+ return 'done'
1172
+ }
1039
1173
  const result = await host.execute(action)
1040
1174
  if (result.type === 'cursor_position') {
1041
- const frame = frames.latest()
1042
- if (!frame) return `at display pixel ${pointLabel(result.point)}`
1043
- return `at ${pointLabel(toImagePoint(frame, result.point))} in ${frame.id}`
1175
+ const selected = frame ?? frames.latest()
1176
+ if (!selected) return `at display pixel ${pointLabel(result.point)}`
1177
+ return `at ${pointLabel(toImagePoint(selected, result.point))} in ${selected.id}`
1044
1178
  }
1045
1179
  return 'done'
1046
1180
  } catch (error) {
1181
+ if (error instanceof StepFailure) throw error
1047
1182
  if (isOutcomeUnknown(error, action.type)) throw new StepFailure(error.message, error)
1048
1183
  throw new StepFailure(errorText(error))
1049
1184
  }
@@ -1060,15 +1195,28 @@ export function createComputerUseTool(
1060
1195
  run,
1061
1196
  })
1062
1197
  switch (item.type) {
1063
- case 'cursor_position':
1064
- frameFor(frameId)
1065
- return step(() => runHost({ type: 'cursor_position' }))
1198
+ case 'cursor_position': {
1199
+ const frame = frameFor(frameId)
1200
+ if (frame.window)
1201
+ throw new StepFailure(
1202
+ 'cursor_position is display-relative; take a display screenshot first',
1203
+ )
1204
+ return step(() => runHost({ type: 'cursor_position' }, frame))
1205
+ }
1066
1206
  case 'mouse_move': {
1067
1207
  const frame = frameFor(frameId)
1208
+ if (frame.window)
1209
+ throw new StepFailure(
1210
+ 'mouse_move cannot move the OS pointer in window scope; use a click or a display screenshot',
1211
+ )
1068
1212
  const to = mapPoint(frame, item.to, 'to')
1069
1213
  return step(() => runHost({ type: 'mouse_move', to }))
1070
1214
  }
1071
1215
  case 'mouse_click': {
1216
+ if (item.delivery_mode)
1217
+ throw new StepFailure(
1218
+ 'window pointer delivery_mode is unavailable; foreground input cannot verify which window is under the cursor',
1219
+ )
1072
1220
  const buttons = caps.mouseClickButtons
1073
1221
  if (buttons && !buttons.includes(item.button))
1074
1222
  throw new StepFailure(
@@ -1076,9 +1224,13 @@ export function createComputerUseTool(
1076
1224
  )
1077
1225
  const frame = frameFor(frameId)
1078
1226
  const at = mapPoint(frame, item.at, 'at')
1079
- return step(() => runHost({ type: 'mouse_click', at, button: item.button }))
1227
+ return step(() => runHost({ type: 'mouse_click', at, button: item.button }, frame))
1080
1228
  }
1081
1229
  case 'mouse_drag': {
1230
+ if (item.delivery_mode)
1231
+ throw new StepFailure(
1232
+ 'window pointer delivery_mode is unavailable; foreground input cannot verify which window is under the cursor',
1233
+ )
1082
1234
  const buttons = caps.mouseDragButtons
1083
1235
  if (buttons && !buttons.includes(item.button))
1084
1236
  throw new StepFailure(
@@ -1087,22 +1239,74 @@ export function createComputerUseTool(
1087
1239
  const frame = frameFor(frameId)
1088
1240
  const from = mapPoint(frame, item.from, 'from')
1089
1241
  const to = mapPoint(frame, item.to, 'to')
1090
- return step(() => runHost({ type: 'mouse_drag', from, to, button: item.button }))
1242
+ return step(() => runHost({ type: 'mouse_drag', from, to, button: item.button }, frame))
1091
1243
  }
1092
1244
  case 'scroll': {
1093
1245
  const frame = frameFor(frameId)
1246
+ if (item.delivery_mode)
1247
+ throw new StepFailure(
1248
+ 'window pointer delivery_mode is unavailable; foreground scrolling cannot verify which window receives the wheel',
1249
+ )
1250
+ if (frame.window && caps.windowScroll === false)
1251
+ throw new StepFailure(
1252
+ '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',
1253
+ )
1094
1254
  const at = mapPoint(frame, item.at, 'at')
1095
1255
  return step(() =>
1096
- runHost({ type: 'scroll', at, direction: item.direction, amount: item.amount }),
1256
+ runHost({ type: 'scroll', at, direction: item.direction, amount: item.amount }, frame),
1097
1257
  )
1098
1258
  }
1099
- case 'type_text':
1259
+ case 'type_text': {
1260
+ if (item.at)
1261
+ throw new StepFailure(
1262
+ 'type_text at is unavailable: a foreground focus click cannot verify which window is under the point; click the field first, then type',
1263
+ )
1100
1264
  requireLook()
1101
- return step(() => runHost({ type: 'type_text', text: item.text }))
1102
- case 'key':
1265
+ const frame = frameFor(frameId)
1266
+ if (item.delivery_mode && !frame.window)
1267
+ throw new StepFailure('type_text delivery_mode requires a window screenshot')
1268
+ if (frame.window && TERMINAL_APPS.test(frame.window.window.app))
1269
+ throw new StepFailure('computer_use never types into a terminal window')
1270
+ return step(() =>
1271
+ runHost(
1272
+ {
1273
+ type: 'type_text',
1274
+ text: item.text,
1275
+ ...(item.delivery_mode ? { delivery_mode: item.delivery_mode } : {}),
1276
+ },
1277
+ frame,
1278
+ ),
1279
+ )
1280
+ }
1281
+ case 'key': {
1282
+ if (item.at)
1283
+ throw new StepFailure(
1284
+ 'key at is unavailable: a foreground focus click cannot verify which window is under the point; click the field first, then send the key',
1285
+ )
1103
1286
  requireLook()
1104
- return step(() => runHost({ type: 'key', keys: item.keys }))
1287
+ const frame = frameFor(frameId)
1288
+ if (item.delivery_mode && !frame.window)
1289
+ throw new StepFailure(
1290
+ 'key delivery_mode requires a window screenshot; take one with screenshot {window_id}',
1291
+ )
1292
+ if (frame.window && TERMINAL_APPS.test(frame.window.window.app))
1293
+ throw new StepFailure('computer_use never sends keys to a terminal window')
1294
+ return step(() =>
1295
+ runHost(
1296
+ {
1297
+ type: 'key',
1298
+ keys: item.keys,
1299
+ ...(item.delivery_mode ? { delivery_mode: item.delivery_mode } : {}),
1300
+ },
1301
+ frame,
1302
+ ),
1303
+ )
1304
+ }
1105
1305
  case 'wait':
1306
+ if (!hostActionAvailable(caps, 'screenshot') && !frames.latest() && !uiSnapshotWindowId)
1307
+ throw new StepFailure(
1308
+ 'this host has no display screenshot; capture or inspect a named window before waiting',
1309
+ )
1106
1310
  return step(async (signal) => {
1107
1311
  await sleep(item.ms, signal)
1108
1312
  return 'done'
@@ -1176,7 +1380,8 @@ export function createComputerUseTool(
1176
1380
  uiSnapshots += 1
1177
1381
  const id = `u${uiSnapshots}`
1178
1382
  const refs = new Map<string, UiRef>()
1179
- const frame = frames.latest()
1383
+ const latest = frames.latest()
1384
+ const frame = latest?.window ? undefined : latest
1180
1385
  const lines: string[] = []
1181
1386
  let chars = 0
1182
1387
  let shown = 0
@@ -1208,11 +1413,13 @@ export function createComputerUseTool(
1208
1413
  }
1209
1414
  visit(snapshot.root, 0)
1210
1415
  uiRefs = refs
1416
+ uiSnapshotWindowId = snapshot.windowId ?? input.window_id
1417
+ uiSnapshotFrame = frames.latest()
1211
1418
  const window =
1212
1419
  snapshot.windowId && /^[\w.:-]{1,64}$/.test(snapshot.windowId) ? snapshot.windowId : undefined
1213
1420
  const header = [
1214
1421
  `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.`,
1215
- ...(frame
1422
+ ...(frame && !frame.window
1216
1423
  ? [
1217
1424
  `@(x, y) is a control's centre on screenshot ${frame.id}, for a click when ui_act cannot reach it.`,
1218
1425
  ]
@@ -1298,7 +1505,8 @@ export function createComputerUseTool(
1298
1505
  throw new StepFailure(errorText(error))
1299
1506
  }
1300
1507
  if (windows.length === 0) return 'no windows are open'
1301
- const frame = frames.latest()
1508
+ const latest = frames.latest()
1509
+ const frame = latest?.window ? undefined : latest
1302
1510
  const lines = windows.slice(0, MAX_LISTED_WINDOWS).map((window) => {
1303
1511
  const where = frame
1304
1512
  ? (() => {
@@ -1337,6 +1545,17 @@ export function createComputerUseTool(
1337
1545
  }
1338
1546
 
1339
1547
  const describeFrame = (frame: ScreenshotFrame): string => {
1548
+ if (frame.window) {
1549
+ const { window } = frame.window
1550
+ const focusWarning = window.focused
1551
+ ? ''
1552
+ : ' 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.'
1553
+ const coverage =
1554
+ frame.window.captureCoverage === 'not_observable_in_window_scope'
1555
+ ? ' Browser permission bubbles outside the window may be absent.'
1556
+ : ''
1557
+ 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}`
1558
+ }
1340
1559
  const { display } = frame
1341
1560
  const scaled = frame.imageWidth !== display.width || frame.imageHeight !== display.height
1342
1561
  return scaled
@@ -1348,7 +1567,16 @@ export function createComputerUseTool(
1348
1567
  id: frame.id,
1349
1568
  width: frame.imageWidth,
1350
1569
  height: frame.imageHeight,
1351
- display: frame.display,
1570
+ ...(frame.window
1571
+ ? {
1572
+ window: {
1573
+ id: visibleWindowId(frame.window.window.id),
1574
+ pid: frame.window.window.pid,
1575
+ bounds: frame.window.window.bounds,
1576
+ },
1577
+ captureCoverage: frame.window.captureCoverage,
1578
+ }
1579
+ : { display: frame.display }),
1352
1580
  mimeType: 'image/png' as const,
1353
1581
  encoding: 'base64' as const,
1354
1582
  })
@@ -1356,7 +1584,7 @@ export function createComputerUseTool(
1356
1584
  const pin = (frame: ScreenshotFrame): NonNullable<ToolResult['workingState']> => [
1357
1585
  {
1358
1586
  key: 'computer_use.screenshot',
1359
- text: `computer_use coordinates are pixels of screenshot ${frame.id} (${frame.imageWidth}x${frame.imageHeight}), origin top-left.`,
1587
+ 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)}` : ''}.`,
1360
1588
  },
1361
1589
  ]
1362
1590
 
@@ -1378,7 +1606,9 @@ export function createComputerUseTool(
1378
1606
 
1379
1607
  const screenshotResult = (shot: Capture): ToolResult => ({
1380
1608
  success: true,
1381
- 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).`,
1609
+ output: shot.frame.window
1610
+ ? `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of window ${visibleWindowId(shot.frame.window.window.id)}).`
1611
+ : `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of the ${shot.frame.display.width}x${shot.frame.display.height} display).`,
1382
1612
  content: [
1383
1613
  { type: 'text', text: [describeFrame(shot.frame), ...firstLookHint(shot.frame)].join('\n') },
1384
1614
  imageBlock(shot.image),
@@ -1402,7 +1632,11 @@ export function createComputerUseTool(
1402
1632
  )
1403
1633
  const rect = toDisplayRect(frame, onImage) as Rect
1404
1634
  let fitted: FittedImage
1405
- if (caps.regionCapture === true && typeof host.captureRegion === 'function') {
1635
+ if (frame.window) {
1636
+ if (!frame.window.data)
1637
+ throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`)
1638
+ fitted = await cropAndFitPng(frame.window.data, rect, settings.limits)
1639
+ } else if (caps.regionCapture === true && typeof host.captureRegion === 'function') {
1406
1640
  const piece = await host.captureRegion(rect)
1407
1641
  fitted = await fitPng(piece.data, settings.limits)
1408
1642
  } else {
@@ -1477,6 +1711,30 @@ export function createComputerUseTool(
1477
1711
  // the screenshot in step 5 should not arrive after steps 1–4 changed
1478
1712
  // the desktop.
1479
1713
  const planned: PlannedStep[] = []
1714
+ const actionFrame = frameId ? frames.get(frameId) : frames.latest()
1715
+ const explicitDisplayFrame =
1716
+ frameId !== undefined && actionFrame !== undefined && !actionFrame.window
1717
+ if (
1718
+ actionFrame?.window &&
1719
+ items.some(
1720
+ (item, index) =>
1721
+ item.type === 'focus_window' &&
1722
+ items
1723
+ .slice(index + 1)
1724
+ .some((later) =>
1725
+ ['mouse_click', 'mouse_drag', 'scroll', 'type_text', 'key'].includes(later.type),
1726
+ ),
1727
+ )
1728
+ )
1729
+ return {
1730
+ success: false,
1731
+ output: '',
1732
+ error:
1733
+ '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.',
1734
+ }
1735
+ let postWindowId =
1736
+ actionFrame?.window?.window.id ??
1737
+ (!hostActionAvailable(caps, 'screenshot') ? uiSnapshotWindowId : undefined)
1480
1738
  for (const [index, item] of items.entries()) {
1481
1739
  try {
1482
1740
  planned.push(plan(item, frameId))
@@ -1507,12 +1765,35 @@ export function createComputerUseTool(
1507
1765
  }
1508
1766
  try {
1509
1767
  const keyboard = step.item.type === 'type_text' || step.item.type === 'key'
1510
- if (keyboard && !frontChecked) {
1768
+ if (keyboard && !actionFrame?.window && !frontChecked) {
1511
1769
  await refuseTerminalInFront()
1512
1770
  frontChecked = true
1513
1771
  }
1514
1772
  if (step.item.type !== 'type_text') frontChecked = false
1515
1773
  const note = await step.run(signal)
1774
+ if (step.item.type === 'focus_window' && windowCaptureAvailable(host, caps))
1775
+ postWindowId = step.item.window_id
1776
+ else if (
1777
+ step.item.type === 'ui_act' &&
1778
+ uiSnapshotWindowId &&
1779
+ !explicitDisplayFrame &&
1780
+ windowCaptureAvailable(host, caps)
1781
+ )
1782
+ postWindowId = uiSnapshotWindowId
1783
+ else if (
1784
+ step.item.type === 'wait' &&
1785
+ !changed &&
1786
+ uiSnapshotWindowId &&
1787
+ uiSnapshotFrame === frames.latest() &&
1788
+ frameId === undefined &&
1789
+ windowCaptureAvailable(host, caps)
1790
+ )
1791
+ postWindowId = uiSnapshotWindowId
1792
+ else if (
1793
+ actionFrame?.window &&
1794
+ ['mouse_click', 'mouse_drag', 'scroll', 'type_text', 'key'].includes(step.item.type)
1795
+ )
1796
+ postWindowId = actionFrame.window.window.id
1516
1797
  records.push({ label: step.label, status: 'done', note })
1517
1798
  if (step.mutating) changed = true
1518
1799
  } catch (error) {
@@ -1537,6 +1818,7 @@ export function createComputerUseTool(
1537
1818
  const looks =
1538
1819
  settings.screenshotAfterActions &&
1539
1820
  available.has('screenshot') &&
1821
+ (hostActionAvailable(caps, 'screenshot') || postWindowId !== undefined) &&
1540
1822
  (changed || planned.some((step) => step.item.type === 'wait')) &&
1541
1823
  !signal?.aborted
1542
1824
  let shot: Capture | undefined
@@ -1544,7 +1826,7 @@ export function createComputerUseTool(
1544
1826
  if (looks) {
1545
1827
  try {
1546
1828
  await sleep(settings.settleMs, signal)
1547
- shot = await capture()
1829
+ shot = await capture(postWindowId)
1548
1830
  } catch (error) {
1549
1831
  shotError = signal?.aborted ? 'cancelled' : errorText(error)
1550
1832
  }
@@ -1632,7 +1914,7 @@ export function createComputerUseTool(
1632
1914
  description: buildDescription(host, caps, settings),
1633
1915
  inputSchema: actionSchema,
1634
1916
  modelInputSchema: hostModelSchema(host, caps, settings),
1635
- 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.`,
1917
+ 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.`,
1636
1918
  category: 'custom',
1637
1919
  permissions: [],
1638
1920
  readOnly: (input: ActionInput) => isReadOnlyInput(input),
@@ -1659,7 +1941,7 @@ export function createComputerUseTool(
1659
1941
  try {
1660
1942
  switch (input.type) {
1661
1943
  case 'screenshot':
1662
- return screenshotResult(await capture())
1944
+ return screenshotResult(await capture(input.window_id))
1663
1945
  case 'zoom':
1664
1946
  return await zoom(input)
1665
1947
  case 'ui_snapshot':