@remcp/runtime 0.2.111 → 0.2.112

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.112 — 2026-09-25
4
+
5
+ - Linux EIS absolute pointer motion converts desktop-wide coordinates into the selected libei region before dispatch and keeps the current pointer device when it remains valid, eliminating accepted-but-undelivered absolute moves.
6
+ - Wayland portal drag-and-drop uses one region-safe target transition while pressed, then reasserts the final target before release so `pointerdown → pointermove → pointerup` remains intact.
7
+ - The live computer-use smoke is serialized per user to prevent concurrent desktop-input races; the final ASUS GNOME Wayland run passed 82/82 checks. Runtime tool names and schemas are unchanged.
8
+
3
9
  ## 0.2.111 — 2026-09-25
4
10
 
5
11
  - `read_document` reads `.xlsx` files again while `allowedRoots` confinement is enabled: the reader no longer re-enters its own temp staging path through the allowlist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.111",
3
+ "version": "0.2.112",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1270,22 +1270,16 @@ export async function dragDrop(args = {}) {
1270
1270
  await portalPointerButton(button, true, portalOptions);
1271
1271
  await new Promise(resolve => setTimeout(resolve, clamp(args.hold_ms, 120, 60, 5000)));
1272
1272
 
1273
- // A single absolute jump while the button is held can be coalesced by Mutter/
1274
- // XWayland and never become a target-visible pointermove. Send a short,
1275
- // human-like path so drag-aware toolkits observe movement before button-up.
1276
- const distance = Math.hypot(toX - fromX, toY - fromY);
1277
- const steps = Math.max(4, Math.min(20, Math.ceil(distance / 24)));
1278
- const durationMs = clamp(args.duration_ms, 160, 80, 5000);
1279
- const stepDelay = Math.max(8, Math.round(durationMs / steps));
1280
- for (let step = 1; step <= steps; step += 1) {
1281
- const ratio = step / steps;
1282
- await portalMoveTo(
1283
- fromX + (toX - fromX) * ratio,
1284
- fromY + (toY - fromY) * ratio,
1285
- portalOptions,
1286
- );
1287
- await new Promise(resolve => setTimeout(resolve, stepDelay));
1288
- }
1273
+ // Keep the pressed pointer on a single absolute-motion transition. Repeated
1274
+ // absolute moves can cross EIS region/device contexts and lose the logical
1275
+ // pressed state. The EIS helper converts the desktop target into region-local
1276
+ // coordinates, so one target move is sufficient and preserves buttons=1.
1277
+ await portalMoveTo(toX, toY, portalOptions);
1278
+ await new Promise(resolve => setTimeout(resolve, clamp(args.duration_ms, 160, 80, 5000)));
1279
+ // Reassert the final target immediately before release. On Mutter/XWayland this
1280
+ // mirrors a real pointer-up gesture and ensures the release is delivered as a
1281
+ // pointerup/mouseup at the dragged target instead of only clearing button state.
1282
+ await portalMoveTo(toX, toY, portalOptions);
1289
1283
  await new Promise(resolve => setTimeout(resolve, 40));
1290
1284
  await portalPointerButton(button, false, portalOptions);
1291
1285
  await new Promise(resolve => setTimeout(resolve, 80));
@@ -216,6 +216,8 @@ class EiSender:
216
216
  self.lib.ei_device_has_capability.restype = ctypes.c_bool
217
217
  self.lib.ei_device_get_region_at.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
218
218
  self.lib.ei_device_get_region_at.restype = c_void_p
219
+ self.lib.ei_region_convert_point.argtypes = [c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double)]
220
+ self.lib.ei_region_convert_point.restype = ctypes.c_bool
219
221
  self.lib.ei_device_start_emulating.argtypes = [c_void_p, ctypes.c_uint32]
220
222
  self.lib.ei_device_stop_emulating.argtypes = [c_void_p]
221
223
  self.lib.ei_device_keyboard_key.argtypes = [c_void_p, ctypes.c_uint32, ctypes.c_bool]
@@ -367,17 +369,46 @@ class EiSender:
367
369
  return state["ptr"]
368
370
  raise RuntimeError(f"no resumed EIS device with capability {cap}")
369
371
 
372
+ def _absolute_motion_for_device(self, device, x, y):
373
+ if not self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
374
+ return None
375
+ region = self.lib.ei_device_get_region_at(device, float(x), float(y))
376
+ if not region:
377
+ return None
378
+ motion_x = ctypes.c_double(float(x))
379
+ motion_y = ctypes.c_double(float(y))
380
+ if not self.lib.ei_region_convert_point(region, ctypes.byref(motion_x), ctypes.byref(motion_y)):
381
+ raise RuntimeError(f"failed to convert desktop coordinate ({x}, {y}) into the selected EIS region")
382
+ return motion_x.value, motion_y.value
383
+
370
384
  def device_for_absolute(self, x, y):
371
385
  candidates = []
386
+
387
+ # Preserve pointer identity across absolute motion whenever the current
388
+ # device still covers the destination. This is required for pointer
389
+ # capture during drags: switching EIS devices mid-gesture can drop the
390
+ # captured pointer before its button release is observed.
391
+ preferred = self.devices.get(self.pointer_device_key)
392
+ if preferred and preferred["resumed"] and preferred["emulating"]:
393
+ device = preferred["ptr"]
394
+ if self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
395
+ candidates.append(device)
396
+ converted = self._absolute_motion_for_device(device, x, y)
397
+ if converted is not None:
398
+ return device, converted[0], converted[1]
399
+
372
400
  for state in self.devices.values():
373
401
  if not state["resumed"] or not state["emulating"]:
374
402
  continue
375
403
  device = state["ptr"]
404
+ if preferred and self._device_key(device) == self.pointer_device_key:
405
+ continue
376
406
  if not self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
377
407
  continue
378
408
  candidates.append(device)
379
- if self.lib.ei_device_get_region_at(device, float(x), float(y)):
380
- return device
409
+ converted = self._absolute_motion_for_device(device, x, y)
410
+ if converted is not None:
411
+ return device, converted[0], converted[1]
381
412
  if candidates:
382
413
  raise RuntimeError(f"no absolute-pointer EIS region contains desktop coordinate ({x}, {y})")
383
414
  raise RuntimeError("no resumed EIS device with absolute-pointer capability")
@@ -424,8 +455,8 @@ class EiSender:
424
455
  elif op == "motion_absolute":
425
456
  x = float(payload["x"])
426
457
  y = float(payload["y"])
427
- device = self.device_for_absolute(x, y)
428
- self.lib.ei_device_pointer_motion_absolute(device, x, y)
458
+ device, motion_x, motion_y = self.device_for_absolute(x, y)
459
+ self.lib.ei_device_pointer_motion_absolute(device, motion_x, motion_y)
429
460
  self.remember_pointer(device, (x, y))
430
461
  self.frame(device)
431
462
  elif op == "button":