Skip to content

Releases: mikegreiling/things-api

v0.20.12

Choose a tag to compare

@github-actions github-actions released this 06 Sep 11:57
a7e6d18

See CHANGELOG.md for full history.

  • Fixed — a repeat command could stop with "a Things update has redesigned it again" when nothing of the sort had happened. Setting up a repeating to-do picks a frequency and then reads the dialog to see which controls that frequency produced. The pick was reported as done the moment it was clicked, without checking that the dialog had taken it, and the read that followed did not wait for Things to rebuild that part of the dialog — so on some Macs the command read the dialog as it was a moment earlier, found the controls it expected missing, and blamed the app. It now confirms the frequency actually changed before going on, waits for the rebuilt controls, and if it genuinely cannot recognize the dialog it says what it found there instead of what it assumes happened. Refs #695.

  • Fixed — making a to-do repeat on two weekdays (or on a day of the month that was not its own) refused, and created nothing. things todo make-repeating <to-do> --frequency weekly --weekdays monday,thursday stopped with "the Repeat dialog does not hold what this drive entered — 1 control(s) differ: Next (first occurrence)" and exited 3, on a request Things is perfectly happy to accept. The command had read the dialog's first-occurrence field before setting the weekdays, found it already showing the date it wanted, and decided it had nothing to do there — and then setting the weekdays moved that date to the earliest matching day, so the check it runs before committing found the wrong one and stopped. Nothing was ever created wrongly; the request was simply refused. The same thing happened for --on-day, --on-weekday/--on-ordinal and --yearly-month whenever the day asked for was not the to-do's own. The first occurrence is now set whenever anything else in the rule can move it. Refs #695.

  • Changed — the repeat-rule dialog is now driven through the Accessibility API directly, which makes every repeat command substantially faster. Setting up a repeating to-do means reading and setting a dozen controls in Things' Repeat dialog. Each of those used to be a separate round trip through System Events, and the round trips — not the work — were most of what the command spent its time on. The same reads and clicks are now made directly, in far fewer processes, and the parts of the drive that must stay as they were (typing into the number fields, selecting a row) are unchanged. The command asks the app for exactly what it asked before, checks everything it checked before, and refuses in the same words; THINGS_API_TRACE=1 now reports each individual step with its own duration. If anything about it misbehaves, THINGS_API_REPEAT_RAWAX=0 restores the previous behavior exactly. Refs #695, #687.

  • Fixed — turning a to-do into a repeating one with the Things window closed told you to click a Dock icon; now it opens the window and gets on with it. Reordering an area already did this. The repeat verbs did not: they saw no window, said "Things is running but has no open window", and stopped — on a Mac that was unlocked, in front of someone who had simply closed the window. They now do what the sidebar move does — ask whether the screen is locked, and if it is not, ask Things to reopen its window, check that one actually appeared, and carry on. The window is left open afterwards, and the result says so: "Things had no open window, so one was reopened to run this — it was left open". If the screen IS locked, or the reopen produces no window, the refusal is unchanged and nothing has been created. Refs #732.

  • Improved — on a Mac with the helpers installed, the repeat-rule dialog stops asking Things questions it has already been told the answer to. Two of the waits in that dialog belong between one step and the next: after picking a frequency, Things rebuilds the row of cadence controls, and after any pop-up selection it takes a moment to put the menu away. The next step used to discover both by asking the app the same question over and over — and on a Mac where the helpers carry the automation, that was the only way it could, because the app's own announcements were only reachable from inside a script and scripts routed through the helpers are not allowed to open a socket.

    They are reachable from outside one. The helper has been listening on behalf of the command since before the step ran, so the command now waits for Things to say the row was rebuilt, and for it to say the menu closed, and only then sends the next step — which is generated without the loop that would have asked. It is the same script that a Mac with the developer tools has been running all along, so nothing new is being tried; what is new is that a routed Mac can now get to it. The wait is armed only where Things is certain to announce something, and if the announcement does not arrive the command falls back to asking, exactly as before.

    A Mac without the helpers is unaffected — byte for byte, the same scripts as before. THINGS_API_TRACE=1 records each wait, whether it was satisfied, and which step dropped its loop. Refs #695, #676.

  • Fixed — making a to-do that already had a deadline repeat reported a failure, for doing exactly what Things does. Turn a deadlined to-do into a repeating one and Things' own Repeat dialog opens with "Add deadlines" already ticked and the gap between the start and the due date already filled in — so the series it creates is deadlined, and its first occurrence starts where the to-do started and is due the same number of days later. That is the app's default and it is what you would get by hand. This command committed exactly that and then compared it against a rule that had never mentioned a deadline, decided the first occurrence had moved, and exited 3 on a series that was correct.

    The deadline the item already carries is now the series' deadline: the first occurrence lands on the to-do's own scheduled date, every occurrence is due the same number of days later, and the result says so — "the to-do's own deadline came with it: every occurrence is due 3 days after its start — pass --deadline (or --start-days-earlier) to set a different one". Passing --deadline (or --start-days-earlier) overrides it, as before. A deadline that falls BEFORE the start is not inherited, because the dialog discards it.

  • Changed — a command that makes several changes to do one thing now holds the write lock for the whole thing, so two commands can no longer interleave halfway through each other. Moving a set of to-dos, reordering a list, editing one checklist item, clearing a reminder, archiving a heading with its children, dragging an area in the sidebar: each of those is several changes in a row, and each one used to take and release the lock per change. Another things command running at the same time could land in any of the gaps — between reading a checklist and writing the edited version back (its edit disappears), between working out where an area will land in the sidebar and dragging it there (it lands somewhere else), between parking a to-do and putting it back (it comes back to the wrong place).

    A second command now WAITS for the first to finish, for up to thirty seconds, and if it is still waiting after that it says what it is waiting for: "another operation holds the mutation lock: area.reorder (pid 4321), since 2026-09-05T10:02:11Z, held for 12s — pid 4321 is still running". If the holder is gone, it says the lock is stale and that running the command again takes it. A --dry-run never waits for anything: it changes nothing, so it has nothing to serialize against.

  • Fixed — a command killed mid-write no longer leaves its lock behind for the next one to wait out. A command interrupted by a timeout or a Ctrl-C prints its "outcome uncertain" line and exits, and that exit path never released the lock — so the next write spent its full wait discovering the holder was dead. It now releases on the way out. A hard kill (kill -9, a power cut) still cannot run any code, and that case is unchanged in behavior but sturdier underneath: a lock is recognized as abandoned by the holder's process identity rather than by its process number alone, so a recycled number can no longer make a dead command's lock look alive forever. things rescue status now also names the operation that holds the lock, not just its process number.

  • Changed — reordering sidebar areas stays off by default, and the reason it gives is now the real one. The refusal used to say the command was off "until it completes inside five seconds on real hardware", which framed it as a countdown to either promotion or deletion. The ruling is different: a command that drives the window is fragile in the presence of a person using the app, and belongs on a Mac nobody is working in — so it stays available behind things config set experimental-area-reorder true rather than being removed, and the refusal now says that, along with the fact that other changes wait while it runs. Refs #676.

  • Fixed — a GUI command run while the Mac was locked said Things had no open window and told you to click its Dock icon. It could not know that. A locked screen hides every window from every application, so "no window" is what the command sees whether the window is closed, the screen is locked, or the window is on another desktop — and it picked the first of the three and said it as a fact, behind a lock screen, after five and a half seconds of looking. It also reported the result as a failed change, though it had not touched anything.

    Every command that has to read or click the Things window now asks the Mac whether the screen is locked before it looks at anything else. Locked, it stops immediately — in about a fifth of a second, with nothing sent and nothing changed — and says so: *"Refused to drive the Things window: the screen is locked, so n...

