aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md43
1 files changed, 43 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
index 205710a..6fd40c1 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
@@ -64,6 +64,7 @@ taking that too literally.
| 23 | No way to save a search query from the UI | workflow | M | open |
| 24 | No right-click actions on the thread list | discoverability | S | open |
| 25 | No select-all, and bulk actions are undiscoverable | workflow | S | open |
+| 26 | No way to add or remove an arbitrary tag from the UI | workflow | S | open |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1117,6 +1118,48 @@ combined-query design is what makes this plausible at all, but "plausible" is
not "measured", and this item is the one that turns a rare accident into a
routine keystroke. Measure it against a real query before shipping the binding.
+## 26. No way to add or remove an arbitrary tag from the UI
+
+**Observed (user, 2026-08-04), as a question:** "as it is today, how do I add a
+new tag to a message?" The answer is that you cannot. The only route is
+`notmuch tag` in a terminal, or the companion `mailctl`.
+
+**Cause: every tagging action writes a hardcoded tag name.** `registerActions()`
+offers exactly five: `archive` (removes `inbox`), `delete` (adds `deleted`),
+`spam` (adds `spam`, removes `inbox`), `flag` (adds `flagged`) and
+`toggle_unread`. Nothing accepts a tag the user types. For an application whose
+entire purpose is organising mail by tag, that is a conspicuous hole, and it is
+the reason this item exists at all rather than being folded into item 25.
+
+**The machinery is already complete.** `tagSelected(add, remove, description)`
+takes arbitrary lists and is the single funnel every mutation already uses;
+`applyTagsToThreads()` resolves a multi-row selection in one combined query;
+undo works through `TagChange::inverted()`; and `QueryCompleter` already holds
+every tag in the database, refreshed on the worker thread. This item is a
+dialog and two actions, not new plumbing.
+
+**Approach.**
+
+- An `add_tag` action opening a small dialog with a line edit, and a `remove_tag`
+ counterpart. Both act on the whole selection, like every other tag action.
+- **Complete against the known tag list.** The completer's tag vocabulary is the
+ same data, so offering it here costs nothing and prevents the obvious failure
+ mode: typing `shoppping` and silently creating a new tag next to `shopping`.
+ This is the strongest argument for the dialog over a free-text prompt.
+- Removing should offer the tags actually present on the selection rather than
+ every tag in the database, since removing a tag that is not there is a no-op
+ the user cannot see.
+
+**Constraints.**
+
+- **A tag name is not free text.** notmuch accepts a lot, but a leading `-`, an
+ embedded space or an empty string will produce confusing results or a failed
+ write. Validate before sending, and say what was rejected.
+- No confirmation dialog, per `CLAUDE.md`; undo covers a mistyped tag.
+- A tag the user invents is new to the completer, and `onTagsApplied()` already
+ refreshes the list when a mutation introduces an unknown tag, so that path is
+ in place and should be relied on rather than duplicated.
+
---
## Deferred, unsized, or split out