summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md54
1 files changed, 54 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 980aefb..15377ea 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
@@ -150,6 +150,7 @@ taking that too literally.
| 83 | A rule named with spaces is written to the file and dropped by every reader | defect | S | **done** 2026-08-14, unreleased. The name is sanitised into an id, save validates, a bad id loads for repair |
| 84 | A config problem blocks `test_mainwindow` on a modal nobody can dismiss | testing | S | open; measured 2026-08-14, `showWarnings()` calls `QMessageBox::warning` from the constructor |
| 85 | Nothing on screen can be searched for by right-clicking it | workflow | M | **done** 2026-08-14, unreleased; see `specs/2026-08-14-search-from-message-design.md`. Split from 78; rebuilt the details dialog as rows |
+| 86 | A right-click search can replace or narrow, but never exclude | workflow | S | open; follows 85. The `extend` bool has no room for a third choice, so this widens a signature rather than adding a menu entry |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -577,6 +578,59 @@ here.
**Size: S.** The diagnosis is the expensive part and it is already done.
+## 86. A right-click search can replace or narrow, but never exclude
+
+**Observed (user, 2026-08-14):** "we've added the possibility to search clicking
+on any element in the message pane, the current available search options are to
+run the query from scratch or add to the existing query. A missing option is to
+add negatively (not)."
+
+**Cause (verified in the code, 2026-08-14).** Item 85 shipped exactly two
+operations and wired them through as a single boolean. Every surface builds the
+same pair: `MessageView::addSearchEntries` (`src/messageview.cpp:566`) adds
+"Search for this" and "Add to search" per offer, and
+`MessageDetailsDialog` (`src/messagedetailsdialog.cpp:92`) repeats that pair per
+row. Both emit `searchRequested(query, extend)`, and
+`MainWindow::runSearchFromPane` (`src/mainwindow.cpp:1654`) branches on that one
+bool: `extend ? SearchTerm::extend(m_queryEdit->text(), query) : query`.
+`SearchTerm` has `extend()` and no exclusion form at all. So the gap is not a
+missing menu entry over an existing capability; there is no third state for a
+menu entry to select, and the signature cannot express one.
+
+**Approach.** Three changes, in this order, and the first is the only design
+decision.
+
+- **Widen the signal past a bool.** A third operation makes `bool extend` wrong
+ at four call sites. An enum (replace, narrow, exclude) carried through
+ `MessageView`, `MessageDetailsDialog` and `MainWindow` is the honest shape.
+ Note that it crosses no thread boundary, so the `Q_ENUM` metatype trap in
+ `CLAUDE.md` does not apply here; these are direct connections.
+- **Add `SearchTerm::exclude(existing, addition)`**, parenthesising both sides
+ exactly as `extend()` does, as `(existing) AND NOT (addition)`. The
+ parenthesising is load-bearing for the same reason it is in `extend()`: the
+ query bar can hold a hand-written disjunction, and an unparenthesised
+ `NOT a or b` binds so that the exclusion covers only the first term. This is
+ the whole of the testable surface and it needs no widget.
+- **A third menu entry per offer**, wrapped in `tr()`.
+
+**Constraints.**
+
+- **Excluding from an empty query is not a search.** `extend()` returns the
+ addition alone when `existing` is empty; the same rule here would produce
+ `NOT (x)`, meaning "all mail except", which is a plausible thing to ask for
+ and an implausible thing to have meant by right-clicking a sender. Decide
+ deliberately: either suppress the entry when the query bar is empty, or let it
+ through. Do not let it fall out of the code by accident.
+- **notmuch reports nothing for a malformed query**, so correctness is asserted
+ against the constructed string, never against a provoked failure or a result
+ count. `SearchTerm`'s own header records this.
+- The details dialog must keep calling `accept()` BEFORE emitting, per item 85's
+ trap. A new action added to that menu inherits the same requirement, and its
+ mutation check hangs rather than fails.
+
+**Size: S.** The grammar is one function beside an existing one with existing
+tests; the spread is the signature change across three files.
+
## Deferred, unsized, or split out
Items noted while triaging but not part of the original list. Same numbering