@remcp/runtime 0.2.102 → 0.2.103
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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/extended/desktop-linux.mjs +91 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.103 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
- Native Wayland `window_action` targets exact PID/title Accessibility windows for `close` and verifies that the requested window disappeared.
|
|
6
|
+
- Shortcut-backed Wayland window actions require verified focus on the exact requested window before minimize, maximize, move, resize, or close proceeds; ambiguous targeting fails closed.
|
|
7
|
+
- Live smoke coverage isolates XDG open/reveal handlers and stabilizes browser input focus without changing the runtime tool surface.
|
|
8
|
+
- Verified on GNOME Wayland with 80 passing computer-use checks and a two-window isolation proof: target A closed while sibling B stayed open.
|
|
9
|
+
|
|
3
10
|
## 0.2.102 — 2026-09-22
|
|
4
11
|
|
|
5
12
|
- `runWithInput` installs a stdin error handler before writing input, so an early child pipe close is returned as a `ToolError` instead of an uncaught `EPIPE` that can terminate the runtime.
|
package/package.json
CHANGED
|
@@ -312,19 +312,100 @@ async function verifyNativeWaylandGeometry(row, expected, label) {
|
|
|
312
312
|
throw new Error(`Could not verify native Wayland window ${label} for ${row.title || row.app || row.id}: wanted ${wanted}; observed ${actual}`);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
async function nativeWaylandAccessibilityAction(row, actionNames) {
|
|
316
|
+
const pid = Number(row?.pid);
|
|
317
|
+
if (!Number.isInteger(pid) || pid <= 0 || !Array.isArray(actionNames) || !actionNames.length) {
|
|
318
|
+
return { applied:false, available:[] };
|
|
319
|
+
}
|
|
320
|
+
const script = pyAtSpiPrelude() + `
|
|
321
|
+
want_pid=${pid};want_window=${JSON.stringify(String(row.title || ''))};wanted_actions=${JSON.stringify(actionNames)}
|
|
322
|
+
target=None
|
|
323
|
+
for root,app_name,pid,active in q:
|
|
324
|
+
if pid!=want_pid: continue
|
|
325
|
+
try:
|
|
326
|
+
for child in root:
|
|
327
|
+
try:
|
|
328
|
+
role=(child.getRoleName() or "").lower();name=child.name or ""
|
|
329
|
+
if role not in ("frame","window","dialog"): continue
|
|
330
|
+
if want_window and want_window.strip().lower()!=name.strip().lower(): continue
|
|
331
|
+
target=child;break
|
|
332
|
+
except: pass
|
|
333
|
+
except: pass
|
|
334
|
+
if target is not None: break
|
|
335
|
+
if target is None:
|
|
336
|
+
print(json.dumps({"applied":False,"available":[],"reason":"target window not found"}))
|
|
337
|
+
else:
|
|
338
|
+
try:
|
|
339
|
+
actions=target.queryAction();available=[];chosen=-1;chosen_name=""
|
|
340
|
+
for i in range(actions.nActions):
|
|
341
|
+
try: action_name=actions.getName(i) or ""
|
|
342
|
+
except: action_name=""
|
|
343
|
+
available.append(action_name)
|
|
344
|
+
if chosen<0 and action_name in wanted_actions:
|
|
345
|
+
chosen=i;chosen_name=action_name
|
|
346
|
+
if chosen<0:
|
|
347
|
+
print(json.dumps({"applied":False,"available":available,"reason":"requested accessibility action unavailable"}))
|
|
348
|
+
else:
|
|
349
|
+
applied=actions.doAction(chosen)
|
|
350
|
+
if applied is False: raise Exception("accessibility window action returned false")
|
|
351
|
+
print(json.dumps({"applied":True,"action":chosen_name,"available":available}))
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
print(json.dumps({"applied":False,"available":[],"reason":str(exc)}))
|
|
354
|
+
`;
|
|
355
|
+
const result = await runFile('python3', ['-c', script], { label:'AT-SPI window action', timeout:2000, allowFailure:true });
|
|
356
|
+
try {
|
|
357
|
+
const payload = JSON.parse(result.stdout || '{}');
|
|
358
|
+
return payload && typeof payload === 'object' ? payload : { applied:false, available:[] };
|
|
359
|
+
} catch {
|
|
360
|
+
return { applied:false, available:[], reason:String(result.stderr || result.stdout || 'AT-SPI window action failed').trim() };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function verifyNativeWaylandWindowClosed(row, timeoutMs = 2500) {
|
|
365
|
+
const deadline = Date.now() + timeoutMs;
|
|
366
|
+
while (Date.now() < deadline) {
|
|
367
|
+
try { await windowMatch({ id:row.id }); }
|
|
368
|
+
catch { return; }
|
|
369
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
370
|
+
}
|
|
371
|
+
throw new Error(`Could not verify native Wayland window close for ${row.title || row.app || row.id}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function focusNativeWaylandForAction(row, backend) {
|
|
375
|
+
await windowAction({ action:'focus', id:row.id, backend });
|
|
376
|
+
const focusState = await targetAccessibilityFocus(row);
|
|
377
|
+
if (!focusState.exact) throw new Error(`Could not verify native Wayland focus for ${row.title || row.app || row.id}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
315
380
|
async function nativeWaylandWindowAction(row, action, args) {
|
|
316
381
|
const backend = inputBackend(args);
|
|
317
382
|
if (backend === 'x11') unavailable('Native Wayland window action', 'use backend=portal for compositor-managed windows');
|
|
318
383
|
if (!waylandPortalCandidate()) unavailable('Native Wayland window action', 'no XDG RemoteDesktop portal is available');
|
|
319
384
|
if (backend === 'auto' && !hasWaylandRemoteDesktopGrant()) portalPermissionHint('Native Wayland window action');
|
|
320
|
-
if (row.ui_id) {
|
|
321
|
-
try { await uiAction({ action:'focus', id:row.ui_id }); } catch {}
|
|
322
|
-
}
|
|
323
385
|
|
|
324
386
|
const portalBackend = backend === 'portal' ? 'portal' : 'auto';
|
|
325
387
|
const timeoutMs = backend === 'portal' ? 120_000 : 2500;
|
|
326
|
-
if (action === 'close')
|
|
327
|
-
|
|
388
|
+
if (action === 'close') {
|
|
389
|
+
const semantic = await nativeWaylandAccessibilityAction(row, ['window.close']);
|
|
390
|
+
if (semantic.applied) {
|
|
391
|
+
await verifyNativeWaylandWindowClosed(row);
|
|
392
|
+
return jsonResult({ action, backend:'atspi-window-action', id:row.id, accessibility_action:semantic.action });
|
|
393
|
+
}
|
|
394
|
+
// Some GTK apps disconnect from AT-SPI while handling window.close, before the action
|
|
395
|
+
// call can return a success payload. Trust the observed target state, not that race.
|
|
396
|
+
try {
|
|
397
|
+
await verifyNativeWaylandWindowClosed(row, 400);
|
|
398
|
+
return jsonResult({ action, backend:'atspi-window-action', id:row.id, accessibility_action:'window.close', verified_after_disconnect:true });
|
|
399
|
+
} catch {}
|
|
400
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
401
|
+
await portalShortcut('ALT+F4', { timeoutMs });
|
|
402
|
+
await verifyNativeWaylandWindowClosed(row);
|
|
403
|
+
return jsonResult({ action, backend:'xdg-desktop-portal', id:row.id });
|
|
404
|
+
}
|
|
405
|
+
if (action === 'minimize') {
|
|
406
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
407
|
+
await portalShortcut('ALT+F9', { timeoutMs });
|
|
408
|
+
}
|
|
328
409
|
else if (action === 'maximize' || action === 'restore') {
|
|
329
410
|
let maximized = false;
|
|
330
411
|
try {
|
|
@@ -346,9 +427,11 @@ async function nativeWaylandWindowAction(row, action, args) {
|
|
|
346
427
|
}
|
|
347
428
|
} catch {}
|
|
348
429
|
if ((action === 'maximize' && !maximized) || (action === 'restore' && maximized)) {
|
|
430
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
349
431
|
await portalShortcut('ALT+F10', { timeoutMs });
|
|
350
432
|
}
|
|
351
433
|
} else {
|
|
434
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
352
435
|
let currentX = Number(row.x), currentY = Number(row.y), currentWidth = Number(row.width), currentHeight = Number(row.height);
|
|
353
436
|
if (![currentX,currentY,currentWidth,currentHeight].every(Number.isFinite)) unavailable('Native Wayland window geometry', 'accessibility bounds are unavailable');
|
|
354
437
|
|
|
@@ -495,6 +578,9 @@ except Exception:
|
|
|
495
578
|
def currentValue(self,v): self.a.set_current_value(float(v))
|
|
496
579
|
class _Action:
|
|
497
580
|
def __init__(self,a): self.a=a
|
|
581
|
+
@property
|
|
582
|
+
def nActions(self): return self.a.get_n_actions()
|
|
583
|
+
def getName(self,i): return self.a.get_action_name(i)
|
|
498
584
|
def doAction(self,i): return self.a.do_action(i)
|
|
499
585
|
class _Editable:
|
|
500
586
|
def __init__(self,a): self.a=a
|