@anionex/dsh-computer-use 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +13 -7
  2. package/README.zh.md +13 -7
  3. package/cordis.patch.yml +3 -0
  4. package/docs/interaction-policy.md +6 -4
  5. package/docs/interaction-policy.zh.md +7 -5
  6. package/lib/client.js +31 -11
  7. package/lib/client.js.map +1 -1
  8. package/lib/config.js +13 -3
  9. package/lib/config.js.map +1 -1
  10. package/lib/providers/macos.js +79 -14
  11. package/lib/providers/macos.js.map +1 -1
  12. package/lib/providers/native-helper.js +240 -17
  13. package/lib/providers/native-helper.js.map +1 -1
  14. package/lib/providers/unsupported.js +6 -0
  15. package/lib/providers/unsupported.js.map +1 -1
  16. package/lib/service.js +74 -32
  17. package/lib/service.js.map +1 -1
  18. package/lib/types/backend.d.ts +18 -3
  19. package/lib/types/backend.d.ts.map +1 -1
  20. package/lib/types/client/index.d.ts +7 -2
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/config.d.ts +7 -1
  23. package/lib/types/config.d.ts.map +1 -1
  24. package/lib/types/providers/macos.d.ts +8 -2
  25. package/lib/types/providers/macos.d.ts.map +1 -1
  26. package/lib/types/providers/native-helper.d.ts +9 -1
  27. package/lib/types/providers/native-helper.d.ts.map +1 -1
  28. package/lib/types/providers/unsupported.d.ts +6 -2
  29. package/lib/types/providers/unsupported.d.ts.map +1 -1
  30. package/lib/types/service.d.ts.map +1 -1
  31. package/native/macos/Sources/Fixture/main.swift +46 -0
  32. package/native/macos/Sources/Helper/CursorMotion.swift +67 -0
  33. package/native/macos/Sources/Helper/CursorOverlay.swift +334 -60
  34. package/native/macos/Sources/Helper/TargetedPointer.swift +23 -10
  35. package/native/macos/Sources/Helper/main.swift +80 -5
  36. package/native/macos/Sources/Monitor/main.swift +15 -1
  37. package/native/macos/bin/dsh-computer-use-helper +0 -0
  38. package/native/macos/manifest.json +3 -3
  39. package/package.json +16 -18
  40. package/scripts/build-client.mjs +0 -0
  41. package/scripts/build-native.mjs +0 -0
  42. package/scripts/model-e2e.mjs +0 -0
  43. package/scripts/validate.mjs +0 -0
  44. package/src/backend.ts +18 -3
  45. package/src/client/index.tsx +50 -13
  46. package/src/config.ts +20 -4
  47. package/src/providers/macos.ts +102 -14
  48. package/src/providers/native-helper.ts +234 -17
  49. package/src/providers/unsupported.ts +11 -1
  50. package/src/service.ts +100 -37
package/src/service.ts CHANGED
@@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto'
4
4
  import { setTimeout as delay } from 'node:timers/promises'
5
5
  import { Context, Service } from '@deepseek-ai/cordis'
6
6
  import type { Agent } from '@deepseek-ai/dsh-agent'
7
- import type { BackendCursorAction, BackendHealth, BackendObservation, ComputerUseBackend, CursorVisibility } from './backend.ts'
7
+ import type { BackendActionResult, BackendCursorAction, BackendHealth, BackendObservation, ComputerUseBackend, CursorVisibility } from './backend.ts'
8
8
  import { allocateScreenshotPath, describeScreenshot } from './artifacts.ts'
9
9
  import type { ResolvedComputerUseConfig } from './config.ts'
10
10
  import { ComputerConfirmationManager } from './confirmations.ts'
@@ -124,7 +124,6 @@ function requiresPointerInput(
124
124
  case 'click':
125
125
  if (action.x !== undefined || action.y !== undefined) return true
126
126
  return element !== undefined
127
- && !element.actions.includes('AXPress')
128
127
  && action.allowCoordinateFallback === true
129
128
  case 'scroll':
130
129
  case 'drag': return true
@@ -364,75 +363,139 @@ export class ComputerUseService extends Service {
364
363
  )
365
364
  }
366
365
  await this.leases.ensure(context.agent, stored.backend.app, 'control', `computer_${action.kind}`, context.callId, signal)
