things-api 0.19.0 → 0.19.2
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 +3 -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 +19 -2
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +166 -4
- package/dist/capability.js +303 -13
- package/dist/capability.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +13 -1
- package/dist/cli/commands/doctor.js +52 -22
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +14 -3
- package/dist/cli/commands/helpers.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/repeat-flags.js +34 -2
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/setup.js +16 -3
- package/dist/cli/commands/setup.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/commands/writes.js +102 -67
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/interrupt.d.ts +54 -6
- package/dist/cli/interrupt.js +78 -12
- package/dist/cli/interrupt.js.map +1 -1
- package/dist/cli/main.js +10 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/contracts.d.ts +4 -4
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +15 -1
- package/dist/deputy/install.js +46 -6
- package/dist/deputy/install.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 +34 -0
- package/dist/deputy/routing.js +95 -0
- package/dist/deputy/routing.js.map +1 -1
- package/dist/deputy/wake.d.ts +61 -0
- package/dist/deputy/wake.js +149 -0
- package/dist/deputy/wake.js.map +1 -0
- package/dist/diagnose.d.ts +6 -4
- package/dist/diagnose.js +20 -4
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +31 -5
- package/dist/direct-setup.js +200 -26
- package/dist/direct-setup.js.map +1 -1
- package/dist/host-access.d.ts +12 -0
- package/dist/host-access.js +12 -0
- package/dist/host-access.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +23 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.js +8 -1
- package/dist/op-result.js.map +1 -1
- package/dist/session-grant.js +16 -1
- package/dist/session-grant.js.map +1 -1
- package/dist/surface-copy.d.ts +11 -3
- package/dist/surface-copy.js +11 -3
- package/dist/surface-copy.js.map +1 -1
- package/dist/ui-state.d.ts +46 -0
- package/dist/ui-state.js +82 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/wizard.js +8 -1
- package/dist/wizard.js.map +1 -1
- 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/availability.d.ts +0 -15
- package/dist/write/availability.js +7 -62
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.js +17 -6
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +10 -6
- package/dist/write/commands.js +120 -31
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +36 -7
- package/dist/write/failure-hints.js +33 -14
- 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/guards.js +3 -2
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.js +9 -12
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +15 -8
- package/dist/write/operations.js +9 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +67 -23
- package/dist/write/opid.js +141 -19
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +17 -44
- package/dist/write/param-schema.js +74 -7
- package/dist/write/param-schema.js.map +1 -1
- package/dist/write/pipeline.d.ts +38 -7
- package/dist/write/pipeline.js +134 -18
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +30 -16
- package/dist/write/pre-state.js +88 -20
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.d.ts +26 -0
- package/dist/write/promote-clone.js +333 -41
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/repeat-collateral.d.ts +31 -0
- package/dist/write/repeat-collateral.js +153 -0
- package/dist/write/repeat-collateral.js.map +1 -0
- package/dist/write/resolution-timestamps.js +25 -2
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/template-mutation.js +163 -16
- package/dist/write/template-mutation.js.map +1 -1
- package/dist/write/vectors/applescript.js +3 -10
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/simulator.js +8 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +236 -4
- package/dist/write/vectors/ui-certification.js +63 -13
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-chord.d.ts +144 -0
- package/dist/write/vectors/ui-chord.js +428 -0
- package/dist/write/vectors/ui-chord.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +8 -4
- package/dist/write/vectors/ui-drag.js +2 -0
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +51 -3
- package/dist/write/vectors/ui-recipes.js +452 -44
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui-state.d.ts +157 -0
- package/dist/write/vectors/ui-state.js +237 -0
- package/dist/write/vectors/ui-state.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +386 -13
- package/dist/write/vectors/ui.js +1543 -124
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +3 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +41 -0
- package/dist/write/verify/delta.js +36 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +4 -1
- package/dist/write/verify/poller.js +14 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +2 -1
- package/skills/things-cli/SKILL.md +2 -2
|
@@ -112,12 +112,20 @@ export function convertToProjectRecipe(op, targetUuid) {
|
|
|
112
112
|
steps: [
|
|
113
113
|
...preamble(targetUuid),
|
|
114
114
|
menuPress("Items ▸ Convert to Project…", `menu item "Convert to Project…" of ${ITEMS_MENU}`),
|
|
115
|
+
// positional-ok: a window presents at most ONE attached sheet at a time, so
|
|
116
|
+
// `sheet 1` is a uniqueness statement, not an index among peers. It is also
|
|
117
|
+
// only ever a WAIT target here — the control acted on inside it is
|
|
118
|
+
// AXIdentifier-addressed below.
|
|
115
119
|
waitFor("the confirmation sheet", `sheet 1 of ${MAIN_WINDOW}`),
|
|
116
120
|
{
|
|
117
121
|
// The alert's primary button carries a stable, locale-proof
|
|
118
122
|
// AXIdentifier "action-button-1" (UIC1); prefer it over the English title.
|
|
123
|
+
// positional-ok: the `sheet 1` container is the one-sheet-per-window law
|
|
124
|
+
// above; the BUTTON is discriminated by AXIdentifier, never by position.
|
|
119
125
|
primitive: "press",
|
|
120
126
|
label: 'confirm — press "Convert"',
|
|
127
|
+
// positional-ok: `sheet 1` is the one attached sheet a window can present;
|
|
128
|
+
// the BUTTON inside it is AXIdentifier-discriminated, never positional.
|
|
121
129
|
path: `(first button of sheet 1 of ${MAIN_WINDOW} whose value of attribute "AXIdentifier" is "action-button-1")`,
|
|
122
130
|
dynamic: true,
|
|
123
131
|
addressing: "axidentifier",
|
|
@@ -137,6 +145,60 @@ export function convertToProjectRecipe(op, targetUuid) {
|
|
|
137
145
|
* DB effect (UI2-d / HEADCERT1): the heading uuid dies, a new type=1 project is
|
|
138
146
|
* promoted into the parent project's area, its children reparent (heading→NULL).
|
|
139
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* Reorder a project's HEADINGS with the arrow chords (CHORDMH1, on the HEADORD1
|
|
150
|
+
* law). Things exposes heading order through four bare keybindings and nothing
|
|
151
|
+
* else — no menu item, no context-menu item, no AX action anywhere carries the
|
|
152
|
+
* equivalent — so the recipe has no element to press: it reveals the project,
|
|
153
|
+
* confirms the content table is there, and hands the whole move to the chord
|
|
154
|
+
* driver, which selects each heading positionally and posts one verified chord
|
|
155
|
+
* at a time (src/write/vectors/ui-chord.ts).
|
|
156
|
+
*
|
|
157
|
+
* NO `activate` step, deliberately. The reveal is a background `open -g`, the
|
|
158
|
+
* row selection is pure System Events, and the chord is posted to the Things
|
|
159
|
+
* PROCESS rather than to the focused surface — so the entire gesture runs with
|
|
160
|
+
* Things behind whatever the user is looking at. Measured end to end on Things
|
|
161
|
+
* 3.23 / golden-v4 with Finder frontmost at every stage and Things never
|
|
162
|
+
* activated at all (docs/lab/chordmh1-move-heading-build.md §1).
|
|
163
|
+
*
|
|
164
|
+
* `needsWindowReachability` is set even though no sheet opens: the heading rows
|
|
165
|
+
* only exist in a rendered project view, so a locked screen or a full-screen
|
|
166
|
+
* Space must refuse (SESSGATE) rather than post chords at a window nothing can
|
|
167
|
+
* read back.
|
|
168
|
+
*/
|
|
169
|
+
export function moveHeadingChordRecipe(projectUuid, targetOrder, movees) {
|
|
170
|
+
const spec = {
|
|
171
|
+
projectUuid,
|
|
172
|
+
targetOrder,
|
|
173
|
+
movees,
|
|
174
|
+
tablePath: PROJECT_CONTENT_TABLE,
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
op: "project.move-heading",
|
|
178
|
+
targetUuid: spec.projectUuid,
|
|
179
|
+
needsWindowReachability: true,
|
|
180
|
+
steps: [
|
|
181
|
+
{
|
|
182
|
+
primitive: "reveal",
|
|
183
|
+
label: "reveal the project in Things (things:///show?id=<project>)",
|
|
184
|
+
value: spec.projectUuid,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
// Canaried: if the project view's content table is not there, the drive
|
|
188
|
+
// refuses before a single chord is posted.
|
|
189
|
+
primitive: "resolve",
|
|
190
|
+
label: "confirm the project view's content table is present",
|
|
191
|
+
path: PROJECT_CONTENT_TABLE,
|
|
192
|
+
addressing: "title",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
primitive: "chord-reorder",
|
|
196
|
+
label: `reorder ${spec.movees.length} heading(s) with the arrow chords`,
|
|
197
|
+
chord: spec,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
140
202
|
export function headingConvertToProjectRecipe(projectUuid, ordinal) {
|
|
141
203
|
return {
|
|
142
204
|
op: "project.promote-heading",
|
|
@@ -165,12 +227,17 @@ export function headingConvertToProjectRecipe(projectUuid, ordinal) {
|
|
|
165
227
|
// With the heading selected, Convert to Project… is enabled (it exists
|
|
166
228
|
// regardless, so the canary resolves it; the press lands post-selection).
|
|
167
229
|
menuPress("Items ▸ Convert to Project…", `menu item "Convert to Project…" of ${ITEMS_MENU}`),
|
|
230
|
+
// positional-ok: one attached sheet per window (see convertToProjectRecipe).
|
|
168
231
|
waitFor("the confirmation sheet", `sheet 1 of ${MAIN_WINDOW}`),
|
|
169
232
|
{
|
|
170
233
|
// The alert's primary button carries the locale-proof AXIdentifier
|
|
171
234
|
// "action-button-1" (UIC1); prefer it over the English title.
|
|
235
|
+
// positional-ok: one attached sheet per window; the button itself is
|
|
236
|
+
// AXIdentifier-discriminated.
|
|
172
237
|
primitive: "press",
|
|
173
238
|
label: 'confirm — press "Convert"',
|
|
239
|
+
// positional-ok: `sheet 1` is the one attached sheet a window can present;
|
|
240
|
+
// the BUTTON inside it is AXIdentifier-discriminated, never positional.
|
|
174
241
|
path: `(first button of sheet 1 of ${MAIN_WINDOW} whose value of attribute "AXIdentifier" is "action-button-1")`,
|
|
175
242
|
dynamic: true,
|
|
176
243
|
addressing: "axidentifier",
|
|
@@ -198,9 +265,16 @@ export function headingConvertToProjectRecipe(projectUuid, ordinal) {
|
|
|
198
265
|
// The reminder-time control is undrivable (its AXDateTimeArea ignores AX writes),
|
|
199
266
|
// so `--reminder` is refused upstream in assertRepeatRule; its recipe step is
|
|
200
267
|
// retained but unreachable.
|
|
201
|
-
/** The content list's table (row 0 = area/Someday header, then projects/to-dos). Confirmed UIC5.
|
|
268
|
+
/** The content list's table (row 0 = area/Someday header, then projects/to-dos). Confirmed UIC5.
|
|
269
|
+
* positional-ok: a CONTAINER handle only — the main window's list area holds one
|
|
270
|
+
* scroll area holding one table (UIC5), and no value is read or written through
|
|
271
|
+
* this path. Every step that uses it resolves its target WITHIN the table by
|
|
272
|
+
* title readback (select-row), by selection-readback ordinal (select-heading-row)
|
|
273
|
+
* or by exact AXDescription (click-element's rowCellDescription walk, HXPC1 §B0). */
|
|
202
274
|
const PROJECT_CONTENT_TABLE = `table 1 of scroll area 1 of ${MAIN_WINDOW}`;
|
|
203
|
-
/** The Repeat editor when Things is frontmost — an attached sheet (interval nested in group 1, UIC1).
|
|
275
|
+
/** The Repeat editor when Things is frontmost — an attached sheet (interval nested in group 1, UIC1).
|
|
276
|
+
* positional-ok: a window presents at most ONE attached sheet at a time, so
|
|
277
|
+
* `sheet 1` names the only sheet there can be rather than picking among peers. */
|
|
204
278
|
const REPEAT_SHEET = `sheet 1 of ${MAIN_WINDOW}`;
|
|
205
279
|
/** The Repeat editor when Things is backgrounded — a detached AXUnknown window (UIC4-a). Its
|
|
206
280
|
* controls sit at the SAME depth as the sheet's (frequency a direct child, interval in group 1) — UIC5-e. */
|
|
@@ -212,10 +286,25 @@ function dualForm(inner) {
|
|
|
212
286
|
return DIALOG_SHELLS.map((shell) => `${inner} of ${shell}`);
|
|
213
287
|
}
|
|
214
288
|
// --- CERTIFIED controls (UIC1/UIC5) --------------------------------------
|
|
215
|
-
|
|
289
|
+
//
|
|
290
|
+
// WHY THE POP-UPS BELOW MAY STAY POSITIONAL WHILE THE TEXT FIELDS MAY NOT.
|
|
291
|
+
// A pop-up drive is SELF-DISCRIMINATING: `select-popup` opens the control and
|
|
292
|
+
// clicks a menu item matched by its pinned English NAME, so a mis-addressed
|
|
293
|
+
// pop-up has no such item and the step fails closed ("none of the candidate menu
|
|
294
|
+
// items exist"). A text field accepts any string silently — which is precisely
|
|
295
|
+
// how #589 wrote an interval into the ends-count field. The pop-up label sets are
|
|
296
|
+
// also disjoint across the cadence group (`never`/`after`/`on date` for the ends
|
|
297
|
+
// bound; `day(s)`…`year(s)` for the after-completion unit; weekday names, ordinals
|
|
298
|
+
// and month names for the anchors), so a swapped index cannot land a plausible
|
|
299
|
+
// value. Belt and braces: the PRE-COMMIT AUDIT re-reads every one of them by value
|
|
300
|
+
// before the OK press, so a pop-up that took a wrong-but-existing item is caught
|
|
301
|
+
// before anything is committed.
|
|
302
|
+
/** Frequency pop-up — a direct child of the dialog.
|
|
303
|
+
* positional-ok: MEASURED sole direct-child pop-up of the dialog shell in every
|
|
304
|
+
* reachable state — the shell carries `popups=1`, every other pop-up living in the
|
|
305
|
+
* cadence group (CGRD1 §B census, all four frequencies + after-completion). Its
|
|
306
|
+
* menu items are matched by name, and the pre-commit audit re-reads its value. */
|
|
216
307
|
const DIALOG_FREQUENCY = dualForm("pop up button 1");
|
|
217
|
-
/** Interval field — nested in group 1 (UIC5-e). */
|
|
218
|
-
const DIALOG_INTERVAL = dualForm("text field 1 of group 1");
|
|
219
308
|
/** OK button. */
|
|
220
309
|
const DIALOG_OK = dualForm(`button "OK"`);
|
|
221
310
|
// --- UIC6-CERTIFIED controls, RE-INDEXED PER DIALOG SHAPE (RDLG2) ----------
|
|
@@ -229,18 +318,45 @@ const DIALOG_OK = dualForm(`button "OK"`);
|
|
|
229
318
|
// driver measures in the open dialog (`probe-dialog-shape`), never to the app
|
|
230
319
|
// version. Titles/`_NS:` ids are never used (both drift). Evidence:
|
|
231
320
|
// docs/lab/uic6-rule-vocabulary.md (≤3.22) and docs/lab/rdlg2-323-recipe-cert.md.
|
|
232
|
-
/** After-completion cadence unit pop-up — the ONLY group pop-up in that mode (both shapes).
|
|
321
|
+
/** After-completion cadence unit pop-up — the ONLY group pop-up in that mode (both shapes).
|
|
322
|
+
* positional-ok: this and DIALOG_ENDS are the SAME spelling because they are the
|
|
323
|
+
* same slot in two MUTUALLY EXCLUSIVE dialog states, and the recipe emits exactly
|
|
324
|
+
* one of them per drive. Measured (CGRD1 §A census): an after-completion cadence
|
|
325
|
+
* group has `popups=1` and no `Ends:` label at all, while every fixed frequency
|
|
326
|
+
* puts the ends bound at group pop-up 1. Their menu-item sets are disjoint
|
|
327
|
+
* (`day(s)`…`year(s)` vs `never`/`after`/`on date`), so a wrong-state drive fails
|
|
328
|
+
* closed on the item name; the pre-commit audit re-reads whichever one ran. */
|
|
233
329
|
const DIALOG_AC_UNIT = dualForm("pop up button 1 of group 1");
|
|
234
|
-
/** "Ends" bound pop-up (`never` · `after` · `on date`) — always pop up button 1 of the group.
|
|
330
|
+
/** "Ends" bound pop-up (`never` · `after` · `on date`) — always pop up button 1 of the group.
|
|
331
|
+
* positional-ok: see DIALOG_AC_UNIT — one slot, two exclusive states, name-matched
|
|
332
|
+
* items, audited before the commit. */
|
|
235
333
|
const DIALOG_ENDS = dualForm("pop up button 1 of group 1");
|
|
236
|
-
/** The cadence group itself — the handle the shape probe and the weekday converge address.
|
|
334
|
+
/** The cadence group itself — the handle the shape probe and the weekday converge address.
|
|
335
|
+
* positional-ok: MEASURED sole group of the dialog shell (`groups=1` in every
|
|
336
|
+
* state, CGRD1 §B), and a CONTAINER handle only — every primitive that takes it
|
|
337
|
+
* (set-group-number, probe-dialog-shape, converge-weekdays, audit-dialog)
|
|
338
|
+
* discriminates WITHIN it by label row or control class and fails closed on
|
|
339
|
+
* anything but exactly one match. */
|
|
237
340
|
const DIALOG_GROUP = dualForm("group 1");
|
|
238
|
-
/** The 3.23 `Next:` first-occurrence pop-up — group pop-up 2 (this shape only).
|
|
341
|
+
/** The 3.23 `Next:` first-occurrence pop-up — group pop-up 2 (this shape only).
|
|
342
|
+
* positional-ok: reachable ONLY under a MEASURED dialog shape — the recipe's
|
|
343
|
+
* probe-dialog-shape step must have returned `next-popup` (a positive match on the
|
|
344
|
+
* control class occupying the `Next:` row) before this step runs, and an
|
|
345
|
+
* unrecognized shape refuses the drive. The drive then matches a menu item by
|
|
346
|
+
* PARSED DATE and reads the pop-up back; the pre-commit audit re-parses it. */
|
|
239
347
|
const DIALOG_NEXT_POPUP = dualForm("pop up button 2 of group 1");
|
|
240
|
-
/** A group pop-up addressed at DIFFERENT indices in the two dialog shapes (RDLG2).
|
|
348
|
+
/** A group pop-up addressed at DIFFERENT indices in the two dialog shapes (RDLG2).
|
|
349
|
+
* positional-ok: the index is not a guess but the MEASURED consequence of the
|
|
350
|
+
* shape the drive probed — CGRD1 §A counts the cadence group's pop-ups per
|
|
351
|
+
* frequency on 3.23 (daily 2 · weekly 3 · monthly 4 · yearly 5, i.e. Ends + Next +
|
|
352
|
+
* the per-frequency controls), which is the +1 fork these two index sets encode.
|
|
353
|
+
* A step carrying `shaped` with no shape probed fails closed. Every one of these
|
|
354
|
+
* is a name-matched pop-up, and all of them are re-read by the pre-commit audit. */
|
|
241
355
|
function shapedPopup(nextPopupIndex, legacyIndex) {
|
|
242
356
|
return {
|
|
357
|
+
// positional-ok: shape-MEASURED index, per this function's contract above.
|
|
243
358
|
"next-popup": { pathCandidates: dualForm(`pop up button ${nextPopupIndex} of group 1`) },
|
|
359
|
+
// positional-ok: shape-MEASURED index, per this function's contract above.
|
|
244
360
|
legacy: { pathCandidates: dualForm(`pop up button ${legacyIndex} of group 1`) },
|
|
245
361
|
};
|
|
246
362
|
}
|
|
@@ -258,13 +374,23 @@ const DIALOG_YEAR_ORDINAL = shapedPopup(5, 4);
|
|
|
258
374
|
* the add button from live structure).
|
|
259
375
|
*/
|
|
260
376
|
const WEEKDAY_BASE = { "next-popup": 3, legacy: 2 };
|
|
261
|
-
/** "Ends after [n]" count field — becomes text field 1 of the group once shown (interval was set earlier while it was the sole field). */
|
|
262
|
-
const DIALOG_ENDS_COUNT = dualForm("text field 1 of group 1");
|
|
263
377
|
/** "Add reminders" checkbox (sheet-level, title-pinned). The time is an AXDateTimeArea driven by set-datetime. */
|
|
264
378
|
const DIALOG_ADD_REMINDERS = dualForm(`checkbox "Add reminders"`);
|
|
265
|
-
/** "Add deadlines" checkbox + the "start
|
|
379
|
+
/** "Add deadlines" checkbox + the "and start [n] days earlier" offset field it reveals. */
|
|
266
380
|
const DIALOG_ADD_DEADLINES = dualForm(`checkbox "Add deadlines"`);
|
|
267
|
-
|
|
381
|
+
/**
|
|
382
|
+
* The pinned English static text whose ROW the start-offset field sits on. The
|
|
383
|
+
* field shipped as `text field 1` of the dialog shell — a value-bearing numeric
|
|
384
|
+
* field picked by index out of a tree whose shape depends on the "Add deadlines"
|
|
385
|
+
* checkbox, verified only by re-reading the same index it wrote. That is the #589
|
|
386
|
+
* error class, and it happened to be right rather than provably right: measured on
|
|
387
|
+
* 3.23, the shell carries 0 direct text fields with deadlines OFF and exactly 1
|
|
388
|
+
* with them ON — `[and start] [ 0 ] [days earlier]`, the field at y=409 against the
|
|
389
|
+
* label's y=413 — whether or not reminders are also on (CGRD1 §B census). Now the
|
|
390
|
+
* field is found by that label's row, exactly like the cadence numbers, and
|
|
391
|
+
* anything but one match on it fails closed with the shell's field inventory.
|
|
392
|
+
*/
|
|
393
|
+
const DIALOG_START_EARLIER_LABEL = "days earlier";
|
|
268
394
|
/**
|
|
269
395
|
* After-completion cadence-unit pop-up labels. The options are NOT the frequency
|
|
270
396
|
* word (`weekly`) — they are the time unit, and the app PLURALIZES them by the
|
|
@@ -347,11 +473,52 @@ function selectPopupAny(label, pathCandidates, valueCandidates) {
|
|
|
347
473
|
addressing: "title",
|
|
348
474
|
};
|
|
349
475
|
}
|
|
350
|
-
|
|
476
|
+
/**
|
|
477
|
+
* Drive a dialog-SHELL text field addressed by the pinned English label sharing
|
|
478
|
+
* its row — the {@link setGroupNumber} discrimination law, applied outside the
|
|
479
|
+
* cadence group. Its one caller is the start-days-earlier offset; see
|
|
480
|
+
* {@link DIALOG_START_EARLIER_LABEL} for why the old `text field 1` spelling had
|
|
481
|
+
* to go even though it resolved correctly on 3.23.
|
|
482
|
+
*/
|
|
483
|
+
function setRowField(label, rowLabel, value) {
|
|
351
484
|
return {
|
|
352
|
-
primitive: "set-
|
|
485
|
+
primitive: "set-row-field",
|
|
353
486
|
label,
|
|
354
|
-
pathCandidates,
|
|
487
|
+
pathCandidates: DIALOG_SHELLS,
|
|
488
|
+
rowLabel,
|
|
489
|
+
value,
|
|
490
|
+
dynamic: true,
|
|
491
|
+
addressing: "title",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Drive one of the Repeat dialog's TWO numeric fields — the cadence interval
|
|
496
|
+
* ("Every [n] days") or the "Ends: after [n] times" count. Both are text fields
|
|
497
|
+
* of the cadence group, and WHICH INDEX each holds depends on the dialog's
|
|
498
|
+
* state rather than on which one the caller means. Measured on Things 3.23
|
|
499
|
+
* (HXPC1, docs/lab/hxpc1-picker-assert.md §A):
|
|
500
|
+
*
|
|
501
|
+
* Ends: never → 1 text field · #1 interval @[311,283]
|
|
502
|
+
* Ends: after N → 2 text fields · #1 COUNT @[402,372] (`Ends:` label y=375)
|
|
503
|
+
* #2 interval @[311,283] (`Every` label y=286)
|
|
504
|
+
*
|
|
505
|
+
* Both used to be spelled `text field 1 of group 1`, which the create path
|
|
506
|
+
* survived only because it drives the interval BEFORE selecting the ends bound,
|
|
507
|
+
* while it is still the sole field. A RESCHEDULE opens the dialog PRE-POPULATED:
|
|
508
|
+
* a rule that already ends after N presents both fields from the first step, so
|
|
509
|
+
* the interval drive wrote the requested interval into the count field and the
|
|
510
|
+
* count drive then overwrote it — the interval never changed. The driver now
|
|
511
|
+
* resolves each field by the ROW it sits on (the count shares the `Ends:`
|
|
512
|
+
* label's row, the interval does not), which is order-independent and covers the
|
|
513
|
+
* after-completion dialog too: that one offers no ends bound at all, so the
|
|
514
|
+
* interval is again the only field. See ui.ts `axSetGroupNumberScript`.
|
|
515
|
+
*/
|
|
516
|
+
function setGroupNumber(label, numberTarget, value) {
|
|
517
|
+
return {
|
|
518
|
+
primitive: "set-group-number",
|
|
519
|
+
label,
|
|
520
|
+
pathCandidates: DIALOG_GROUP,
|
|
521
|
+
numberTarget,
|
|
355
522
|
value,
|
|
356
523
|
dynamic: true,
|
|
357
524
|
addressing: "title",
|
|
@@ -453,7 +620,7 @@ function repeatDialogEntry(rule) {
|
|
|
453
620
|
else {
|
|
454
621
|
steps.push(selectPopup(`frequency = ${rule.frequency}`, DIALOG_FREQUENCY, rule.frequency));
|
|
455
622
|
}
|
|
456
|
-
steps.push(
|
|
623
|
+
steps.push(setGroupNumber(`interval = ${rule.interval}`, "interval", String(rule.interval)));
|
|
457
624
|
// MEASURE the dialog before touching any control the 3.23 redesign moved
|
|
458
625
|
// (RDLG2). Emitted only when such a control is actually addressed, so the
|
|
459
626
|
// certified two-control path (frequency + interval + OK) costs no extra hop —
|
|
@@ -501,6 +668,31 @@ function repeatDialogEntry(rule) {
|
|
|
501
668
|
steps.push(selectPopupShaped(`yearly month = ${y.month}`, DIALOG_YEAR_MONTH, MONTH_TITLE[y.month - 1] ?? ""));
|
|
502
669
|
steps.push(...monthlyAnchorSteps(y, DIALOG_YEAR_MODE, DIALOG_YEAR_ORDINAL));
|
|
503
670
|
}
|
|
671
|
+
// LET THE `Next:` POP-UP ABSORB THE RULE (NEXTPOP1) before any further input.
|
|
672
|
+
//
|
|
673
|
+
// The 3.23 dialog recomputes its first-occurrence pop-up — the displayed date
|
|
674
|
+
// AND the menu of occurrences behind it — ASYNCHRONOUSLY, ~0.4s after the
|
|
675
|
+
// calendar anchor moves, and an input that lands inside that window CANCELS
|
|
676
|
+
// the recompute permanently: the control keeps describing the PREVIOUS rule.
|
|
677
|
+
// The very next thing this recipe drives is the deadline checkbox, which is
|
|
678
|
+
// exactly such an input, so every deadlined monthly/yearly promote reached
|
|
679
|
+
// `select-next-occurrence` with the SEED's occurrence series in the menu and
|
|
680
|
+
// failed closed on a date the rule really does produce (VMRES1 §4.3).
|
|
681
|
+
//
|
|
682
|
+
// Emitted for the pop-up shape only — the ≤3.22 `Next:` is a free date area
|
|
683
|
+
// with no menu to recompute — and never for after-completion, which has no
|
|
684
|
+
// first-occurrence control at all. It is a WAIT, not a setter, so it
|
|
685
|
+
// contributes no control to the pre-commit audit.
|
|
686
|
+
if (needsShape && rule.afterCompletion !== true) {
|
|
687
|
+
steps.push({
|
|
688
|
+
primitive: "settle-occurrences",
|
|
689
|
+
label: "let the first-occurrence pop-up absorb the rule",
|
|
690
|
+
pathCandidates: DIALOG_NEXT_POPUP,
|
|
691
|
+
onlyShape: "next-popup",
|
|
692
|
+
dynamic: true,
|
|
693
|
+
addressing: "title",
|
|
694
|
+
});
|
|
695
|
+
}
|
|
504
696
|
// "Add deadlines" / "start N days earlier" — DEADLINE MODE, converged (RRD1)
|
|
505
697
|
// BEFORE the "Next:" field is driven below. In deadline mode the "Next:" field IS
|
|
506
698
|
// the deadline date and the instance start = deadline − startDaysEarlier (YANCH1
|
|
@@ -526,7 +718,7 @@ function repeatDialogEntry(rule) {
|
|
|
526
718
|
// startDaysEarlier is requested-fields-only too: drive the offset field only
|
|
527
719
|
// when it was given (>0), else leave it at its pre-populated value.
|
|
528
720
|
if (deadlineTarget && (rule.startDaysEarlier ?? 0) > 0) {
|
|
529
|
-
steps.push(
|
|
721
|
+
steps.push(setRowField(`start ${rule.startDaysEarlier} days earlier`, DIALOG_START_EARLIER_LABEL, String(rule.startDaysEarlier)));
|
|
530
722
|
}
|
|
531
723
|
}
|
|
532
724
|
// Ends bound + "Next:" first-occurrence field (ANCH2, issue #476). ORDER MATTERS:
|
|
@@ -538,7 +730,7 @@ function repeatDialogEntry(rule) {
|
|
|
538
730
|
const endsOnDate = rule.ends !== undefined && rule.ends.kind === "on-date" ? rule.ends : null;
|
|
539
731
|
if (rule.ends !== undefined && rule.ends.kind === "after") {
|
|
540
732
|
steps.push(selectPopup("ends = after", DIALOG_ENDS, "after"));
|
|
541
|
-
steps.push(
|
|
733
|
+
steps.push(setGroupNumber(`ends after = ${rule.ends.count}`, "ends-count", String(rule.ends.count)));
|
|
542
734
|
}
|
|
543
735
|
else if (endsOnDate !== null) {
|
|
544
736
|
steps.push(selectPopup("ends = on date", DIALOG_ENDS, "on date"));
|
|
@@ -580,9 +772,159 @@ function repeatDialogEntry(rule) {
|
|
|
580
772
|
steps.push(ensureCheckbox("Add reminders", DIALOG_ADD_REMINDERS, true));
|
|
581
773
|
steps.push(setDateTime(`reminder = ${rule.reminder}`, `time:${rule.reminder}`, "reminder"));
|
|
582
774
|
}
|
|
775
|
+
const audit = dialogAuditStep(steps);
|
|
776
|
+
if (audit !== null)
|
|
777
|
+
steps.push(audit);
|
|
583
778
|
steps.push(pressControl('press "OK"', DIALOG_OK));
|
|
584
779
|
return steps;
|
|
585
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Build the PRE-COMMIT FULL-DIALOG AUDIT step from the drive's OWN step list
|
|
783
|
+
* (CGRD1) — the last thing that runs before the OK press.
|
|
784
|
+
*
|
|
785
|
+
* Derivation from the steps, rather than a second hand-written list of controls,
|
|
786
|
+
* is the point: every setter the recipe emits contributes exactly one audit
|
|
787
|
+
* control, so a rule field added to `repeatDialogEntry` is audited the moment it
|
|
788
|
+
* is driven, and a control can never be silently left out. It also means the audit
|
|
789
|
+
* addresses each control through the SAME candidate paths / shape overrides / row
|
|
790
|
+
* anchors the drive used, so the two can never disagree about what was touched.
|
|
791
|
+
*
|
|
792
|
+
* Why it is needed at all: each setter's own read-back is SELF-REFERENTIAL — it
|
|
793
|
+
* re-reads the element it addressed, so it proves the keystrokes landed where they
|
|
794
|
+
* were aimed and nothing more. #589 was a wrong ADDRESS, invisible to that check.
|
|
795
|
+
* The audit is the outside view; any mismatch aborts the drive fail-closed before
|
|
796
|
+
* anything is committed.
|
|
797
|
+
*
|
|
798
|
+
* Steps that set nothing (waits, the menu press, the shape probe, the OK press)
|
|
799
|
+
* contribute nothing; a recipe that drove no control at all yields no audit step.
|
|
800
|
+
*/
|
|
801
|
+
function weekdayBaseOfEncoded(value) {
|
|
802
|
+
const base = Number(value.split("|", 1)[0]);
|
|
803
|
+
return Number.isFinite(base) && base > 0 ? Math.trunc(base) : 2;
|
|
804
|
+
}
|
|
805
|
+
/** The per-shape PATH overrides of a step, in the audit control's shape. */
|
|
806
|
+
function shapedPaths(step) {
|
|
807
|
+
if (step.shaped === undefined)
|
|
808
|
+
return undefined;
|
|
809
|
+
return {
|
|
810
|
+
...(step.shaped["next-popup"]?.pathCandidates !== undefined && {
|
|
811
|
+
"next-popup": { pathCandidates: step.shaped["next-popup"].pathCandidates },
|
|
812
|
+
}),
|
|
813
|
+
...(step.shaped.legacy?.pathCandidates !== undefined && {
|
|
814
|
+
legacy: { pathCandidates: step.shaped.legacy.pathCandidates },
|
|
815
|
+
}),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function dialogAuditStep(steps) {
|
|
819
|
+
const controls = [];
|
|
820
|
+
for (const step of steps) {
|
|
821
|
+
const base = {
|
|
822
|
+
label: step.label,
|
|
823
|
+
...(step.onlyShape !== undefined && { onlyShape: step.onlyShape }),
|
|
824
|
+
};
|
|
825
|
+
switch (step.primitive) {
|
|
826
|
+
case "select-popup": {
|
|
827
|
+
const shaped = shapedPaths(step);
|
|
828
|
+
controls.push({
|
|
829
|
+
...base,
|
|
830
|
+
kind: "popup",
|
|
831
|
+
...(step.pathCandidates !== undefined && { pathCandidates: step.pathCandidates }),
|
|
832
|
+
...(shaped !== undefined && { shaped }),
|
|
833
|
+
// valueCandidates carries the singular/plural pair the app pluralizes by
|
|
834
|
+
// interval; ANY of them satisfies, exactly as the drive accepted any.
|
|
835
|
+
expected: step.valueCandidates ?? [step.value ?? ""],
|
|
836
|
+
});
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case "set-group-number":
|
|
840
|
+
controls.push({
|
|
841
|
+
...base,
|
|
842
|
+
kind: "group-number",
|
|
843
|
+
numberTarget: step.numberTarget ?? "interval",
|
|
844
|
+
expected: [step.value ?? ""],
|
|
845
|
+
});
|
|
846
|
+
break;
|
|
847
|
+
case "set-row-field":
|
|
848
|
+
controls.push({
|
|
849
|
+
...base,
|
|
850
|
+
kind: "row-field",
|
|
851
|
+
rowLabel: step.rowLabel ?? "",
|
|
852
|
+
expected: [step.value ?? ""],
|
|
853
|
+
});
|
|
854
|
+
break;
|
|
855
|
+
case "ensure-checkbox":
|
|
856
|
+
controls.push({
|
|
857
|
+
...base,
|
|
858
|
+
kind: "checkbox",
|
|
859
|
+
...(step.pathCandidates !== undefined && { pathCandidates: step.pathCandidates }),
|
|
860
|
+
expected: [step.checkboxTarget === true ? "1" : "0"],
|
|
861
|
+
expectedLabel: step.checkboxTarget === true ? "checked" : "unchecked",
|
|
862
|
+
});
|
|
863
|
+
break;
|
|
864
|
+
case "converge-weekdays": {
|
|
865
|
+
// The weekday step encodes "<base>|<Weekday>,<Weekday>…" per dialog shape
|
|
866
|
+
// (the base is the group pop-up index of the first weekday row, which the
|
|
867
|
+
// +1 fork moves). The audit reads every row pop-up from that base and
|
|
868
|
+
// compares as a SET: the converge law assigns EVERY row from the target set
|
|
869
|
+
// cycling, so a surplus row duplicates a target weekday rather than keeping
|
|
870
|
+
// a stale one, and set equality is the exact property to check.
|
|
871
|
+
const nextValue = step.shaped?.["next-popup"]?.value;
|
|
872
|
+
const legacyValue = step.shaped?.legacy?.value;
|
|
873
|
+
const encoded = nextValue ?? legacyValue;
|
|
874
|
+
if (encoded === undefined)
|
|
875
|
+
break;
|
|
876
|
+
const titles = encoded
|
|
877
|
+
.slice(encoded.indexOf("|") + 1)
|
|
878
|
+
.split(",")
|
|
879
|
+
.filter((t) => t !== "");
|
|
880
|
+
if (titles.length === 0)
|
|
881
|
+
break;
|
|
882
|
+
controls.push({
|
|
883
|
+
...base,
|
|
884
|
+
kind: "weekdays",
|
|
885
|
+
expected: titles,
|
|
886
|
+
expectedLabel: titles.join(" + "),
|
|
887
|
+
shaped: {
|
|
888
|
+
...(nextValue !== undefined && {
|
|
889
|
+
"next-popup": { weekdayBase: weekdayBaseOfEncoded(nextValue) },
|
|
890
|
+
}),
|
|
891
|
+
...(legacyValue !== undefined && {
|
|
892
|
+
legacy: { weekdayBase: weekdayBaseOfEncoded(legacyValue) },
|
|
893
|
+
}),
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "select-next-occurrence":
|
|
899
|
+
controls.push({
|
|
900
|
+
...base,
|
|
901
|
+
kind: "occurrence-popup",
|
|
902
|
+
...(step.pathCandidates !== undefined && { pathCandidates: step.pathCandidates }),
|
|
903
|
+
expected: [step.value ?? ""],
|
|
904
|
+
});
|
|
905
|
+
break;
|
|
906
|
+
case "set-datetime":
|
|
907
|
+
controls.push({
|
|
908
|
+
...base,
|
|
909
|
+
kind: "date-area",
|
|
910
|
+
dtTarget: step.dtTarget ?? "next",
|
|
911
|
+
dtSpec: step.value ?? "",
|
|
912
|
+
});
|
|
913
|
+
break;
|
|
914
|
+
default:
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (controls.length === 0)
|
|
919
|
+
return null;
|
|
920
|
+
return {
|
|
921
|
+
primitive: "audit-dialog",
|
|
922
|
+
label: "audit the Repeat dialog against the requested rule (before committing)",
|
|
923
|
+
audit: { shells: DIALOG_SHELLS, groups: DIALOG_GROUP, controls },
|
|
924
|
+
dynamic: true,
|
|
925
|
+
addressing: "title",
|
|
926
|
+
};
|
|
927
|
+
}
|
|
586
928
|
export function makeRepeatingRecipe(targetUuid, frequency, interval, extras = {}) {
|
|
587
929
|
return {
|
|
588
930
|
op: "todo.make-repeating",
|
|
@@ -715,9 +1057,17 @@ export function rescheduleRepeatRecipe(targetUuid, frequency, interval, extras =
|
|
|
715
1057
|
// repeat bar, the popover, and the popover items are best-guess structural
|
|
716
1058
|
// paths derived from the UIC2 AX inventory; the certification pass confirms or
|
|
717
1059
|
// corrects them exactly as the to-do recipes were corrected in UIC1.
|
|
718
|
-
/** The header cell of the project view (row 1 of the content table).
|
|
1060
|
+
/** The header cell of the project view (row 1 of the content table).
|
|
1061
|
+
* positional-ok: the project view's header IS its first content-table row by
|
|
1062
|
+
* construction (UIC2/UIC3) — the row the app renders above the item list — and
|
|
1063
|
+
* this path only ever reaches a CLICK target, never a value. */
|
|
719
1064
|
const PROJECT_HEADER_CELL = `UI element 1 of row 1 of table 1 of scroll area 1 of ${MAIN_WINDOW}`;
|
|
720
|
-
/** The always-visible repeat bar of a repeating project (UIC2/UIC3: text area 2).
|
|
1065
|
+
/** The always-visible repeat bar of a repeating project (UIC2/UIC3: text area 2).
|
|
1066
|
+
* positional-ok: the bar carries no title or description to address it by (UIC2),
|
|
1067
|
+
* so its index in the header cell is the only handle the app offers. It is not a
|
|
1068
|
+
* value-bearing field: the step that uses it is a click whose `assertPath` REQUIRES
|
|
1069
|
+
* the repeat popover to appear afterwards, so a click that lands on the wrong text
|
|
1070
|
+
* area fails closed (Escape + abort) instead of cascading into blind presses. */
|
|
721
1071
|
const PROJECT_REPEAT_BAR = `text area 2 of ${PROJECT_HEADER_CELL}`;
|
|
722
1072
|
/**
|
|
723
1073
|
* The popover opened by clicking the repeat bar. Confirmed by UIC3 discovery: it
|
|
@@ -728,6 +1078,9 @@ const PROJECT_REPEAT_BAR = `text area 2 of ${PROJECT_HEADER_CELL}`;
|
|
|
728
1078
|
* utility window; its items live in the window's scroll area.
|
|
729
1079
|
*/
|
|
730
1080
|
const PROJECT_REPEAT_POPOVER = `(first window whose subrole is "AXUnknown" and size is not {40, 40})`;
|
|
1081
|
+
// positional-ok: a CONTAINER handle only — these custom AXUnknown popovers hold
|
|
1082
|
+
// exactly one scroll area (MEASURED on the sibling Move… picker, HXPC1 §B2:
|
|
1083
|
+
// `scroll areas=1`), and every item inside is addressed by exact AXDescription.
|
|
731
1084
|
const PROJECT_REPEAT_POPOVER_ITEMS = `scroll area 1 of ${PROJECT_REPEAT_POPOVER}`;
|
|
732
1085
|
/** A project view + foreground preamble — the mouse segment needs Things frontmost. */
|
|
733
1086
|
function projectPreamble(targetUuid) {
|
|
@@ -805,6 +1158,8 @@ export function projectRescheduleRepeatRecipe(targetUuid, frequency, interval, e
|
|
|
805
1158
|
...projectPreamble(targetUuid),
|
|
806
1159
|
openProjectRepeatPopover(),
|
|
807
1160
|
popoverItemClick("repeat menu ▸ Change…", "Change…", {
|
|
1161
|
+
// positional-ok: a post-click EXISTENCE assert on the one attached sheet a
|
|
1162
|
+
// window can present; nothing is read or written through this path.
|
|
808
1163
|
path: `sheet 1 of ${MAIN_WINDOW}`,
|
|
809
1164
|
label: "the Repeat dialog",
|
|
810
1165
|
}),
|
|
@@ -824,27 +1179,74 @@ export function projectRescheduleRepeatRecipe(targetUuid, frequency, interval, e
|
|
|
824
1179
|
// repeat popover. The popover (Archive / Move… / Convert to Project… / Delete) is
|
|
825
1180
|
// the same custom AXUnknown-window shape as the repeat popover.
|
|
826
1181
|
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1182
|
+
// CERTIFIED by HXPC1 (2026-08-25, Things 3.23 / golden-v4 —
|
|
1183
|
+
// docs/lab/hxpc1-picker-assert.md), which corrected three of the four provisional
|
|
1184
|
+
// paths the way UIC1 corrected the repeat recipes:
|
|
1185
|
+
// * the `…` button is three levels below the content table, so no `whose`
|
|
1186
|
+
// clause can reach it (§B0) — the click resolves it by walking rows/cells;
|
|
1187
|
+
// * the picker is a DETACHED `MovePopUpDialog-` window, not a sheet of the main
|
|
1188
|
+
// window (§B2), and its filter field is not addressable as a child of it;
|
|
1189
|
+
// * the commit is a CLICK on the destination row, never a Return — the picker
|
|
1190
|
+
// publishes no highlight to read back, and its `New Project "<typed>"` row
|
|
1191
|
+
// turns an absent destination into a newly created project (§B4).
|
|
1192
|
+
// The popover-item enumeration (`whose description is "Move…"`) was the one
|
|
1193
|
+
// provisional path that measured correct: those items are direct children of the
|
|
1194
|
+
// popover's scroll area.
|
|
1195
|
+
/**
|
|
1196
|
+
* The heading `…` "More. <title>" button. It carries the heading's title in its
|
|
1197
|
+
* `AXDescription`, but it sits at `UI element N of cell 1 of row M` of the
|
|
1198
|
+
* content table — three levels down — and a `whose` clause searches only DIRECT
|
|
1199
|
+
* children, so the button is NOT addressable as a path. The click step names the
|
|
1200
|
+
* table here and the description separately (`rowCellDescription`), and the
|
|
1201
|
+
* driver walks rows → cells → children for the exact match (HXPC1 §B0; the
|
|
1202
|
+
* one-level spelling this replaces matched nothing on 3.23, which is why every
|
|
1203
|
+
* ellipsis drive died at its own frame resolution).
|
|
1204
|
+
*/
|
|
1205
|
+
function headingMoreDescription(headingTitle) {
|
|
1206
|
+
return `More. ${headingTitle}`;
|
|
834
1207
|
}
|
|
835
1208
|
/** The ellipsis popover — a custom AXUnknown window (same shape as the repeat popover). */
|
|
836
1209
|
const HEADING_ELLIPSIS_POPOVER = `(first window whose subrole is "AXUnknown" and size is not {40, 40})`;
|
|
1210
|
+
// positional-ok: container handle only — one scroll area per popover (HXPC1 §B2),
|
|
1211
|
+
// items addressed by exact AXDescription.
|
|
837
1212
|
const HEADING_POPOVER_ITEMS = `scroll area 1 of ${HEADING_ELLIPSIS_POPOVER}`;
|
|
838
|
-
/**
|
|
839
|
-
|
|
840
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* The Move… project picker. NOT a sheet of the main window (the shape this
|
|
1215
|
+
* recipe first assumed) — a DETACHED top-level window of the same custom
|
|
1216
|
+
* `AXUnknown` class as the ellipsis popover, distinguished by an `AXIdentifier`
|
|
1217
|
+
* beginning `MovePopUpDialog-` (HXPC1 §B2). The popover that opened it is gone
|
|
1218
|
+
* by the time it appears, so the generic detached-window selector resolves it
|
|
1219
|
+
* unambiguously; the row-commit script re-checks the identifier before clicking
|
|
1220
|
+
* anything, so a different detached window can never be committed into.
|
|
1221
|
+
*/
|
|
1222
|
+
const HEADING_MOVE_PICKER = `(first window whose subrole is "AXUnknown" and size is not {40, 40})`;
|
|
1223
|
+
/**
|
|
1224
|
+
* What proves the detached window that appeared IS the picker and not the
|
|
1225
|
+
* ellipsis popover it replaced: the picker carries a direct child described
|
|
1226
|
+
* `Move` (its header), the popover carries only its item list. Without this the
|
|
1227
|
+
* post-click assert would be satisfied by the popover that was already open —
|
|
1228
|
+
* i.e. by the click having done nothing (HXPC1 §B2).
|
|
1229
|
+
*/
|
|
1230
|
+
const HEADING_MOVE_PICKER_MARKER = `(first UI element of ${HEADING_MOVE_PICKER} whose description is "Move")`;
|
|
841
1231
|
/**
|
|
842
1232
|
* Move a HEADING to a different project via the ellipsis `Move…` menu (HEADXPROJ).
|
|
843
1233
|
* `sourceProjectUuid` is revealed to render the heading row; `headingTitle` is the
|
|
844
|
-
* `"More. <title>"` click target; `destProjectTitle`
|
|
845
|
-
* then
|
|
846
|
-
* `project` FK becomes the destination; its children follow via their intact
|
|
1234
|
+
* `"More. <title>"` click target; `destProjectTitle` narrows the picker's list and
|
|
1235
|
+
* then names the row that is CLICKED to commit. DB effect (HEADXPROJ): the heading
|
|
1236
|
+
* row's `project` FK becomes the destination; its children follow via their intact
|
|
847
1237
|
* heading FK (a single-row change — no child rewrite, no index churn).
|
|
1238
|
+
*
|
|
1239
|
+
* The commit used to be a Return pressed BLIND on whatever the filter had
|
|
1240
|
+
* highlighted, which is how a move could CREATE a project instead of performing
|
|
1241
|
+
* one: the picker offers a trailing `New Project "<typed text>"` row, and it omits
|
|
1242
|
+
* completed and canceled projects entirely — so a destination our pre-state had
|
|
1243
|
+
* resolved from the database could be absent from the picker, leaving that row as
|
|
1244
|
+
* the only offer. Measured on Things 3.23: the heading landed in a brand-new
|
|
1245
|
+
* second project of the same name (HXPC1 §B4). The picker publishes no selection
|
|
1246
|
+
* or highlight attribute on any row, so there is nothing to read back from a
|
|
1247
|
+
* keyboard commit; the recipe therefore addresses the destination row by its exact
|
|
1248
|
+
* title and clicks it, and fails closed — naming every row the picker offered —
|
|
1249
|
+
* when that row is absent, duplicated, or scrolled out of view.
|
|
848
1250
|
*/
|
|
849
1251
|
export function moveHeadingToProjectRecipe(sourceProjectUuid, headingTitle, destProjectTitle) {
|
|
850
1252
|
return {
|
|
@@ -866,7 +1268,8 @@ export function moveHeadingToProjectRecipe(sourceProjectUuid, headingTitle, dest
|
|
|
866
1268
|
{
|
|
867
1269
|
primitive: "click-element",
|
|
868
1270
|
label: `open the heading's ellipsis menu ("More. ${headingTitle}")`,
|
|
869
|
-
path:
|
|
1271
|
+
path: PROJECT_CONTENT_TABLE,
|
|
1272
|
+
rowCellDescription: headingMoreDescription(headingTitle),
|
|
870
1273
|
assertPath: HEADING_ELLIPSIS_POPOVER,
|
|
871
1274
|
assertLabel: "the heading ellipsis menu",
|
|
872
1275
|
assertTimeoutMs: 5000,
|
|
@@ -877,24 +1280,28 @@ export function moveHeadingToProjectRecipe(sourceProjectUuid, headingTitle, dest
|
|
|
877
1280
|
primitive: "click-element",
|
|
878
1281
|
label: "ellipsis menu ▸ Move…",
|
|
879
1282
|
path: `(first UI element of ${HEADING_POPOVER_ITEMS} whose description is "Move…")`,
|
|
880
|
-
assertPath:
|
|
1283
|
+
assertPath: HEADING_MOVE_PICKER_MARKER,
|
|
881
1284
|
assertLabel: "the Move… project picker",
|
|
882
1285
|
assertTimeoutMs: 5000,
|
|
883
1286
|
dynamic: true,
|
|
884
1287
|
addressing: "title",
|
|
885
1288
|
},
|
|
886
1289
|
{
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1290
|
+
// The picker focuses its own filter field as it opens, and that field is
|
|
1291
|
+
// not addressable as a child of the picker window — so the destination is
|
|
1292
|
+
// typed at the focus. Nothing is committed on the strength of the
|
|
1293
|
+
// keystroke: the next step resolves the destination row by name.
|
|
1294
|
+
primitive: "type-text",
|
|
1295
|
+
label: `narrow the Move… picker to "${destProjectTitle}"`,
|
|
890
1296
|
value: destProjectTitle,
|
|
891
1297
|
dynamic: true,
|
|
892
1298
|
addressing: "title",
|
|
893
1299
|
},
|
|
894
1300
|
{
|
|
895
|
-
primitive: "
|
|
896
|
-
label:
|
|
897
|
-
|
|
1301
|
+
primitive: "click-picker-row",
|
|
1302
|
+
label: `commit the Move… picker on the "${destProjectTitle}" row`,
|
|
1303
|
+
path: HEADING_MOVE_PICKER,
|
|
1304
|
+
value: destProjectTitle,
|
|
898
1305
|
dynamic: true,
|
|
899
1306
|
addressing: "title",
|
|
900
1307
|
},
|
|
@@ -930,7 +1337,8 @@ export function dissolveHeadingRecipe(projectReveal, headingTitle) {
|
|
|
930
1337
|
{
|
|
931
1338
|
primitive: "click-element",
|
|
932
1339
|
label: `open the heading's ellipsis menu ("More. ${headingTitle}")`,
|
|
933
|
-
path:
|
|
1340
|
+
path: PROJECT_CONTENT_TABLE,
|
|
1341
|
+
rowCellDescription: headingMoreDescription(headingTitle),
|
|
934
1342
|
assertPath: HEADING_ELLIPSIS_POPOVER,
|
|
935
1343
|
assertLabel: "the heading ellipsis menu",
|
|
936
1344
|
assertTimeoutMs: 5000,
|