things-api 0.19.1 → 0.19.3

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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  3. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  5. package/dist/cli/commands/doctor.js +28 -8
  6. package/dist/cli/commands/doctor.js.map +1 -1
  7. package/dist/cli/commands/ui-state.d.ts +11 -0
  8. package/dist/cli/commands/ui-state.js +27 -0
  9. package/dist/cli/commands/ui-state.js.map +1 -0
  10. package/dist/cli/help.js +2 -0
  11. package/dist/cli/help.js.map +1 -1
  12. package/dist/cli/main.js +2 -0
  13. package/dist/cli/main.js.map +1 -1
  14. package/dist/contracts.d.ts +2 -2
  15. package/dist/contracts.js +1 -1
  16. package/dist/contracts.js.map +1 -1
  17. package/dist/deputy/osa.d.ts +42 -2
  18. package/dist/deputy/osa.js +79 -5
  19. package/dist/deputy/osa.js.map +1 -1
  20. package/dist/deputy/routing.d.ts +18 -0
  21. package/dist/deputy/routing.js +25 -0
  22. package/dist/deputy/routing.js.map +1 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/ui-state.d.ts +46 -0
  27. package/dist/ui-state.js +107 -0
  28. package/dist/ui-state.js.map +1 -0
  29. package/dist/write/accessibility-probe.js +4 -1
  30. package/dist/write/accessibility-probe.js.map +1 -1
  31. package/dist/write/automation-probe.js +7 -1
  32. package/dist/write/automation-probe.js.map +1 -1
  33. package/dist/write/failure-hints.d.ts +7 -1
  34. package/dist/write/failure-hints.js +24 -0
  35. package/dist/write/failure-hints.js.map +1 -1
  36. package/dist/write/field-limits.d.ts +100 -0
  37. package/dist/write/field-limits.js +144 -0
  38. package/dist/write/field-limits.js.map +1 -0
  39. package/dist/write/param-schema.d.ts +17 -0
  40. package/dist/write/param-schema.js +64 -4
  41. package/dist/write/param-schema.js.map +1 -1
  42. package/dist/write/pipeline.js +30 -0
  43. package/dist/write/pipeline.js.map +1 -1
  44. package/dist/write/promote-clone.d.ts +26 -0
  45. package/dist/write/promote-clone.js +201 -23
  46. package/dist/write/promote-clone.js.map +1 -1
  47. package/dist/write/vectors/types.d.ts +25 -3
  48. package/dist/write/vectors/ui-state.d.ts +245 -0
  49. package/dist/write/vectors/ui-state.js +452 -0
  50. package/dist/write/vectors/ui-state.js.map +1 -0
  51. package/dist/write/vectors/ui.d.ts +136 -4
  52. package/dist/write/vectors/ui.js +665 -65
  53. package/dist/write/vectors/ui.js.map +1 -1
  54. package/package.json +1 -1
  55. package/schema/envelope.schema.json +1 -0
  56. package/skills/things-cli/SKILL.md +2 -2
