Skip to content

Commit 8203a05

Browse files
fixup! preserve Maestro swipe endpoint-hold execution profile via internal seam
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 61e4908 commit 8203a05

6 files changed

Lines changed: 51 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
## Unreleased
44

5-
- Breaking: removed deprecated gesture duration and rotate velocity inputs (#1218, #1216).
5+
- Breaking: removed deprecated gesture duration and rotate velocity inputs (#1218).
66
- `swipe x1 y1 x2 y2` no longer accepts a trailing `durationMs` positional; use `gesture pan x1 y1 (x2-x1) (y2-y1) durationMs` for deliberate timed drags.
7+
- Maestro `swipe` operations with a duration continue to normalize to `gesture pan` with the `endpoint-hold` execution profile, preserving the Maestro-compatible fast-swipe-then-hold behavior on iOS.
78
- `gesture fling direction x y` no longer accepts a trailing `durationMs` positional; use `gesture pan` for timed movement.
89
- `gesture swipe preset` no longer accepts a trailing `durationMs` positional; use `gesture pan` for timed movement.
910
- `gesture rotate degrees [x] [y]` no longer accepts a trailing `velocity` positional; rotation pacing is derived from `degrees`.

src/compat/maestro/__tests__/daemon-runtime-port.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ test('uses the direct viewport without snapshot and pairs it with the nested ges
125125
delta: { x: -320, y: 0 },
126126
durationMs: 300,
127127
},
128-
internal: { gestureViewport: viewport },
128+
internal: {
129+
gestureExecutionProfile: 'endpoint-hold',
130+
gestureViewport: viewport,
131+
},
129132
});
130133
expect(resolveGestureViewport).toHaveBeenCalledOnce();
131134
expect(requests.map(({ command }) => command)).toEqual(['gesture']);

src/compat/maestro/__tests__/daemon-runtime-public-operation.test.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import assert from 'node:assert/strict';
12
import { describe, expect, test } from 'vitest';
3+
import { buildGesturePlan } from '../../../contracts/gesture-plan.ts';
4+
import { readGesturePayload } from '../../../contracts/gesture-input.ts';
5+
import { normalizePublicGesture } from '../../../contracts/gesture-normalization.ts';
26
import {
37
projectMaestroPublicOperation,
48
type MaestroPublicOperation,
@@ -104,7 +108,10 @@ describe('Maestro public operation projection', () => {
104108
durationMs: 400,
105109
},
106110
flags: { postGestureStabilization: false },
107-
internal: { gestureViewport: { x: 0, y: 0, width: 100, height: 200 } },
111+
internal: {
112+
gestureExecutionProfile: 'endpoint-hold',
113+
gestureViewport: { x: 0, y: 0, width: 100, height: 200 },
114+
},
108115
},
109116
},
110117
{
@@ -168,15 +175,40 @@ describe('Maestro public operation projection', () => {
168175
expect(projectMaestroPublicOperation(operation)).toEqual(expected);
169176
});
170177

171-
test('omits optional swipe and scroll fields', () => {
178+
test('preserves endpoint-hold for swipes without a viewport and omits scroll duration', () => {
172179
expect(
173180
projectMaestroPublicOperation({
174181
kind: 'swipe',
175182
gesture: { from: { x: 1, y: 2 }, to: { x: 3, y: 4 }, durationMs: 5 },
176183
}),
177-
).not.toHaveProperty('internal');
184+
).toMatchObject({
185+
command: 'gesture',
186+
internal: { gestureExecutionProfile: 'endpoint-hold' },
187+
});
178188
expect(projectMaestroPublicOperation({ kind: 'scroll', direction: 'up' })).not.toHaveProperty(
179189
'input',
180190
);
181191
});
192+
193+
test('a 400 ms Maestro swipe reaches plan execution with endpoint-hold profile', () => {
194+
const viewport = { x: 0, y: 0, width: 400, height: 800 };
195+
const projected = projectMaestroPublicOperation({
196+
kind: 'swipe',
197+
gesture: { from: { x: 360, y: 430 }, to: { x: 40, y: 430 }, durationMs: 400 },
198+
viewport,
199+
});
200+
const input = readGesturePayload(projected.input);
201+
const normalized = normalizePublicGesture(input);
202+
if (normalized.gesture.intent === 'pan' && projected.internal?.gestureExecutionProfile) {
203+
normalized.gesture.executionProfile = projected.internal.gestureExecutionProfile;
204+
}
205+
const plan = buildGesturePlan(
206+
normalized.gesture,
207+
projected.internal?.gestureViewport ?? viewport,
208+
);
209+
assert.equal(plan.topology, 'single');
210+
assert.equal(plan.intent, 'pan');
211+
assert.equal(plan.executionProfile, 'endpoint-hold');
212+
assert.equal(plan.durationMs, 400);
213+
});
182214
});

src/compat/maestro/daemon-runtime-public-operation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ function projectSwipe(
169169
durationMs,
170170
},
171171
flags: { postGestureStabilization: false },
172-
...(operation.viewport ? { internal: { gestureViewport: operation.viewport } } : {}),
172+
internal: {
173+
gestureExecutionProfile: 'endpoint-hold',
174+
...(operation.viewport ? { gestureViewport: operation.viewport } : {}),
175+
},
173176
};
174177
}
175178

src/daemon/handlers/interaction-gesture.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export async function dispatchGestureViaRuntime(
4545
return await dispatchGestureInteraction(params, 'gesture', async (session) => {
4646
const input = readGesturePayload(params.req.input);
4747
const normalized = normalizePublicGesture(input);
48+
if (normalized.gesture.intent === 'pan' && params.req.internal?.gestureExecutionProfile) {
49+
normalized.gesture.executionProfile = params.req.internal.gestureExecutionProfile;
50+
}
4851
requireGestureSupported(normalized.gesture, session.device);
4952
const result = await createGestureRuntime(params).interactions.gesture({
5053
session: params.sessionName,

src/daemon/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { RecordingScope } from '../contracts/recording-scope.ts';
1818
import type { DeviceInfo, Platform, PlatformSelector } from '../kernel/device.ts';
1919
import type { ExecBackgroundResult, ExecResult } from '../utils/exec.ts';
2020
import type { Rect, SnapshotState } from '../kernel/snapshot.ts';
21+
import type { GestureExecutionProfile } from '../contracts/gesture-plan-types.ts';
2122
// Type-only import; erased at runtime. ref-frame.ts imports SessionState from
2223
// here, so this back-edge must stay type-only to avoid a runtime cycle.
2324
import type { RefFrameScope, RefFrameState } from './ref-frame.ts';
@@ -65,6 +66,8 @@ type DaemonRequestInternal = {
6566
closeAppOnly?: boolean;
6667
/** Provider-owned viewport already resolved while normalizing a nested gesture command. */
6768
gestureViewport?: Rect;
69+
/** Maestro-compat execution profile for timed coordinate swipes projected to `gesture pan`. */
70+
gestureExecutionProfile?: GestureExecutionProfile;
6871
/**
6972
* ADR 0012 step 4 post-resolution guard: the verified target member's
7073
* normalized local identity AND structural denotation (document order +

0 commit comments

Comments
 (0)