Read more

v0.20.11

Choose a tag to compare

@github-actions github-actions released this 05 Sep 01:44
33ba068

See CHANGELOG.md for full history.

  • Fixed — moving an area to the bottom of the sidebar could put it second-to-last instead, and then report that it had failed. The command worked out where to let go of the area before it picked it up, using an arithmetic model of what the list looks like mid-drag: Things closes the gap where the dragged area used to be, so everything below it moves up by the height of what you lifted.

    That is not what Things does. It takes the area and its projects out of the list and leaves a landing gap in their place — a gap that follows the pointer as you move — so rows below the insertion point sit lower than the model expected, by about the height of two rows. Every placement except "to the bottom" aims between two rows, where being off by a little does not change the answer. "To the bottom" was the one that aimed just past the end of the list, where it does. On the reporting Mac it landed one slot short, twice, and the command exited reporting the mismatch it had verified for itself.

    The command no longer computes a point and hopes. It picks the area up, reads the list back while holding it, works out from the row positions where Things is currently offering to drop it, and adjusts until that is the slot you asked for — then lets go without moving again. If it cannot tell where the drop would land, it presses Escape while still holding the button, which Things treats as a cancelled drag and which leaves the sidebar exactly as it was, and reports what it saw. Refs #729, #676.

  • Improved — a GUI-driven sidebar move that fails now says what it measured. "The drag gesture did not complete" was every failure's report, including the ones where the script itself had died and macOS had said why. A refusal now carries the numbers behind it: where the drop point ended up relative to the visible list, whether the list scrolled out from under the pointer, the last few positions it tried and what Things offered at each, or — when the script did not run at all — the first line of the interpreter's own error. THINGS_API_TRACE=1 additionally records, for every drag, the point the old arithmetic would have aimed at, the point the live reading chose, and the difference between them. Refs #729.

