Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ jobs:
exit 1
}
Write-Host 'WINDOWS_HELPER_PARSE_OK'
- name: Exercise Windows UIA list materialization
if: runner.os == 'Windows'
shell: powershell
run: |
$output = New-Object System.Collections.Generic.List[object]
$output.Add([pscustomobject]@{ role = 'window'; name = 'fixture' }) | Out-Null
$elements = @($output.ToArray())
if ($elements.Count -ne 1 -or $elements[0].name -ne 'fixture') {
throw 'Windows PowerShell failed to materialize the UIA Generic.List[object]'
}
$helper = Get-Content 'src/desktop/helpers/windows.ps1' -Raw
if ($helper -notmatch 'return\s+\$output\.ToArray\(\)') {
throw 'Windows helper does not use the verified list materialization path'
}
if ($helper -match 'return\s+@\(\$output\)') {
throw 'Windows helper still contains the PowerShell 5.1 incompatible return path'
}
Write-Host 'WINDOWS_UIA_LIST_MATERIALIZATION_OK'
- name: Compile macOS native helper
if: runner.os == 'macOS'
run: /usr/bin/osacompile -l JavaScript -o "$RUNNER_TEMP/deepseekeyes-macos-helper.scpt" src/desktop/helpers/macos.jxa
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.5.9 - 2026-08-19

- Fix Windows PowerShell 5.1 UI Automation observations returning an empty semantic tree by materializing `Generic.List[object]` with `ToArray()` before returning it through the PowerShell pipeline.
- Preserve UIA elements whose virtual/offscreen bounds are non-finite while omitting only those unusable bounds, preventing PowerShell's bare `Infinity` token from corrupting native JSON.
- Add source-level regression coverage and a Windows PowerShell CI smoke test for the exact list-materialization failure reported in Issue #1.
- Require the Windows native window-observation acceptance test to return a real non-empty UI Automation tree.
- Prevent a helper's late asynchronous stdin `EPIPE` from escaping after its authoritative close/result event, removing the cross-platform native-runner CI race exposed while validating this patch.

## 0.5.8 - 2026-08-17

- Make desktop `type` target-bound: require an `elementRef` or complete screenshot coordinates, while retaining an explicit `allowFocusedTarget` compatibility escape hatch.
Expand Down
4 changes: 4 additions & 0 deletions acceptance/desktop-native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ if (target !== undefined) {
assert.ok(windowResult.screen.width > 0)
assert.ok(windowResult.screen.height > 0)
assert.ok(Array.isArray(windowResult.elements))
if (process.platform === 'win32') {
assert.ok(windowResult.elementTotal > 0, 'Windows UI Automation returned no window elements')
assert.ok(windowResult.elements.length > 0, 'Windows UI Automation element tree was not delivered')
}
assert.equal(windowResult.stateDelta.fromStateId, windowSource.stateId)
assert.equal(renderDesktopResult(windowResult).filter(block => block.type === 'image').length, windowResult.screenshot.tileCount)
}
Expand Down
2 changes: 1 addition & 1 deletion lib/client.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dttxorg/deepseekeyes",
"version": "0.5.8",
"version": "0.5.9",
"description": "Auditable vision and cross-platform Computer Use runtime for DeepSeek Harness with source-preserving evidence.",
"type": "module",
"main": "./dsh/index.js",
Expand Down
38 changes: 31 additions & 7 deletions src/desktop/helpers/windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ function Get-AutomationActions($Element) {
return @($actions)
}

function Test-FiniteDouble($Value) {
try {
$number = [double]$Value
return -not [double]::IsNaN($number) -and -not [double]::IsInfinity($number)
} catch { return $false }
}