@@ -0,0 +1,452 @@
1
+ /** How deep a stack of dialogs the walk will follow before giving up. */
2
+ const MAX_SHEET_DEPTH = 6;
3
+ /**
4
+ * Resolve the dialog IN FRONT — the innermost of a stack — and record how deep
5
+ * the stack goes. Written as a snippet because the census and the dismissal must
6
+ * agree on which dialog they mean, to the element.
7
+ *
8
+ * Sheets STACK (MODALX1 §6, golden-v4 / 3.23): a dialog raised while another is
9
+ * standing becomes an `AXSheet` CHILD of the one below it, not a sibling on the
10
+ * window — measured with two `things:///add` URL-consent alerts nesting inside a
11
+ * Repeat sheet — and dismissal is strictly LIFO. So a census that reads only the
12
+ * window's own `sheet 1` sees the bottom of the stack and misidentifies what
13
+ * actually owns the screen, and a dismissal aimed there presses a button behind
14
+ * a modal. Both walk to the top instead.
15
+ *
16
+ * Runs inside a `tell process "Things3"` block; leaves `shellRef` (or
17
+ * `missing value`), `sheetForm` and `sheetDepth` bound.
18
+ */
19
+ export const AX_DIALOG_SHELL_SNIPPET = ` set shellRef to missing value
20
+ set sheetForm to "none"
21
+ set sheetDepth to 0
22
+ try
23
+ -- positional-ok: the one attached sheet a window can present, which is
24
+ -- the BOTTOM of any stack; the walk below climbs to the top.
25
+ set shellRef to sheet 1 of (first window whose subrole is "AXStandardWindow")
26
+ set sheetForm to "attached"
27
+ set sheetDepth to 1
28
+ end try
29
+ if shellRef is missing value then
30
+ try
31
+ set dws to (windows whose subrole is "AXUnknown" and size is not {40, 40})
32
+ if (count of dws) > 0 then
33
+ set shellRef to item 1 of dws
34
+ set sheetForm to "detached"
35
+ set sheetDepth to 1
36
+ end if
37
+ end try
38
+ end if
39
+ if shellRef is not missing value then
40
+ repeat ${MAX_SHEET_DEPTH} times
41
+ set nested to missing value
42
+ try
43
+ if (exists sheet 1 of shellRef) then set nested to sheet 1 of shellRef
44
+ end try
45
+ if nested is missing value then exit repeat
46
+ set shellRef to nested
47
+ set sheetDepth to sheetDepth + 1
48
+ end repeat
49
+ end if`;
50
+ /** Probes whose absence makes the census unusable for a keystroke decision. */
51
+ const CRITICAL_PROBES = ["running", "frontmost", "dialog"];
52
+ /** Every probe name the census can report, for parsing its record back. */
53
+ const KNOWN_PROBES = new Set([
54
+ "running",
55
+ "frontmost",
56
+ "dialog",
57
+ "frontapp",
58
+ "focus",
59
+ ]);
60
+ /**
61
+ * Per-APPLE-EVENT budget for every read in the census (`with timeout of N
62
+ * seconds`). Without one, osascript's default is two MINUTES: a System Events
63
+ * call that does not come back is then bounded only by the caller's own process
64
+ * deadline, which is how issue #629's field incident spent ~15s per inspection
65
+ * and ~56s per drive discovering nothing. With one, a read that will not answer
66
+ * raises AppleScript error -1712 and the census carries on to the next probe —
67
+ * or stops, if the probe was one the caller decides on.
68
+ */
69
+ const PROBE_TIMEOUT_S = 2;
70
+ /**
71
+ * Wall-clock budget for the WHOLE census, checked between probes. `with
72
+ * timeout` bounds one Apple event, not a run of them, so a surface that is
73
+ * merely slow (rather than wedged) could still add up. Past this, the remaining
74
+ * probes are skipped and reported as stalled rather than waited on.
75
+ */
76
+ const CENSUS_BUDGET_S = 8;
77
+ /**
78
+ * The transport deadline for one census hop — the backstop under the in-script
79
+ * budgets, not the mechanism. Deliberately far below the 15s step timeout that
80
+ * bounded the old single-shot script: nothing here may take this long, and if
81
+ * it does the caller must hear about it while the drive can still be aborted
82
+ * cleanly rather than after four of them have gone by.
83
+ */
84
+ export const CENSUS_TIMEOUT_MS = 12_000;
85
+ /** Did a probe the caller has to DECIDE on fail to answer? */
86
+ export function censusUnverifiable(state) {
87
+ if (state === null)
88
+ return true;
89
+ return CRITICAL_PROBES.some((p) => state.stalledProbes.includes(p) || state.failedProbes.includes(p));
90
+ }
91
+ /** Name the probes that did not answer, for a diagnostic someone has to act on. */
92
+ export function describeUnprovenProbes(state) {
93
+ const label = {
94
+ running: "whether Things is running",
95
+ frontmost: "whether Things owns the screen",
96
+ dialog: "which dialog is open",
97
+ frontapp: "which application owns the screen",
98
+ focus: "which element has keyboard focus",
99
+ };
100
+ const parts = [];
101
+ if (state.stalledProbes.length > 0) {
102
+ parts.push(`did not answer in time: ${state.stalledProbes.map((p) => label[p]).join(", ")}`);
103
+ }
104
+ if (state.failedProbes.length > 0) {
105
+ parts.push(`could not be read: ${state.failedProbes.map((p) => label[p]).join(", ")}`);
106
+ }
107
+ return parts.join("; ");
108
+ }
109
+ /** A recognizable token in the script so a test runner can key off the ui-state command. */
110
+ export const UI_STATE_MARKER = "-- ui-state census (read-only)";
111
+ /** The label every ui-state dispatch carries (one stable command shape). */
112
+ export const UI_STATE_LABEL = "read the window and focus state";
113
+ /** The Things application's process name — the frontmost value that means "us". */
114
+ export const THINGS_PROCESS = "Things3";
115
+ /**
116
+ * The census script — ADDRESSED probes, each on its own Apple-event budget
117
+ * (issue #629).
118
+ *
119
+ * WHAT CHANGED, AND WHY IT HAD TO. The 0.19.2 census was one unbounded script
120
+ * that opened with two UNADDRESSED queries: `first application process whose
121
+ * frontmost is true` (the entire process table enumerated) and `value of
122
+ * attribute "AXFocusedUIElement"` on whatever that returned (a system-wide
123
+ * focused-element resolution). Everything else the ui vector runs — every step
124
+ * the field incident's log shows succeeding, inside the very sheet the census
125
+ * could not describe — is addressed: `tell process "Things3" to …`. So the one
126
+ * script that stalled was the one script that left the addressed style, and it
127
+ * stalled on the critical path of BOTH the per-step guard and the cleanup that
128
+ * was supposed to recover from it. Rebuilt here so that:
129
+ *
130
+ * - the decision-critical facts are ADDRESSED and are proven FIRST;
131
+ * - every read carries `with timeout of ${PROBE_TIMEOUT_S} seconds`, so a
132
+ * surface that will not answer costs seconds, not the caller's whole
133
+ * deadline;
134
+ * - a critical probe that does not answer STOPS the census then and there —
135
+ * the remaining probes cannot change what the caller must now do (abort and
136
+ * clean up), so waiting for them is pure latency;
137
+ * - the two unaddressed queries survive only as DECORATION, last, skipped
138
+ * entirely when the addressed probes already answered the question they
139
+ * were there to answer.
140
+ *
141
+ * Every probe still degrades one field rather than failing the census, and what
142
+ * could not be proven is NAMED (`stalled=` / `failed=`) instead of silently
143
+ * reading as a clean "nothing is open".
144
+ */
145
+ export function axUiStateScript() {
146
+ return `${UI_STATE_MARKER}
147
+ set frontName to ""
148
+ set frontIsThings to false
149
+ set focusRole to ""
150
+ set focusSub to ""
151
+ set canInspect to true
152
+ set thingsRunning to false
153
+ set sheetForm to "none"
154
+ set sheetKind to "none"
155
+ set sheetDepth to 0
156
+ set census to ""
157
+ set stalled to ""
158
+ set failed to ""
159
+ set halted to false
160
+ set t0 to (current date)
161
+
162
+ -- P1 running: a name-keyed lookup, no Accessibility round-trip.
163
+ try
164
+ with timeout of ${PROBE_TIMEOUT_S} seconds
165
+ tell application "System Events" to set thingsRunning to (exists application process "${THINGS_PROCESS}")
166
+ end timeout
167
+ on error errMsg number errNum
168
+ if errNum is -1712 then
169
+ set stalled to stalled & "running "
170
+ else
171
+ set failed to failed & "running "
172
+ end if
173
+ set halted to true
174
+ end try
175
+
176
+ -- P2 frontmost, ADDRESSED: the single fact the per-step input guard decides on.
177
+ if (not halted) and thingsRunning then
178
+ try
179
+ with timeout of ${PROBE_TIMEOUT_S} seconds
180
+ tell application "System Events" to tell process "${THINGS_PROCESS}" to set frontIsThings to (frontmost as boolean)
181
+ end timeout
182
+ if frontIsThings then set frontName to "${THINGS_PROCESS}"
183
+ on error errMsg number errNum
184
+ if errNum is -1712 then
185
+ set stalled to stalled & "frontmost "
186
+ else
187
+ set failed to failed & "frontmost "
188
+ end if
189
+ set halted to true
190
+ end try
191
+ end if
192
+
193
+ -- P3 dialog: shell, form, stack depth and control census — all addressed
194
+ -- inside the Things process, which is the shape every working drive step uses.
195
+ if (not halted) and thingsRunning then
196
+ try
197
+ with timeout of ${PROBE_TIMEOUT_S} seconds
198
+ tell application "System Events" to tell process "${THINGS_PROCESS}"
199
+ ${AX_DIALOG_SHELL_SNIPPET}
200
+ if shellRef is not missing value then
201
+ set nCb to -1
202
+ set nPu to -1
203
+ set nBt to -1
204
+ set nGp to -1
205
+ set nTf to -1
206
+ try
207
+ set nCb to (count of checkboxes of shellRef)
208
+ end try
209
+ try
210
+ set nPu to (count of pop up buttons of shellRef)
211
+ end try
212
+ try
213
+ set nBt to (count of buttons of shellRef)
214
+ end try
215
+ try
216
+ set nGp to (count of groups of shellRef)
217
+ end try
218
+ try
219
+ set nTf to (count of text fields of shellRef)
220
+ end try
221
+ set census to "cb:" & nCb & " pu:" & nPu & " bt:" & nBt & " gp:" & nGp & " tf:" & nTf
222
+ set winId to ""
223
+ try
224
+ set winId to (value of attribute "AXIdentifier" of shellRef) as text
225
+ end try
226
+ if winId starts with "MovePopUpDialog-" then
227
+ set sheetKind to "move-picker"
228
+ else if nCb is 2 and nPu is 1 and nBt is 2 and nGp is 1 and nTf is 0 then
229
+ set groupOk to false
230
+ try
231
+ set g to group 1 of shellRef
232
+ if ((count of text fields of g) + (count of pop up buttons of g)) > 0 then set groupOk to true
233
+ end try
234
+ if groupOk then set sheetKind to "repeat"
235
+ end if
236
+ if sheetKind is "none" then set sheetKind to "other"
237
+ end if
238
+ end tell
239
+ end timeout
240
+ on error errMsg number errNum
241
+ if errNum is -1712 then
242
+ set stalled to stalled & "dialog "
243
+ else
244
+ set failed to failed & "dialog "
245
+ end if
246
+ set halted to true
247
+ end try
248
+ end if
249
+
250
+ -- P4 frontapp: the ONLY enumeration left, and it runs only when the addressed
251
+ -- probe has already said the screen is not ours — its whole job is naming the
252
+ -- application in the refusal sentence.
253
+ if (not halted) and (not frontIsThings) then
254
+ if ((current date) - t0) > ${CENSUS_BUDGET_S} then
255
+ set stalled to stalled & "frontapp "
256
+ else
257
+ try
258
+ with timeout of ${PROBE_TIMEOUT_S} seconds
259
+ tell application "System Events" to set frontName to (name of first application process whose frontmost is true) as text
260
+ end timeout
261
+ on error errMsg number errNum
262
+ if errNum is -1712 then
263
+ set stalled to stalled & "frontapp "
264
+ else
265
+ set failed to failed & "frontapp "
266
+ end if
267
+ end try
268
+ end if
269
+ end if
270
+
271
+ -- P5 focus: decoration, and the most expensive read in the census. Addressed at
272
+ -- the process that owns the screen rather than resolved system-wide, run last,
273
+ -- and skipped outright once the budget is spent. An ERROR here (as opposed to a
274
+ -- timeout) is the secure-system-modal signature: macOS exposes no tree for one.
275
+ if not halted then
276
+ if ((current date) - t0) > ${CENSUS_BUDGET_S} then
277
+ set stalled to stalled & "focus "
278
+ else
279
+ set focusTarget to frontName
280
+ if focusTarget is "" then
281
+ set stalled to stalled & "focus "
282
+ else
283
+ try
284
+ with timeout of ${PROBE_TIMEOUT_S} seconds
285
+ tell application "System Events" to tell process focusTarget
286
+ set fe to value of attribute "AXFocusedUIElement"
287
+ -- No focused element is a perfectly ordinary state (an app
288
+ -- with no key window); it is not an unreadable screen.
289
+ if fe is not missing value then
290
+ set focusRole to (role of fe) as text
291
+ try
292
+ set focusSub to (subrole of fe) as text
293
+ end try
294
+ end if
295
+ end tell
296
+ end timeout
297
+ on error errMsg number errNum
298
+ if errNum is -1712 then
299
+ set stalled to stalled & "focus "
300
+ else
301
+ set canInspect to false
302
+ set failed to failed & "focus "
303
+ end if
304
+ end try
305
+ end if
306
+ end if
307
+ end if
308
+
309
+ return "front=" & frontName & linefeed & "isfront=" & frontIsThings & linefeed & "running=" & thingsRunning & linefeed & "form=" & sheetForm & linefeed & "depth=" & sheetDepth & linefeed & "kind=" & sheetKind & linefeed & "census=" & census & linefeed & "role=" & focusRole & linefeed & "subrole=" & focusSub & linefeed & "inspectable=" & canInspect & linefeed & "stalled=" & stalled & linefeed & "failed=" & failed`;
310
+ }
311
+ /**
312
+ * Parse the census record into a {@link UiState}. Returns null when the output
313
+ * carries none of the expected keys (a transport error, or a script that never
314
+ * ran) — every caller treats that as UNKNOWN, never as "all clear".
315
+ */
316
+ export function parseUiState(stdout) {
317
+ const fields = new Map();
318
+ for (const line of stdout.split(/\r?\n/)) {
319
+ const eq = line.indexOf("=");
320
+ if (eq <= 0)
321
+ continue;
322
+ fields.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
323
+ }
324
+ if (!fields.has("kind") || !fields.has("inspectable"))
325
+ return null;
326
+ const probes = (key) => (fields.get(key) ?? "").split(/\s+/).filter((p) => KNOWN_PROBES.has(p));
327
+ const kindRaw = fields.get("kind") ?? "none";
328
+ const sheetKind = kindRaw === "repeat" || kindRaw === "move-picker" || kindRaw === "other" ? kindRaw : "none";
329
+ const formRaw = fields.get("form") ?? "none";
330
+ const sheetForm = formRaw === "attached" || formRaw === "detached" ? formRaw : "none";
331
+ const frontRaw = fields.get("front") ?? "";
332
+ const frontmostApp = frontRaw === "" ? null : frontRaw;
333
+ const role = fields.get("role") ?? "";
334
+ // AppleScript coerces an absent attribute to the literal words "missing
335
+ // value"; that is "no subrole", not a subrole named that.
336
+ const subroleRaw = fields.get("subrole") ?? "";
337
+ const subrole = subroleRaw === "missing value" ? "" : subroleRaw;
338
+ const census = fields.get("census") ?? "";
339
+ const depth = Number(fields.get("depth") ?? "0");
340
+ return {
341
+ thingsRunning: fields.get("running") === "true",
342
+ // Decided by the ADDRESSED probe, not by comparing a name the enumeration
343
+ // may never have been asked for (issue #629).
344
+ thingsFrontmost: fields.get("isfront") === "true",
345
+ frontmostApp,
346
+ sheetOpen: sheetKind !== "none",
347
+ sheetKind,
348
+ sheetForm,
349
+ sheetDepth: Number.isFinite(depth) ? depth : 0,
350
+ sheetControls: census === "" ? null : census,
351
+ focusOwner: frontmostApp === null ? null : { app: frontmostApp, role, subrole: subrole || null },
352
+ inspectable: fields.get("inspectable") === "true",
353
+ stalledProbes: probes("stalled"),
354
+ failedProbes: probes("failed"),
355
+ };
356
+ }
357
+ /**
358
+ * Read the census through the injected runner. Returns null on a transport
359
+ * failure — an UNKNOWN state, which every caller treats fail-closed (the guard
360
+ * refuses; the cleanup path does not send a blind Escape).
361
+ *
362
+ * `timeoutMs` defaults to {@link CENSUS_TIMEOUT_MS}: a census is not a drive
363
+ * step and must not borrow a drive step's patience (issue #629).
364
+ */
365
+ export async function readUiState(run, timeoutMs = CENSUS_TIMEOUT_MS) {
366
+ const res = await run({ primitive: "resolve", label: UI_STATE_LABEL, script: axUiStateScript() }, timeoutMs);
367
+ if (!res.ok)
368
+ return null;
369
+ return parseUiState(res.stdout);
370
+ }
371
+ /**
372
+ * How to name the surface that owns the screen, for a refusal someone has to
373
+ * act on. Names the APPLICATION and the focused element's ROLE — never its
374
+ * contents. An un-inspectable frontmost surface is named as what it is: a
375
+ * system dialog macOS does not expose, which is the one case where the person
376
+ * at the keyboard has to look at the screen themselves.
377
+ */
378
+ export function describeFocusOwner(state) {
379
+ if (state === null)
380
+ return "the window state could not be read";
381
+ // #629: an inspection that did not come back says nothing about who owns the
382
+ // screen — and saying "an unidentified application is frontmost" would read
383
+ // as a measurement. Name the probe that stalled instead.
384
+ if (censusUnverifiable(state)) {
385
+ return `the window state inspection did not complete (${describeUnprovenProbes(state)})`;
386
+ }
387
+ if (!state.inspectable) {
388
+ return ("a system dialog owns the screen — macOS does not expose it to other apps, so it cannot be " +
389
+ `identified from here${state.frontmostApp === null ? "" : ` (frontmost application: ${state.frontmostApp})`}`);
390
+ }
391
+ const app = state.frontmostApp ?? "an unidentified application";
392
+ const role = state.focusOwner?.role ?? "";
393
+ return role === ""
394
+ ? `${app} is frontmost`
395
+ : `${app} is frontmost and keyboard focus is on a ${role}`;
396
+ }
397
+ /**
398
+ * A one-line human summary of the census, for a diagnostic line or a warning.
399
+ *
400
+ * Reports WHAT EACH PROBE PROVED. A stalled probe leaves its clause reading
401
+ * "could not be determined" and is named at the end — never omitted, and never
402
+ * rendered as its default (issue #629: a census that could not see the open
403
+ * Repeat sheet used to report a clean screen).
404
+ */
405
+ export function describeUiState(state) {
406
+ const unproven = (p) => state.stalledProbes.includes(p) || state.failedProbes.includes(p);
407
+ const trailer = () => {
408
+ const unprovenText = describeUnprovenProbes(state);
409
+ return unprovenText === "" ? "" : ` — ${unprovenText}`;
410
+ };
411
+ if (unproven("running")) {
412
+ return `nothing about the screen could be established${trailer()}`;
413
+ }
414
+ if (!state.thingsRunning && !unproven("frontmost")) {
415
+ return `Things is not running${trailer()}`;
416
+ }
417
+ const front = unproven("frontmost")
418
+ ? "whether Things owns the screen could not be determined"
419
+ : state.thingsFrontmost
420
+ ? "Things is frontmost"
421
+ : unproven("frontapp")
422
+ ? "another application is frontmost (it could not be named)"
423
+ : `${state.frontmostApp ?? "an unidentified application"} is frontmost`;
424
+ if (unproven("dialog")) {
425
+ return `${front}; whether a dialog is open in Things could not be determined${trailer()}`;
426
+ }
427
+ const sheet = state.sheetKind === "none"
428
+ ? "no dialog is open in Things"
429
+ : state.sheetKind === "repeat"
430
+ ? `the Repeat dialog is open (${state.sheetForm})`
431
+ : state.sheetKind === "move-picker"
432
+ ? `the Move… picker is open (${state.sheetForm})`
433
+ : `an unrecognized dialog is open in Things (${state.sheetForm})`;
434
+ // A stack means the thing in front is sitting ON another dialog, and each one
435
+ // has to be dismissed in turn (MODALX1 §6).
436
+ const stacked = state.sheetDepth > 1 ? `, on top of ${state.sheetDepth - 1} more` : "";
437
+ return `${front}; ${sheet}${stacked}${trailer()}`;
438
+ }
439
+ /**
440
+ * The warning an OPEN Things dialog earns, wherever it is reported.
441
+ *
442
+ * FIELD-MEASURED (2026-08-27, controlled A/B on the maintainer's Mac): with a
443
+ * Repeat sheet left open, a write landed in the local database but Things Cloud
444
+ * did NOT attempt a sync — not on the write, and not when Things was brought
445
+ * back to the front. Dismissing the sheet released the queued sync immediately,
446
+ * with no further write. A stranded dialog is therefore not cosmetic: it holds
447
+ * the account's sync until someone dismisses it. Recorded in
448
+ * docs/things-app-oddities.md (the open-dialog sync gate).
449
+ */
450
+ export const SYNC_GATE_WARNING = "while a dialog is open in Things the app stops sending changes to Things Cloud — anything " +
451
+ "written on this Mac stays on this Mac until the dialog is dismissed";
452
+ //# sourceMappingURL=ui-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-state.js","sourceRoot":"","sources":["../../../src/write/vectors/ui-state.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { type ThingsApiConfig } from "../../config.ts";
2
2
  import { type UiDriveAux } from "./ui-drag.ts";
