Skip to content

fix(list): include projects in trash - #216

Merged
ryanlewis merged 1 commit into
mainfrom
fix/issue-212-trash-projects
Sep 10, 2026
Merged

ryanlewis merged 1 commit into
mainfrom
fix/issue-212-trash-projects

Conversation

@ryanlewis

Copy link
Copy Markdown
Owner

Closes #212

What changed

trash now lists projects alongside to-dos, the same rule #205 and #209 applied to the other views. Trashing a project in Things puts the project row itself in the app's Trash, and things projects filters trashed rows, so a trashed project was visible nowhere in the CLI — excluded by construction, not by data.

The view now selects t.type IN (0, 1) through the existing todoOrProject constant. Nothing else about it moved: headings (type 2) stay out, and trash keeps the literal reading of the database it shares with logbook, so the children of a trashed project and any repeating template that ends up in the bin still list as they are stored.

inbox and deadlines stay to-do only. #213 covers deadlines.

Telling a project row from a to-do

No output change was needed. printTasks already appends a dim (project) for project rows, and since #214 the JSON carries "type": "project".

Verified against the real database

Read the live database with sqlite3 -readonly, and compared against the app's own list membership via AppleScript (id of to dos of list "Trash"), by uuid rather than title.

count
App's Trash list 162
CLI list trash before 148
CLI list trash after 165
Trashed project rows gained 17

All 17 project rows the CLI now returns are in the app's Trash, and nothing the app shows is missing from the CLI. No false positives: the database holds no trashed headings and no trashed repeating templates, so neither could leak in.

The 3 rows the CLI returns that the app does not are unchanged by this PR. All three are to-dos whose parent project is itself trashed; the app shows the trashed project as one row and folds its children into it. They were already returned before this change, under the viewsIncludingTrashedProjects decision from #155, and narrowing that is a separate question from this issue.

Tests

Three tests in internal/db/tasks_test.go over a new seedTrashedProjects helper: trashed projects of both statuses are listed and carry model.TypeProject; headings and live rows of either kind stay out; --area matches a trashed project through its own area while --project cannot.

TestTrashAndLogbookKeepTrashedProjectChildren seeds a trashed project as the parent of its fixture, so its trash expectation gained the project row it was already seeding. Its subject, that the child is kept, is unchanged.

Docs updated in internal/skill/SKILL.md, docs/content/commands.md and docs/content/agents.md: the sentence naming which views carry projects, and the repeating-template note that said a project template never reaches trash.

Closes #212

Trashing a project in Things puts the project row itself in the app's
Trash, but the CLI's trash view pinned t.type = 0, so it never appeared.
`things projects` filters trashed rows too, which left a trashed project
visible nowhere in the CLI — excluded by construction, not by data.

The view now selects t.type IN (0, 1) through the existing todoOrProject
constant, the same rule #205 and #209 applied to the other views.
Headings stay out, and trash keeps the literal reading of the database it
shares with logbook, so the children of a trashed project and any
repeating template in the bin still list as they are stored.

Verified against the live database with sqlite3 -readonly, compared by
uuid against the app's own Trash membership via AppleScript: the app
lists 162 items, the CLI listed 148 before and 165 after, and all 17
project rows gained are in the app's Trash with no false positives.

inbox and deadlines stay to-do only; #213 covers deadlines.
@ryanlewis
ryanlewis merged commit b63276b into main Sep 10, 2026
10 checks passed
@ryanlewis
ryanlewis deleted the fix/issue-212-trash-projects branch September 10, 2026 10:58
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #213

## What changed

`deadlines` now lists projects alongside to-dos. A project takes a
deadline exactly as a to-do does, `things projects -j` has reported it
since #204, and agents.md advertises this view as the way to sweep what
is due — so an agent following the docs missed every project deadline.
The view was pinned to `t.type = 0`, so those rows were excluded by
construction, not by data.

The view now selects `t.type IN (0, 1)` through the existing
`todoOrProject` constant, the same rule #205, #209 and #216 applied to
the other views. Its `ORDER BY t.deadline ASC` is unchanged, so project
rows fall in among the to-dos by date rather than forming a block of
their own. `--on`, `--from` and `--to` already filter `t.deadline` on
this view and now filter project rows by their own deadline.

`inbox` is now the only named view that stays to-do only, which is a
property of the Inbox rather than a limitation: an inbox item has not
been filed anywhere yet, so it is never a project. A bare `-p`/`-a`/`-t`
filter with no view named is the other exception, and the docs now say
so explicitly — it routes to the internal catch-all view, which is still
pinned to to-dos.

## Verification: negative only, and worth being plain about

There is nothing on this database that exercises the change, and I would
rather say so than imply a check I did not make.

The app has no Deadlines list to compare against, so the fallback is
`things projects -j`, which reports no project deadlines at all. Reading
the live database with `sqlite3 -readonly` shows why. Seven projects
carry a deadline, and every one is already excluded by the `status = 0
AND trashed = 0` clause this PR does not touch:

| why excluded | count |
|---|---|
| completed or cancelled | 4 |
| trashed | 3 |
| would list | 0 |

So `things deadlines` returns the same 13 to-do rows before and after
this change, and the SQL the new filter generates selects those same 13.
What I have verified is that no project is wrongly admitted and none is
wrongly held back for a reason this PR introduces. What I have not
verified is a project deadline appearing in the app and then in the CLI,
because no such project exists here.

The tests below cover what the live data cannot.

## Tests