function Convert-AutomationElement($Element, $Window, [int[]]$Path) {
try {
$current = $Element.Current
Expand All @@ -277,7 +284,7 @@ function Convert-AutomationElement($Element, $Window, [int[]]$Path) {
$value = if ($null -eq $valuePattern -or $current.IsPassword) { $null } else { [string]$valuePattern.Current.Value }
$checked = if ($null -eq $togglePattern) { $null } else { [string]$togglePattern.Current.ToggleState -eq 'On' }
$selected = if ($null -eq $selectionPattern) { $null } else { [bool]$selectionPattern.Current.IsSelected }
return [pscustomobject]@{
$record = [ordered]@{
nativeId = "$($Window.NativeId):$runtime"
windowNativeId = $Window.NativeId
pid = [int]$current.ProcessId
Expand All @@ -295,12 +302,22 @@ function Convert-AutomationElement($Element, $Window, [int[]]$Path) {
editable = $null -ne $valuePattern -and -not [bool]$valuePattern.Current.IsReadOnly
selected = $selected
checked = $checked
x = [double]$rectangle.X
y = [double]$rectangle.Y
width = [double]$rectangle.Width
height = [double]$rectangle.Height
actions = @(Get-AutomationActions $Element)
}
# UIA uses infinite rectangles for some virtual/offscreen controls.
# PowerShell emits Infinity as a bare token, which is not valid JSON.
# Keep the semantic element and omit only its unusable pixel bounds.
$finiteBounds = (Test-FiniteDouble $rectangle.X) `
-and (Test-FiniteDouble $rectangle.Y) `
-and (Test-FiniteDouble $rectangle.Width) `
-and (Test-FiniteDouble $rectangle.Height)
if ($finiteBounds) {
$record.Add('x', [double]$rectangle.X)
$record.Add('y', [double]$rectangle.Y)
$record.Add('width', [double]$rectangle.Width)
$record.Add('height', [double]$rectangle.Height)
}
return [pscustomobject]$record
} catch { return $null }
}

Expand All @@ -325,7 +342,10 @@ function Get-AutomationElements($Window, [int]$Maximum) {
if ($null -eq $root) { return @() }
$output = New-Object System.Collections.Generic.List[object]
Add-AutomationChildren $root $Window @() 0 $Maximum $output ([System.Windows.Automation.TreeWalker]::RawViewWalker)
return @($output)
# Windows PowerShell 5.1 cannot array-wrap Generic.List[object]
# directly (ArgumentException: "Argument types do not match").
# Materialize the list first so callers receive the collected UIA tree.
return $output.ToArray()
} catch { return @() }
}

Expand Down Expand Up @@ -355,7 +375,11 @@ function Resolve-AutomationElement($InputObject) {

function Get-AutomationCenter($Element) {
$rectangle = $Element.Current.BoundingRectangle
if ($rectangle.Width -le 0 -or $rectangle.Height -le 0) { throw 'accessibility element has no clickable bounds' }
$finiteBounds = (Test-FiniteDouble $rectangle.X) `
-and (Test-FiniteDouble $rectangle.Y) `
-and (Test-FiniteDouble $rectangle.Width) `
-and (Test-FiniteDouble $rectangle.Height)
if (-not $finiteBounds -or $rectangle.Width -le 0 -or $rectangle.Height -le 0) { throw 'accessibility element has no clickable bounds' }
$centerX = [int][Math]::Round([double]$rectangle.X + ([double]$rectangle.Width / 2))
$centerY = [int][Math]::Round([double]$rectangle.Y + ([double]$rectangle.Height / 2))
return [int[]]@($centerX, $centerY)
Expand Down
11 changes: 11 additions & 0 deletions src/desktop/native-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ export function runNativeJson(command, args, input, options = {}) {
`native desktop helper failed to start: ${errorMessage(error)}`,
'DESKTOP_HELPER_START_FAILED',
)))
child.stdin.on('error', error => {
// A helper can exit after producing its result while Node is still
// completing the stdin write. Broken-pipe errors are then followed by
// the authoritative child close event and must not escape asynchronously.
if (settled || error?.code === 'EPIPE' || error?.code === 'ERR_STREAM_DESTROYED') return
terminate()
finish(new DeepSeekEyesError(
`native desktop helper input failed: ${errorMessage(error)}`,
'DESKTOP_HELPER_INPUT_FAILED',
))
})
child.stdout.on('data', chunk => {
stdoutBytes += chunk.length
if (stdoutBytes > MAX_OUTPUT_BYTES) {
Expand Down
32 changes: 32 additions & 0 deletions tests/desktop-driver.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import { readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { PassThrough } from 'node:stream'
import test from 'node:test'
import { resolveConfig } from '../src/config.js'
import { NativeDesktopDriver, runNativeJson } from '../src/desktop/index.js'
Expand Down Expand Up @@ -70,6 +72,31 @@ test('native JSON runner keeps input on stdin and validates helper output', asyn
)
})

test('native JSON runner absorbs a late broken pipe after the helper closes', async () => {
const child = new EventEmitter()
child.stdin = new PassThrough()
child.stdout = new PassThrough()
child.stderr = new PassThrough()
child.kill = () => {}
const end = child.stdin.end.bind(child.stdin)
child.stdin.end = (...args) => {
end(...args)
queueMicrotask(() => {
child.stdout.end(JSON.stringify({ ok: true, source: 'fixture' }))
child.emit('close', 0, null)
const error = new Error('write EPIPE')
error.code = 'EPIPE'
child.stdin.emit('error', error)
})
}

const result = await runNativeJson('fixture', [], { action: 'observe' }, {
spawnImpl: () => child,
})
assert.equal(result.source, 'fixture')
await new Promise(resolve => setImmediate(resolve))
})

test('packaged helpers retain both native platforms and avoid the macOS CFRelease crash', async () => {
const mac = await readFile(new URL('../src/desktop/helpers/macos.jxa', import.meta.url), 'utf8')
const windows = await readFile(new URL('../src/desktop/helpers/windows.ps1', import.meta.url), 'utf8')
Expand Down Expand Up @@ -116,6 +143,11 @@ test('packaged helpers retain both native platforms and avoid the macOS CFReleas
assert.match(windows, /SendUnicode/)
assert.match(windows, /System\.Windows\.Automation/)
assert.match(windows, /Get-AutomationElements/)
assert.match(windows, /return \$output\.ToArray\(\)/)
assert.doesNotMatch(windows, /return @\(\$output\)/)
assert.match(windows, /function Test-FiniteDouble\(\$Value\)/)
assert.match(windows, /\$record\.Add\('x', \[double\]\$rectangle\.X\)/)
assert.doesNotMatch(windows, /x = \[double\]\$rectangle\.X/)
assert.match(windows, /\$elementCount = @\(\$elements\)\.Count/)
assert.match(windows, /public static void Scroll/)
assert.match(windows, /\[Console\]::InputEncoding = \$script:DeepSeekEyesUtf8/)
Expand Down
Loading