3
- import type { UiPrimitive, UiStep, WriteVector } from "./types.ts";
3
+ import { type UiSheetKind, type UiState } from "./ui-state.ts";
4
+ import type { UiClearOutcome, UiPrimitive, UiStep, WriteVector } from "./types.ts";
4
5
  /**
5
6
  * Command-level primitives. Extends the recipe `UiPrimitive` set with the
6
7
  * INTERNAL sub-steps composite recipe steps decompose into: a `click-element`
@@ -12,7 +13,14 @@ import type { UiPrimitive, UiStep, WriteVector } from "./types.ts";
12
13
  */
13
14
  export type UiCommandPrimitive = UiPrimitive | "resolve-frame" | "click-point" | "sidebar-snapshot" | "sidebar-scroll" | "sidebar-drag" | "sidebar-held-drag"
14
15
  /** One modifier-bearing key event pair posted straight at the Things process (ui-chord.ts). */
15
- | "chord-post";
16
+ | "chord-post"
17
+ /**
18
+ * The cleanup ladder's first rung: press the open dialog's own Cancel button
19
+ * (issue #620). Its own primitive rather than a `press` so a recipe's
20
+ * actuations and the cleanup's are never confused — in a trace, in a test, or
21
+ * in the completed-steps trail.
22
+ */
23
+ | "dismiss-dialog";
16
24
  /** A single primitive dispatch — one stable shape per primitive. */