v0.20.10

Choose a tag to compare

@github-actions github-actions released this 04 Sep 12:56
4d1c047

See CHANGELOG.md for full history.

  • Changed — reordering an area in the sidebar reads the sidebar the way a screen reader would, instead of the way a web crawler does. Every time the command needed to know where the areas were — before each scroll, after each fold, before each hop — it read the CONTENT of every row in the sidebar. On a large sidebar that is around 900 requests to Things and 16–18 seconds, each time, because the expensive part is not asking but making the app draw a row it was not drawing. A single move to the end of a 174-row sidebar took over seven minutes on real hardware, and the gestures were about five seconds of it.

    It now reads the POSITION of every row — which costs nothing, because positions need no drawing — and reads the content only of the rows it believes are the areas, which it predicts from the order the database already holds and then confirms by reading them. If any one of them is not what was predicted, it falls back to the full read it always did, and says so in the trace. The scroll, the wheel fallback and the disclosure arrow are addressed the same way: they no longer re-read the whole sidebar to find it, and each confirms it is aimed at the right list by reading exactly one row first.

    Waiting is now the app's job too: after clicking a disclosure arrow the command waits for Things to say the row count changed rather than for a fixed six-tenths of a second, on the Macs that can listen. Where nothing can listen — a Mac without the developer tools, or one where the helpers carry the automation — it waits exactly as long as it did before. Refs #676.

  • Added — THINGS_API_TRACE=1 now says what one sidebar move cost. Each move records how many times it read the sidebar and how each read was answered (predicted, or fallen back to the full read), how many requests it made to Things and how many rows it made the app draw, how many gestures it sent, and whether each wait ended because the app said something or because a timer ran out. Those are the numbers that transfer between machines, so a move's cost on a real Mac can be read off the trace instead of estimated. THINGS_API_SIDEBAR_SPARSE=0 turns the predicted reads off entirely and restores the previous behavior. Refs #676.

v0.20.9

Choose a tag to compare

@github-actions github-actions released this 04 Sep 08:10
91f581c

