summaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md34
1 files changed, 33 insertions, 1 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 6fd40c1..4df48dd 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,7 +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 |
+| 26 | No way to add or remove an arbitrary tag from the UI | workflow | S | **done** |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1160,6 +1160,38 @@ dialog and two actions, not new plumbing.
refreshes the list when a mutation introduces an unknown tag, so that path is
in place and should be relied on rather than duplicated.
+### Outcome (done)
+
+Built as the user chose: one **Edit tags** dialog on `Ctrl+T` rather than
+separate add and remove actions, since filing something under a new tag while
+dropping `inbox` is one thought.
+
+`TagDialog` is pure UI in `qtmaildir_lib`. It is handed the vocabulary and the
+selection's current tags and returns two lists; it contacts no worker and holds
+no database handle, which is what lets fifteen tests run without a notmuch
+database. Integration is one call to the existing `tagSelected()`, so undo, the
+optimistic model update, the one-query multi-row resolution and the completer
+refresh all come for free.
+
+**Tri-state is the part that needed the tests.** With several threads selected a
+tag can be on some, and `PartiallyChecked` means "leave alone" rather than
+"apply to all". The opposite reading silently tags threads the user never
+looked at. `m_fullyTagged` exists for the neighbouring case: a tag already on
+every thread and left checked is not a change and must not be sent as one.
+
+**Validation is a free function** so the rules are testable directly. It rejects
+empty, a leading `-` (notmuch's CLI reads that as removal, so such a tag is a
+trap), whitespace, and control characters. Nothing is applied until every name
+passes, since a half-applied change is worse than none: the user cannot tell
+which half landed.
+
+**One test assumption was wrong and the code was right.** A case asserted that
+`QStringLiteral("null\0byte")` truncates at the null and reads as Empty. It
+does not; the literal is kept whole, so the null is caught as a control
+character. The test was corrected, not the validator.
+
+Rendered and inspected rather than only asserted.
+
---
## Deferred, unsized, or split out