fix(list): fold a closed or trashed project's to-dos into its row - #235
Merged
Merged
Conversation
Things shows a closed project in the Logbook as one row, not as a row plus every to-do inside it, and shows a trashed project in Trash the same way. The CLI listed each child separately, so `things logbook` returned 1328 rows where the app's Logbook showed 910. Measured against the app on 10 Sep 2026 by uuid. The app's Logbook held no to-do at all whose parent project was closed or trashed: 328 and 77 such rows respectively in the CLI. Trash is not the same case — the app's Trash held 23 to-dos whose parent project was closed but not trashed, and none whose parent was trashed, so throwing a to-do away out of a finished project keeps it a Trash row on its own account. Only the trashed-parent fold applies there, and it drops 4 rows. That leaves viewsIncludingTrashedProjects empty, so the clause it gated is now unconditional and the map is gone. The folded rows have to stay reachable, and naming the project is how. A closed or trashed project's contents were unreachable before this: the catch-all view pins t.status = 0, so `things --project <closed uuid>` returned nothing and `things show <uuid> --agent` reported "the project has no open to-dos". It now returns the contents whatever their status, which is what the app answers for the same question — `to dos of project id` returned 78 for a project holding 65 completed, 13 cancelled and 3 trashed children, and 49 for one holding 42 and 7. So the status pin drops and t.trashed = 0 stays. The widening is scoped to a named project: a bare --area sweep is still the open set. A closed project's brief would have listed those rows under "Open to-dos", so the heading drops "Open" and each row carries [x], [~] or [ ]. A to-do thrown away out of a project that is itself in the Trash is now listed nowhere, as in the app: asking Things for a trashed project's contents returns nothing for a row already in the Trash on its own account. After the change `things logbook` and `things trash` match the app uuid for uuid, 910 and 165 rows with no difference in either direction. Closes #229
This was referenced Sep 10, 2026
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
…views (#252) Closes #249 ## What changed Since #235 the logbook and trash fold a closed project's to-dos into the project's own row, as the app does. The `--include-completed` variants of today and anytime folded nothing, so the same to-do was folded in one view and listed in another. Both now apply the fold, from a constant shared with the logbook so the two cannot disagree about what "closed" means. ## This is not a fresh measurement, and the PR should not be read as one The case does not occur in the data. Nothing was closed today under a closed or trashed parent, no project was closed today, and there is no open to-do under a closed project anywhere. So the before and after are identical and can only show no regression: | check | before | after | | --- | --- | --- | | `today --include-completed` vs the app | 27 / 27 exact | 27 / 27 exact | | `anytime --include-completed` vs the app | 114 / 114 exact | 114 / 114 exact | | `today`, `anytime`, `logbook`, `trash` | 18, 105, 910, 165 | unchanged | The rule comes from the #229 measurement instead, which was decisive where it could be observed: the app's Logbook held **no** to-do at all whose parent project was closed or trashed, and its Trash none whose parent was trashed. I did not create the case in the live app to measure it. ## Scoping The fold sits **inside** the closed branch of the shared status test, so it can only ever remove a row `--include-completed` just added. An open to-do under a closed project is left alone: folding that would be a larger claim, it would take real work out of Today if wrong, and #249 does not ask it. `TestIncludeCompletedKeepsOpenTodosUnderClosedProject` pins it across all four view-and-flag combinations. ## Nothing is stranded The trap on both #230 and #238, so worth stating explicitly. The logbook already rejected these rows on its own parent clause, so no row moves into or out of it, and `things --project <closed uuid>` still returns them — which is exactly what #229 settled when it folded the same rows away. `TestFoldedJustClosedRowIsReachableByProject` asserts the row is in none of the three views and comes back from the project filter. ## Follow-up noticed, not fixed `/code-review` found that the fold is unconditional even when the user has explicitly named the closed parent. `things --project "Launch v2" --include-completed` errors and points at `things today --project NAME --include-completed`, which then returns an empty list, because the fold drops every closed child and a project row never matches `p.uuid`. Everywhere else, naming a closed project lifts the fold. Lifting it here means plumbing the filter into a pure string builder and is a behavioural call worth measuring, so it wants its own issue. The bare `things --project <uuid>` form works today. ## Docs Four passages said such a row is in neither `logbook` nor `today`, which was aspirational before this change and left `anytime` out. All four now say none of the three, and the agents page notes the fold applies to both flag variants, so a day sweep after `things complete <project> --yes` reports the project rather than the to-dos it closed. `make test` and `make lint` are clean.
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #253 ## What changed Since #252 the `--include-completed` variants of today and anytime fold a task closed inside a closed project into the project's row, as logbook and trash do. The fold was unconditional, so it applied even when that project had been named: `things today --project "Launch v2" --include-completed` on a finished project returned nothing at all. The fold exists so a closed project is one row rather than a row plus its contents. Naming the project is asking for the contents — the answer `things --project <uuid>` and `show --agent` already give. It now comes off in the two views as well. ## How it is plumbed Through the spec seam #240 created rather than string surgery: `where()` takes a two-field `whereOpts`, the status test reads it, and `buildListQuery` fills it at the one call site. With the parent pinned by the `--project` predicate the lifted clause is a constant — true for an open project, false for a closed one — so dropping it can only ever affect the closed case. Naming an open project changes nothing, which has its own test. ## The golden file is the record Exactly **6 of the 200** view-and-filter combinations moved: | view | includeCompleted | filters | | --- | --- | --- | | today | true | `project`, `project+area+tag`, `all` | | anytime | true | `project`, `project+area+tag`, `all` | Each differs only by the removed `AND NOT (COALESCE(p.status, 0) IN (2, 3))`. No other view, filter shape or flag state moved. ## Not measured against the app, and why The case does not occur in the data: no closed project has a child that is open and untrashed, or closed today. Every unfiltered view returns what it did before, which I checked by building `origin/main` and comparing rather than assuming. The rule comes from #229 and #235, where the fold itself was measured against the app, and from the catch-all view, which has answered this way since #235. One thing that looked like a regression was not: `anytime --include-completed` reads 115 where my earlier notes said 114. The base build gives 115 too — a task was closed during the session — and it still matches the app exactly, membership and order. ## Two asymmetries left in, recorded not guessed - **Trashed projects.** Naming one still returns nothing in these two views, because `untrashedParent` zeroes the result before the fold matters. The catch-all returns its contents. Fixing it would change what the *unfiltered* views show — 3 open rows in this data — which is beyond a change scoped to the `--include-completed` fold. Worth its own issue. - **`--project` takes a LIKE pattern**, so `--project '%'` lifts the fold for every project at once. Pre-existing and not specific to this change: no filter value is escaped anywhere, and the bare `things --project '%'` already widens the same way. ## Docs `internal/skill/SKILL.md`, `docs/content/commands.md` and `docs/content/agents.md` each said such a task is "in none of the three", which this makes false when the project is named. All three now say the fold holds for the unfiltered sweeps and comes off when the project is named, with the concrete command. The skill is scoped to closed projects specifically, since the trashed case still only works in the bare form. `make test` and `make lint` are clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #229
What changed
Things shows a closed project in the Logbook as one row, not a row plus every to-do inside it, and shows a trashed project in Trash the same way. The CLI listed each child separately, so
things logbookreturned 1328 rows where the app showed 910.logbooknow folds a to-do whose parent project is closed or trashed.trashfolds only the trashed-parent case. The folded rows stay reachable by naming the project.The trash half is narrower than the issue says
The issue asks for "closed or trashed" in both views. Measured, that is wrong for
trash:Throwing a to-do away out of a finished project is an ordinary thing to do, and the project is not in Trash to fold it into. Applying "closed or trashed" to
trashwould have dropped 27 rows rather than the 4 expected.This leaves
viewsIncludingTrashedProjectsempty, so the clause it gated is unconditional and the map is gone.Reaching the folded rows
A closed or trashed project's contents were unreachable before this, which is what made the flagless option workable only alongside a widening. The catch-all view pins
t.status = 0, sothings --project <closed uuid>returned nothing andthings show <uuid> --agentreported "the project has no open to-dos" for a project holding 49.Naming a closed or trashed project now returns its contents whatever their status. That is what the app answers:
to dos of project idreturned 78 for a project holding 65 completed, 13 cancelled and 3 trashed children, and 49 for one holding 42 and 7. So the status pin drops andt.trashed = 0stays — a trashed child is in the Trash on its own account, not part of the project's contents. The widening is scoped to a named project, so a bare--areasweep is still the open set, which has its own test.A closed project's brief would have listed those rows under "Open to-dos". The heading drops "Open" and each row carries
[x],[~]or[ ].Accepted consequences
A to-do thrown away out of a project that is itself in the Trash is listed nowhere, as agreed and as in the app, which returns nothing for a trashed project's contents.
things logbook --project Xandthings trash --project Xon a closed or trashed project now return an empty list, where the barethings --project Xreturns its contents. The view genuinely no longer holds those rows, so this is parity rather than a bug, and the same commands on an open project are unchanged. Raising it because the repo has precedent for erroring on a contradictory view-and-filter pair, assomeday --projectdoes, and someone may prefer that here. Not changed in this PR.Also unchanged and pre-existing: the brief's closing section still offers
complete --yesandcancel --yeson an already-closed project.How verified
By uuid against the app on live data:
things --projecton the two closed test projects returns 78 and 49.Tests cover the fold in both views, that
trashkeeps a closed project's trashed child, the contents of a closed project, of a trashed one, and of an open one as the control, and that--areadoes not widen. Two existing tests asserted the behaviour this reverses and were rewritten to assert the fold plus the route that keeps the rows reachable.make testandmake lintare clean.Docs
internal/skill/SKILL.md,docs/content/commands.md,docs/content/agents.mdand the README describe the fold, how to read a closed project's contents, and the one case that is reachable nowhere. The #230 wording about a closed item being in exactly one of the two lists was qualified: that holds while the parent project is open, and a to-do closed inside a project that is later closed is folded into the project row instead.