Six tests in `internal/db/tasks_test.go` over a new `seedDeadlines`
helper. The fixture interleaves a project between two to-dos by deadline
while running `"index"` against that order, so an ordering that fell
back to `"index"` or grouped by type would fail:

- projects with a deadline are listed and carry `model.TypeProject`
- the project sorts between the two to-dos, by deadline
- a project with no deadline, a completed one, a trashed one and a
heading all stay out
- `--on` matches a project row by its own deadline
- `--area` finds a project through its own area; `--project` cannot
match one
- a repeating project template with a deadline, and the to-do inside it,
stay out — the one exclusion path that comes from
`viewsIncludingTemplates` rather than the view filter

## Docs

`internal/skill/SKILL.md`, `docs/content/commands.md`,
`docs/content/agents.md` and `README.md`: the view lists now say every
named view except `inbox` carries projects, and name the bare-filter
exception so an agent knows to name a view when project rows matter. The
README gains the general rule it never stated, so its `deadlines` table
row no longer reads as though that view is the only one carrying
projects. `internal/output/output.go` had a comment enumerating the old
set of views.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #222

## What changed

The internal catch-all view — the one a bare
`--project`/`--area`/`--tag` filter routes to when no view is named —
was still pinned to `t.type = 0` while every named view except `inbox`
had been widened to `todoOrProject` (#205, #209, #216, #220). It now
uses the same set.

`--project` is unaffected in practice: a project has no parent project
of its own, so `p.uuid` never matches a project row and the filter still
returns a project's contents. The visible change is for `--area` and
`--tag`.

## Why

An agent sweeping an area with `things --area Work -j` got none of that
area's projects, which is exactly the miss #222 describes. Parity with
what Things.app shows in the matching list is the project's stated goal.

## How verified

Measured by uuid against Things.app on 10 Sep 2026. For one area the app
reports four projects; the CLI returned zero project rows before this
change and returns the same four uuids after. `things --project <uuid>
-j` still returns zero project rows.

Tests added in `internal/db/tasks_test.go` cover the widened set, the
`--area`/`--tag`/`--project` split, and the exclusions that must not
widen with it — headings, trashed projects, repeating project templates
and the to-dos inside them. Four existing tests asserted the old
to-do-only behaviour and now assert the project rows: two area filters,
the catch-all grouping order, and the CLI-level default-view test.

`make test` and `make lint` are clean.

## Docs

The paragraph naming the bare-filter exception was rewritten once, in
`internal/skill/SKILL.md`, `docs/content/commands.md`,
`docs/content/agents.md` and the README. It now states the `--project`
case rather than a whole-view exception.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #217

## What changed

Three changes to the two views the app arranges differently from the
CLI, all measured against it on 10 Sep 2026 by uuid.

**`anytime` no longer lists project rows.** This is a revision of what
#205 and #216 widened, for this view alone. Every active project is
trivially "anytime", so listing them all buries the to-dos; the app uses
the project as the group header above its own to-dos instead. Measured:
the app's Anytime held **none** of the 23 active projects as a row, and
all 107 of their to-dos, which the CLI already matched exactly. `today`
keeps its project rows, and so do `upcoming` and `someday`, where a
project has actually been put somewhere.

**`anytime` is ordered to match that arrangement:** items filed nowhere
lead, then areas in area order, and inside an area its own loose to-dos
before those of its projects. It listed in bare `t."index"` order
before, which scattered each project's to-dos and made the rendered
group header repeat.

**`upcoming` reads by date**, then by the within-day `todayIndex` the
app also keys Today on. Bare `t."index"` order interleaved the dates.

## Getting the keys right took two corrections

Worth recording, because both were wrong in ways that still produced
plausible-looking output:

1. Sorting on the area index alone put the unfiled items **last**.
Things writes its indexes negative, so the COALESCE default of 0 that
stands for "not filed here" outranks every real area.
2. Sorting on the project index alone put each area's project to-dos
**before** its own loose ones, for the same reason. The app does the
opposite.

Each key is therefore a CASE rather than a plain index.

## How verified

By uuid against the app on live data:

| view | rows | order vs the app |
| --- | --- | --- |
| anytime | 105 shared | identical for all 105 |
| upcoming | 30 | identical for all 30, no membership difference either
way |

Plain output now renders the app's shape: unfiled items, then each area,
then each project name once as a group header above its to-dos.

## Deliberately left alone

`today` and the catch-all `--project`/`--area`/`--tag` ordering are
byte-identical to before. An earlier revision of this branch had
factored their ORDER BY into shared constants; I backed that out,
because neither was asked to change and `today` is already at parity.

Two ordering gaps are recorded in the comments rather than guessed at,
both raised by `/code-review`:

- A to-do inside a project that carries **no area** falls to the
COALESCE default and sorts after every area. There is no such project in
this data, so the app's answer could not be measured.
- `upcoming` orders on a nullable `todayIndex`, and SQLite sorts NULL
first. No upcoming row carries a NULL, and the `today` ordering has the
same shape, so changing one and not the other would invent a rule rather
than match one.

One consequence worth a decision, not fixed here: because `upcoming` now
orders by date, a project with to-dos on several days gets its group
header re-printed per day, since the CLI has no day header to group
under the way the app does.

## Docs

The "which views carry projects" paragraph was rewritten once in
`internal/skill/SKILL.md`, `docs/content/commands.md`,
`docs/content/agents.md` and the README: every named view except `inbox`
and `anytime`. Each says why `anytime` is the exception and points at
`things projects` for sweeping projects themselves.

`make test` and `make lint` are clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: things list trash omits trashed projects

1 participant