See CHANGELOG.md for full history.

  • Fixed — the new pointer-gesture safety check refused every GUI-driven command on every Mac. The check added just before this release asks whether anything is covering the point it is about to click or drag. It asked the question two ways and let the wrong answer win: macOS keeps invisible, click-through windows the size of the whole screen permanently in front of every application — Notification Center and the Dock on any Mac, and two more belonging to the login window — and the check counted those as something being in the way. So area reorder and the --dangerously-drive-gui repeat verbs refused with "Notification Center owns the screen", or "Dock", having sent nothing, every time.

    The reliable answer, which the check was already getting and then discarding, comes from asking the window server what a click at that point would actually reach; it knows which windows pass clicks through and which do not. That answer now decides. The window list is consulted only when the window server has no answer at all, and there a window is ignored only if it both belongs to macOS itself and covers the entire display — so a notification banner, which is small and does swallow clicks, still stops the gesture and is named. Refs #676.

  • Developer-facing — the lab now runs its end-to-end suite under BOTH identities, including the one every real Mac uses. Every arm the lab had ever run executed its scripts directly, in the guest's own shell; a Mac with the helpers installed brokers the same scripts through things-deputy, which refuses whole classes of them on purpose. Two releases shipped green in the lab and broken in the field on nothing but that difference. npm run lab:regress now clones a new golden carrying the signed helper pair with its macOS permissions already granted, runs the write-layer suite through the helpers as well as directly, reports each arm by name, drives one real repeat-rule dialog through the broker, and requires a write attempted with the helper stopped to refuse rather than quietly run itself. Nothing about the shipped CLI changes; what changes is that the release gate can now be met inside a disposable virtual machine instead of on a real Mac. Refs #695, #696.

  • Improved — on a Mac with the helpers installed, GUI-driven commands now wait for the app instead of the clock. Waiting for Things to finish something — a dialog to open, a section to rebuild — is either a matter of asking the app to say when it is done, or of guessing at a duration. Machines that drive Things directly have had the first since 0.20.7. Machines where the helpers carry the automation could not: the listener is a small watcher process, and starting one means running a command, which the helper refuses by design.

    The helper now keeps the listener itself. It already has the permission the listener needs and it is already running, so there is nothing to start and nothing to refuse. Commands driven through the helpers now wait on the app's own notifications for the waits the command makes between steps — including the pause after picking a repeat frequency, which was the longest single wait a Mac could spend on todo add-repeating.

    Two things this does not change. Waits that happen inside one step still count time rather than listen, because that half still has no way to reach the listener that the helper will permit — so this is a real improvement on such Macs, not the whole one. And a Mac with older helpers behaves exactly as it did: the command asks what the installed helper can do, is told it cannot host a listener, and runs the timed version. Updating is bash scripts/build-helpers.sh, then things helpers install. things doctor and things helpers status both name which of the two you have. Refs #695, #676.

  • Fixed — a GUI-driven command that would have clicked or dragged inside another application now refuses instead, and names what was in the way. The commands that drive Things through its window — area reorder, and the --dangerously-drive-gui repeat verbs — move the real mouse pointer to positions they read out of Things a moment earlier. If anything changed in between, those positions could belong to something else by the time the pointer arrived: another app brought to the front, a window moved over the Things sidebar, a screen lock, or the sidebar scrolled under the pointer. The gesture went where the pointer was pointing, which could be a text selection in another app, a file dragged somewhere in the Finder, or a browser tab torn off.

    Every synthesized click, drag and scroll now checks, in the same breath as sending it, that Things is the front application, that the point is inside Things' own window, that no other application's window covers that point, and that the thing under the pointer is still the row or control the command aimed at. If any of that does not hold, nothing is sent and the command says so — naming the application that owns the screen at that point, and the coordinates. A drag already under way re-checks before it lets go, and cancels the drag rather than dropping somewhere unintended.

    It errs toward refusing: an always-on-top window over the sidebar will stop a reorder that might have worked. Re-run it with the window moved. The checks cost about 4 ms and a handful of reads per gesture, so nothing gets slower. Refs #676.

  • Fixed — clearing a reminder on a to-do that turned up in Today on its own no longer reports a failure for a change that worked. todo update <ref> --when today --clear-reminder aimed at an item that arrived in Today by itself — a repeating series' current occurrence, or anything scheduled for today — removed the reminder and then said verify-failed:mismatch, describing the item as being in Someday and in Today at once. Both readings were true and neither was wrong: an item Things puts in Today on its own stays marked the way it arrived until someone acknowledges it in the app, and that is the state the app leaves it in. The check now accepts it, so the command reports the success it achieved. Refs #699.

  • Fixed — a reminder can be cleared while moving the item to Anytime, in one command. todo update <ref> --when anytime --clear-reminder was refused with instructions to say today or evening instead. Moving an item to Anytime already removes its reminder — an Anytime item has no date for a reminder to fire on — so the refusal was standing in front of the shortest way to do exactly what it recommended in two steps. --when anytime now takes --clear-reminder. A reminder on a specific future date still cannot be cleared in place; that refusal is unchanged. Refs #699.

  • Fixed — --exception aimed at the wrong half of a repeating series now says which half, and what to do instead. Aimed at one of the series' occurrences it answered "this to-do is no longer a repeating series" and suggested retrying: the item was an occurrence of a live series, and an occurrence is already the exception — it can simply be edited. Aimed at the series itself, for a series that counts from each completion and so has no next date yet, it sent the caller to "one of its occurrences" without naming one, which is where the first message had just come from. Both now name the occurrence's id and the command to run on it. Refs #699.

v0.20.8

Choose a tag to compare

@github-actions github-actions released this 03 Sep 18:13
83e77fa

See CHANGELOG.md for full history.

  • Fixed — making a to-do repeat works again on a Mac without the developer tools, or with the helpers installed. todo add-repeating / make-repeating / reschedule-repeat --dangerously-drive-gui had been failing after about nine seconds with nothing entered, reporting that the Repeat dialog "matched neither known shape — a Things update has redesigned it again". Things had not changed anything. The dialog rebuilds its middle section a fraction of a second after you pick a frequency, and the command was looking at it during the rebuild: for that moment the section really is empty, and the app answers the question as readily as it would have a moment later.

    Why it only happened on some Macs: the listener added in 0.20.7 hears Things announce "that section is rebuilt", so wherever it runs, the command was already waiting for exactly the right thing. Wherever it does not run — a Mac without the Command Line Tools, or one where the helpers carry the automation — nothing was waiting at all, because the step that used to absorb the pause by accident had been made faster and then skipped entirely in 0.20.7. So the command now waits for the rebuilt section itself, by looking until it has settled, on every Mac that has nothing to listen with. Macs with the listener behave exactly as before.

    And if the section genuinely never settles, the message now says that, instead of blaming an app update for it.

  • Fixed — making a to-do repeat works again on a Mac with the helpers installed. On a machine set up with things helpers setup, todo add-repeating --dangerously-drive-gui had been failing after about two seconds, with nothing driven and a message about the helper refusing to run a shell command. The cause was the settle listener added in 0.20.7: it starts a small watcher process, and it asks the helper to start it — but the helper deliberately refuses to run anything that shells out, which is what keeps "drive the Things GUI" from becoming "run any command with the helper's permissions". That refusal is correct and stays.

    So the listener now stands down wherever the helper carries the automation, and those Macs run exactly the code that shipped before 0.20.7 — the same commands, the same results, none of the speed-up the listener buys. Macs that drive Things directly keep it. THINGS_API_TRACE=1 names which of the two ran, and why.

    Every command that has ever been reported working on such a machine had been certified only on machines that drive Things directly, which is why this was invisible until the field found it. Two guards now close that: every command the GUI drive can send is checked against the helper's rules before release, and the real helper binary is asked to accept every one of them.

  • Changed — a release is now certified the way it is actually used, not only the way it is probed. Every version was already blocked on a fully green lab regression suite. That suite runs headless, and it runs each operation by executing its script directly — which is not how a real machine runs them: with the optional helpers installed, every operation is brokered by the helper process, on real hardware, with a real display. Two releases shipped green-in-lab and broken-in-field on that difference. So the gate is broadened: before a version is tagged, every operation whose driver changed in the batch is run end-to-end through the helpers using the ordinary command line, and anything that drives the app's own windows is run at least once on a real display. The drill is written down in docs/reference/release-checklist.md.

