|
1 | 1 | import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; |
2 | 2 | import { sleep } from '@agent-device/host-kit/retry'; |
3 | | -import type { PostGestureAction } from '@agent-device/kernel/snapshot'; |
| 3 | +import type { PostGestureAction, PostGestureOutcome } from '@agent-device/kernel/snapshot'; |
4 | 4 |
|
5 | 5 | /** |
6 | 6 | * Pure post-gesture stability mechanics: the quiet-window polling loop and the |
@@ -59,41 +59,37 @@ export type PostGestureStabilityHooks<T, S extends readonly unknown[]> = { |
59 | 59 |
|
60 | 60 | export type PostGestureStabilityOutcome<T> = { |
61 | 61 | value: T; |
62 | | - /** Present when the deadline expired while the last two captures still disagreed. */ |
63 | | - unsettledGesture?: PostGestureAction; |
64 | 62 | /** |
65 | | - * Present ONLY when the accept-stale verdict is corroborated by full-surface |
66 | | - * evidence (`surfacesIdentical`). The bare verdict is NOT enough — it is |
67 | | - * subset-tolerant by design, and a successful scroll that replaced every |
68 | | - * list cell under fixed chrome still reads accept-stale (#1601 review P1). |
69 | | - * Callers surface this to the agent: a diagnostics-only signal let one |
70 | | - * benchmark run burn 40 calls re-issuing scrolls that moved nothing (#1600). |
| 63 | + * `unsettled` when the deadline expired while the last two captures still disagreed. |
| 64 | + * `no-effect` ONLY when the accept-stale verdict is corroborated by full-surface evidence |
| 65 | + * (`surfacesIdentical`): the bare verdict is subset-tolerant by design, and a successful scroll |
| 66 | + * that replaced every list cell under fixed chrome still reads accept-stale (#1601 review P1). |
| 67 | + * Callers surface it to the agent: a diagnostics-only signal let one benchmark run burn 40 calls |
| 68 | + * re-issuing scrolls that moved nothing (#1600). |
71 | 69 | */ |
72 | | - gestureNoEffect?: PostGestureAction; |
| 70 | + postGestureOutcome?: PostGestureOutcome; |
73 | 71 | }; |
74 | 72 |
|
75 | | -function describePostGestureAction(gesture: PostGestureAction): string { |
76 | | - return [gesture.action, ...gesture.positionals].join(' ').trim(); |
77 | | -} |
78 | | - |
79 | 73 | /** |
80 | | - * The agent-facing wording for a proven no-effect gesture. Names the exact |
81 | | - * gesture, admits the honest ambiguity (at-edge is a legitimate no-op the |
82 | | - * platform cannot distinguish), and hands over the one escape hatch that |
83 | | - * moved a stuck list when synthesized scrolls did not (#1600, element-18: |
84 | | - * raw `swipe` worked where scroll/fling/pan all silently no-opped). |
| 74 | + * The agent-facing sentence for a post-gesture outcome, true whether the read that carries it found |
| 75 | + * its target or not. A no-effect gesture admits the honest ambiguity (at-edge is a legitimate no-op |
| 76 | + * the platform cannot distinguish) and hands over the escape hatch that moved a stuck list when |
| 77 | + * synthesized scrolls did not (#1600: raw `swipe` worked where scroll/fling/pan all no-opped). |
85 | 78 | */ |
86 | | -export function formatGestureNoEffectWarning(action: string, positionals: string[]): string { |
87 | | - return ( |
88 | | - `${describePostGestureAction({ action, positionals })} produced no visible change: the tree still matches its pre-gesture state. ` + |
89 | | - 'Either the container is already at its edge, or it ignores synthesized scrolls — ' + |
90 | | - 'a raw drag moves such lists: swipe x1 y1 x2 y2 (start inside the list).' |
91 | | - ); |
| 79 | +export function formatPostGestureOutcomeWarning({ kind, gesture }: PostGestureOutcome): string { |
| 80 | + const named = [gesture.action, ...gesture.positionals].join(' ').trim(); |
| 81 | + return kind === 'unsettled' |
| 82 | + ? `The surface was still changing after ${named} when this tree was read, so it may not match where the surface comes to rest: an element missing from it is not proof of absence.` |
| 83 | + : `${named} produced no visible change: the tree still matches its pre-gesture state. ` + |
| 84 | + 'Either the container is already at its edge, or it ignores synthesized scrolls — ' + |
| 85 | + 'a raw drag moves such lists: swipe x1 y1 x2 y2 (start inside the list).'; |
92 | 86 | } |
93 | 87 |
|
94 | | -/** A miss on a tree read while a gesture's surface was still changing is not proof of absence. */ |
95 | | -export function formatGestureUnsettledWarning(gesture: PostGestureAction): string { |
96 | | - return `The surface was still changing after ${describePostGestureAction(gesture)} when this was read, so an element missing from it may still be on screen. Read again before treating it as absent.`; |
| 88 | +function postGestureOutcome( |
| 89 | + kind: PostGestureOutcome['kind'], |
| 90 | + pending: PostGestureAction, |
| 91 | +): PostGestureOutcome { |
| 92 | + return { kind, gesture: { action: pending.action, positionals: pending.positionals } }; |
97 | 93 | } |
98 | 94 |
|
99 | 95 | /** |
@@ -199,10 +195,7 @@ export async function runPostGestureStabilityLoop<T, S extends readonly unknown[ |
199 | 195 | }, |
200 | 196 | }); |
201 | 197 | if (lastPairAgreed) return { value: previous.value }; |
202 | | - return { |
203 | | - value: previous.value, |
204 | | - unsettledGesture: { action: pending.action, positionals: pending.positionals }, |
205 | | - }; |
| 198 | + return { value: previous.value, postGestureOutcome: postGestureOutcome('unsettled', pending) }; |
206 | 199 | } |
207 | 200 |
|
208 | 201 | type CapturedSurface<T, S> = { |
@@ -266,13 +259,7 @@ function buildAcceptedOutcome<T, S extends readonly unknown[]>( |
266 | 259 | baselineSignature !== undefined && |
267 | 260 | hooks.surfacesIdentical(baselineSignature, current.signature) |
268 | 261 | ) { |
269 | | - return { |
270 | | - value: current.value, |
271 | | - gestureNoEffect: { |
272 | | - action: pending.action, |
273 | | - positionals: pending.positionals, |
274 | | - }, |
275 | | - }; |
| 262 | + return { value: current.value, postGestureOutcome: postGestureOutcome('no-effect', pending) }; |
276 | 263 | } |
277 | 264 | emitDiagnostic({ |
278 | 265 | level: 'info', |
|
0 commit comments