17
25
  export interface UiCommand {
18
26
  primitive: UiCommandPrimitive;
@@ -38,6 +46,21 @@ export interface UiRunResult {
38
46
  * (CLAUDE.md safety rails — the production app is never a valid target).
39
47
  */
40
48
  export type UiRunner = (command: UiCommand, timeoutMs: number) => Promise<UiRunResult>;
49
+ /**
50
+ * The IN-SCRIPT half of the per-step focus guard (issue #620).
51
+ *
52
+ * A synthetic keystroke is not addressed at an element — System Events hands it
53
+ * to whatever application owns the screen at that instant. So every script that
54
+ * types re-asserts, in the same osascript hop that will do the typing, that
55
+ * Things is still frontmost; the drive-level census (see {@link guardedRun})
56
+ * runs a moment earlier and cannot close the last few milliseconds. The
57
+ * assertion FAILS CLOSED and names the application that owns the screen
58
+ * instead — never the contents of its window.
59
+ *
60
+ * This is the cheapest possible check: one System Events property read, no
61
+ * sleeps, no polling (UI-automation determinism doctrine).
62
+ */
63
+ export declare const AX_FOCUS_GUARD_HANDLERS = "on fgFrontApp()\n\tset frontName to \"\"\n\ttry\n\t\ttell application \"System Events\" to set frontName to (name of first application process whose frontmost is true) as text\n\tend try\n\treturn frontName\nend fgFrontApp\n\non fgAssertFront(what)\n\tset f to my fgFrontApp()\n\tif f is \"Things3\" then return true\n\tif f is \"\" then\n\t\terror \"refused to \" & what & \": the frontmost application could not be read, so there is no proof the keystrokes would reach Things \u2014 nothing was typed\"\n\tend if\n\terror \"refused to \" & what & \": \" & f & \" is frontmost, not Things \u2014 a keystroke goes to whatever owns the screen, so nothing was typed\"\nend fgAssertFront";
41
64
  /** resolve-element: does the element exist right now? Returns "true"/"false". */
42
65
  export declare function axResolveScript(path: string): string;
43
66
  /** press: AXPress the element. */
@@ -78,8 +101,25 @@ export declare function axPressScript(path: string): string;
78
101
  * so a retry starts from a clean field without ⌘A. Fail-closed (an `error`, i.e.
79
102
  * a transport failure the pipeline re-verifies) if it never holds — the
80
103
  * create/reschedule delta's rule assertion is the final DB-level authority.
104
+ *
105
+ * READ-BACK FIRST (issue #620 item 7): a field that ALREADY holds the requested
106
+ * value is left alone and the script returns {@link OK_ALREADY} — the whole
107
+ * keystroke class disappears for the defaults, which is most drives (the field
108
+ * incident died typing interval `1` into a field already showing `1`). The skip
109
+ * is proven by TWO reads a settle apart, because the one way a matching value
110
+ * can go stale is the UIC7 re-layout revert, which lands within that window; and
111
+ * whatever this decides, the pre-commit audit ({@link axAuditDialogScript})
112
+ * re-reads every control through its own address before the OK press, so a
113
+ * wrongly-skipped field cannot commit.
81
114
  */
82
115
  export declare function axSetValueScript(path: string, value: string, attempts?: number): string;
116
+ /**
117
+ * What a typing primitive returns when it typed NOTHING because the field
118
+ * already held the requested value (issue #620 item 7). Distinct from "OK" so
119
+ * the drive can disclose the skip — and so a lab cell can assert that no
120
+ * keystroke hop fired.
121
+ */
122
+ export declare const OK_ALREADY = "OK-ALREADY";
83
123
  /**
84
124
  * set-group-number: drive ONE of the Repeat dialog's two numeric fields —
85
125
  * the cadence INTERVAL or the ENDS-AFTER COUNT — addressed by the LABEL ROW it
@@ -421,7 +461,13 @@ export declare function axSelectHeadingRowScript(tablePath: string, ordinal: num
421
461
  export declare function axAssertEligibleScript(targetUuid: string, menuItemPath: string): string;
422
462
  /** activate: foreground Things (the fallback preamble step). */
423
463
  export declare function axActivateScript(): string;
424
- /** key: a space-separated keystroke spec (e.g. "down down return"). */
464
+ /**
465
+ * key: a space-separated keystroke spec (e.g. "down down return").
466
+ *
467
+ * Frontmost-guarded in-script (issue #620): `key code`/`keystroke` reach
468
+ * whatever application owns the screen, so the script refuses — naming that
469
+ * application — rather than firing keys into someone else's window.
470
+ */
425
471
  export declare function axKeyScript(keys: string): string;
426
472
  /**
427
473
  * type-text: send literal text to whatever control holds focus (HXPC1). The
@@ -487,8 +533,40 @@ export declare function axRowCellFrameScript(tablePath: string, description: str
487
533
  * actually offered, so the caller learns what the app was willing to move to.
488
534
  */
489
535
  export declare function axPickerRowFrameScript(pickerPath: string, title: string): string;
490
- /** The abort keystroke sent to dismiss a half-open sheet/popover on failure. */
536
+ /**
537
+ * The abort keystroke, sent ONLY from the audited cleanup ladder (issue #620)
538
+ * and only once that ladder has proven Things owns the screen. It is scoped to
539
+ * the Things process for readability, but scoping is not what makes it safe —
540
+ * a synthetic key goes to whatever is frontmost, which is why the script
541
+ * carries the same in-script frontmost assertion every other keystroke does.
542
+ */
491
543
  export declare function axAbortScript(): string;
544
+ /**
545
+ * Dismiss the open dialog by PRESSING ITS OWN CANCEL BUTTON (issue #620).
546
+ *
547
+ * Preferred over Escape wherever it works, for two independent reasons: an
548
+ * AXPress is addressed at an ELEMENT, so it cannot leak into another
549
+ * application the way a keystroke can, and it works while Things is in the
550
+ * BACKGROUND — the cleanup never has to steal the user's focus to undo its own
551
+ * half-finished dialog. The button is addressed by its pinned English title,
552
+ * exactly like every other selector in this vector, and the dialog shell is
553
+ * resolved the same two ways the census resolves it (attached sheet, or the
554
+ * detached editor window Things presents when it is not frontmost).
555
+ *
556
+ * Returns "OK" after pressing, or a diagnostic ("NO-DIALOG" / "NO-CANCEL") the
557
+ * ladder falls through on — it never claims a dismissal; the caller re-reads
558
+ * the census to decide that.
559
+ */
560
+ export declare function axCancelDialogScript(): string;
561
+ /**
562
+ * The Cancel button's on-screen FRAME, resolved through the same addressed
563
+ * dialog-shell path the AXPress dismissal uses (issue #629). Feeds the pointer
564
+ * fallback: if `AXPress` on the button reports success and the dialog is still
565
+ * standing, a real click at the button's own AX-resolved centre is the next
566
+ * thing to try before discarding the window wholesale. The frame comes from the
567
+ * tree, never from a remembered coordinate, so a moved dialog fails closed.
568
+ */
569
+ export declare function axCancelFrameScript(): string;
492
570
  /**
493
571
  * The PROVEN app-level clearance / relocation maneuver (SESSGATE, #480, live-host
494
572
  * recovery): close the front Things window — which takes an attached modal sheet
@@ -514,6 +592,37 @@ export declare function axCloseReopenActivateScript(): string;
514
592
  * command shape.
515
593
  */
516
594
  export declare function axSheetOpenScript(): string;
595
+ /**
596
+ * The outcome of clearing a half-open dialog after a failed drive:
597
+ * - "none" — the census found no dialog open (nothing to clear);
598
+ * - "dismissed" — it is gone, and a fresh census CONFIRMED that;
599
+ * - "cleared-blind" — the session was AX-blind (locked / off-Space), so no
600
+ * census and no keystroke can be trusted; the PROVEN
601
+ * app-level close+reopen ran to clear it (cannot be
602
+ * AX-confirmed, but the maneuver works blind — SESSGATE);
603
+ * - "foreign" — a dialog is open that this drive did not open, so it was
604
+ * LEFT ALONE (a cleanup must never dismiss the dialog the
605
+ * person at the keyboard opened after our failure);
606
+ * - "may-remain" — ours, and nothing in the ladder would close it
607
+ * (fail-closed: report it precisely, with the sync gate).
608
+ */
609
+ export type ClearOutcome = UiClearOutcome;
610
+ export interface ClearResult {
611
+ state: ClearOutcome;
612
+ /** How it was closed, for the trace and the disclosure. */
613
+ how?: "cancel-button" | "escape" | "window-close";
614
+ /** What the census identified as open at cleanup time. */
615
+ sheetKind?: UiSheetKind;
616
+ /** Who owned the screen when the cleanup started, when it was not Things. */
617
+ focusOwner?: string;
618
+ /**
619
+ * True when the cleanup ran WITHOUT a working window-state inspection (issue
620
+ * #629) — the dismissal and its proof came from addressed reads alone, so the
621
+ * outcome is real but the identity of what was dismissed was taken from what
622
+ * this drive had already observed rather than re-confirmed.
623
+ */
624
+ unverified?: boolean;
625
+ }
517
626
  /**
518
627
  * resolve-frame: read the element's on-screen frame (top-left origin, points)
519
628
  * from the live AX tree and print "x y w h". Used by `click-element` to target
@@ -577,6 +686,29 @@ export declare function parseFrameCenter(stdout: string): {
577
686
  x: number;
578
687
  y: number;
579
688
  } | null;
689
+ /**
690
+ * Read the live window/focus census through the shipped dispatch seam — the
691
+ * transport behind the `ui-state` diagnostic (src/ui-state.ts) and, in tests,
692
+ * behind any injected runner. READ-ONLY: the census clicks nothing, types
693
+ * nothing, and changes no state; see src/write/vectors/ui-state.ts.
694
+ */
695
+ export declare function readLiveUiState(run?: UiRunner): Promise<UiState | null>;
696
+ /**
697
+ * Judge one guard reading. Returns null to proceed, or the refusal sentence —
698
+ * which always names who owns the screen, because that is the one fact the
699
+ * person reading it cannot recover after the fact.
700
+ *
701
+ * Exported for the unit matrix: every branch here is a fail-closed decision
702
+ * about synthetic input, and each one is worth a test.
703
+ */
704
+ export declare function judgeFocusGuard(state: UiState | null, expectedSheet: UiSheetKind | null, label: string): string | null;
705
+ /**
706
+ * The cleanup disclosure (issue #620). Every branch states what is TRUE of the
707
+ * app right now, and — whenever a dialog may still be open — that Things Cloud
708
+ * sync is held until someone dismisses it, which is the consequence a caller
709
+ * cannot see and would otherwise discover hours later on another device.
710
+ */
711
+ export declare function describeCleanup(clear: ClearResult): string;
580
712
  /** Compile one recipe step into its primitive command (no dispatch). */
581
713
  export declare function commandForStep(step: UiStep, targetUuid: string): UiCommand;
582
714
  /**