Skip to content

fix: allow deleting a reminder by external id after it's been completed - #106

Merged
keith merged 1 commit into
keith:mainfrom
rameshbaskaran:fix/delete-completed-reminder-by-id
Jul 31, 2026
Merged

fix: allow deleting a reminder by external id after it's been completed#106
keith merged 1 commit into
keith:mainfrom
rameshbaskaran:fix/delete-completed-reminder-by-id

Conversation

@rameshbaskaran

Copy link
Copy Markdown
Contributor

Bug

reminders delete <list> <id> fails with No reminder at index <id> on <list> for a reminder that has already been marked complete, even when <id> is a valid calendarItemExternalIdentifier.

Repro:

$ reminders add Soon "Task"
$ reminders show Soon --format json   # note externalId
$ reminders complete Soon 0
$ reminders delete Soon <externalId>
No reminder at index <externalId> on Soon

Cause

delete(itemAtIndex:onListNamed:) always fetches with displayOptions: .incomplete before searching for the given index/id:

self.reminders(on: [calendar], displayOptions: .incomplete) { reminders in
    guard let reminder = self.getReminder(from: reminders, at: index) else { ...

Once the reminder is completed it's no longer in that fetched set, so lookup fails regardless of whether the id itself is valid. setComplete/edit have the same .incomplete-only pattern; I left those alone to keep this PR minimal, but flagging in case they're wanted too (e.g. uncomplete on a reminder that was actually completed via Reminders.app rather than this CLI can hit the same class of issue).

Fix

Widen the fetch to .all only when the argument is an external id (non-numeric). Numeric indexes stay scoped to .incomplete, since that's the same set show's default output enumerates — widening that path too would let a numeric index silently resolve against a different array than what the user actually saw.

let displayOptions: DisplayOptions = Int(index) == nil ? .all : .incomplete

Testing

No automated test added — Reminders talks directly to a live EKEventStore() with no injection seam, and CI (macos-14 runner) has no granted Reminders permission, so this isn't exercisable in swift test today. That's consistent with the existing suite, which only covers pure date-parsing logic in NaturalLanguageTests.swift.

Manually verified against a real Reminders store with the built binary:

  • create → complete → delete <externalId> → now succeeds, item is gone from show --include-completed (previously failed with the error above)
  • create → delete <numeric index> on an incomplete item → unchanged, still works exactly as before
  • swift build -Xswiftc -warnings-as-errors and swift test -Xswiftc -warnings-as-errors both pass locally (matching .github/workflows/swift.yml)

Disclosure

I ran into this bug while automating reminder cleanup from a script and diagnosed/wrote the fix with AI assistance (an agent reading the source, reproducing the bug against a live Reminders store, and drafting this patch), which I reviewed and tested manually as described above. Happy to adjust the approach (e.g. widen setComplete/edit too, or handle this differently) if you'd prefer.

`delete` fetched only incomplete reminders before resolving the given
index/id, so `reminders delete <list> <external-id>` failed with
"No reminder at index ... on ..." whenever that reminder had already
been marked complete, even though the id is valid and stable.

Widen the fetch to the full (`.all`) display set when the argument is
an external id (non-numeric), since ids are unambiguous regardless of
completion state. Numeric indexes are left untouched (still scoped to
`.incomplete`, matching what `show`'s default output displays), so
existing index-based delete behavior is unaffected.

Manually verified against a live Reminders store:
- create -> complete -> delete by external id -> now succeeds and the
  item is gone from `--include-completed` output (previously failed).
- create -> delete by numeric index on an incomplete item -> still
  works exactly as before.

No automated regression test added: `Reminders` talks directly to a
real `EKEventStore()` with no injection seam, and CI has no granted
Reminders permission, so this can't be exercised in `swift test`
today (same constraint the existing `NaturalLanguageTests.swift`
suite works around by only covering pure date-parsing logic).
@keith
keith merged commit e41ba9a into keith:main Jul 31, 2026
1 check passed
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.

2 participants