@remcp/runtime 0.2.58 → 0.2.60
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 +10 -0
- package/package.json +1 -1
- package/src/catalog.mjs +18 -18
- package/src/extended/catalog.mjs +8 -8
- package/src/extended/desktop-linux.mjs +11 -2
- package/src/helpers/wayland-eis-helper.py +58 -2
- package/src/wayland-remote-desktop.mjs +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.60 — 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Lockstep branding release with the ReMCP client at `0.2.60`; runtime tool behavior is unchanged from `0.2.59`.
|
|
8
|
+
|
|
9
|
+
## 0.2.59 — 2026-09-20
|
|
10
|
+
|
|
11
|
+
- Tool descriptions now make routing boundaries explicit across overlapping filesystem, search, process, system, desktop, diagnostics, and document operations so AI hosts prefer the narrowest semantic tool instead of broad shell or fallback tools.
|
|
12
|
+
- Wayland absolute pointer delivery uses the EIS helper path and avoids stale cached-position assumptions when falling back, improving deterministic desktop input on GNOME/Wayland.
|
|
13
|
+
- Lockstep patch release with the ReMCP client at `0.2.59`.
|
|
14
|
+
|
|
5
15
|
## 0.2.58 — 2026-09-20
|
|
6
16
|
|
|
7
17
|
- `computer_snapshot` normalizes partial platform-capture failures into schema-stable collections/objects and bounded `{ source, message }` errors instead of leaking error strings into structured output.
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export const toolDefinitions = [
|
|
|
109
109
|
{
|
|
110
110
|
name: 'read_files',
|
|
111
111
|
title: 'Read files by glob',
|
|
112
|
-
description: 'Read files discovered by one glob under a directory, each section prefixed with its path and line count. Use this when the
|
|
112
|
+
description: 'Read the contents of files discovered by one glob under a directory, each section prefixed with its path and line count. Use this when the matched file contents are the result you want. Use start_search instead to find filenames or text matches without reading every matched file. If you already know the exact paths, use read_multiple_files; for one known path use read_file.',
|
|
113
113
|
inputSchema: {
|
|
114
114
|
type: 'object',
|
|
115
115
|
properties: {
|
|
@@ -142,7 +142,7 @@ export const toolDefinitions = [
|
|
|
142
142
|
{
|
|
143
143
|
name: 'read_image',
|
|
144
144
|
title: 'Read image',
|
|
145
|
-
description: 'Return an image file (PNG, JPEG, GIF, WebP, BMP, AVIF, or SVG) as
|
|
145
|
+
description: 'Return an image file (PNG, JPEG, GIF, WebP, BMP, AVIF, or SVG) as native MCP image content so screenshots, photos, and diagrams can be inspected or shown. Use read_binary only for byte-for-byte transfer, not merely to display an image. Fails above the inline size limit.',
|
|
146
146
|
inputSchema: {
|
|
147
147
|
type: 'object',
|
|
148
148
|
properties: {
|
|
@@ -157,7 +157,7 @@ export const toolDefinitions = [
|
|
|
157
157
|
{
|
|
158
158
|
name: 'read_binary',
|
|
159
159
|
title: 'Read binary chunk',
|
|
160
|
-
description: 'Read any file as base64
|
|
160
|
+
description: 'Read any file as base64 chunks for byte-for-byte transfer of binaries, archives, documents, or oversized media. For a normal image that should be inspected or shown, use read_image instead. Returns size, offset, and nextOffsetBytes; call again with offset_bytes set to nextOffsetBytes until complete is true.',
|
|
161
161
|
inputSchema: {
|
|
162
162
|
type: 'object',
|
|
163
163
|
properties: {
|
|
@@ -239,7 +239,7 @@ export const toolDefinitions = [
|
|
|
239
239
|
{
|
|
240
240
|
name: 'write_file',
|
|
241
241
|
title: 'Write file',
|
|
242
|
-
description: 'Create or replace exactly one text file. Parent directories are created automatically; mode "append" adds to the end. Use write_files for two or more independent files, and write_binary for binary bytes.',
|
|
242
|
+
description: 'Create or replace the complete contents of exactly one text file. Parent directories are created automatically; mode "append" adds to the end. For a small exact-block edit use edit_block, for a known line range use replace_lines, and when you already have a unified diff use apply_patch. Use write_files for two or more independent complete files, and write_binary for binary bytes.',
|
|
243
243
|
inputSchema: {
|
|
244
244
|
type: 'object',
|
|
245
245
|
properties: {
|
|
@@ -256,7 +256,7 @@ export const toolDefinitions = [
|
|
|
256
256
|
{
|
|
257
257
|
name: 'write_files',
|
|
258
258
|
title: 'Write multiple files',
|
|
259
|
-
description: 'Create or replace two or more text files in one batch, each with its own path, content, and optional mode. Use this for scaffolding or coherent multi-file writes; for one path use write_file.',
|
|
259
|
+
description: 'Create or replace the complete contents of two or more text files in one batch, each with its own path, content, and optional mode. Use this for scaffolding or coherent multi-file writes; for one path use write_file. For partial edits use edit_block, replace_lines, replace_in_files, or apply_patch instead of rewriting whole files.',
|
|
260
260
|
inputSchema: {
|
|
261
261
|
type: 'object',
|
|
262
262
|
properties: {
|
|
@@ -284,7 +284,7 @@ export const toolDefinitions = [
|
|
|
284
284
|
{
|
|
285
285
|
name: 'apply_patch',
|
|
286
286
|
title: 'Apply patch',
|
|
287
|
-
description: 'Apply
|
|
287
|
+
description: 'Apply an already-prepared unified diff to one file or several files, matching each hunk with a little fuzz so small offsets and whitespace differences still apply. Prefer this when the change is naturally patch-shaped; use edit_block for one exact text block and write_file only when replacing a complete file. Pass dry_run to see the result first.',
|
|
288
288
|
inputSchema: {
|
|
289
289
|
type: 'object',
|
|
290
290
|
properties: {
|
|
@@ -320,7 +320,7 @@ export const toolDefinitions = [
|
|
|
320
320
|
{
|
|
321
321
|
name: 'edit_block',
|
|
322
322
|
title: 'Edit file',
|
|
323
|
-
description: 'Replace
|
|
323
|
+
description: 'Replace one known text block in one file. Provide enough surrounding context to make old_string unique; the call fails unless the number of matches equals expected_replacements. Use write_file for a whole-file replacement and replace_lines for a known line range. When exact text is not found, a whitespace-tolerant match is attempted and reported. Pass dry_run to preview without writing.',
|
|
324
324
|
inputSchema: {
|
|
325
325
|
type: 'object',
|
|
326
326
|
properties: {
|
|
@@ -340,7 +340,7 @@ export const toolDefinitions = [
|
|
|
340
340
|
{
|
|
341
341
|
name: 'replace_lines',
|
|
342
342
|
title: 'Replace lines',
|
|
343
|
-
description: 'Replace an inclusive 1-based line range with new text. The rest of the file, including its line endings, is preserved. Pass dry_run to preview
|
|
343
|
+
description: 'Replace an inclusive 1-based line range with new text. Use this when line numbers define the target; use edit_block when matching existing text and write_file for a complete rewrite. The rest of the file, including its line endings, is preserved. Pass dry_run to preview without writing.',
|
|
344
344
|
inputSchema: {
|
|
345
345
|
type: 'object',
|
|
346
346
|
properties: {
|
|
@@ -359,7 +359,7 @@ export const toolDefinitions = [
|
|
|
359
359
|
{
|
|
360
360
|
name: 'replace_in_files',
|
|
361
361
|
title: 'Replace in files',
|
|
362
|
-
description: '
|
|
362
|
+
description: 'Search and replace the same text or regular expression across multiple text files under a path and report what changed. Use edit_block for one known block in one file and write_file for one complete known file. Applies immediately; pass dry_run true to preview the affected files first.',
|
|
363
363
|
inputSchema: {
|
|
364
364
|
type: 'object',
|
|
365
365
|
properties: {
|
|
@@ -598,7 +598,7 @@ export const toolDefinitions = [
|
|
|
598
598
|
{
|
|
599
599
|
name: 'start_search',
|
|
600
600
|
title: 'Start search',
|
|
601
|
-
description: 'Start a filename or content search on this computer and return the first
|
|
601
|
+
description: 'Start a filename or content search on this computer and return the first matches without reading every matched file in full. Use read_files instead when a glob is already known and the contents of all matching files are the desired result. Content searches return "path:line: text" rows. Use get_more_search_results to page and stop_search to stop a long search.',
|
|
602
602
|
inputSchema: {
|
|
603
603
|
type: 'object',
|
|
604
604
|
properties: {
|
|
@@ -662,7 +662,7 @@ export const toolDefinitions = [
|
|
|
662
662
|
{
|
|
663
663
|
name: 'start_process',
|
|
664
664
|
title: 'Start process',
|
|
665
|
-
description: 'Run a shell command
|
|
665
|
+
description: 'Run a shell command, script, build, test suite, or terminal server and return its initial output. Prefer dedicated semantic tools for jobs they already cover: launch_app for GUI apps, service for service-manager operations, power_action for lock/sleep/restart/shutdown, network for routine network inspection/tests, event_log for OS logs, installed_apps for software inventory, environment for environment facts, and open_path/reveal_path for opening or revealing files. The process keeps running so read_process_output or interact_with_process can be used later.',
|
|
666
666
|
inputSchema: {
|
|
667
667
|
type: 'object',
|
|
668
668
|
properties: {
|
|
@@ -736,7 +736,7 @@ export const toolDefinitions = [
|
|
|
736
736
|
{
|
|
737
737
|
name: 'force_terminate',
|
|
738
738
|
title: 'Stop session',
|
|
739
|
-
description: 'Stop a session started with start_process, escalating from SIGTERM to SIGKILL when it does not exit.',
|
|
739
|
+
description: 'Stop a ReMCP terminal session started with start_process, escalating from SIGTERM to SIGKILL when it does not exit. Use kill_process for an arbitrary operating-system PID that is not a ReMCP terminal session.',
|
|
740
740
|
inputSchema: {
|
|
741
741
|
type: 'object',
|
|
742
742
|
properties: {
|
|
@@ -752,7 +752,7 @@ export const toolDefinitions = [
|
|
|
752
752
|
{
|
|
753
753
|
name: 'list_sessions',
|
|
754
754
|
title: 'List sessions',
|
|
755
|
-
description: 'List terminal sessions started
|
|
755
|
+
description: 'List only terminal sessions started through ReMCP start_process, with status and runtime. Use list_processes for the computer-wide operating-system process list.',
|
|
756
756
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
757
757
|
annotations: readOnly,
|
|
758
758
|
outputSchema: LIST_SESSIONS_OUTPUT_SCHEMA,
|
|
@@ -761,7 +761,7 @@ export const toolDefinitions = [
|
|
|
761
761
|
{
|
|
762
762
|
name: 'get_system_info',
|
|
763
763
|
title: 'Get system info',
|
|
764
|
-
description: 'Report host
|
|
764
|
+
description: 'Report machine health and host facts for the paired computer: operating system and kernel, CPU model/load, memory pressure, free disk space, uptime, and default shell. Use environment for PATH/shell/runtime environment variables and get_runtime_info for ReMCP-specific version, policy, roots, and limits.',
|
|
765
765
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
766
766
|
annotations: readOnly,
|
|
767
767
|
handler: systemToolHandlers.get_system_info,
|
|
@@ -769,7 +769,7 @@ export const toolDefinitions = [
|
|
|
769
769
|
{
|
|
770
770
|
name: 'list_processes',
|
|
771
771
|
title: 'List processes',
|
|
772
|
-
description: 'List
|
|
772
|
+
description: 'List the computer-wide operating-system processes, highest CPU first, with pid, parent pid, CPU/memory usage, and command. Use list_sessions when the question is only about commands started through ReMCP start_process. Values that look like secrets are masked.',
|
|
773
773
|
inputSchema: {
|
|
774
774
|
type: 'object',
|
|
775
775
|
properties: {
|
|
@@ -783,7 +783,7 @@ export const toolDefinitions = [
|
|
|
783
783
|
{
|
|
784
784
|
name: 'kill_process',
|
|
785
785
|
title: 'Kill process',
|
|
786
|
-
description: 'Terminate an operating-system process by pid. Terminates the process and its children on Windows.',
|
|
786
|
+
description: 'Terminate an arbitrary operating-system process by pid. If the pid belongs to a ReMCP terminal session created by start_process, prefer force_terminate so ReMCP updates the session consistently and handles its process tree. Terminates the process and its children on Windows.',
|
|
787
787
|
inputSchema: {
|
|
788
788
|
type: 'object',
|
|
789
789
|
properties: {
|
|
@@ -798,7 +798,7 @@ export const toolDefinitions = [
|
|
|
798
798
|
{
|
|
799
799
|
name: 'get_runtime_info',
|
|
800
800
|
title: 'Get runtime info',
|
|
801
|
-
description: 'Report this
|
|
801
|
+
description: 'Report this computer\'s local ReMCP runtime: version, allowed roots, command policy, output limits, settable preferences, and usage-metric setting. Use get_system_info for OS/CPU/memory/disk facts and hosted get_configuration for deployment/account configuration. Read-only; use set_config_value only for telemetryEnabled, maxReadLines, maxBufferedLines, or maxOutputBytes. Access roots and command security stay local to the computer.',
|
|
802
802
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
803
803
|
annotations: readOnly,
|
|
804
804
|
handler: statsToolHandlers.get_runtime_info,
|
|
@@ -806,7 +806,7 @@ export const toolDefinitions = [
|
|
|
806
806
|
{
|
|
807
807
|
name: 'get_runtime_stats',
|
|
808
808
|
title: 'Get runtime stats',
|
|
809
|
-
description: 'Report
|
|
809
|
+
description: 'Report counters from the current local runtime session on this machine: tool calls/failures, blocked commands, active terminal/search sessions, and usage-metric queue state. Use hosted get_usage_statistics for account-level historical usage aggregated across devices and days.',
|
|
810
810
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
811
811
|
annotations: readOnly,
|
|
812
812
|
handler: statsToolHandlers.get_runtime_stats,
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -30,9 +30,9 @@ const o = (properties = {}, required = []) => ({ type: 'object', properties, ...
|
|
|
30
30
|
const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
31
31
|
computer_snapshot: 'Use this for the first look at an unfamiliar desktop state when you need one compact bundle of windows, semantic UI, displays, clipboard metadata and optional pixels/browser/OCR. Prefer it over repeated screenshots; narrow later with ui_find, browser_find or screenshot_region.',
|
|
32
32
|
computer_action: 'Use this only when one semantic click/edit/focus/selection action must work across an uncertain target backend and may need Accessibility → browser DOM/CDP → OCR → coordinate fallback, or when multi_select/multi_edit avoids many identical native UI calls. Do not use it when the backend or operation is already known: use ui_action for native controls, browser_action for page DOM, type_text for prose, keyboard for shortcuts, pointer for raw pointer gestures, drag_drop or scroll for those gestures, window_action for windows, and the dedicated clipboard/launch/open/reveal/notification tools for those jobs.',
|
|
33
|
-
list_windows: 'Use this to discover top-level desktop windows or obtain a stable window id/PID/bounds before window_action, screenshot_region or targeted input.
|
|
34
|
-
window_action: 'Use this to focus, minimize, maximize, restore, move, resize or close one known top-level window. Select it by id/PID/app/title from list_windows; do not use pointer coordinates for window management when this semantic tool can express the action.',
|
|
35
|
-
launch_app: 'Use this to start a desktop application by executable/path or friendly application name without a shell. Prefer it over start_process for GUI application launch; use start_process for terminal commands, build tools and shell pipelines.',
|
|
33
|
+
list_windows: 'Use this to discover top-level desktop windows or obtain a stable window id/PID/bounds before window_action, screenshot_region or targeted input. Use ui_snapshot instead when you need controls inside a window; prefer list_windows over screenshot-based window guessing.',
|
|
34
|
+
window_action: 'Use this to focus, minimize, maximize, restore, move, resize or close one known top-level window. Select it by id/PID/app/title from list_windows. Use ui_action for controls inside the window; do not use pointer coordinates for window management when this semantic tool can express the action.',
|
|
35
|
+
launch_app: 'Use this to start a desktop application by executable/path or friendly application name without a shell. Prefer it over start_process for GUI application launch. If the user wants to open a specific file or directory in its associated application, use open_path instead; use start_process for terminal commands, build tools and shell pipelines.',
|
|
36
36
|
ui_snapshot: 'Use this to inspect native desktop Accessibility/UI Automation semantics for the active app before clicking or typing. Prefer scope=active; use scope=desktop only for cross-application discovery. For a Chromium web page with CDP available, prefer browser_snapshot for page DOM semantics.',
|
|
37
37
|
ui_find: 'Use this after ui_snapshot when you need a small set of native UI elements by role/name/label/AutomationId. It requires an element criterion; use ui_snapshot, not an empty ui_find, to enumerate UI. Prefer returned id or compact label for subsequent ui_action; refresh only when the UI has changed.',
|
|
38
38
|
ui_action: 'Use this to invoke, focus, edit, select, toggle, expand/collapse, range-edit or scroll a native accessible UI element. Prefer this over pointer clicks because semantic actions survive layout changes; fall back to pointer only when accessibility cannot perform the requested action.',
|
|
@@ -44,8 +44,8 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
44
44
|
wait_for_ui: 'Use this after a desktop action to wait for an explicit native UI state or condition instead of sleeping or polling screenshots. Presence/absence waits need a semantic target; use state=changed for a broad tree change and set a bounded timeout.',
|
|
45
45
|
clipboard: 'Use this for explicit text clipboard read/write/clear operations. Do not use clipboard as a typing workaround directly; type_text already uses safe clipboard paste with restoration when needed.',
|
|
46
46
|
display_inventory: 'Use this before monitor-specific screenshots, cross-monitor geometry or coordinate work to obtain monitor bounds, scale and primary-display information.',
|
|
47
|
-
screenshot_region: 'Use this when pixels are required for visual verification but only one window, monitor or rectangle matters. Prefer
|
|
48
|
-
open_path: 'Use this to open a permitted file or directory in its normal desktop application. Prefer
|
|
47
|
+
screenshot_region: 'Use this when static pixels are required for visual verification but only one window, monitor or rectangle matters. Prefer it over take_screenshot for a full desktop and over record_screen when motion/timing is not required; use ui_snapshot/browser_snapshot first when semantic structure is sufficient.',
|
|
48
|
+
open_path: 'Use this to open a permitted file or directory in its normal desktop application. Use launch_app when the task is to start an application itself without a particular file. Prefer open_path over shell-specific open/start/xdg-open commands because ReMCP applies filesystem confinement.',
|
|
49
49
|
reveal_path: 'Use this to reveal a permitted local file or directory in Finder/Explorer/file manager without opening the document itself.',
|
|
50
50
|
notification: 'Use this only when the user wants a visible native desktop notification on the paired computer. Do not use it as a substitute for replying in chat.',
|
|
51
51
|
browser_tabs: 'Use this first for CDP browser automation when the target page/tab is not already unambiguous. It lists debuggable Chromium pages from a loopback-only endpoint; use the returned target_id/title/url to scope later browser tools.',
|
|
@@ -59,11 +59,11 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
59
59
|
event_log: 'Use this to read bounded recent OS logs (Windows Event Log, macOS unified log or systemd journal) when diagnosing a service/app/system problem. Prefer it over an unbounded shell log dump.',
|
|
60
60
|
network: 'Use this for structured interfaces/DNS/routes/listeners inspection or a bounded TCP connectivity test. Prefer it over shell ip/netstat/nslookup commands for routine diagnostics.',
|
|
61
61
|
installed_apps: 'Use this to answer what software/packages are installed and their versions. Prefer a filter when looking for one product instead of returning the full inventory.',
|
|
62
|
-
environment: 'Use this for
|
|
62
|
+
environment: 'Use this for PATH, shell, Node/runtime environment paths, architecture and optionally sanitized environment variables. Use get_system_info for host health such as CPU/memory/disk/uptime, and get_runtime_info for ReMCP policy, roots and limits. Secret-looking values remain masked.',
|
|
63
63
|
audio: 'Use this to read or change the default output volume/mute state. Use action=status for inspection; set_volume requires a 0–100 value.',
|
|
64
|
-
power_action: 'Use this only when the user explicitly asks to lock, sleep, restart or shut down the computer.
|
|
64
|
+
power_action: 'Use this only when the user explicitly asks to lock, sleep, restart or shut down the computer. Prefer it over invoking those system actions through start_process; it is intentionally guarded by device command policy because restart/shutdown can interrupt work.',
|
|
65
65
|
record_screen: 'Use this for a short bounded screen recording when motion/timing is necessary to diagnose a UI issue. Prefer screenshot_region for static visual verification because it is cheaper and easier to inspect.',
|
|
66
|
-
read_document: 'Use this
|
|
66
|
+
read_document: 'Use this for structured document content, especially PDF, DOCX and XLSX, and for CSV/JSON/XML when document-style parsing is useful. Use read_file for ordinary source code, logs, configuration, Markdown, or other plain line-oriented text. Prefer read_document over shell extraction or launching Office when the goal is document content; PDF reading uses the built-in parser first and may fall back to local pdftotext when embedded fonts prevent direct decoding.',
|
|
67
67
|
edit_spreadsheet: 'Use this for direct XLSX cell/range/formula edits without launching Excel. Prefer explicit cells/ranges and write to output when preserving the original matters.',
|
|
68
68
|
edit_document: 'Use this for structured DOCX paragraph edits without launching Word. It is best for replace/insert/delete paragraph operations; use native UI automation only when document formatting/layout interaction is required.',
|
|
69
69
|
pdf_action: 'Use this for PDF metadata/annotations inspection or structural merge/split/page extraction. For reading PDF text use read_document; supply an output/output_dir for write actions when preserving originals matters.',
|
|
@@ -3,7 +3,7 @@ import process from 'node:process';
|
|
|
3
3
|
import { readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { image, multi, text } from '../util.mjs';
|
|
5
5
|
import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
|
|
6
|
-
import { hasWaylandRemoteDesktopGrant, portalPointerButton, portalPointerMotion, portalScroll, portalShortcut, portalTypeText, waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
6
|
+
import { hasWaylandRemoteDesktopGrant, portalPointerButton, portalPointerMotion, portalPointerMotionAbsolute, portalScroll, portalShortcut, portalTypeText, waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
7
7
|
import {
|
|
8
8
|
clamp,
|
|
9
9
|
commandExists,
|
|
@@ -863,9 +863,18 @@ async function portalMoveTo(x, y, options = {}) {
|
|
|
863
863
|
const targetX = Math.trunc(Number(x));
|
|
864
864
|
const targetY = Math.trunc(Number(y));
|
|
865
865
|
if (!Number.isFinite(targetX) || !Number.isFinite(targetY)) throw new Error('x and y must be finite coordinates');
|
|
866
|
-
if (options.recalibrate || !portalPointerState) await calibratePortalPointer(options);
|
|
867
866
|
const motionOptions = { ...options };
|
|
868
867
|
delete motionOptions.recalibrate;
|
|
868
|
+
try {
|
|
869
|
+
await portalPointerMotionAbsolute(targetX, targetY, motionOptions);
|
|
870
|
+
portalPointerState = { x:targetX, y:targetY };
|
|
871
|
+
return;
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (!/requires the EIS Remote Desktop backend/i.test(String(error?.message || error))) throw error;
|
|
874
|
+
}
|
|
875
|
+
// Legacy portal fallback: relative motion has no absolute position feedback,
|
|
876
|
+
// so recalibrate on every move instead of trusting cached coordinates.
|
|
877
|
+
await calibratePortalPointer(motionOptions);
|
|
869
878
|
await portalPointerMotion(targetX - portalPointerState.x, targetY - portalPointerState.y, motionOptions);
|
|
870
879
|
portalPointerState = { x:targetX, y:targetY };
|
|
871
880
|
}
|
|
@@ -214,10 +214,13 @@ class EiSender:
|
|
|
214
214
|
self.lib.ei_device_unref.restype = c_void_p
|
|
215
215
|
self.lib.ei_device_has_capability.argtypes = [c_void_p, ctypes.c_int]
|
|
216
216
|
self.lib.ei_device_has_capability.restype = ctypes.c_bool
|
|
217
|
+
self.lib.ei_device_get_region_at.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
|
|
218
|
+
self.lib.ei_device_get_region_at.restype = c_void_p
|
|
217
219
|
self.lib.ei_device_start_emulating.argtypes = [c_void_p, ctypes.c_uint32]
|
|
218
220
|
self.lib.ei_device_stop_emulating.argtypes = [c_void_p]
|
|
219
221
|
self.lib.ei_device_keyboard_key.argtypes = [c_void_p, ctypes.c_uint32, ctypes.c_bool]
|
|
220
222
|
self.lib.ei_device_pointer_motion.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
|
|
223
|
+
self.lib.ei_device_pointer_motion_absolute.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
|
|
221
224
|
self.lib.ei_device_button_button.argtypes = [c_void_p, ctypes.c_uint32, ctypes.c_bool]
|
|
222
225
|
self.lib.ei_device_scroll_discrete.argtypes = [c_void_p, ctypes.c_int32, ctypes.c_int32]
|
|
223
226
|
self.lib.ei_device_frame.argtypes = [c_void_p, ctypes.c_uint64]
|
|
@@ -243,6 +246,8 @@ class EiSender:
|
|
|
243
246
|
self.devices = {}
|
|
244
247
|
self.sequence = 1
|
|
245
248
|
self.connected = False
|
|
249
|
+
self.pointer_device_key = None
|
|
250
|
+
self.pointer_position = None
|
|
246
251
|
|
|
247
252
|
def _device_key(self, ptr):
|
|
248
253
|
return int(ptr or 0)
|
|
@@ -259,6 +264,9 @@ class EiSender:
|
|
|
259
264
|
def _drop_device(self, ptr):
|
|
260
265
|
key = self._device_key(ptr)
|
|
261
266
|
state = self.devices.pop(key, None)
|
|
267
|
+
if key == self.pointer_device_key:
|
|
268
|
+
self.pointer_device_key = None
|
|
269
|
+
self.pointer_position = None
|
|
262
270
|
if state:
|
|
263
271
|
if state["emulating"]:
|
|
264
272
|
try:
|
|
@@ -349,6 +357,46 @@ class EiSender:
|
|
|
349
357
|
return state["ptr"]
|
|
350
358
|
raise RuntimeError(f"no resumed EIS device with capability {cap}")
|
|
351
359
|
|
|
360
|
+
def device_for_absolute(self, x, y):
|
|
361
|
+
candidates = []
|
|
362
|
+
for state in self.devices.values():
|
|
363
|
+
if not state["resumed"] or not state["emulating"]:
|
|
364
|
+
continue
|
|
365
|
+
device = state["ptr"]
|
|
366
|
+
if not self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
|
|
367
|
+
continue
|
|
368
|
+
candidates.append(device)
|
|
369
|
+
if self.lib.ei_device_get_region_at(device, float(x), float(y)):
|
|
370
|
+
return device
|
|
371
|
+
if candidates:
|
|
372
|
+
raise RuntimeError(f"no absolute-pointer EIS region contains desktop coordinate ({x}, {y})")
|
|
373
|
+
raise RuntimeError("no resumed EIS device with absolute-pointer capability")
|
|
374
|
+
|
|
375
|
+
def device_for_pointer_context(self, cap):
|
|
376
|
+
state = self.devices.get(self.pointer_device_key)
|
|
377
|
+
if (
|
|
378
|
+
state
|
|
379
|
+
and state["resumed"]
|
|
380
|
+
and state["emulating"]
|
|
381
|
+
and self.lib.ei_device_has_capability(state["ptr"], cap)
|
|
382
|
+
):
|
|
383
|
+
return state["ptr"]
|
|
384
|
+
if self.pointer_position is not None:
|
|
385
|
+
x, y = self.pointer_position
|
|
386
|
+
for candidate in self.devices.values():
|
|
387
|
+
if not candidate["resumed"] or not candidate["emulating"]:
|
|
388
|
+
continue
|
|
389
|
+
device = candidate["ptr"]
|
|
390
|
+
if not self.lib.ei_device_has_capability(device, cap):
|
|
391
|
+
continue
|
|
392
|
+
if self.lib.ei_device_get_region_at(device, float(x), float(y)):
|
|
393
|
+
return device
|
|
394
|
+
return self.device_for(cap)
|
|
395
|
+
|
|
396
|
+
def remember_pointer(self, device, position=None):
|
|
397
|
+
self.pointer_device_key = self._device_key(device)
|
|
398
|
+
self.pointer_position = position
|
|
399
|
+
|
|
352
400
|
def frame(self, device):
|
|
353
401
|
self.lib.ei_device_frame(device, self.lib.ei_now(self.ei))
|
|
354
402
|
|
|
@@ -361,13 +409,21 @@ class EiSender:
|
|
|
361
409
|
elif op == "motion":
|
|
362
410
|
device = self.device_for(EI_CAP_POINTER)
|
|
363
411
|
self.lib.ei_device_pointer_motion(device, float(payload.get("dx", 0)), float(payload.get("dy", 0)))
|
|
412
|
+
self.remember_pointer(device)
|
|
413
|
+
self.frame(device)
|
|
414
|
+
elif op == "motion_absolute":
|
|
415
|
+
x = float(payload["x"])
|
|
416
|
+
y = float(payload["y"])
|
|
417
|
+
device = self.device_for_absolute(x, y)
|
|
418
|
+
self.lib.ei_device_pointer_motion_absolute(device, x, y)
|
|
419
|
+
self.remember_pointer(device, (x, y))
|
|
364
420
|
self.frame(device)
|
|
365
421
|
elif op == "button":
|
|
366
|
-
device = self.
|
|
422
|
+
device = self.device_for_pointer_context(EI_CAP_BUTTON)
|
|
367
423
|
self.lib.ei_device_button_button(device, int(payload["button"]), bool(payload.get("pressed")))
|
|
368
424
|
self.frame(device)
|
|
369
425
|
elif op == "scroll":
|
|
370
|
-
device = self.
|
|
426
|
+
device = self.device_for_pointer_context(EI_CAP_SCROLL)
|
|
371
427
|
self.lib.ei_device_scroll_discrete(device, int(payload.get("dx", 0)), int(payload.get("dy", 0)))
|
|
372
428
|
self.frame(device)
|
|
373
429
|
elif op == "ping":
|
|
@@ -590,6 +590,15 @@ export async function portalPointerMotion(dx, dy, options = {}) {
|
|
|
590
590
|
await state.remote.NotifyPointerMotion(state.session, {}, Number(dx), Number(dy));
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
export async function portalPointerMotionAbsolute(x, y, options = {}) {
|
|
594
|
+
const state = options.state || await ensureWaylandRemoteDesktop(options);
|
|
595
|
+
if (state.backend === 'xdg-eis') {
|
|
596
|
+
await state.send({ op:'motion_absolute', x:Number(x), y:Number(y) });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
throw new ToolError('Absolute pointer motion requires the EIS Remote Desktop backend');
|
|
600
|
+
}
|
|
601
|
+
|
|
593
602
|
export async function portalPointerButton(button, pressed, options = {}) {
|
|
594
603
|
const state = options.state || await ensureWaylandRemoteDesktop(options);
|
|
595
604
|
const code = String(button || 'left').toLowerCase() === 'right' ? 0x111 : String(button || 'left').toLowerCase() === 'middle' ? 0x112 : 0x110;
|