v0.20.7

Choose a tag to compare

@github-actions github-actions released this 03 Sep 00:04
0231925

See CHANGELOG.md for full history.

  • Improved — making a to-do repeat now reads the Repeat dialog instead of retyping what it already says. The dialog fills itself in from the to-do it was opened on: give it a to-do scheduled for a Thursday and ask for a weekly repeat, and it comes up already saying "every 1 week, on Thursday, next occurrence that Thursday". The command used to click every one of those controls anyway. It now checks them — one look at the dialog, comparing each control against what you asked for — and only touches the ones that disagree.

    Nothing is taken on trust. A control that does not already say the right thing is set exactly as it was before, and the dialog is still audited control by control before anything is committed, whether the value was typed or found. The commands land the same series either way: every shape was run twice, once reading and once typing, and the stored rules came out byte for byte identical.

    What that removes is real work. The most expensive step in the whole command was confirming that the interval box said "1" — nearly a second of it, on a Mac, to type nothing at all — and it is gone. So are the weekday, the day-of-month, the month, and the first-occurrence date, whenever the to-do's own schedule already implies them. Depending on the repeat you ask for, that is between one eighth and nearly half of everything the command says to Things, and up to half again of the parts of the dialog it has to load.

  • Improved — a repeat with a deadline now starts from the right place instead of being steered there. Things anchors a series with a deadline on the due date and works the start date backwards from it. The command used to create the to-do on its start date and then walk the dialog over to the due date. It now creates it on the due date to begin with, so the dialog's own suggestion is already correct — the series still starts exactly when you asked, because the app does the subtraction.

  • Fixed — an impossible "start N days earlier" on an after-completion repeat is now refused instead of being quietly changed. A series that repeats a week after each completion cannot have occurrences that start more than six days before they are due — the start would land on or before the previous one's deadline. Things enforces that, but it enforces it by replacing your number without saying so: asking for 30 days on a weekly after-completion repeat committed 6, and on a three-day one committed 0. --start-days-earlier above the limit is now refused up front, naming the limit and the two ways around it (a longer interval, or a fixed schedule, which has no limit).

  • Added — THINGS_API_TRACE=1 now records which dialog controls were already correct. Each GUI repeat command's trace names the controls it checked, which of them already held the requested value, and which setter it therefore skipped — so a slow run can be read as "the dialog needed all of this driven" rather than leaving it a guess. THINGS_API_PREFILL=0 turns the whole thing off and runs the previous behaviour, unchanged.

  • Improved — making a to-do repeat no longer waits on a clock. It waits for Things to say it is ready. Every step of the Repeat dialog used to guess: click a pop-up and re-ask "is your menu open yet?" every fiftieth of a second; ask a field for keyboard focus and then sleep a fixed 0.15 s in the hope it took; change the frequency and then re-read the whole section until two readings agreed. Things has been announcing all of it the whole time — macOS has an accessibility notification for "this menu opened", "this field took focus", "this control now holds the value you set" — and the app is completely silent when nothing is happening, so every announcement belongs to the thing the command just did. The command now listens, and each step ends the moment the app says it is done rather than when a timer runs out.

    The practical difference on a Mac: entering an interval took 1.21 s and now takes 0.56 s, and no step's timing depends any more on how fast the rest of the command happens to be — which is the failure this fixes at the root. A wait sized by "however long two readings take" breaks the moment the readings get faster, and that had already happened once.

    This needs the Command Line Tools, which most Macs with developer tools installed already have. Without them — or with THINGS_API_AX_OBSERVER=0 set — every command runs exactly the code that shipped before, unchanged, so nothing is lost by not having them.

  • Fixed — three seconds of pure waiting removed from making a to-do repeat, reported from the field. Watching the command run on an M1 showed two pauses that were doing nothing useful, and both are gone.

    The first was a ~1.5 s stall before the dialog's "Next:" field was touched. The command was waiting for Things to recompute which dates the rule produces — a real thing to wait for — but it started waiting after the recompute had already finished, so it spent its whole budget re-reading a control thirteen times to discover nothing had changed. It now knows immediately, because it was already listening; and when the step before it changed nothing at all, it does not wait at all.

    The second was the command opening the "Next:" menu, walking its list of dates, and clicking the one the field was already showing. Since making an item repeat starts the series on the item's own scheduled date, that is the normal case rather than a corner. It now reads the field once and skips the whole thing when it already says the right date — including when the field reads "Today". Nothing about the checking changes: the dialog is still audited control by control before anything is committed, and the resulting series is still verified against the database afterwards.

    Working out which version of the Repeat dialog is open got cheaper too — the same question, asked in three questions instead of fifteen.

  • Added — THINGS_API_TRACE=1 now records what each step of a GUI command WAITED for. Beside how long a step took and how many controls it read, a step's trace record now names the notification it waited for and how long the app took to send it. That is the part of the time a command cannot make smaller, so a slow run can now be read as "the app took this long" or "we asked too many questions", rather than leaving both possible.

