things-api 0.19.1 → 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 +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 +82 -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 +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 +120 -4
- package/dist/write/vectors/ui.js +542 -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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The READ-ONLY window/focus census (issue #620, field report M1).
|
|
3
|
+
*
|
|
4
|
+
* One stable osascript shape answers four questions without touching anything —
|
|
5
|
+
* no click, no keystroke, no menu opened, no state changed:
|
|
6
|
+
*
|
|
7
|
+
* - is a Things sheet/dialog open, and WHICH one (identified from its control
|
|
8
|
+
* census, never from a localized title alone);
|
|
9
|
+
* - is Things the frontmost application;
|
|
10
|
+
* - who owns keyboard focus instead (application + element ROLE);
|
|
11
|
+
* - could the frontmost surface be inspected at all (a secure system modal —
|
|
12
|
+
* a macOS privacy/consent dialog — belongs to no application's
|
|
13
|
+
* Accessibility tree, and that is reported as exactly that rather than
|
|
14
|
+
* guessed at).
|
|
15
|
+
*
|
|
16
|
+
* It has three consumers, deliberately ONE shape for all of them:
|
|
17
|
+
* 1. the PER-STEP GUARD before every keystroke-class hop (ui.ts) — a keystroke
|
|
18
|
+
* goes to whatever is frontmost, so typing while another app owns the
|
|
19
|
+
* screen types into the void, or into someone else's window;
|
|
20
|
+
* 2. the PRE-CLEANUP oracle — Escape is itself a keystroke, so the abort path
|
|
21
|
+
* must know who would receive it before sending it;
|
|
22
|
+
* 3. the `ui-state` diagnostic (and `things doctor --ui-state`).
|
|
23
|
+
*
|
|
24
|
+
* PRIVACY: the census emits ROLE COUNTS and element ROLES only — never a
|
|
25
|
+
* control's value, title or description, and never a window title. A Things
|
|
26
|
+
* sheet routinely displays the user's own to-do text, and a diagnostic that
|
|
27
|
+
* gets pasted into a bug report must not carry it.
|
|
28
|
+
*/
|
|
29
|
+
import type { UiCommand, UiRunResult } from "./ui.ts";
|
|
30
|
+
/**
|
|
31
|
+
* Which dialog is up, decided by STRUCTURE (the control census measured in
|
|
32
|
+
* docs/lab/rdlg1-323-repeat-dialog-census.md §2.1) rather than by a localized
|
|
33
|
+
* string:
|
|
34
|
+
*
|
|
35
|
+
* - `repeat` — the Repeat sheet: exactly two checkboxes, exactly one
|
|
36
|
+
* direct pop-up button (the frequency), exactly two buttons
|
|
37
|
+
* (OK / Cancel), no direct text field, and one cadence group
|
|
38
|
+
* carrying its own field/pop-up. That shape holds in every
|
|
39
|
+
* mode of the 3.23 dialog and in both of its forms (attached
|
|
40
|
+
* sheet when Things is frontmost, detached editor window when
|
|
41
|
+
* it is not), and no other Things sheet presents it.
|
|
42
|
+
* - `move-picker` — the Move… project picker, identified POSITIVELY by its own
|
|
43
|
+
* `AXIdentifier` prefix (`MovePopUpDialog-`) — the same
|
|
44
|
+
* identity check the picker-row resolver makes.
|
|
45
|
+
* - `other` — something modal is up that is neither of those.
|
|
46
|
+
* - `none` — no attached sheet and no detached editor window.
|
|
47
|
+
*/
|
|
48
|
+
export type UiSheetKind = "none" | "repeat" | "move-picker" | "other";
|
|
49
|
+
/** How the dialog presents: attached to the standard window, or detached (Things backgrounded). */
|
|
50
|
+
export type UiSheetForm = "none" | "attached" | "detached";
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the dialog IN FRONT — the innermost of a stack — and record how deep
|
|
53
|
+
* the stack goes. Written as a snippet because the census and the dismissal must
|
|
54
|
+
* agree on which dialog they mean, to the element.
|
|
55
|
+
*
|
|
56
|
+
* Sheets STACK (MODALX1 §6, golden-v4 / 3.23): a dialog raised while another is
|
|
57
|
+
* standing becomes an `AXSheet` CHILD of the one below it, not a sibling on the
|
|
58
|
+
* window — measured with two `things:///add` URL-consent alerts nesting inside a
|
|
59
|
+
* Repeat sheet — and dismissal is strictly LIFO. So a census that reads only the
|
|
60
|
+
* window's own `sheet 1` sees the bottom of the stack and misidentifies what
|
|
61
|
+
* actually owns the screen, and a dismissal aimed there presses a button behind
|
|
62
|
+
* a modal. Both walk to the top instead.
|
|
63
|
+
*
|
|
64
|
+
* Runs inside a `tell process "Things3"` block; leaves `shellRef` (or
|
|
65
|
+
* `missing value`), `sheetForm` and `sheetDepth` bound.
|
|
66
|
+
*/
|
|
67
|
+
export declare const AX_DIALOG_SHELL_SNIPPET = "\t\tset shellRef to missing value\n\t\tset sheetForm to \"none\"\n\t\tset sheetDepth to 0\n\t\ttry\n\t\t\t-- positional-ok: the one attached sheet a window can present, which is\n\t\t\t-- the BOTTOM of any stack; the walk below climbs to the top.\n\t\t\tset shellRef to sheet 1 of (first window whose subrole is \"AXStandardWindow\")\n\t\t\tset sheetForm to \"attached\"\n\t\t\tset sheetDepth to 1\n\t\tend try\n\t\tif shellRef is missing value then\n\t\t\ttry\n\t\t\t\tset dws to (windows whose subrole is \"AXUnknown\" and size is not {40, 40})\n\t\t\t\tif (count of dws) > 0 then\n\t\t\t\t\tset shellRef to item 1 of dws\n\t\t\t\t\tset sheetForm to \"detached\"\n\t\t\t\t\tset sheetDepth to 1\n\t\t\t\tend if\n\t\t\tend try\n\t\tend if\n\t\tif shellRef is not missing value then\n\t\t\trepeat 6 times\n\t\t\t\tset nested to missing value\n\t\t\t\ttry\n\t\t\t\t\tif (exists sheet 1 of shellRef) then set nested to sheet 1 of shellRef\n\t\t\t\tend try\n\t\t\t\tif nested is missing value then exit repeat\n\t\t\t\tset shellRef to nested\n\t\t\t\tset sheetDepth to sheetDepth + 1\n\t\t\tend repeat\n\t\tend if";
|
|
68
|
+
/** Who owns keyboard focus. Role only — never the element's value or title (see PRIVACY). */
|
|
69
|
+
export interface UiFocusOwner {
|
|
70
|
+
/** The frontmost application's process name (e.g. "Things3"). */
|
|
71
|
+
app: string;
|
|
72
|
+
/** The focused element's `AXRole`, or "" when the tree would not answer. */
|
|
73
|
+
role: string;
|
|
74
|
+
/** The focused element's `AXSubrole`, or null when it has none. */
|
|
75
|
+
subrole: string | null;
|
|
76
|
+
}
|
|
77
|
+
export interface UiState {
|
|
78
|
+
/** Is the Things process present at all? */
|
|
79
|
+
thingsRunning: boolean;
|
|
80
|
+
/** Is Things the frontmost application — the surface a keystroke would reach? */
|
|
81
|
+
thingsFrontmost: boolean;
|
|
82
|
+
/** The frontmost application's process name; null when it could not be read. */
|
|
83
|
+
frontmostApp: string | null;
|
|
84
|
+
/** Is a modal sheet / detached editor open in Things? */
|
|
85
|
+
sheetOpen: boolean;
|
|
86
|
+
/** What the dialog IN FRONT is — the innermost of a stack (see {@link AX_DIALOG_SHELL_SNIPPET}). */
|
|
87
|
+
sheetKind: UiSheetKind;
|
|
88
|
+
sheetForm: UiSheetForm;
|
|
89
|
+
/**
|
|
90
|
+
* How many dialogs are stacked (0 when none, 1 for the ordinary case). Sheets
|
|
91
|
+
* nest as children of the sheet below and dismiss strictly LIFO, so a depth
|
|
92
|
+
* above 1 means one dismissal is not enough — and that the thing in front is
|
|
93
|
+
* NOT the dialog a drive opened (MODALX1 §6).
|
|
94
|
+
*/
|
|
95
|
+
sheetDepth: number;
|
|
96
|
+
/**
|
|
97
|
+
* The open dialog's control census — role COUNTS only, the evidence behind
|
|
98
|
+
* `sheetKind` (e.g. `cb:2 pu:1 bt:2 gp:1 tf:0`). Null when no dialog is open.
|
|
99
|
+
*/
|
|
100
|
+
sheetControls: string | null;
|
|
101
|
+
/** Who owns keyboard focus; null when nothing could be read. */
|
|
102
|
+
focusOwner: UiFocusOwner | null;
|
|
103
|
+
/**
|
|
104
|
+
* False when the frontmost surface could not be walked at all — the signature
|
|
105
|
+
* of a SECURE SYSTEM MODAL (a macOS privacy/consent dialog), which belongs to
|
|
106
|
+
* no application's Accessibility tree. Everything else in the census is then
|
|
107
|
+
* "what could still be proven", never a guess.
|
|
108
|
+
*/
|
|
109
|
+
inspectable: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** A recognizable token in the script so a test runner can key off the ui-state command. */
|
|
112
|
+
export declare const UI_STATE_MARKER = "-- ui-state census (read-only)";
|
|
113
|
+
/** The label every ui-state dispatch carries (one stable command shape). */
|
|
114
|
+
export declare const UI_STATE_LABEL = "read the window and focus state";
|
|
115
|
+
/** The Things application's process name — the frontmost value that means "us". */
|
|
116
|
+
export declare const THINGS_PROCESS = "Things3";
|
|
117
|
+
/**
|
|
118
|
+
* The census script. Every read is wrapped: a surface that will not answer
|
|
119
|
+
* degrades that ONE field rather than failing the whole census, because the
|
|
120
|
+
* cases this exists for — a foreign modal, an AX-blind session — are exactly
|
|
121
|
+
* the cases where half the tree is unreadable.
|
|
122
|
+
*/
|
|
123
|
+
export declare function axUiStateScript(): string;
|
|
124
|
+
/**
|
|
125
|
+
* Parse the census record into a {@link UiState}. Returns null when the output
|
|
126
|
+
* carries none of the expected keys (a transport error, or a script that never
|
|
127
|
+
* ran) — every caller treats that as UNKNOWN, never as "all clear".
|
|
128
|
+
*/
|
|
129
|
+
export declare function parseUiState(stdout: string): UiState | null;
|
|
130
|
+
/**
|
|
131
|
+
* Read the census through the injected runner. Returns null on a transport
|
|
132
|
+
* failure — an UNKNOWN state, which every caller treats fail-closed (the guard
|
|
133
|
+
* refuses; the cleanup path does not send a blind Escape).
|
|
134
|
+
*/
|
|
135
|
+
export declare function readUiState(run: (command: UiCommand, timeoutMs: number) => Promise<UiRunResult>, timeoutMs: number): Promise<UiState | null>;
|
|
136
|
+
/**
|
|
137
|
+
* How to name the surface that owns the screen, for a refusal someone has to
|
|
138
|
+
* act on. Names the APPLICATION and the focused element's ROLE — never its
|
|
139
|
+
* contents. An un-inspectable frontmost surface is named as what it is: a
|
|
140
|
+
* system dialog macOS does not expose, which is the one case where the person
|
|
141
|
+
* at the keyboard has to look at the screen themselves.
|
|
142
|
+
*/
|
|
143
|
+
export declare function describeFocusOwner(state: UiState | null): string;
|
|
144
|
+
/** A one-line human summary of the census, for a diagnostic line or a warning. */
|
|
145
|
+
export declare function describeUiState(state: UiState): string;
|
|
146
|
+
/**
|
|
147
|
+
* The warning an OPEN Things dialog earns, wherever it is reported.
|
|
148
|
+
*
|
|
149
|
+
* FIELD-MEASURED (2026-08-27, controlled A/B on the maintainer's Mac): with a
|
|
150
|
+
* Repeat sheet left open, a write landed in the local database but Things Cloud
|
|
151
|
+
* did NOT attempt a sync — not on the write, and not when Things was brought
|
|
152
|
+
* back to the front. Dismissing the sheet released the queued sync immediately,
|
|
153
|
+
* with no further write. A stranded dialog is therefore not cosmetic: it holds
|
|
154
|
+
* the account's sync until someone dismisses it. Recorded in
|
|
155
|
+
* docs/things-app-oddities.md (the open-dialog sync gate).
|
|
156
|
+
*/
|
|
157
|
+
export declare const SYNC_GATE_WARNING: string;
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
/** A recognizable token in the script so a test runner can key off the ui-state command. */
|
|
51
|
+
export const UI_STATE_MARKER = "-- ui-state census (read-only)";
|
|
52
|
+
/** The label every ui-state dispatch carries (one stable command shape). */
|
|
53
|
+
export const UI_STATE_LABEL = "read the window and focus state";
|
|
54
|
+
/** The Things application's process name — the frontmost value that means "us". */
|
|
55
|
+
export const THINGS_PROCESS = "Things3";
|
|
56
|
+
/**
|
|
57
|
+
* The census script. Every read is wrapped: a surface that will not answer
|
|
58
|
+
* degrades that ONE field rather than failing the whole census, because the
|
|
59
|
+
* cases this exists for — a foreign modal, an AX-blind session — are exactly
|
|
60
|
+
* the cases where half the tree is unreadable.
|
|
61
|
+
*/
|
|
62
|
+
export function axUiStateScript() {
|
|
63
|
+
return `${UI_STATE_MARKER}
|
|
64
|
+
set frontName to ""
|
|
65
|
+
set focusRole to ""
|
|
66
|
+
set focusSub to ""
|
|
67
|
+
set canInspect to true
|
|
68
|
+
set thingsRunning to false
|
|
69
|
+
set sheetForm to "none"
|
|
70
|
+
set sheetKind to "none"
|
|
71
|
+
set sheetDepth to 0
|
|
72
|
+
set census to ""
|
|
73
|
+
tell application "System Events"
|
|
74
|
+
try
|
|
75
|
+
set fp to first application process whose frontmost is true
|
|
76
|
+
set frontName to (name of fp) as text
|
|
77
|
+
try
|
|
78
|
+
set fe to value of attribute "AXFocusedUIElement" of fp
|
|
79
|
+
set focusRole to (role of fe) as text
|
|
80
|
+
try
|
|
81
|
+
set focusSub to (subrole of fe) as text
|
|
82
|
+
end try
|
|
83
|
+
on error
|
|
84
|
+
set canInspect to false
|
|
85
|
+
end try
|
|
86
|
+
on error
|
|
87
|
+
set canInspect to false
|
|
88
|
+
end try
|
|
89
|
+
try
|
|
90
|
+
if (exists application process "${THINGS_PROCESS}") then set thingsRunning to true
|
|
91
|
+
end try
|
|
92
|
+
end tell
|
|
93
|
+
if thingsRunning then
|
|
94
|
+
tell application "System Events" to tell process "${THINGS_PROCESS}"
|
|
95
|
+
${AX_DIALOG_SHELL_SNIPPET}
|
|
96
|
+
if shellRef is not missing value then
|
|
97
|
+
set nCb to -1
|
|
98
|
+
set nPu to -1
|
|
99
|
+
set nBt to -1
|
|
100
|
+
set nGp to -1
|
|
101
|
+
set nTf to -1
|
|
102
|
+
try
|
|
103
|
+
set nCb to (count of checkboxes of shellRef)
|
|
104
|
+
end try
|
|
105
|
+
try
|
|
106
|
+
set nPu to (count of pop up buttons of shellRef)
|
|
107
|
+
end try
|
|
108
|
+
try
|
|
109
|
+
set nBt to (count of buttons of shellRef)
|
|
110
|
+
end try
|
|
111
|
+
try
|
|
112
|
+
set nGp to (count of groups of shellRef)
|
|
113
|
+
end try
|
|
114
|
+
try
|
|
115
|
+
set nTf to (count of text fields of shellRef)
|
|
116
|
+
end try
|
|
117
|
+
set census to "cb:" & nCb & " pu:" & nPu & " bt:" & nBt & " gp:" & nGp & " tf:" & nTf
|
|
118
|
+
set winId to ""
|
|
119
|
+
try
|
|
120
|
+
set winId to (value of attribute "AXIdentifier" of shellRef) as text
|
|
121
|
+
end try
|
|
122
|
+
if winId starts with "MovePopUpDialog-" then
|
|
123
|
+
set sheetKind to "move-picker"
|
|
124
|
+
else if nCb is 2 and nPu is 1 and nBt is 2 and nGp is 1 and nTf is 0 then
|
|
125
|
+
set groupOk to false
|
|
126
|
+
try
|
|
127
|
+
set g to group 1 of shellRef
|
|
128
|
+
if ((count of text fields of g) + (count of pop up buttons of g)) > 0 then set groupOk to true
|
|
129
|
+
end try
|
|
130
|
+
if groupOk then set sheetKind to "repeat"
|
|
131
|
+
end if
|
|
132
|
+
if sheetKind is "none" then set sheetKind to "other"
|
|
133
|
+
end if
|
|
134
|
+
end tell
|
|
135
|
+
end if
|
|
136
|
+
return "front=" & frontName & linefeed & "running=" & thingsRunning & linefeed & "form=" & sheetForm & linefeed & "depth=" & sheetDepth & linefeed & "kind=" & sheetKind & linefeed & "census=" & census & linefeed & "role=" & focusRole & linefeed & "subrole=" & focusSub & linefeed & "inspectable=" & canInspect`;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Parse the census record into a {@link UiState}. Returns null when the output
|
|
140
|
+
* carries none of the expected keys (a transport error, or a script that never
|
|
141
|
+
* ran) — every caller treats that as UNKNOWN, never as "all clear".
|
|
142
|
+
*/
|
|
143
|
+
export function parseUiState(stdout) {
|
|
144
|
+
const fields = new Map();
|
|
145
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
146
|
+
const eq = line.indexOf("=");
|
|
147
|
+
if (eq <= 0)
|
|
148
|
+
continue;
|
|
149
|
+
fields.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
|
|
150
|
+
}
|
|
151
|
+
if (!fields.has("kind") || !fields.has("inspectable"))
|
|
152
|
+
return null;
|
|
153
|
+
const kindRaw = fields.get("kind") ?? "none";
|
|
154
|
+
const sheetKind = kindRaw === "repeat" || kindRaw === "move-picker" || kindRaw === "other" ? kindRaw : "none";
|
|
155
|
+
const formRaw = fields.get("form") ?? "none";
|
|
156
|
+
const sheetForm = formRaw === "attached" || formRaw === "detached" ? formRaw : "none";
|
|
157
|
+
const frontRaw = fields.get("front") ?? "";
|
|
158
|
+
const frontmostApp = frontRaw === "" ? null : frontRaw;
|
|
159
|
+
const role = fields.get("role") ?? "";
|
|
160
|
+
const subrole = fields.get("subrole") ?? "";
|
|
161
|
+
const census = fields.get("census") ?? "";
|
|
162
|
+
const depth = Number(fields.get("depth") ?? "0");
|
|
163
|
+
return {
|
|
164
|
+
thingsRunning: fields.get("running") === "true",
|
|
165
|
+
thingsFrontmost: frontmostApp === THINGS_PROCESS,
|
|
166
|
+
frontmostApp,
|
|
167
|
+
sheetOpen: sheetKind !== "none",
|
|
168
|
+
sheetKind,
|
|
169
|
+
sheetForm,
|
|
170
|
+
sheetDepth: Number.isFinite(depth) ? depth : 0,
|
|
171
|
+
sheetControls: census === "" ? null : census,
|
|
172
|
+
focusOwner: frontmostApp === null ? null : { app: frontmostApp, role, subrole: subrole || null },
|
|
173
|
+
inspectable: fields.get("inspectable") === "true",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Read the census through the injected runner. Returns null on a transport
|
|
178
|
+
* failure — an UNKNOWN state, which every caller treats fail-closed (the guard
|
|
179
|
+
* refuses; the cleanup path does not send a blind Escape).
|
|
180
|
+
*/
|
|
181
|
+
export async function readUiState(run, timeoutMs) {
|
|
182
|
+
const res = await run({ primitive: "resolve", label: UI_STATE_LABEL, script: axUiStateScript() }, timeoutMs);
|
|
183
|
+
if (!res.ok)
|
|
184
|
+
return null;
|
|
185
|
+
return parseUiState(res.stdout);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* How to name the surface that owns the screen, for a refusal someone has to
|
|
189
|
+
* act on. Names the APPLICATION and the focused element's ROLE — never its
|
|
190
|
+
* contents. An un-inspectable frontmost surface is named as what it is: a
|
|
191
|
+
* system dialog macOS does not expose, which is the one case where the person
|
|
192
|
+
* at the keyboard has to look at the screen themselves.
|
|
193
|
+
*/
|
|
194
|
+
export function describeFocusOwner(state) {
|
|
195
|
+
if (state === null)
|
|
196
|
+
return "the window state could not be read";
|
|
197
|
+
if (!state.inspectable) {
|
|
198
|
+
return ("a system dialog owns the screen — macOS does not expose it to other apps, so it cannot be " +
|
|
199
|
+
`identified from here${state.frontmostApp === null ? "" : ` (frontmost application: ${state.frontmostApp})`}`);
|
|
200
|
+
}
|
|
201
|
+
const app = state.frontmostApp ?? "an unidentified application";
|
|
202
|
+
const role = state.focusOwner?.role ?? "";
|
|
203
|
+
return role === ""
|
|
204
|
+
? `${app} is frontmost`
|
|
205
|
+
: `${app} is frontmost and keyboard focus is on a ${role}`;
|
|
206
|
+
}
|
|
207
|
+
/** A one-line human summary of the census, for a diagnostic line or a warning. */
|
|
208
|
+
export function describeUiState(state) {
|
|
209
|
+
const front = state.thingsFrontmost
|
|
210
|
+
? "Things is frontmost"
|
|
211
|
+
: `${state.frontmostApp ?? "an unidentified application"} is frontmost`;
|
|
212
|
+
const sheet = state.sheetKind === "none"
|
|
213
|
+
? "no dialog is open in Things"
|
|
214
|
+
: state.sheetKind === "repeat"
|
|
215
|
+
? `the Repeat dialog is open (${state.sheetForm})`
|
|
216
|
+
: state.sheetKind === "move-picker"
|
|
217
|
+
? `the Move… picker is open (${state.sheetForm})`
|
|
218
|
+
: `an unrecognized dialog is open in Things (${state.sheetForm})`;
|
|
219
|
+
// A stack means the thing in front is sitting ON another dialog, and each one
|
|
220
|
+
// has to be dismissed in turn (MODALX1 §6).
|
|
221
|
+
const stacked = state.sheetDepth > 1 ? `, on top of ${state.sheetDepth - 1} more` : "";
|
|
222
|
+
return `${front}; ${sheet}${stacked}`;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The warning an OPEN Things dialog earns, wherever it is reported.
|
|
226
|
+
*
|
|
227
|
+
* FIELD-MEASURED (2026-08-27, controlled A/B on the maintainer's Mac): with a
|
|
228
|
+
* Repeat sheet left open, a write landed in the local database but Things Cloud
|
|
229
|
+
* did NOT attempt a sync — not on the write, and not when Things was brought
|
|
230
|
+
* back to the front. Dismissing the sheet released the queued sync immediately,
|
|
231
|
+
* with no further write. A stranded dialog is therefore not cosmetic: it holds
|
|
232
|
+
* the account's sync until someone dismisses it. Recorded in
|
|
233
|
+
* docs/things-app-oddities.md (the open-dialog sync gate).
|
|
234
|
+
*/
|
|
235
|
+
export const SYNC_GATE_WARNING = "while a dialog is open in Things the app stops sending changes to Things Cloud — anything " +
|
|
236
|
+
"written on this Mac stays on this Mac until the dialog is dismissed";
|
|
237
|
+
//# 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,31 @@ 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;
|
|
492
561
|
/**
|
|
493
562
|
* The PROVEN app-level clearance / relocation maneuver (SESSGATE, #480, live-host
|
|
494
563
|
* recovery): close the front Things window — which takes an attached modal sheet
|
|
@@ -514,6 +583,30 @@ export declare function axCloseReopenActivateScript(): string;
|
|
|
514
583
|
* command shape.
|
|
515
584
|
*/
|
|
516
585
|
export declare function axSheetOpenScript(): string;
|
|
586
|
+
/**
|
|
587
|
+
* The outcome of clearing a half-open dialog after a failed drive:
|
|
588
|
+
* - "none" — the census found no dialog open (nothing to clear);
|
|
589
|
+
* - "dismissed" — it is gone, and a fresh census CONFIRMED that;
|
|
590
|
+
* - "cleared-blind" — the session was AX-blind (locked / off-Space), so no
|
|
591
|
+
* census and no keystroke can be trusted; the PROVEN
|
|
592
|
+
* app-level close+reopen ran to clear it (cannot be
|
|
593
|
+
* AX-confirmed, but the maneuver works blind — SESSGATE);
|
|
594
|
+
* - "foreign" — a dialog is open that this drive did not open, so it was
|
|
595
|
+
* LEFT ALONE (a cleanup must never dismiss the dialog the
|
|
596
|
+
* person at the keyboard opened after our failure);
|
|
597
|
+
* - "may-remain" — ours, and nothing in the ladder would close it
|
|
598
|
+
* (fail-closed: report it precisely, with the sync gate).
|
|
599
|
+
*/
|
|
600
|
+
export type ClearOutcome = UiClearOutcome;
|
|
601
|
+
export interface ClearResult {
|
|
602
|
+
state: ClearOutcome;
|
|
603
|
+
/** How it was closed, for the trace and the disclosure. */
|
|
604
|
+
how?: "cancel-button" | "escape" | "window-close";
|
|
605
|
+
/** What the census identified as open at cleanup time. */
|
|
606
|
+
sheetKind?: UiSheetKind;
|
|
607
|
+
/** Who owned the screen when the cleanup started, when it was not Things. */
|
|
608
|
+
focusOwner?: string;
|
|
609
|
+
}
|
|
517
610
|
/**
|
|
518
611
|
* resolve-frame: read the element's on-screen frame (top-left origin, points)
|
|
519
612
|
* from the live AX tree and print "x y w h". Used by `click-element` to target
|
|
@@ -577,6 +670,29 @@ export declare function parseFrameCenter(stdout: string): {
|
|
|
577
670
|
x: number;
|
|
578
671
|
y: number;
|
|
579
672
|
} | null;
|
|
673
|
+
/**
|
|
674
|
+
* Read the live window/focus census through the shipped dispatch seam — the
|
|
675
|
+
* transport behind the `ui-state` diagnostic (src/ui-state.ts) and, in tests,
|
|
676
|
+
* behind any injected runner. READ-ONLY: the census clicks nothing, types
|
|
677
|
+
* nothing, and changes no state; see src/write/vectors/ui-state.ts.
|
|
678
|
+
*/
|
|
679
|
+
export declare function readLiveUiState(run?: UiRunner): Promise<UiState | null>;
|
|
680
|
+
/**
|
|
681
|
+
* Judge one guard reading. Returns null to proceed, or the refusal sentence —
|
|
682
|
+
* which always names who owns the screen, because that is the one fact the
|
|
683
|
+
* person reading it cannot recover after the fact.
|
|
684
|
+
*
|
|
685
|
+
* Exported for the unit matrix: every branch here is a fail-closed decision
|
|
686
|
+
* about synthetic input, and each one is worth a test.
|
|
687
|
+
*/
|
|
688
|
+
export declare function judgeFocusGuard(state: UiState | null, expectedSheet: UiSheetKind | null, label: string): string | null;
|
|
689
|
+
/**
|
|
690
|
+
* The cleanup disclosure (issue #620). Every branch states what is TRUE of the
|
|
691
|
+
* app right now, and — whenever a dialog may still be open — that Things Cloud
|
|
692
|
+
* sync is held until someone dismisses it, which is the consequence a caller
|
|
693
|
+
* cannot see and would otherwise discover hours later on another device.
|
|
694
|
+
*/
|
|
695
|
+
export declare function describeCleanup(clear: ClearResult): string;
|
|
580
696
|
/** Compile one recipe step into its primitive command (no dispatch). */
|
|
581
697
|
export declare function commandForStep(step: UiStep, targetUuid: string): UiCommand;
|
|
582
698
|
/**
|