Skip to content

Commit 567e4e8

Browse files
committed
fix(ios): surface a hinted unsupported error for synthesis gestures at admission
Removing macOS pinch from the capability matrix makes macOS pinch (and the already-excluded rotate-gesture/transform-gesture on macOS/tvOS/physical iOS) fail fast in ensureGenericCommandReady before reaching the runner. That left the runner's macOS-specific hint unreachable on the daemon path, so callers only saw the generic "<cmd> is not supported on this device". Add an optional unsupportedHint to the capability matrix and surface it at admission, so the synthesis-only gestures fail fast (no runner round-trip) AND return an actionable hint pointing to where they work (Android + iOS simulator). Applied to pinch / rotate-gesture / transform-gesture. Addresses PR #645 review (P2: route macOS pinch to the hinted failure).
1 parent 7db7726 commit 567e4e8

3 files changed

Lines changed: 56 additions & 2 deletions

File tree

src/core/__tests__/capabilities.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from 'vitest';
22
import assert from 'node:assert/strict';
3-
import { isCommandSupportedOnDevice } from '../capabilities.ts';
3+
import { isCommandSupportedOnDevice, unsupportedHintForDevice } from '../capabilities.ts';
44
import type { DeviceInfo } from '../../utils/device.ts';
55

66
const iosSimulator: DeviceInfo = {
@@ -360,3 +360,33 @@ test('unknown commands default to supported', () => {
360360
assert.equal(isCommandSupportedOnDevice('some-future-cmd', androidDevice), true);
361361
assert.equal(isCommandSupportedOnDevice('some-future-cmd', linuxDevice), true);
362362
});
363+
364+
test('synthesis gestures carry an actionable unsupported hint at admission', () => {
365+
// macOS / tvOS / physical iOS are rejected at admission; the hint redirects to where the
366+
// two-finger synthesis path actually works, so callers do not just see a bare "not supported".
367+
for (const command of ['pinch', 'rotate-gesture', 'transform-gesture']) {
368+
assert.match(
369+
unsupportedHintForDevice(command, macOsDevice) ?? '',
370+
/multi-touch/i,
371+
`${command} macOS hint`,
372+
);
373+
assert.match(
374+
unsupportedHintForDevice(command, tvOsSimulator) ?? '',
375+
/touch/i,
376+
`${command} tvOS hint`,
377+
);
378+
assert.match(
379+
unsupportedHintForDevice(command, iosDevice) ?? '',
380+
/simulator/i,
381+
`${command} iOS device hint`,
382+
);
383+
// Where the gesture IS supported there is nothing to hint.
384+
assert.equal(
385+
unsupportedHintForDevice(command, iosSimulator),
386+
undefined,
387+
`${command} iOS sim (supported) hint`,
388+
);
389+
}
390+
// Commands without a hint hook return undefined (admission keeps its generic message).
391+
assert.equal(unsupportedHintForDevice('tap', macOsDevice), undefined);
392+
});

src/core/capabilities.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export type CommandCapability = {
1212
android?: KindMatrix;
1313
linux?: KindMatrix;
1414
supports?: (device: DeviceInfo) => boolean;
15+
/** Optional actionable hint surfaced when this command is rejected at admission for `device`. */
16+
unsupportedHint?: (device: DeviceInfo) => string | undefined;
1517
};
1618

1719
const isNotMacOs = (device: DeviceInfo): boolean => device.platform !== 'macos';
@@ -20,6 +22,19 @@ const isMacOsOrAppleSimulator = (device: DeviceInfo): boolean =>
2022
const isIosMobileSimulator = (device: DeviceInfo): boolean =>
2123
device.platform === 'ios' && device.kind === 'simulator' && device.target !== 'tv';
2224

25+
// Two-finger gesture synthesis (RunnerSynthesizedGesture) is iOS-simulator-only (plus Android).
26+
// When such a gesture is rejected at admission, explain where it IS available so an agent can
27+
// redirect instead of getting a bare "not supported on this device".
28+
const synthesisGestureUnsupportedHint = (device: DeviceInfo): string | undefined => {
29+
if (device.platform === 'macos')
30+
return 'macOS automation has no multi-touch input — this gesture is supported on Android and the iOS simulator only.';
31+
if (device.platform === 'ios' && device.target === 'tv')
32+
return 'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.';
33+
if (device.platform === 'ios' && device.kind === 'device')
34+
return 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.';
35+
return undefined;
36+
};
37+
2338
// Linux desktop supports these commands via xdotool/ydotool + AT-SPI2.
2439
// Linux device kind is always 'device' (local desktop).
2540
const LINUX_DEVICE: KindMatrix = { device: true };
@@ -61,18 +76,21 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
6176
// it is excluded and fails fast at admission rather than round-tripping to an unsupported
6277
// runner. Matches rotate-gesture / transform-gesture.
6378
supports: (device) => device.platform === 'android' || isIosMobileSimulator(device),
79+
unsupportedHint: synthesisGestureUnsupportedHint,
6480
},
6581
'rotate-gesture': {
6682
apple: { simulator: true, device: true },
6783
android: { emulator: true, device: true, unknown: true },
6884
linux: LINUX_NONE,
6985
supports: (device) => device.platform === 'android' || isIosMobileSimulator(device),
86+
unsupportedHint: synthesisGestureUnsupportedHint,
7087
},
7188
'transform-gesture': {
7289
apple: { simulator: true, device: true },
7390
android: { emulator: true, device: true, unknown: true },
7491
linux: LINUX_NONE,
7592
supports: (device) => device.platform === 'android' || isIosMobileSimulator(device),
93+
unsupportedHint: synthesisGestureUnsupportedHint,
7694
},
7795
'app-switcher': {
7896
apple: { simulator: true, device: true },
@@ -240,6 +258,10 @@ export function isCommandSupportedOnDevice(command: string, device: DeviceInfo):
240258
return byPlatform[kind] === true;
241259
}
242260

261+
export function unsupportedHintForDevice(command: string, device: DeviceInfo): string | undefined {
262+
return COMMAND_CAPABILITY_MATRIX[command]?.unsupportedHint?.(device);
263+
}
264+
243265
export function listCapabilityCommands(): string[] {
244266
return Object.keys(COMMAND_CAPABILITY_MATRIX).sort();
245267
}

src/daemon/request-generic-dispatch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { dispatchCommand, type CommandFlags } from '../core/dispatch.ts';
22
import { DAEMON_COMMAND_GROUPS, GESTURE_SUBCOMMAND_ERROR } from '../command-catalog.ts';
3-
import { isCommandSupportedOnDevice } from '../core/capabilities.ts';
3+
import { isCommandSupportedOnDevice, unsupportedHintForDevice } from '../core/capabilities.ts';
44
import { SessionStore } from './session-store.ts';
55
import type { DaemonCommandContext } from './context.ts';
66
import type { DaemonRequest, DaemonResponse, SessionState } from './types.ts';
@@ -154,11 +154,13 @@ async function ensureGenericCommandReady(
154154
platformCommand: string,
155155
): Promise<DaemonResponse | null> {
156156
if (!isCommandSupportedOnDevice(platformCommand, session.device)) {
157+
const hint = unsupportedHintForDevice(platformCommand, session.device);
157158
return {
158159
ok: false,
159160
error: {
160161
code: 'UNSUPPORTED_OPERATION',
161162
message: `${platformCommand} is not supported on this device`,
163+
...(hint ? { hint } : {}),
162164
},
163165
};
164166
}

0 commit comments

Comments
 (0)