v0.20.6

Choose a tag to compare

@github-actions github-actions released this 02 Sep 17:53
7550467

See CHANGELOG.md for full history.

  • Fixed — setting a repeat frequency and its interval no longer races the dialog's own rebuild. Changing the frequency makes Things rebuild that section of the Repeat dialog. The command went looking for the interval field before the rebuild had happened, found the previous layout, and picked its target out of that. It worked — the value it typed survived the rebuild, and the check before committing read the correct number back out — but the field was chosen from a stale picture of the dialog, which is precisely the class of mistake that once wrote an interval into the wrong box. It now waits for the rebuilt section to actually appear before deciding which field to type into.

    Found by counting, not by a failure: a new per-step count of how many controls each step reads came back at 17 where the dialog's own structure demanded about 39, and the missing ones were the controls the rebuild had not created yet.

  • Added — THINGS_API_TRACE=1 now records how many controls each step of a GUI-driving command reads. Alongside how long a step took, its trace record now carries how many distinct dialog controls it read the content of. On a real Mac that count is the better predictor of how long a step will take: reading a control's contents makes the app build that part of its interface on demand, while reading positions and sizes is free. Unlike the accessibility round-trip counter added in the same release, this one needs no extra switch and works with the helpers running.

    If you are reporting that a repeat command is slow, a run with THINGS_API_TRACE=1 now says both what your machine cost and what any machine would.

  • Fixed — the sidebar probe's five-second estimate was wrong, and said "reachable" when it is not. lab/scripts/field-probe-sidebar.jxa.js — the one-file instrument you can copy to any Mac and run against Things — ended with a predicted time for a sidebar reorder. It worked that prediction out by multiplying the number of questions a sidebar read asks by how long one question takes, and printed "REACHABLE: 3,510 ms". Its own measurements say otherwise: reading the sidebar shallowly and reading it deeply take the same twenty seconds for four times as many elements, while asking every row where it is takes about two milliseconds. The cost is not per question. It is paid once per row whose content is touched, and paid again the next time. The model now prices those two kinds of read separately — they differ by roughly a thousandfold on a real display — and reports the honest number. The full-sweep measurement also now reports milliseconds per row read, beside the per-question figure it always had, with a note saying which of the two means anything.

  • Added — the field probe can now measure the three things a screen reader does differently. Three new cells, and a --areas <n> option so the arithmetic is priced at your own sidebar rather than a stand-in.

    • Hit-test: ask the accessibility API what is under a point instead of listing everything, then read one row to confirm it. Reports whether the point resolved to a row and what the whole route cost.
    • Read strategies: the full sweep, the visible-rows-only sweep, and a sparse read (ask every row where it is — which is nearly free — and read the content of only a handful) side by side, in rows-touched and in milliseconds per row touched.
    • Notifications: register for the accessibility notifications a waiting step would want, nudge the scroll bar, and report which ones actually arrived and how quickly. This cell makes one change and undoes it immediately — it moves the sidebar's scroll position and puts it straight back, reporting whether the original value was restored. Nothing else in the probe writes anything, and nothing anywhere in it reads your database.

    The probe's own instructions and safety notes were updated to say all of that plainly, including that two cells now touch the app rather than one.

v0.20.5

Choose a tag to compare

@github-actions github-actions released this 02 Sep 16:44
c9bc24d

