Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/daemon/handlers/__tests__/interaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2262,7 +2262,6 @@ test('fill @ref preserves fallback coordinates for recording when platform resul
expect(response?.ok).toBe(true);
if (response?.ok) {
expect(response.data?.filled).toBe(true);
expect(response.data?.x).toBeUndefined();
}

const stored = sessionStore.get(sessionName);
Expand Down
144 changes: 103 additions & 41 deletions src/daemon/handlers/interaction-touch-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
PressCommandResult,
} from '../../contracts/interaction.ts';
import { successText } from '../../utils/success-text.ts';
import { interactionResultExtra, stripAtPrefix } from './interaction-touch-targets.ts';
import { interactionResultExtra } from './interaction-touch-targets.ts';

/**
* The single construction site for interaction response payloads (ADR 0011
Expand All @@ -25,17 +25,12 @@ export type InteractionResponseSource =
| {
kind: 'runtime';
result: InteractionRuntimeResult;
/**
* Wire-compat for fill @ref: the response echoes backendResult (or a
* minimal ref/point fallback) plus the identity extras instead of the
* visualization shape. Only applies when the result resolved as a ref.
*/
refBackendWireShape?: boolean;
}
| {
// Direct iOS selector dispatch: no runtime result exists, only the raw
// runner payload; identity extras are a declared gap on that path.
kind: 'runner-payload';
targetKind: InteractionRuntimeResult['kind'];
data: Record<string, unknown>;
point: { x: number; y: number };
};
Expand Down Expand Up @@ -67,37 +62,48 @@ export function buildInteractionResponseData(params: {
}): InteractionResponsePayloads {
const { source, referenceFrame, extra } = params;
if (source.kind === 'runner-payload') {
const payload = buildTouchVisualizationResult({
const commonExtra = {
targetKind: source.targetKind,
...(extra ?? {}),
};
const result = buildTouchPayload({
data: source.data,
fallbackX: source.point.x,
fallbackY: source.point.y,
referenceFrame,
extra,
extra: commonExtra,
});
const responseData = buildTouchPayload({
data: sanitizeWireBackendData(source.data),
fallbackX: source.point.x,
fallbackY: source.point.y,
referenceFrame,
extra: commonExtra,
});
return { result: payload, responseData: payload };
return { result, responseData };
}

const { result } = source;
const visualization = buildTouchVisualizationResult({
const resultExtra = interactionResultExtra(result);
const commonExtra = {
targetKind: result.kind,
...resultExtra,
...(extra ?? {}),
};
const visualization = buildTouchPayload({
data: result.backendResult,
fallbackX: result.point?.x,
fallbackY: result.point?.y,
referenceFrame,
extra: {
...interactionResultExtra(result),
...(extra ?? {}),
},
extra: commonExtra,
});
const responseData = buildTouchPayload({
data: sanitizeWireBackendData(result.backendResult),
fallbackX: result.point?.x,
fallbackY: result.point?.y,
referenceFrame,
extra: commonExtra,
});
const responseData =
source.refBackendWireShape && result.kind === 'ref'
? {
...(result.backendResult ?? {
ref: stripAtPrefix(result.target?.kind === 'ref' ? result.target.ref : undefined),
...(result.point ? { x: result.point.x, y: result.point.y } : {}),
}),
...interactionResultExtra(result),
}
: visualization;
const warning = composeResponseWarning(
'warning' in result ? result.warning : undefined,
params.staleRefsWarning,
Expand All @@ -117,7 +123,7 @@ function composeResponseWarning(
return resultWarning ? `${resultWarning} ${staleRefsWarning}` : staleRefsWarning;
}

function buildTouchVisualizationResult(params: {
function buildTouchPayload(params: {
data: Record<string, unknown> | undefined;
fallbackX?: number;
fallbackY?: number;
Expand All @@ -128,40 +134,96 @@ function buildTouchVisualizationResult(params: {
const message =
buildTouchMessage(extra, fallbackX, fallbackY) ??
(typeof data?.message === 'string' ? data.message : undefined);
return {
return stripUndefinedFields({
...(data ?? {}),
...(fallbackX === undefined || fallbackY === undefined ? {} : { x: fallbackX, y: fallbackY }),
...(referenceFrame ?? {}),
...(extra ?? {}),
...(data ?? {}),
...successText(message),
};
});
}

function stripUndefinedFields(data: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined));
}

function sanitizeWireBackendData(
data: Record<string, unknown> | undefined,
): Record<string, unknown> | undefined {
if (!data) return undefined;
const sanitized = Object.fromEntries(
Object.entries(data).filter(([key, value]) => shouldKeepWireBackendField(key, value)),
);
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}

function shouldKeepWireBackendField(key: string, value: unknown): boolean {
switch (key) {
case 'gestureStartUptimeMs':
case 'gestureEndUptimeMs':
case 'sequenceResults':
return false;
case 'count':
return value !== 1;
case 'intervalMs':
case 'holdMs':
case 'jitterPx':
return value !== 0;
case 'doubleTap':
return value !== false;
default:
return true;
}
}

function buildTouchMessage(
extra: Record<string, unknown> | undefined,
x: number | undefined,
y: number | undefined,
): string | undefined {
if (typeof extra?.text === 'string') {
return `Filled ${Array.from(extra.text).length} chars`;
}
const ref = typeof extra?.ref === 'string' ? extra.ref : undefined;
if (!ref) return undefined;
const pointSuffix = x === undefined || y === undefined ? '' : ` (${x}, ${y})`;
return buildRefTouchMessage(ref, extra ?? {}, pointSuffix);
const fillText = readString(extra, 'text');
if (fillText !== undefined) return `Filled ${Array.from(fillText).length} chars`;

const pointSuffix = buildPointSuffix(x, y);
const label = buildTouchTargetLabel(extra);
if (label) return buildTouchTargetMessage(label, extra ?? {}, pointSuffix);
if (!pointSuffix) return undefined;

return buildPointTouchMessage(extra, pointSuffix);
}

function readString(data: Record<string, unknown> | undefined, key: string): string | undefined {
const value = data?.[key];
return typeof value === 'string' ? value : undefined;
}

function buildPointSuffix(x: number | undefined, y: number | undefined): string {
return x === undefined || y === undefined ? '' : ` (${x}, ${y})`;
}

function buildTouchTargetLabel(extra: Record<string, unknown> | undefined): string | undefined {
const ref = readString(extra, 'ref');
return ref === undefined ? readString(extra, 'selector') : `@${ref}`;
}

function buildPointTouchMessage(
extra: Record<string, unknown> | undefined,
pointSuffix: string,
): string {
return extra?.gesture === 'longpress' ? `Long pressed${pointSuffix}` : `Tapped${pointSuffix}`;
}

function buildRefTouchMessage(
ref: string,
function buildTouchTargetMessage(
label: string,
extra: Record<string, unknown>,
pointSuffix: string,
): string {
const button = typeof extra.button === 'string' ? extra.button : undefined;
if (extra.gesture === 'longpress') {
return `Long pressed @${ref}${pointSuffix}`;
return `Long pressed ${label}${pointSuffix}`;
}
if (button && button !== 'primary') {
return `Clicked ${button} @${ref}${pointSuffix}`;
return `Clicked ${button} ${label}${pointSuffix}`;
}
return `Tapped @${ref}${pointSuffix}`;
return `Tapped ${label}${pointSuffix}`;
}
8 changes: 2 additions & 6 deletions src/daemon/handlers/interaction-touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ async function dispatchDirectIosSelectorInteraction(params: {
const actionFinishedAt = Date.now();
const point = readPointFromDirectSelectorTapResult(data);
const { result, responseData } = buildInteractionResponseData({
source: { kind: 'runner-payload', data, point },
source: { kind: 'runner-payload', targetKind: 'selector', data, point },
referenceFrame: readReferenceFrameFromDirectSelectorTapResult(data),
extra: {
...extra,
Expand Down Expand Up @@ -484,11 +484,7 @@ async function dispatchFillViaRuntime(
? undefined
: readSnapshotNodesReferenceFrame(session.snapshot?.nodes ?? []);
return buildInteractionResponseData({
// refBackendWireShape keeps the historical fill @ref wire response
// (backendResult + identity extras) while the shared builder owns the
// extras — the hand-rolled version of this branch dropped evidence
// (PR #1064 review).
source: { kind: 'runtime', result, refBackendWireShape: true },
source: { kind: 'runtime', result },
referenceFrame,
extra: { text: parsedTarget.text },
staleRefsWarning,
Expand Down
Loading
Loading