Skip to content

Commit 0dcc1aa

Browse files
authored
fix: normalize interaction response wire shapes (#1114)
* fix: normalize interaction response wire shapes * fix: reduce interaction response complexity
1 parent 7e583c4 commit 0dcc1aa

7 files changed

Lines changed: 547 additions & 156 deletions

File tree

src/daemon/handlers/__tests__/interaction.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2262,7 +2262,6 @@ test('fill @ref preserves fallback coordinates for recording when platform resul
22622262
expect(response?.ok).toBe(true);
22632263
if (response?.ok) {
22642264
expect(response.data?.filled).toBe(true);
2265-
expect(response.data?.x).toBeUndefined();
22662265
}
22672266

22682267
const stored = sessionStore.get(sessionName);

src/daemon/handlers/interaction-touch-response.ts

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
SettleObservation,
77
} from '../../contracts/interaction.ts';
88
import { successText } from '../../utils/success-text.ts';
9-
import { interactionResultExtra, stripAtPrefix } from './interaction-touch-targets.ts';
9+
import { interactionResultExtra } from './interaction-touch-targets.ts';
1010

1111
/**
1212
* The single construction site for interaction response payloads (ADR 0011
@@ -26,17 +26,12 @@ export type InteractionResponseSource =
2626
| {
2727
kind: 'runtime';
2828
result: InteractionRuntimeResult;
29-
/**
30-
* Wire-compat for fill @ref: the response echoes backendResult (or a
31-
* minimal ref/point fallback) plus the identity extras instead of the
32-
* visualization shape. Only applies when the result resolved as a ref.
33-
*/
34-
refBackendWireShape?: boolean;
3529
}
3630
| {
3731
// Direct iOS selector dispatch: no runtime result exists, only the raw
3832
// runner payload; identity extras are a declared gap on that path.
3933
kind: 'runner-payload';
34+
targetKind: InteractionRuntimeResult['kind'];
4035
data: Record<string, unknown>;
4136
point: { x: number; y: number };
4237
};
@@ -77,39 +72,49 @@ export function buildInteractionResponseData(params: {
7772
}): InteractionResponsePayloads {
7873
const { source, referenceFrame, extra } = params;
7974
if (source.kind === 'runner-payload') {
80-
const payload = buildTouchVisualizationResult({
75+
const commonExtra = {
76+
targetKind: source.targetKind,
77+
...(extra ?? {}),
78+
};
79+
const result = buildTouchPayload({
8180
data: source.data,
8281
fallbackX: source.point.x,
8382
fallbackY: source.point.y,
8483
referenceFrame,
85-
extra,
84+
extra: commonExtra,
85+
});
86+
const responseData = buildTouchPayload({
87+
data: sanitizeWireBackendData(source.data),
88+
fallbackX: source.point.x,
89+
fallbackY: source.point.y,
90+
referenceFrame,
91+
extra: commonExtra,
8692
});
87-
return { result: payload, responseData: payload };
93+
return { result, responseData };
8894
}
8995

9096
const { result } = source;
91-
const visualization = buildTouchVisualizationResult({
97+
const resultExtra = interactionResultExtra(result);
98+
const commonExtra = {
99+
targetKind: result.kind,
100+
...resultExtra,
101+
...settleExtra(result.settle, params.settleRefsGeneration),
102+
...(extra ?? {}),
103+
};
104+
const visualization = buildTouchPayload({
92105
data: result.backendResult,
93106
fallbackX: result.point?.x,
94107
fallbackY: result.point?.y,
95108
referenceFrame,
96-
extra: {
97-
...interactionResultExtra(result),
98-
...settleExtra(result.settle, params.settleRefsGeneration),
99-
...(extra ?? {}),
100-
},
109+
extra: commonExtra,
110+
});
111+
const responseData = buildTouchPayload({
112+
data: sanitizeWireBackendData(result.backendResult),
113+
fallbackX: result.point?.x,
114+
fallbackY: result.point?.y,
115+
referenceFrame,
116+
extra: commonExtra,
101117
});
102-
const responseData =
103-
source.refBackendWireShape && result.kind === 'ref'
104-
? {
105-
...(result.backendResult ?? {
106-
ref: stripAtPrefix(result.target?.kind === 'ref' ? result.target.ref : undefined),
107-
...(result.point ? { x: result.point.x, y: result.point.y } : {}),
108-
}),
109-
...interactionResultExtra(result),
110-
...settleExtra(result.settle, params.settleRefsGeneration),
111-
}
112-
: visualization;
113118
const warning = composeResponseWarning(
114119
'warning' in result ? result.warning : undefined,
115120
params.staleRefsWarning,
@@ -140,7 +145,7 @@ function composeResponseWarning(
140145
return resultWarning ? `${resultWarning} ${staleRefsWarning}` : staleRefsWarning;
141146
}
142147

143-
function buildTouchVisualizationResult(params: {
148+
function buildTouchPayload(params: {
144149
data: Record<string, unknown> | undefined;
145150
fallbackX?: number;
146151
fallbackY?: number;
@@ -151,40 +156,96 @@ function buildTouchVisualizationResult(params: {
151156
const message =
152157
buildTouchMessage(extra, fallbackX, fallbackY) ??
153158
(typeof data?.message === 'string' ? data.message : undefined);
154-
return {
159+
return stripUndefinedFields({
160+
...(data ?? {}),
155161
...(fallbackX === undefined || fallbackY === undefined ? {} : { x: fallbackX, y: fallbackY }),
156162
...(referenceFrame ?? {}),
157163
...(extra ?? {}),
158-
...(data ?? {}),
159164
...successText(message),
160-
};
165+
});
166+
}
167+
168+
function stripUndefinedFields(data: Record<string, unknown>): Record<string, unknown> {
169+
return Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined));
170+
}
171+
172+
function sanitizeWireBackendData(
173+
data: Record<string, unknown> | undefined,
174+
): Record<string, unknown> | undefined {
175+
if (!data) return undefined;
176+
const sanitized = Object.fromEntries(
177+
Object.entries(data).filter(([key, value]) => shouldKeepWireBackendField(key, value)),
178+
);
179+
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
180+
}
181+
182+
function shouldKeepWireBackendField(key: string, value: unknown): boolean {
183+
switch (key) {
184+
case 'gestureStartUptimeMs':
185+
case 'gestureEndUptimeMs':
186+
case 'sequenceResults':
187+
return false;
188+
case 'count':
189+
return value !== 1;
190+
case 'intervalMs':
191+
case 'holdMs':
192+
case 'jitterPx':
193+
return value !== 0;
194+
case 'doubleTap':
195+
return value !== false;
196+
default:
197+
return true;
198+
}
161199
}
162200

163201
function buildTouchMessage(
164202
extra: Record<string, unknown> | undefined,
165203
x: number | undefined,
166204
y: number | undefined,
167205
): string | undefined {
168-
if (typeof extra?.text === 'string') {
169-
return `Filled ${Array.from(extra.text).length} chars`;
170-
}
171-
const ref = typeof extra?.ref === 'string' ? extra.ref : undefined;
172-
if (!ref) return undefined;
173-
const pointSuffix = x === undefined || y === undefined ? '' : ` (${x}, ${y})`;
174-
return buildRefTouchMessage(ref, extra ?? {}, pointSuffix);
206+
const fillText = readString(extra, 'text');
207+
if (fillText !== undefined) return `Filled ${Array.from(fillText).length} chars`;
208+
209+
const pointSuffix = buildPointSuffix(x, y);
210+
const label = buildTouchTargetLabel(extra);
211+
if (label) return buildTouchTargetMessage(label, extra ?? {}, pointSuffix);
212+
if (!pointSuffix) return undefined;
213+
214+
return buildPointTouchMessage(extra, pointSuffix);
215+
}
216+
217+
function readString(data: Record<string, unknown> | undefined, key: string): string | undefined {
218+
const value = data?.[key];
219+
return typeof value === 'string' ? value : undefined;
220+
}
221+
222+
function buildPointSuffix(x: number | undefined, y: number | undefined): string {
223+
return x === undefined || y === undefined ? '' : ` (${x}, ${y})`;
224+
}
225+
226+
function buildTouchTargetLabel(extra: Record<string, unknown> | undefined): string | undefined {
227+
const ref = readString(extra, 'ref');
228+
return ref === undefined ? readString(extra, 'selector') : `@${ref}`;
229+
}
230+
231+
function buildPointTouchMessage(
232+
extra: Record<string, unknown> | undefined,
233+
pointSuffix: string,
234+
): string {
235+
return extra?.gesture === 'longpress' ? `Long pressed${pointSuffix}` : `Tapped${pointSuffix}`;
175236
}
176237

177-
function buildRefTouchMessage(
178-
ref: string,
238+
function buildTouchTargetMessage(
239+
label: string,
179240
extra: Record<string, unknown>,
180241
pointSuffix: string,
181242
): string {
182243
const button = typeof extra.button === 'string' ? extra.button : undefined;
183244
if (extra.gesture === 'longpress') {
184-
return `Long pressed @${ref}${pointSuffix}`;
245+
return `Long pressed ${label}${pointSuffix}`;
185246
}
186247
if (button && button !== 'primary') {
187-
return `Clicked ${button} @${ref}${pointSuffix}`;
248+
return `Clicked ${button} ${label}${pointSuffix}`;
188249
}
189-
return `Tapped @${ref}${pointSuffix}`;
250+
return `Tapped ${label}${pointSuffix}`;
190251
}

src/daemon/handlers/interaction-touch.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ async function dispatchDirectIosSelectorInteraction(params: {
383383
const actionFinishedAt = Date.now();
384384
const point = readPointFromDirectSelectorTapResult(data);
385385
const { result, responseData } = buildInteractionResponseData({
386-
source: { kind: 'runner-payload', data, point },
386+
source: { kind: 'runner-payload', targetKind: 'selector', data, point },
387387
referenceFrame: readReferenceFrameFromDirectSelectorTapResult(data),
388388
extra: {
389389
...extra,
@@ -558,11 +558,7 @@ function buildFillResponsePayloads(params: {
558558
? undefined
559559
: readSnapshotNodesReferenceFrame(session.snapshot?.nodes ?? []);
560560
return buildInteractionResponseData({
561-
// refBackendWireShape keeps the historical fill @ref wire response
562-
// (backendResult + identity extras) while the shared builder owns the
563-
// extras — the hand-rolled version of this branch dropped evidence
564-
// (PR #1064 review).
565-
source: { kind: 'runtime', result, refBackendWireShape: true },
561+
source: { kind: 'runtime', result },
566562
referenceFrame,
567563
extra: { text: params.text },
568564
staleRefsWarning: params.staleRefsWarning,

0 commit comments

Comments
 (0)