See CHANGELOG.md for full history.

  • Improved — making a to-do repeat is faster, and the speed came from asking the dialog fewer questions. Every command that drives Things' Repeat dialog — make-repeating, add-repeating, reschedule-repeat, and the project versions of each — now reads the dialog in a fraction of the round-trips it used to.

    The driver used to inspect the dialog one control at a time: how many labels are there, what does label 1 say, label 2, label 3, and so on, then the same again for the fields, then the same again for the next step. Each of those is a separate round-trip into Things, and on a real Mac each one costs an order of magnitude more than it does in the lab — which is why an earlier round of work that removed whole steps barely moved the needle on the machine it was meant to help. It now reads the whole set in one go, per property, however many controls there are. The same checks are made, from a single snapshot rather than a series of reads taken over a tenth of a second.

    Two more of the same kind: the check that identifies which dialog is open asked for five separate control counts and now asks for one list of control types, and it no longer reads which element has keyboard focus when Things is already in front — a comparatively expensive question whose answer is only ever printed when the command is refusing because Things is not in front.

    Measured end to end on the lab's own machine, on the exact command the report came from: 117 accessibility round-trips down to 88, 15 osascript processes down to 13, and 4.25 seconds down to 3.34. The round-trip count is the number that carries to a real Mac; the seconds are not.

  • Improved — the pre-commit check on the Repeat dialog now presses OK itself. The command re-reads every control it set before committing, then presses OK. Those were two separate trips into the app, with a gap in between during which the thing just checked could change. They are now one, so what gets committed is the state that was checked. If the OK button will not press, that is reported as its own outcome rather than as a failed check.

  • Fixed — a numeric field in the Repeat dialog that is not ready yet gets another moment, instead of an immediate refusal. After you change the frequency, Things rebuilds that part of the dialog, and for a short while the interval field is fully in place but will not yet accept typing. The command used to refuse the first time it saw that ("the field did not take keyboard focus") — it only worked at all because the checks that ran beforehand happened to be slow enough to cover the gap. It now waits for the field itself to be ready, and still refuses, in the same words, if it never is. Nothing is ever typed into a field that has not been confirmed ready.

  • Fixed — an open Repeat dialog is recognized as one again by things doctor --ui-state and things rescue. A dialog whose structure has changed shape is meant to be reported as unrecognized; briefly, during this work, every Repeat dialog was. The commands that dismiss a dialog need to know which one is theirs, so this is now covered by a lab cell that reads the report directly rather than only by drives that pass.

  • Added — the Repeat dialog is now checked against its expected shape the moment it opens. The command used to satisfy itself that one control was there and press on. It now takes the dialog's full control census as it opens and compares it with the shape this version was certified against; a dialog that does not match is a Things update that has redesigned it, and the command stops with nothing entered rather than pressing controls it can no longer vouch for. On a version of Things this package has not been certified against, nothing is assumed and the command does the full per-step checking it always did.

  • Added — THINGS_API_AX_COUNT=1 records how many accessibility round-trips each step of a GUI-driving command made. With tracing on (THINGS_API_TRACE=1), each step's trace record already carried how long it took; it can now carry how many questions it asked, which is the number that means the same thing on every Mac. Useful when reporting that a command is slow: the durations say what your machine cost, the counts say what any machine would. It is off unless you ask for it. One caveat: the counting happens in whichever process runs the automation, so on a machine with the helpers carrying automation you will need things config set helpers-enabled false for the run that captures it.

v0.20.4

Choose a tag to compare

@github-actions github-actions released this 02 Sep 15:20
8d657be

