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.
- 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/cli/commands/doctor.js +28 -8
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/ui-state.d.ts +11 -0
- package/dist/cli/commands/ui-state.js +27 -0
- package/dist/cli/commands/ui-state.js.map +1 -0
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +2 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/contracts.d.ts +2 -2
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/osa.d.ts +42 -2
- package/dist/deputy/osa.js +79 -5
- package/dist/deputy/osa.js.map +1 -1
- package/dist/deputy/routing.d.ts +18 -0
- package/dist/deputy/routing.js +25 -0
- package/dist/deputy/routing.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ui-state.d.ts +46 -0
- package/dist/ui-state.js +107 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/write/accessibility-probe.js +4 -1
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.js +7 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/failure-hints.d.ts +7 -1
- package/dist/write/failure-hints.js +24 -0
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/field-limits.d.ts +100 -0
- package/dist/write/field-limits.js +144 -0
- package/dist/write/field-limits.js.map +1 -0
- package/dist/write/param-schema.d.ts +17 -0
- package/dist/write/param-schema.js +64 -4
- package/dist/write/param-schema.js.map +1 -1
- package/dist/write/pipeline.js +30 -0
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/promote-clone.d.ts +26 -0
- package/dist/write/promote-clone.js +201 -23
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/vectors/types.d.ts +25 -3
- package/dist/write/vectors/ui-state.d.ts +245 -0
- package/dist/write/vectors/ui-state.js +452 -0
- package/dist/write/vectors/ui-state.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +136 -4
- package/dist/write/vectors/ui.js +665 -65
- package/dist/write/vectors/ui.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +1 -0
- 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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
/**
|