366
+ const cursorRequested = this.config.interaction.cursorVisualization === 'visible'
367
+ && (action.kind === 'click' || action.kind === 'scroll' || action.kind === 'drag')
368
+ const observationOptions = {
369
+ screenshot: 'none' as const,
370
+ maxNodes: this.config.maxNodes,
371
+ maxDepth: this.config.maxDepth,
372
+ maxTextBytes: this.config.maxTextBytes,
373
+ }
367
374
  let actionObservation = stored.backend
375
+ let cursorActivation: 'already-frontmost' | 'activated' | undefined
368
376
  let element = selectedOriginalElement
369
377
  let resolution: ComputerTargetResolutionResult | undefined = selectedOriginalElement === undefined
370
378
  ? undefined
371
379
  : { mode: 'exact-locator', confidence: 1, candidateCount: 1, targetChanged: false }
372
380
  if (descriptor !== undefined) {
373
- const fresh = await this.backend.observe(stored.backend.app, {
374
- screenshot: 'none',
375
- maxNodes: this.config.maxNodes,
376
- maxDepth: this.config.maxDepth,
377
- maxTextBytes: this.config.maxTextBytes,
378
- }, signal)
381
+ const fresh = actionObservation === stored.backend
382
+ ? await this.backend.observe(stored.backend.app, observationOptions, signal)
383
+ : actionObservation
379
384
  const resolved = resolveComputerTarget(stored.backend, fresh, descriptor, allowsTargetRebind(action))
380
385
  actionObservation = resolved.observation
381
386
  element = resolved.element
382
387
  resolution = resolved.resolution
383
- if (action.sensitive === true && resolution.targetChanged) {
384
- this.confirmations.invalidate(context.agent, action.confirmationToken)
385
- throw new ComputerUseError(
386
- 'COMPUTER_TARGET_REBIND_REQUIRES_CONFIRMATION',
387
- 'the sensitive target rebound to a fresh element; observe the current UI and request a new one-use confirmation before acting',
388
- )
388
+ }
389
+ if (action.sensitive === true && resolution?.targetChanged === true) {
390
+ this.confirmations.invalidate(context.agent, action.confirmationToken)
391
+ throw new ComputerUseError(
392
+ 'COMPUTER_TARGET_REBIND_REQUIRES_CONFIRMATION',
393
+ 'the sensitive target rebound to a fresh element; observe the current UI and request a new one-use confirmation before acting',
394
+ )
395
+ }
396
+ if (cursorRequested
397
+ && this.config.interaction.focusPolicy === 'activate'
398
+ && requiresPointerInput(action, selectedOriginalElement)
399
+ && !actionObservation.frontmost) {
400
+ const activated = await this.backend.activateForCursor(
401
+ stored.backend.app,
402
+ actionObservation.stateHash,
403
+ observationOptions,
404
+ signal,
405
+ )
406
+ actionObservation = activated.observation
407
+ cursorActivation = activated.activation
408
+ if (descriptor !== undefined) {
409
+ const resolved = resolveComputerTarget(stored.backend, actionObservation, descriptor, allowsTargetRebind(action))
410
+ actionObservation = resolved.observation
411
+ element = resolved.element
412
+ resolution = resolved.resolution
389
413
  }
390
414
  }
391
- this.confirmations.consume(context.agent, stored.backend.app, action)
415
+ if (action.sensitive === true && resolution?.targetChanged === true) {
416
+ this.confirmations.invalidate(context.agent, action.confirmationToken)
417
+ throw new ComputerUseError(
418
+ 'COMPUTER_TARGET_REBIND_REQUIRES_CONFIRMATION',
419
+ 'the sensitive target rebound to a fresh element; observe the current UI and request a new one-use confirmation before acting',
420
+ )
421
+ }
392
422
  const visualization = cursorAction(action, element, actionObservation.window, actionObservation.app)
393
- const cursorRequested = this.config.interaction.cursorVisualization === 'visible'
394
- && (action.kind === 'click' || action.kind === 'scroll' || action.kind === 'drag')
395
423
  let cursorStarted = false
396
- // The overlay is presentation-only and never blocks native input, but its
397
- // least-visible outcome is reported across both phases of the action.
424
+ // Background targets intentionally hide the overlay. Any other pre-action
425
+ // failure would violate move-before-input ordering and therefore fails closed.
398
426
  let cursorState: CursorVisibility | undefined
399
427
  const recordCursor = (next: CursorVisibility): void => {
400
428
  if (cursorState === undefined || (cursorState.visible && !next.visible)) cursorState = next
401
429
  }
402
430
  if (cursorRequested && visualization === undefined) {
403
- recordCursor({
431
+ const unavailable: CursorVisibility = {
404
432
  visible: false,
405
433
  reason: actionObservation.window?.id === undefined
406
434
  ? 'the agent cursor could not be bound because the target window has no stable window id'
407
435
  : 'the agent cursor could not be placed because this action has no observable cursor position',
408
- })
436
+ ...(!actionObservation.frontmost ? { reasonCode: 'target-not-frontmost' as const } : {}),
437
+ }
438
+ recordCursor(unavailable)
439
+ if (actionObservation.frontmost) {
440
+ throw new ComputerUseError(
441
+ 'COMPUTER_PROVIDER_FAILURE',
442
+ `the agent cursor could not complete before the action: ${unavailable.reason}`,
443
+ )
444
+ }
409
445
  } else if (visualization !== undefined && cursorRequested) {
446
+ let beforeCursor: CursorVisibility
410
447
  try {
411
- recordCursor(await this.backend.visualizeCursor(visualization, 'before', signal))
412
- cursorStarted = true
448
+ beforeCursor = await this.backend.visualizeCursor(visualization, 'before', signal)
413
449
  } catch (error) {
414
- recordCursor({
415
- visible: false,
416
- reason: `the agent cursor could not be driven before the action: ${error instanceof Error ? error.message : String(error)}`,
417
- })
450
+ throw computerUseError(error, 'the agent cursor could not be driven before the action')
418
451
  }
452
+ recordCursor(beforeCursor)
453
+ const intentionalBackground = beforeCursor.reasonCode === 'target-not-frontmost'
454
+ && (this.config.interaction.focusPolicy === 'preserve'
455
+ || !requiresPointerInput(action, selectedOriginalElement))
456
+ if (!beforeCursor.visible && !intentionalBackground) {
457
+ throw new ComputerUseError(
458
+ 'COMPUTER_PROVIDER_FAILURE',
459
+ `the agent cursor could not complete before the action: ${beforeCursor.reason ?? 'the overlay is unavailable'}`,
460
+ )
461
+ }
462
+ cursorStarted = beforeCursor.visible
463
+ }
464
+ let outcome: BackendActionResult
465
+ const request = {
466
+ action,
467
+ app: actionObservation.app,
468
+ expectedStateHash: actionObservation.stateHash,
469
+ interaction: this.config.interaction,
470
+ ...(element === undefined ? {} : { element }),
471
+ ...(actionObservation.window === undefined ? {} : { window: actionObservation.window }),
419
472
  }
420
- let outcome
421
473
  try {
422
- outcome = await this.backend.act({
423
- action,
424
- app: actionObservation.app,
425
- expectedStateHash: actionObservation.stateHash,
426
- interaction: this.config.interaction,
427
- ...(element === undefined ? {} : { element }),
428
- ...(actionObservation.window === undefined ? {} : { window: actionObservation.window }),
429
- }, signal)
474
+ this.confirmations.consume(context.agent, stored.backend.app, action)
475
+ if (cursorStarted && visualization?.kind === 'drag') {
476
+ const tracked = await this.backend.actDragWithCursor(
477
+ request,
478
+ visualization as BackendCursorAction & { kind: 'drag' },
479
+ signal,
480
+ )
481
+ recordCursor(tracked.cursor)
482
+ outcome = tracked.action
483
+ } else {
484
+ outcome = await this.backend.act(request, signal)
485
+ }
486
+ if (cursorActivation === 'activated' && outcome.activation !== 'activated') {
487
+ outcome = { ...outcome, activation: 'activated' }
488
+ }
430
489
  } catch (error) {
431
490
  throw computerUseError(error, `Computer Use ${action.kind} failed`)
432
491
  } finally {
433
492
  if (cursorStarted && visualization !== undefined) {
434
493
  try {
435
- recordCursor(await this.backend.visualizeCursor(visualization, 'after', signal))
494
+ recordCursor(await this.backend.visualizeCursor(
495
+ visualization,
496
+ 'after',
497
+ AbortSignal.timeout(1_500),
498
+ ))
436
499
  } catch (error) {
437
500
  recordCursor({
438
501
  visible: false,