See CHANGELOG.md for full history.

  • Improved — the bundled agent skill now says to look at the open issues before filing a new one. references/bug-reports.md gains a short passage under "Where to file": start with gh issue list --state open, and when an open issue already describes the same command failing with the same symptom, add the new run's data to it as a comment — versions, complete output, trace excerpt — instead of opening a second issue for the same defect. A genuinely distinct failure — a different step, command, or underlying operation — still earns its own issue, cross-referenced by number.

  • Changed — a PR that addresses a tracked issue now references it as Refs #N, never Fixes #N. The repo conventions in AGENTS.md spell out the issue lifecycle: Fixes/Closes/Resolves auto-close an issue the moment the PR merges, and a merge is not confirmation that the defect is gone. A field-reported bug is closed only once the reporter's own re-run on the machine that hit it says the fix resolves it, because lab certification cannot reproduce real-hardware conditions.

  • Changed — things area reorder is now off by default, and says why. Reordering sidebar areas is the one thing this tool does by driving the Things window through the accessibility API: it synthesises a drag, and between gestures it has to read the sidebar back to see where everything ended up. On a large sidebar that read has been measured at 16–18 seconds per read on an M1 MacBook Pro — the same read takes under a second on a test machine — and a single move needs several of them. A command that can take minutes, and can leave an area collapsed if it stops part-way, is not a command to hand someone without warning.

    So it is behind an opt-in now: things config set experimental-area-reorder true. Without it, things area reorder, things reorder on areas, and the MCP reorder tool's areas branch all refuse and explain what the command drives, what it has measured, and what it can leave behind. Nothing else changed about how it works when you do turn it on. The bar it has to clear to come back on by default is finishing a move in about five seconds on real hardware.

    Refs #676.

  • Fixed — the step that collapses a blocking area no longer gives up after 30 seconds and calls it "the disclosure arrow did not respond". Every other sidebar step already got a time allowance that grows with the size of your sidebar; this one was still on the flat 30-second allowance that a big sidebar had already outgrown, so on a large sidebar it was stopped mid-work and the message described a Things problem that was not happening. The allowance now scales like the others, the message now distinguishes stopped after N seconds from would not run from the arrow refused the click, and every sidebar-touching step derives its allowance from one place, so a future step cannot quietly be left out.

    The step also got much faster. Finding the right row used to walk six levels of the accessibility tree for all 174 rows with three separate reads per node — 8,185 round-trips into Things, 3.9 seconds on a test machine and, at the speeds measured on an M1, well over two minutes. It now uses the same batched two-level read the sidebar snapshot uses, which finds exactly the same rows: 506 round-trips, 0.5 seconds.

    Refs #676.

  • Added — the collapse step reports what it did, step by step, when tracing is on. THINGS_API_TRACE=1 now records the row scroll, the row census, the click, the settle and the confirming re-census as separate timed records, and the click record carries the script's own internal split (locating the sidebar, harvesting the rows, finding the arrow, dispatching the click). A step that hangs and a step that is merely slow no longer look the same in a trace.

    Refs #676.

  • Added — lab/scripts/field-probe-sidebar.jxa.js, a standalone read-only measurement script. One file, no install, no checkout: osascript -l JavaScript field-probe-sidebar.jxa.js prints how long a full sidebar sweep takes on your Mac, what one accessibility call costs there, whether the sidebar supports reading only the visible rows, and a predicted time for a whole reorder. Its output is counts, durations and geometry only — no task, project or area names — so it is safe to paste into a bug report.

    Refs #676.

v0.20.3

Choose a tag to compare

@github-actions github-actions released this 02 Sep 00:08
601521b

See CHANGELOG.md for full history.

  • Fixed — things area reorder no longer depends on where you left the mouse pointer, and can read a big sidebar without giving up. Two separate problems in the same command, both of which made it fail on a large sidebar and neither of which said what was really wrong.

    The command scrolls the sidebar to bring rows into view. It did that by sending scroll-wheel events — and macOS delivers those to whatever is under the pointer, so unless your cursor happened to be sitting over the sidebar, the sidebar did not move at all. No error, no warning: six wheel clicks moved it exactly zero pixels. It now scrolls by setting the sidebar's scroll position directly, which works no matter where the pointer is and gets there in one step instead of a dozen. The wheel is kept only for the case where a sidebar exposes no scroll position at all, and it moves the pointer first — because that is the only way it ever worked.

    Separately, reading a large sidebar could take longer than the command's fixed 30-second allowance and stop the whole move before a single thing was dragged, with a message blaming your Mac. Reading a 174-row sidebar is now substantially cheaper — the read was fetching a whole layer of the accessibility tree it then threw away, and the fallback for an unusual sidebar re-read everything, including your task list, a second time. Measured on a 178-row sidebar: 2.09 seconds down to 0.82. What is left of the allowance now grows with the size of your sidebar instead of being the same number for everyone, so a big sidebar gets the time a big sidebar needs.

    Fixes #672.

  • Fixed — when the command cannot scroll a row into view, it now says which of five things went wrong. "…'s row could not be scrolled into view" was the single sentence for a sidebar read that failed, a scroll command that was rejected, a scroll that was accepted and did nothing, a list already at the end of its range, and simply running out of attempts. All five now name themselves alongside that sentence, together with what the last attempt measured — how far off the row still was, whether anything moved, and where the scroll position stood. If you are filing a report, that line is the one worth quoting.

    A failure that leaves nothing behind also carries its step list again. That is the most common kind of failure to report and it was the one case in the whole package that dropped it.

  • Fixed — THINGS_API_TRACE=1 turns tracing on. Only the exact word true used to work; 1 was ignored without a word, so a diagnostic run made specifically to capture a trace could quietly produce nothing. 1, true, yes and on all enable it now, and 0, false, no and off all disable it (the same spellings work for every THINGS_API_* on/off variable). A value that is none of those is still refused rather than guessed at. When a GUI-driving command fails without tracing on, its message now names the variable.