things-api 0.19.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/dist/audit/schema.d.ts +42 -10
- package/dist/audit/schema.js +21 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/op-result.js +25 -7
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +19 -1
- package/dist/cli/commands/repeat-flags.js +71 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/rescue.d.ts +12 -0
- package/dist/cli/commands/rescue.js +83 -0
- package/dist/cli/commands/rescue.js.map +1 -0
- package/dist/cli/commands/writes.js +86 -32
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +25 -25
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +12 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +9 -1
- package/dist/cli/resolve-invocation.js +71 -0
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/unknown-command.d.ts +26 -0
- package/dist/cli/unknown-command.js +135 -0
- package/dist/cli/unknown-command.js.map +1 -0
- package/dist/contracts.d.ts +4 -3
- package/dist/contracts.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +62 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +11 -0
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +15 -0
- package/dist/model/mappers.js.map +1 -1
- package/dist/op-result.d.ts +24 -2
- package/dist/op-result.js +77 -20
- package/dist/op-result.js.map +1 -1
- package/dist/process-instance.d.ts +41 -0
- package/dist/process-instance.js +82 -0
- package/dist/process-instance.js.map +1 -0
- package/dist/rescue.d.ts +247 -0
- package/dist/rescue.js +811 -0
- package/dist/rescue.js.map +1 -0
- package/dist/session-grant.js +4 -15
- package/dist/session-grant.js.map +1 -1
- package/dist/ui-state.d.ts +8 -4
- package/dist/ui-state.js +8 -4
- package/dist/ui-state.js.map +1 -1
- package/dist/write/batch.js +4 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/clone.js +12 -6
- package/dist/write/clone.js.map +1 -1
- package/dist/write/commands.d.ts +9 -0
- package/dist/write/commands.js +135 -50
- package/dist/write/commands.js.map +1 -1
- package/dist/write/disclosures.d.ts +275 -0
- package/dist/write/disclosures.js +277 -0
- package/dist/write/disclosures.js.map +1 -0
- package/dist/write/experimental.d.ts +16 -0
- package/dist/write/experimental.js +19 -0
- package/dist/write/experimental.js.map +1 -1
- package/dist/write/failure-hints.js +3 -2
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +102 -3
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +4 -7
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +85 -1
- package/dist/write/lock.js +121 -6
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +2 -1
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/operations.d.ts +52 -6
- package/dist/write/operations.js +22 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +69 -8
- package/dist/write/opid.js +116 -20
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.js +14 -2
- package/dist/write/param-schema.js.map +1 -1
- package/dist/write/pipeline.d.ts +82 -3
- package/dist/write/pipeline.js +204 -60
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.d.ts +7 -0
- package/dist/write/planner.js +12 -0
- package/dist/write/planner.js.map +1 -1
- package/dist/write/preserve-modified.d.ts +17 -8
- package/dist/write/preserve-modified.js.map +1 -1
- package/dist/write/promote-clone.js +106 -53
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +45 -39
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +25 -1
- package/dist/write/repeat-rule.js +69 -0
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.js +104 -78
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +2 -2
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/spawn-expectation.d.ts +135 -0
- package/dist/write/spawn-expectation.js +237 -0
- package/dist/write/spawn-expectation.js.map +1 -0
- package/dist/write/template-mutation.js +34 -35
- package/dist/write/template-mutation.js.map +1 -1
- package/dist/write/undo.js +45 -5
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/simulator.js +48 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +12 -0
- package/dist/write/vectors/ui-recipes.js +11 -1
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui-state.d.ts +64 -6
- package/dist/write/vectors/ui-state.js +91 -4
- package/dist/write/vectors/ui-state.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +69 -3
- package/dist/write/vectors/ui.js +293 -146
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +26 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -2
- package/dist/write/verify/delta.js +46 -16
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +3 -2
- package/dist/write/verify/poller.js +3 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +1 -2
- package/skills/things-cli/SKILL.md +4 -2
- package/skills/things-cli/references/repeating.md +1 -1
- package/dist/cli/commands/ui-state.d.ts +0 -11
- package/dist/cli/commands/ui-state.js +0 -27
- package/dist/cli/commands/ui-state.js.map +0 -1
package/dist/write/vectors/ui.js
CHANGED
|
@@ -35,37 +35,50 @@ import { createReachabilityCache, H_UI_SESSION_UNREACHABLE, probeSessionReachabi
|
|
|
35
35
|
import { certificationOf } from "./ui-certification.js";
|
|
36
36
|
import { chordCommand, driveHeadingChordReorder } from "./ui-chord.js";
|
|
37
37
|
import { driveSidebarAreaReorder, jxaSidebarSnapshotScript } from "./ui-drag.js";
|
|
38
|
-
import { AX_DIALOG_SHELL_SNIPPET, CENSUS_TIMEOUT_MS, censusUnverifiable, describeFocusOwner, describeUnprovenProbes, readUiState, SYNC_GATE_WARNING, THINGS_PROCESS, } from "./ui-state.js";
|
|
38
|
+
import { AX_DIALOG_SHELL_SNIPPET, axFocusGuardPrelude, CENSUS_TIMEOUT_MS, censusUnverifiable, describeFocusOwner, describeUnprovenProbes, GUARD_REFUSED_TAG, parseGuardLog, readUiState, SYNC_GATE_WARNING, THINGS_PROCESS, } from "./ui-state.js";
|
|
39
39
|
/** GUI driving can stall on an unanswered sheet; give each step headroom. */
|
|
40
40
|
const STEP_TIMEOUT_MS = 15_000;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
* after the PERF2 audit: the control a mode switch reveals takes ~462ms to appear
|
|
44
|
-
* on the golden (S5b, [docs/lab/perf2-step-latency.md]), which EXCEEDS this
|
|
45
|
-
* interval — so a 300ms poll catches it on its second round; a finer interval
|
|
46
|
-
* would only add osascript hops for a marginal detection gain (UIC6 confirmed).
|
|
47
|
-
*/
|
|
48
|
-
const WAIT_POLL_MS = 300;
|
|
49
|
-
/**
|
|
50
|
-
* How long `resolveStepPath` polls a candidate-addressed control before failing
|
|
42
|
+
* How long a candidate-addressed control is polled for before the step fails
|
|
51
43
|
* closed. The full-vocabulary dialog reveals a pop-up/field a beat AFTER the
|
|
52
44
|
* frequency/Ends switch that precedes it (UIC6: ~250 ms), so the effective-form
|
|
53
|
-
* resolution must poll, not snap once.
|
|
45
|
+
* resolution must poll, not snap once. Since DRVLAT1 (issue #633) that poll runs
|
|
46
|
+
* IN-SCRIPT, inside the hop that acts on the control ({@link axCandidatePrelude}),
|
|
47
|
+
* rather than as its own osascript round-trip per candidate per round.
|
|
54
48
|
*/
|
|
55
49
|
const RESOLVE_CANDIDATE_TIMEOUT_MS = 5_000;
|
|
56
50
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
51
|
+
* In-script poll cadence for the element waits the drive folded into their own
|
|
52
|
+
* hops (DRVLAT1, issue #633). The JS-side poll it replaces was kept at a coarse
|
|
53
|
+
* 300ms deliberately — it paid a PROCESS SPAWN per round (PERF2 S5b), so a finer
|
|
54
|
+
* interval bought detection at the price of hops. An in-script poll pays one
|
|
55
|
+
* addressed `exists` per round, so it can be fine enough that a wait ends when
|
|
56
|
+
* the element lands rather than at the next 300ms boundary.
|
|
57
|
+
*/
|
|
58
|
+
const IN_SCRIPT_POLL_S = 0.05;
|
|
59
|
+
/**
|
|
60
|
+
* How long the canary and the eligibility assertion poll for the menu bar to
|
|
61
|
+
* repopulate around the newly-selected target (UIC1: the Items ▸ Repeat submenu
|
|
62
|
+
* appears only once a matching item is selected, and the update is not
|
|
63
|
+
* instantaneous).
|
|
64
|
+
*
|
|
65
|
+
* This REPLACES the fixed post-preamble settle (DRVLAT1, issue #633). That settle
|
|
66
|
+
* was 1500ms, trimmed to 1000ms by PERF2 against a measured ~92ms median / 116ms
|
|
67
|
+
* max menu repopulation (S5a) — i.e. it spent ~900ms of every drive waiting out a
|
|
68
|
+
* margin, on every host, whether or not the menu was already there. The closed-loop
|
|
69
|
+
* form is strictly better on both axes: it proceeds the moment the menu answers
|
|
70
|
+
* (the common case, ~one poll), and it tolerates a host slower than any fixed
|
|
71
|
+
* settle would have covered. Under-margining still only ever costs a fail-closed
|
|
72
|
+
* refusal, never a bad write (determinism doctrine; BEEP1 shape-settle precedent).
|
|
73
|
+
*/
|
|
74
|
+
const MENU_SETTLE_TIMEOUT_MS = 4_000;
|
|
75
|
+
/**
|
|
76
|
+
* Default window for a click's post-condition wait when the step names none.
|
|
77
|
+
* Every recipe that asserts one sets 5000 explicitly; this only keeps the
|
|
78
|
+
* in-script poll comfortably INSIDE the hop's own {@link STEP_TIMEOUT_MS}, which
|
|
79
|
+
* a 15s default would not (the poll would outlive its transport).
|
|
67
80
|
*/
|
|
68
|
-
const
|
|
81
|
+
const WAIT_ASSERT_TIMEOUT_MS = 5_000;
|
|
69
82
|
/**
|
|
70
83
|
* A shape-dependent step reached without the dialog having been measured — a
|
|
71
84
|
* recipe bug (the `probe-dialog-shape` step is missing or ran after its
|
|
@@ -299,6 +312,82 @@ end fgAssertFront`;
|
|
|
299
312
|
export function axResolveScript(path) {
|
|
300
313
|
return `${SE} to return (exists (${path}))`;
|
|
301
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* The AppleScript variable a folded candidate resolution binds the live element
|
|
317
|
+
* to. Every addressed script takes its target as `(<path>)`, so handing it this
|
|
318
|
+
* name — with {@link axCandidatePrelude} in front — makes the resolution and the
|
|
319
|
+
* action ONE hop instead of two (DRVLAT1, issue #633).
|
|
320
|
+
*/
|
|
321
|
+
export const STEP_ELEMENT_REF = "fgStepEl";
|
|
322
|
+
/**
|
|
323
|
+
* The message a folded candidate resolution raises when NONE of a step's expected
|
|
324
|
+
* element shapes appeared. Byte-identical to the driver's own wording for the
|
|
325
|
+
* separate-hop resolution it replaces, so a report reads the same either way.
|
|
326
|
+
*/
|
|
327
|
+
export const CANDIDATES_MISSED = "none of its expected element shapes resolved (neither the attached sheet nor the " +
|
|
328
|
+
"detached repeat editor window)";
|
|
329
|
+
/**
|
|
330
|
+
* IN-SCRIPT CANDIDATE RESOLUTION (DRVLAT1, issue #633).
|
|
331
|
+
*
|
|
332
|
+
* A candidate-addressed step used to dispatch one `resolve` hop PER CANDIDATE PER
|
|
333
|
+
* POLL ROUND before the hop that acted on whichever answered — a process spawn
|
|
334
|
+
* each, on top of a 300ms JS-side poll floor. This prelude does the same work in
|
|
335
|
+
* the acting hop's own process: it polls the candidates in the SAME priority
|
|
336
|
+
* order, binds the first that exists to {@link STEP_ELEMENT_REF}, and fails closed
|
|
337
|
+
* with {@link CANDIDATES_MISSED} when the window elapses with none of them there.
|
|
338
|
+
*
|
|
339
|
+
* Collapsing it is also strictly better against TOCTOU: the element the script
|
|
340
|
+
* acts on is the one it just proved exists, with nothing dispatched in between.
|
|
341
|
+
*/
|
|
342
|
+
export function axCandidatePrelude(paths, timeoutMs = RESOLVE_CANDIDATE_TIMEOUT_MS) {
|
|
343
|
+
const probes = paths
|
|
344
|
+
.map((p) => ` try
|
|
345
|
+
if (exists (${p})) then set ${STEP_ELEMENT_REF} to (${p})
|
|
346
|
+
end try
|
|
347
|
+
if ${STEP_ELEMENT_REF} is not missing value then exit repeat`)
|
|
348
|
+
.join("\n");
|
|
349
|
+
return `set ${STEP_ELEMENT_REF} to missing value
|
|
350
|
+
set fgT0 to (current date)
|
|
351
|
+
${SE}
|
|
352
|
+
repeat
|
|
353
|
+
${probes}
|
|
354
|
+
if ((current date) - fgT0) is greater than or equal to ${pollSeconds(timeoutMs)} then exit repeat
|
|
355
|
+
delay ${IN_SCRIPT_POLL_S}
|
|
356
|
+
end repeat
|
|
357
|
+
end tell
|
|
358
|
+
if ${STEP_ELEMENT_REF} is missing value then error "${escapeAppleScript(CANDIDATES_MISSED)}"`;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* wait: poll for ANY of the awaited element shapes to appear, IN-SCRIPT (DRVLAT1).
|
|
362
|
+
* One hop for the whole wait instead of one per candidate per round — which is
|
|
363
|
+
* what a slow host paid most: the dialog the drive is waiting for is exactly the
|
|
364
|
+
* thing that is slow when the app is busy. Returns "true", or "false" when the
|
|
365
|
+
* window elapses with none of them present (the driver's abort path is unchanged).
|
|
366
|
+
*/
|
|
367
|
+
export function axWaitAnyScript(paths, timeoutMs) {
|
|
368
|
+
const probes = paths
|
|
369
|
+
.map((p) => ` try
|
|
370
|
+
if (exists (${p})) then return "true"
|
|
371
|
+
end try`)
|
|
372
|
+
.join("\n");
|
|
373
|
+
return `set fgT0 to (current date)
|
|
374
|
+
${SE}
|
|
375
|
+
repeat
|
|
376
|
+
${probes}
|
|
377
|
+
if ((current date) - fgT0) is greater than or equal to ${pollSeconds(timeoutMs)} then return "false"
|
|
378
|
+
delay ${IN_SCRIPT_POLL_S}
|
|
379
|
+
end repeat
|
|
380
|
+
end tell`;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* A poll window in whole seconds, as the in-script `current date` deadline reads
|
|
384
|
+
* it (AppleScript dates carry second granularity). Never below 1: a sub-second
|
|
385
|
+
* window would make a loop that checks its deadline after the FIRST probe into a
|
|
386
|
+
* single-shot check, which is the race these loops exist to close.
|
|
387
|
+
*/
|
|
388
|
+
function pollSeconds(timeoutMs) {
|
|
389
|
+
return Math.max(1, Math.round(timeoutMs / 1000));
|
|
390
|
+
}
|
|
302
391
|
/** press: AXPress the element. */
|
|
303
392
|
export function axPressScript(path) {
|
|
304
393
|
return `${SE} to click (${path})`;
|
|
@@ -821,12 +910,20 @@ export function axSelectPopupScript(path, value) {
|
|
|
821
910
|
*/
|
|
822
911
|
export function axSelectPopupCandidatesScript(path, values) {
|
|
823
912
|
const list = values.map((v) => `"${escapeAppleScript(v)}"`).join(", ");
|
|
913
|
+
// The menu is WAITED FOR, not slept on (DRVLAT1, issue #633): the old loop paid
|
|
914
|
+
// a flat 0.3s after every click before it would look again, so the common case —
|
|
915
|
+
// one click, menu up in well under that — spent the remainder of the settle
|
|
916
|
+
// doing nothing. The click cadence is unchanged (one click per round, never a
|
|
917
|
+
// second click into a menu that is opening — BEEP1); only the looking is finer.
|
|
824
918
|
return `${SE}
|
|
825
919
|
set pu to (${path})
|
|
826
920
|
repeat 20 times
|
|
827
921
|
if (exists menu 1 of pu) then exit repeat
|
|
828
922
|
click pu
|
|
829
|
-
|
|
923
|
+
repeat 6 times
|
|
924
|
+
if (exists menu 1 of pu) then exit repeat
|
|
925
|
+
delay ${IN_SCRIPT_POLL_S}
|
|
926
|
+
end repeat
|
|
830
927
|
end repeat
|
|
831
928
|
repeat with candidate in {${list}}
|
|
832
929
|
if (exists menu item candidate of menu 1 of pu) then
|
|
@@ -1257,26 +1354,37 @@ return "NOMATCH"`;
|
|
|
1257
1354
|
* naming expected vs observed. Pure System Events + Things scripting, background-
|
|
1258
1355
|
* capable. One stable command shape per primitive.
|
|
1259
1356
|
*/
|
|
1260
|
-
export function axAssertEligibleScript(targetUuid, menuItemPath) {
|
|
1357
|
+
export function axAssertEligibleScript(targetUuid, menuItemPath, settleMs = MENU_SETTLE_TIMEOUT_MS) {
|
|
1261
1358
|
const u = escapeAppleScript(targetUuid);
|
|
1262
|
-
return `set
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
set
|
|
1273
|
-
tell application "
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
end tell
|
|
1278
|
-
if
|
|
1279
|
-
return "
|
|
1359
|
+
return `set t0 to (current date)
|
|
1360
|
+
set verdict to my aeCheck()
|
|
1361
|
+
repeat until verdict is "OK"
|
|
1362
|
+
if ((current date) - t0) is greater than or equal to ${pollSeconds(settleMs)} then exit repeat
|
|
1363
|
+
delay ${IN_SCRIPT_POLL_S}
|
|
1364
|
+
set verdict to my aeCheck()
|
|
1365
|
+
end repeat
|
|
1366
|
+
return verdict
|
|
1367
|
+
|
|
1368
|
+
on aeCheck()
|
|
1369
|
+
set selIds to {}
|
|
1370
|
+
tell application "Things3"
|
|
1371
|
+
try
|
|
1372
|
+
set selIds to id of selected to dos
|
|
1373
|
+
end try
|
|
1374
|
+
end tell
|
|
1375
|
+
if (count of selIds) is 0 then return "NOTSEL no to-do is selected after the reveal (expected ${u}) — the show URL navigated without selecting an eligible row"
|
|
1376
|
+
if (count of selIds) is greater than 1 then return "NOTSEL " & (count of selIds) & " to-dos are selected, expected exactly the target ${u}"
|
|
1377
|
+
set theId to (item 1 of selIds) as text
|
|
1378
|
+
if theId is not "${u}" then return "WRONGSEL the selected to-do is " & theId & ", expected the target ${u}"
|
|
1379
|
+
set repEnabled to false
|
|
1380
|
+
tell application "System Events" to tell process "Things3"
|
|
1381
|
+
try
|
|
1382
|
+
set repEnabled to enabled of ${menuItemPath}
|
|
1383
|
+
end try
|
|
1384
|
+
end tell
|
|
1385
|
+
if repEnabled is false then return "DISABLED the target ${u} is selected but its Repeat menu item is disabled (not an eligible row for this action)"
|
|
1386
|
+
return "OK"
|
|
1387
|
+
end aeCheck`;
|
|
1280
1388
|
}
|
|
1281
1389
|
/** activate: foreground Things (the fallback preamble step). */
|
|
1282
1390
|
export function axActivateScript() {
|
|
@@ -1613,7 +1721,7 @@ async function clickCancel(run) {
|
|
|
1613
1721
|
const center = parseFrameCenter(frameRes.stdout);
|
|
1614
1722
|
if (center === null)
|
|
1615
1723
|
return false;
|
|
1616
|
-
const res = await run(
|
|
1724
|
+
const res = await run(uiClickPointCommand(center.x, center.y, "click the open dialog's Cancel button"), STEP_TIMEOUT_MS);
|
|
1617
1725
|
return res.ok;
|
|
1618
1726
|
}
|
|
1619
1727
|
/**
|
|
@@ -1872,7 +1980,12 @@ $.CGEventPost($.kCGHIDEventTap, mev(1)); sleep(15);
|
|
|
1872
1980
|
$.CGEventPost($.kCGHIDEventTap, mev(2));`;
|
|
1873
1981
|
}
|
|
1874
1982
|
/** The command that posts an AX-resolved mouse click (one stable JXA shape). */
|
|
1875
|
-
|
|
1983
|
+
/**
|
|
1984
|
+
* A synthesized HID click at a screen point. Exported (issue #640) so the rescue
|
|
1985
|
+
* path's Cancel fallback and the drive's own click are literally the same shape
|
|
1986
|
+
* — a second construction site is a second place for the event sequence to drift.
|
|
1987
|
+
*/
|
|
1988
|
+
export function uiClickPointCommand(x, y, label) {
|
|
1876
1989
|
return { primitive: "click-point", label, lang: "javascript", script: jxaClickScript(x, y) };
|
|
1877
1990
|
}
|
|
1878
1991
|
/**
|
|
@@ -2060,13 +2173,28 @@ async function defaultRun(command, timeoutMs) {
|
|
|
2060
2173
|
}
|
|
2061
2174
|
/**
|
|
2062
2175
|
* Read the live window/focus census through the shipped dispatch seam — the
|
|
2063
|
-
* transport behind the
|
|
2176
|
+
* transport behind the window-state diagnostic (src/ui-state.ts) and, in tests,
|
|
2064
2177
|
* behind any injected runner. READ-ONLY: the census clicks nothing, types
|
|
2065
2178
|
* nothing, and changes no state; see src/write/vectors/ui-state.ts.
|
|
2066
2179
|
*/
|
|
2067
2180
|
export function readLiveUiState(run = defaultRun) {
|
|
2068
2181
|
return readUiState(run, CENSUS_TIMEOUT_MS);
|
|
2069
2182
|
}
|
|
2183
|
+
/**
|
|
2184
|
+
* The production dispatch seam itself, exported (issue #640).
|
|
2185
|
+
*
|
|
2186
|
+
* The drive owns its own runner and always will. But `things rescue` presses a
|
|
2187
|
+
* button on a dialog no drive opened, so it needs the SAME transport — the
|
|
2188
|
+
* deputy routing, the reveal/osascript split, the timeout handling — without
|
|
2189
|
+
* borrowing the drive's recipe machinery. Re-declaring a second dispatcher would
|
|
2190
|
+
* mean a second place for the deputy contract to drift, which is the one thing
|
|
2191
|
+
* this seam exists to prevent.
|
|
2192
|
+
*
|
|
2193
|
+
* Nothing else changes: this is `defaultRun` under a name a caller outside the
|
|
2194
|
+
* vector can read, and every injected-runner test seam still works exactly as
|
|
2195
|
+
* before.
|
|
2196
|
+
*/
|
|
2197
|
+
export const defaultUiRunner = defaultRun;
|
|
2070
2198
|
/**
|
|
2071
2199
|
* Wrap the dispatch seam so every osascript hop is recorded. The last-dispatched
|
|
2072
2200
|
* step is noted on the in-flight-write marker (so a SIGTERM/SIGINT can name it,
|
|
@@ -2186,24 +2314,32 @@ export function judgeFocusGuard(state, expectedSheet, label) {
|
|
|
2186
2314
|
* state, taken immediately before the hop, and the in-script assertions close
|
|
2187
2315
|
* the remaining milliseconds (UI-automation determinism doctrine; the #595
|
|
2188
2316
|
* pre-commit audit and BEEP1 shape-settle are the same pattern).
|
|
2317
|
+
*
|
|
2318
|
+
* FOLDED for keystroke-class hops (DRVLAT1, issue #633). Those hops are
|
|
2319
|
+
* AppleScript, so the census can be — and now is — the PRELUDE OF THE VERY SCRIPT
|
|
2320
|
+
* THAT TYPES ({@link axFocusGuardPrelude}) rather than a hop of its own. Two
|
|
2321
|
+
* things improve at once: the drive stops paying a process spawn per typed
|
|
2322
|
+
* control, and the TOCTOU window between "the census approved this" and "the
|
|
2323
|
+
* keystroke went out" closes to nothing — no dispatch happens in between, because
|
|
2324
|
+
* there is nothing in between. The judgement is still made BEFORE the keystroke
|
|
2325
|
+
* (in-script, fail-closed) and the sentence a caller reads is still built HERE by
|
|
2326
|
+
* {@link judgeFocusGuard}, from the census that same hop logged, so there remains
|
|
2327
|
+
* exactly one wording of every refusal.
|
|
2328
|
+
*
|
|
2329
|
+
* POINTER-class hops keep the separate census: they dispatch JXA, which cannot
|
|
2330
|
+
* carry an AppleScript prelude.
|
|
2189
2331
|
*/
|
|
2190
2332
|
function guardedRun(inner, latch) {
|
|
2191
2333
|
return async (command, timeoutMs) => {
|
|
2192
|
-
|
|
2334
|
+
const keystroke = KEYSTROKE_CLASS.has(command.primitive);
|
|
2335
|
+
if (!keystroke && !POINTER_CLASS.has(command.primitive)) {
|
|
2193
2336
|
return inner(command, timeoutMs);
|
|
2194
2337
|
}
|
|
2195
|
-
const state = await readUiState(inner, CENSUS_TIMEOUT_MS);
|
|
2196
|
-
// An inspection that would not answer poisons every later inspection's
|
|
2197
|
-
// credibility, so the cleanup ladder is told once and never asks again
|
|
2198
|
-
// (issue #629).
|
|
2199
|
-
if (state === null || censusUnverifiable(state))
|
|
2200
|
-
latch.inspectionStalled = true;
|
|
2201
2338
|
// The dialog invariant applies to keystroke-class hops only: a pointer hop
|
|
2202
2339
|
// is aimed at a frame it resolved a moment ago and fails closed on its own
|
|
2203
2340
|
// if that frame moved.
|
|
2204
|
-
const expected =
|
|
2205
|
-
const
|
|
2206
|
-
if (guardRefusal !== null) {
|
|
2341
|
+
const expected = keystroke ? latch.sheet : null;
|
|
2342
|
+
const refuse = (state, why) => {
|
|
2207
2343
|
trace(() => ({
|
|
2208
2344
|
phase: "focus-guard",
|
|
2209
2345
|
event: "refused",
|
|
@@ -2214,10 +2350,41 @@ function guardedRun(inner, latch) {
|
|
|
2214
2350
|
inspectable: state?.inspectable ?? false,
|
|
2215
2351
|
stalled: state?.stalledProbes ?? null,
|
|
2216
2352
|
}));
|
|
2217
|
-
return { ok: false, stdout: "", stderr:
|
|
2353
|
+
return { ok: false, stdout: "", stderr: why };
|
|
2354
|
+
};
|
|
2355
|
+
// An inspection that would not answer poisons every later inspection's
|
|
2356
|
+
// credibility, so the cleanup ladder is told once and never asks again
|
|
2357
|
+
// (issue #629).
|
|
2358
|
+
const noteStall = (state) => {
|
|
2359
|
+
if (state === null || censusUnverifiable(state))
|
|
2360
|
+
latch.inspectionStalled = true;
|
|
2361
|
+
};
|
|
2362
|
+
// Latched only on a census that APPROVED the hop — a dialog seen while
|
|
2363
|
+
// refusing is, by construction, not the one this drive is driving.
|
|
2364
|
+
const latchSheet = (state) => {
|
|
2365
|
+
if (state !== null && state.sheetOpen && latch.sheet === null)
|
|
2366
|
+
latch.sheet = state.sheetKind;
|
|
2367
|
+
};
|
|
2368
|
+
if (keystroke && command.lang !== "javascript" && typeof command.script === "string") {
|
|
2369
|
+
const res = await inner({ ...command, script: `${axFocusGuardPrelude(expected)}\n${command.script}` }, timeoutMs);
|
|
2370
|
+
const { state, stderr } = parseGuardLog(res.stderr);
|
|
2371
|
+
noteStall(state);
|
|
2372
|
+
if (!res.ok && stderr.includes(GUARD_REFUSED_TAG)) {
|
|
2373
|
+
// The script refused. Re-judge the census it logged for the sentence; a
|
|
2374
|
+
// census too damaged to re-judge still refuses — the hop already did.
|
|
2375
|
+
return refuse(state, judgeFocusGuard(state, expected, command.label) ??
|
|
2376
|
+
`refused to run "${command.label}": the window and focus state could not be read, so ` +
|
|
2377
|
+
"there is no proof the input would reach Things — nothing was sent");
|
|
2378
|
+
}
|
|
2379
|
+
latchSheet(state);
|
|
2380
|
+
return { ...res, stderr };
|
|
2218
2381
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2382
|
+
const state = await readUiState(inner, CENSUS_TIMEOUT_MS);
|
|
2383
|
+
noteStall(state);
|
|
2384
|
+
const guardRefusal = judgeFocusGuard(state, expected, command.label);
|
|
2385
|
+
if (guardRefusal !== null)
|
|
2386
|
+
return refuse(state, guardRefusal);
|
|
2387
|
+
latchSheet(state);
|
|
2221
2388
|
return inner(command, timeoutMs);
|
|
2222
2389
|
};
|
|
2223
2390
|
}
|
|
@@ -2295,40 +2462,43 @@ function canaryPaths(recipe) {
|
|
|
2295
2462
|
}
|
|
2296
2463
|
return out;
|
|
2297
2464
|
}
|
|
2298
|
-
/**
|
|
2299
|
-
* Resolve a step's effective element path. A `pathCandidates` step dispatches
|
|
2300
|
-
* against the FIRST candidate that exists (the dialog-form disjunction — attached
|
|
2301
|
-
* sheet vs detached AXUnknown window, UIC4-a). The candidates are POLLED over a
|
|
2302
|
-
* bounded window because the full-vocabulary controls are REVEALED by the
|
|
2303
|
-
* preceding step: switching the frequency pop-up to weekly/monthly/yearly (or
|
|
2304
|
-
* ticking Ends=after) re-lays-out the cadence group, and the new pop-up/field
|
|
2305
|
-
* lands ~250 ms later (UIC6). A single immediate exists-check races that render
|
|
2306
|
-
* and would spuriously fail closed; polling matches the `dynamic` nature these
|
|
2307
|
-
* steps already declare. Returns null when none resolve within the window.
|
|
2308
|
-
*/
|
|
2309
|
-
async function resolveStepPath(step, run) {
|
|
2310
|
-
if (step.pathCandidates === undefined)
|
|
2311
|
-
return step.path ?? null;
|
|
2312
|
-
const candidates = step.pathCandidates;
|
|
2313
|
-
const deadline = Date.now() + (step.timeoutMs ?? RESOLVE_CANDIDATE_TIMEOUT_MS);
|
|
2314
|
-
for (;;) {
|
|
2315
|
-
for (const candidate of candidates) {
|
|
2316
|
-
// candidates are tried in priority order; the first hit wins, so a race would blur which form matched
|
|
2317
|
-
const res = await run({ primitive: "resolve", label: step.label, script: axResolveScript(candidate) }, STEP_TIMEOUT_MS);
|
|
2318
|
-
if (res.ok && res.stdout.trim() === "true")
|
|
2319
|
-
return candidate;
|
|
2320
|
-
}
|
|
2321
|
-
if (Date.now() >= deadline)
|
|
2322
|
-
return null;
|
|
2323
|
-
// the revealed control lands a beat after the mode switch; poll until it does
|
|
2324
|
-
await new Promise((r) => setTimeout(r, WAIT_POLL_MS));
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
2465
|
function refusal(detail) {
|
|
2328
2466
|
return { exitCode: 1, stdout: "", stderr: detail };
|
|
2329
2467
|
}
|
|
2330
|
-
/**
|
|
2468
|
+
/**
|
|
2469
|
+
* Compile one recipe step into its primitive command (no dispatch).
|
|
2470
|
+
*
|
|
2471
|
+
* A CANDIDATE-ADDRESSED step compiles to ONE script that resolves its own element
|
|
2472
|
+
* and then acts on it (DRVLAT1, issue #633): the addressed body is generated
|
|
2473
|
+
* against {@link STEP_ELEMENT_REF} and {@link axCandidatePrelude} is prepended, so
|
|
2474
|
+
* the resolution that used to be a separate `resolve` hop (or several) now rides
|
|
2475
|
+
* the acting hop. Steps whose script is JXA, or that resolve their own target,
|
|
2476
|
+
* are unaffected.
|
|
2477
|
+
*/
|
|
2331
2478
|
export function commandForStep(step, targetUuid) {
|
|
2479
|
+
if (step.primitive === "wait") {
|
|
2480
|
+
// The whole wait is ONE hop: the candidates are polled in-script until one of
|
|
2481
|
+
// them exists or the step's own window elapses (DRVLAT1).
|
|
2482
|
+
const paths = step.pathCandidates ?? [step.path ?? ""];
|
|
2483
|
+
return {
|
|
2484
|
+
primitive: "wait",
|
|
2485
|
+
label: step.label,
|
|
2486
|
+
script: axWaitAnyScript(paths, step.timeoutMs ?? STEP_TIMEOUT_MS),
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
if (step.pathCandidates !== undefined && step.path === undefined) {
|
|
2490
|
+
const candidates = step.pathCandidates;
|
|
2491
|
+
const inner = commandForStep({ ...step, path: STEP_ELEMENT_REF }, targetUuid);
|
|
2492
|
+
// Only an AppleScript body can take the AppleScript prelude; a JXA step
|
|
2493
|
+
// (set-datetime, the pointer primitives) resolves its own target anyway.
|
|
2494
|
+
if (inner.lang === "javascript" || typeof inner.script !== "string" || inner.script === "") {
|
|
2495
|
+
return inner;
|
|
2496
|
+
}
|
|
2497
|
+
return {
|
|
2498
|
+
...inner,
|
|
2499
|
+
script: `${axCandidatePrelude(candidates, RESOLVE_CANDIDATE_TIMEOUT_MS)}\n${inner.script}`,
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2332
2502
|
switch (step.primitive) {
|
|
2333
2503
|
case "reveal":
|
|
2334
2504
|
return { primitive: "reveal", label: step.label, url: revealUrl(step.value ?? targetUuid) };
|
|
@@ -2418,8 +2588,6 @@ export function commandForStep(step, targetUuid) {
|
|
|
2418
2588
|
// shape-selected group pop-up index of the first weekday row (RDLG2).
|
|
2419
2589
|
script: axConvergeWeekdaysScript(step.path ?? "", weekdayBaseOf(step.value ?? ""), weekdayTitlesOf(step.value ?? "")),
|
|
2420
2590
|
};
|
|
2421
|
-
case "wait":
|
|
2422
|
-
return { primitive: "wait", label: step.label, script: axResolveScript(step.path ?? "") };
|
|
2423
2591
|
case "select-row":
|
|
2424
2592
|
return {
|
|
2425
2593
|
primitive: "select-row",
|
|
@@ -2524,7 +2692,7 @@ async function driveClickElement(step, run) {
|
|
|
2524
2692
|
"or the app is not in the expected state; no click was sent",
|
|
2525
2693
|
};
|
|
2526
2694
|
}
|
|
2527
|
-
const clickRes = await run(
|
|
2695
|
+
const clickRes = await run(uiClickPointCommand(center.x, center.y, step.label), STEP_TIMEOUT_MS);
|
|
2528
2696
|
if (!clickRes.ok) {
|
|
2529
2697
|
return {
|
|
2530
2698
|
ok: false,
|
|
@@ -2535,12 +2703,13 @@ async function driveClickElement(step, run) {
|
|
|
2535
2703
|
};
|
|
2536
2704
|
}
|
|
2537
2705
|
if (step.assertPath !== undefined) {
|
|
2538
|
-
|
|
2706
|
+
// one hop, polled in-script (DRVLAT1)
|
|
2707
|
+
const res = await run({
|
|
2539
2708
|
primitive: "wait",
|
|
2540
2709
|
label: step.assertLabel ?? step.label,
|
|
2541
|
-
script:
|
|
2542
|
-
},
|
|
2543
|
-
if (!ok) {
|
|
2710
|
+
script: axWaitAnyScript([step.assertPath], step.assertTimeoutMs ?? WAIT_ASSERT_TIMEOUT_MS),
|
|
2711
|
+
}, STEP_TIMEOUT_MS);
|
|
2712
|
+
if (!(res.ok && res.stdout.trim() === "true")) {
|
|
2544
2713
|
return {
|
|
2545
2714
|
ok: false,
|
|
2546
2715
|
why: `${step.assertLabel ?? "the expected element"} did not appear after the click`,
|
|
@@ -2705,6 +2874,7 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
2705
2874
|
exitCode: 1,
|
|
2706
2875
|
stdout: `ui drive watchdog stopped after ${done.length} step(s): ${done.join(" → ") || "nothing"}`,
|
|
2707
2876
|
stderr: `ui drive exceeded its ${Math.round(budgetMs / 1000)}s budget at "${lastStep}"`,
|
|
2877
|
+
steps: [...done],
|
|
2708
2878
|
timedOut: true,
|
|
2709
2879
|
watchdog: {
|
|
2710
2880
|
budgetMs,
|
|
@@ -2738,7 +2908,11 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
2738
2908
|
// reachable to be driven — which is not the app accepting a command and
|
|
2739
2909
|
// changing nothing, and must not be reported as that.
|
|
2740
2910
|
const cause = clear?.state === "cleared-blind" ? "unreachable" : stepTimedOut ? "unresponsive" : null;
|
|
2741
|
-
|
|
2911
|
+
// The step list rides EVERY partial exit: a failure always carries the
|
|
2912
|
+
// play-by-play, which is what made the field bug reports rich (#632). The
|
|
2913
|
+
// step that stopped the drive is named as the last entry so the list reads
|
|
2914
|
+
// as the whole attempt, not only the part that worked.
|
|
2915
|
+
const res = { ...refusal(base + cleanup), steps: [...done, `${failed} — FAILED: ${why}`] };
|
|
2742
2916
|
if (cause === null)
|
|
2743
2917
|
return res;
|
|
2744
2918
|
return {
|
|
@@ -2772,9 +2946,9 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
2772
2946
|
done.push(step.label);
|
|
2773
2947
|
idx += 1;
|
|
2774
2948
|
}
|
|
2775
|
-
//
|
|
2776
|
-
|
|
2777
|
-
|
|
2949
|
+
// The menu bar repopulates around the new selection a beat after the preamble
|
|
2950
|
+
// (UIC1). That beat is WAITED OUT IN THE CANARY below, which polls each element
|
|
2951
|
+
// it must resolve — no fixed settle stands here any more (DRVLAT1, issue #633).
|
|
2778
2952
|
// 0½. Session-reachability GATE for dialog-class ops (SESSGATE, #480). A recipe
|
|
2779
2953
|
// that opens a sheet on the main window needs that window AX-reachable on
|
|
2780
2954
|
// the current Space. Probed AFTER the preamble (which surfaces a window in a
|
|
@@ -2816,7 +2990,13 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
2816
2990
|
// pressed. (This is also the localization check: English titles must resolve.)
|
|
2817
2991
|
for (const { path, label } of canaryPaths(recipe)) {
|
|
2818
2992
|
// the canary resolves elements one at a time; a single miss aborts before anything is pressed, so parallelizing would waste work and blur which element failed
|
|
2819
|
-
|
|
2993
|
+
// POLLED, not snapped: this is where the drive waits out the menu-bar
|
|
2994
|
+
// repopulation the preamble triggered (DRVLAT1 — it replaces the fixed settle).
|
|
2995
|
+
const res = await run({
|
|
2996
|
+
primitive: "resolve",
|
|
2997
|
+
label,
|
|
2998
|
+
script: axWaitAnyScript([path], MENU_SETTLE_TIMEOUT_MS),
|
|
2999
|
+
}, STEP_TIMEOUT_MS);
|
|
2820
3000
|
if (!res.ok || res.stdout.trim() !== "true") {
|
|
2821
3001
|
// (e) A leftover modal sheet/popover from an earlier aborted drive disables
|
|
2822
3002
|
// the menu bar, so the Items ▸ Repeat path cannot resolve. Detect that
|
|
@@ -2877,9 +3057,11 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
2877
3057
|
return watchdogResult(step.label);
|
|
2878
3058
|
if (step.primitive === "wait") {
|
|
2879
3059
|
// A candidate-addressed wait polls for ANY of its shapes to appear (the
|
|
2880
|
-
// dialog opening as an attached sheet OR a detached AXUnknown window)
|
|
3060
|
+
// dialog opening as an attached sheet OR a detached AXUnknown window) — the
|
|
3061
|
+
// whole poll inside ONE hop (DRVLAT1).
|
|
2881
3062
|
// steps are strictly sequential: this wait must resolve before the step that acts on the awaited element runs
|
|
2882
|
-
const
|
|
3063
|
+
const res = await run(commandForStep(step, recipe.targetUuid), STEP_TIMEOUT_MS);
|
|
3064
|
+
const ok = res.ok && res.stdout.trim() === "true";
|
|
2883
3065
|
if (!ok) {
|
|
2884
3066
|
// the abort keystroke must land (and be verified) before returning the partial-state report
|
|
2885
3067
|
const clear = await clearNow();
|
|
@@ -2917,19 +3099,11 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
2917
3099
|
done.push(`${step.label} (${outcome.detail})`);
|
|
2918
3100
|
continue;
|
|
2919
3101
|
}
|
|
2920
|
-
//
|
|
2921
|
-
//
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
if (effective === null) {
|
|
2926
|
-
// dismiss whatever opened (and verify) before reporting
|
|
2927
|
-
const clear = await clearNow();
|
|
2928
|
-
return partial(step.label, "none of its expected element shapes resolved (neither the attached sheet nor the " +
|
|
2929
|
-
"detached repeat editor window)", clear);
|
|
2930
|
-
}
|
|
2931
|
-
step = { ...step, path: effective };
|
|
2932
|
-
}
|
|
3102
|
+
// A candidate-addressed step resolves its effective element (the
|
|
3103
|
+
// sheet-vs-detached-window disjunction) INSIDE its own script now — see
|
|
3104
|
+
// commandForStep / axCandidatePrelude. A miss raises CANDIDATES_MISSED there
|
|
3105
|
+
// and lands on this step's ordinary failure path, with the same wording and
|
|
3106
|
+
// the same clean abort it had when the resolution was its own hop (DRVLAT1).
|
|
2933
3107
|
const command = commandForStep(step, recipe.targetUuid);
|
|
2934
3108
|
if (step.primitive === "probe-dialog-shape") {
|
|
2935
3109
|
// MEASURE the dialog (RDLG2) before any shape-dependent control is touched.
|
|
@@ -3045,39 +3219,12 @@ async function drive(recipe, rawRun, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS,
|
|
|
3045
3219
|
exitCode: 0,
|
|
3046
3220
|
stdout: `${relocationNote}drove ${done.length} step(s): ${done.join(" → ")}`,
|
|
3047
3221
|
stderr: "",
|
|
3222
|
+
// The same play-by-play as a LIST (#632). `stdout` keeps the prose form the
|
|
3223
|
+
// trace and the transport-failure paths already read; `steps` is what the
|
|
3224
|
+
// change-history record stores and `--verbose` renders.
|
|
3225
|
+
steps: relocationNote === "" ? [...done] : [relocationNote.trim(), ...done],
|
|
3048
3226
|
};
|
|
3049
3227
|
}
|
|
3050
|
-
async function waitForElement(command, timeoutMs, run) {
|
|
3051
|
-
const deadline = Date.now() + timeoutMs;
|
|
3052
|
-
for (;;) {
|
|
3053
|
-
// polling the same element until it appears is inherently sequential
|
|
3054
|
-
const res = await run(command, STEP_TIMEOUT_MS);
|
|
3055
|
-
if (res.ok && res.stdout.trim() === "true")
|
|
3056
|
-
return true;
|
|
3057
|
-
if (Date.now() >= deadline)
|
|
3058
|
-
return false;
|
|
3059
|
-
// inter-poll delay between sequential existence checks
|
|
3060
|
-
await new Promise((r) => setTimeout(r, WAIT_POLL_MS));
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
/** Poll until ANY of the candidate element shapes exists (the sheet-vs-detached-window disjunction). */
|
|
3064
|
-
async function waitForAnyElement(paths, label, timeoutMs, run) {
|
|
3065
|
-
const deadline = Date.now() + timeoutMs;
|
|
3066
|
-
for (;;) {
|
|
3067
|
-
for (const path of paths) {
|
|
3068
|
-
// Emitted as the `wait` primitive (not `resolve`) so the command stream a
|
|
3069
|
-
// caller observes is unchanged from the single-path waitForElement.
|
|
3070
|
-
// candidates checked in priority order; the first present shape ends the wait
|
|
3071
|
-
const res = await run({ primitive: "wait", label, script: axResolveScript(path) }, STEP_TIMEOUT_MS);
|
|
3072
|
-
if (res.ok && res.stdout.trim() === "true")
|
|
3073
|
-
return true;
|
|
3074
|
-
}
|
|
3075
|
-
if (Date.now() >= deadline)
|
|
3076
|
-
return false;
|
|
3077
|
-
// inter-poll delay between sequential existence checks
|
|
3078
|
-
await new Promise((r) => setTimeout(r, WAIT_POLL_MS));
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
3228
|
function enabledMatrix() {
|
|
3082
3229
|
const matrix = {};
|
|
3083
3230
|
for (const op of UI_DRIVE_OPS) {
|