From 83f42ab460820e9fd1b96653e14c08b5d51a2166 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 14 Aug 2026 12:46:56 +0200 Subject: docs: fix a weekday in the plan's date fixtures Qt::RFC2822Date validates the weekday against the date, so 'Thu, 14 Aug 2026' parses as invalid: that day is a Friday. Task 2 hit it and Task 6 carried the same wrong data. The failure is indistinguishable from the timezone-comment trap the date parse exists to handle, so the plan now names it. --- docs/superpowers/plans/2026-08-14-search-from-message.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'docs/superpowers/plans') diff --git a/docs/superpowers/plans/2026-08-14-search-from-message.md b/docs/superpowers/plans/2026-08-14-search-from-message.md index 56cfbc6..824ccd4 100644 --- a/docs/superpowers/plans/2026-08-14-search-from-message.md +++ b/docs/superpowers/plans/2026-08-14-search-from-message.md @@ -462,12 +462,12 @@ void TestMimeParser::parsesADateWithATimezoneComment() // silently: the attachment folder loses its prefix, and a date search // offers nothing with no indication why. const QDateTime withComment = MimeParser::parseDate( - QStringLiteral("Thu, 14 Aug 2026 09:30:00 +0200 (CEST)")); + QStringLiteral("Fri, 14 Aug 2026 09:30:00 +0200 (CEST)")); QVERIFY(withComment.isValid()); QCOMPARE(withComment.date(), QDate(2026, 8, 14)); const QDateTime plain = MimeParser::parseDate( - QStringLiteral("Thu, 14 Aug 2026 09:30:00 +0200")); + QStringLiteral("Fri, 14 Aug 2026 09:30:00 +0200")); QVERIFY(plain.isValid()); QCOMPARE(plain.date(), QDate(2026, 8, 14)); @@ -1316,7 +1316,7 @@ private: item.message.from = QStringLiteral("Sender "); item.message.to = QStringLiteral("Recipient "); item.message.cc = QStringLiteral("Copied "); - item.message.date = QStringLiteral("Thu, 14 Aug 2026 09:30:00 +0200"); + item.message.date = QStringLiteral("Fri, 14 Aug 2026 09:30:00 +0200"); item.message.messageId = QStringLiteral(""); return item; } @@ -1945,6 +1945,12 @@ to a rule already exists as save the query, then build a rule from it." ## Notes for the implementer +**`Qt::RFC2822Date` also validates the weekday against the date.** Found in Task 2: +`Thu, 14 Aug 2026` parses as INVALID because 2026-08-14 is a Friday, and the +failure looks exactly like the timezone-comment bug this feature exists to avoid. +Any `Date:` header written into a test fixture must carry the right weekday. +Check with `date -d +%A` rather than guessing. + **Where this is likely to go wrong, in order of probability:** 1. **The tag strip's spacing constant.** Task 3 requires `paintEvent`, `relayout` and `chipRectAt` to agree. If the whole-suite run in Task 3 shows a message-pane test failing, the constants disagree; fix by making all three call `chipRectAt`. -- cgit v1.2.3 From bbf3c570215688c553fd70d8f372ae215725ca02 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 14 Aug 2026 13:40:17 +0200 Subject: docs: close item 85, searching from the message pane Five surfaces in the message pane offer a search built from what they show, replacing the query or narrowing it. The details dialog became rows along the way, which the user wanted independently of this feature. Item 78 is narrowed to the rule shortcut alone and drops to S: item 85 built the menus and item 81 the seeded dialog, so both halves already exist. Its approach text is corrected too, since it claimed the thread list holds a usable sender and notmuch_thread_get_authors returns a display summary, not an address. Three traps recorded in CLAUDE.md: a modal dialog must close before the action it asked for runs, Qt::RFC2822Date validates the weekday against the date, and every query goes through SearchTerm so five surfaces cannot grow five quoting rules. --- CHANGELOG.md | 21 +++++ CLAUDE.md | 44 ++++++++++- .../2026-08-03-post-0.1.0-usability-closed.md | 61 +++++++++++++++ .../plans/2026-08-03-post-0.1.0-usability.md | 90 +++++++++------------- 4 files changed, 159 insertions(+), 57 deletions(-) (limited to 'docs/superpowers/plans') diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dea47f..7b22d9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,27 @@ point at which they are stable. ## [Unreleased] +### Added + +- Anything on screen in the message pane can be searched for by right-clicking + it. The subject and the date in the header, the sender and recipients when + the thread holds one message, a tag chip, a phrase selected in the message + body, and every header of every message in the details dialog. Each offers + **Search for this**, which replaces the query, and **Add to search**, which + narrows what is already there. + + Narrowing is the half worth knowing about: a query returning a thousand + threads can be cut down by adding a sender or a date to it, without retyping + the query you started from. + + A search is only ever a search. Turning what you find into a tagging rule is + still the existing road: save the query, then create a rule from it. + +### Changed + +- The message details dialog shows labelled rows rather than one block of + text, so each value can be searched for on its own. + ## [0.19.0] - 2026-08-14 A saved query can become a tagging rule without retyping it, and a rule can no diff --git a/CLAUDE.md b/CLAUDE.md index b66c3b2..f6f9996 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,6 +65,7 @@ MainWindow NotmuchWorker └ MessageView (header QLabel, QWebEngineView, attachment bar, TagStrip) CardLayout (pure geometry, no painting) +SearchTerm (pure query strings, no widget) Config (INI) KeyMap MailSync (QProcess) MimeParser (GMime) SyncMonitor (/proc/locks) TagColors QueryCompleter ThreadCidMap ``` @@ -74,9 +75,19 @@ The query row and the message-pane header are **built inline in `MainWindow` and listed `QueryBar`, `SavedQueryBar`, `HeaderWidget` and `AttachmentBar`; none of those types have ever existed, and looking for them wastes a search. The widget classes that do exist are `MessageView`, `ThreadListView`, `TagStrip`, -`TagDialog`, `RowStyleDelegate` and `CardDelegate`; `TagChip` is a namespace of -painting helpers, not a widget, and `ThreadCidMap` and `CardLayout` are structs. -`SubjectDelegate` existed until item 53 and is gone. +`TagDialog`, `MessageDetailsDialog`, `RowStyleDelegate` and `CardDelegate`; +`TagChip` is a namespace of painting helpers, not a widget, `SearchTerm` is a +namespace of query builders, and `ThreadCidMap`, `CardLayout`, `SearchOffer` +and `HeaderRow` are structs. `SubjectDelegate` existed until item 53 and is +gone. + +**`MessageDetailsDialog` was a `QPlainTextEdit` inside `MessageView` until item +85.** It is rows now so each value can carry its own context menu, and its +plain-textness was a SECURITY property rather than a style: header values come +from strangers and plain text cannot interpret markup. Every value label states +`Qt::PlainText` explicitly, because a `QLabel` guesses under `Qt::AutoText`. +Escaping into a rich-text label is the same protection one mistake away from +failing, so do not "simplify" it back. **`ThreadListView` survives only for the expander hit-test.** `CardDelegate` draws the reply count, and a delegate gets no click of its own without an @@ -254,6 +265,17 @@ a failure or a `-1` count fails against correct code. This was recorded in building the rules. Assert on the positional contract, never on a provoked failure. +**Every query this application builds goes through `SearchTerm` +(`src/searchterm.h`), and that is what stops five surfaces growing five quoting +rules.** It holds no widget, so the grammar is tested without a painter or a web +engine. Two of its rules are load-bearing rather than cosmetic. `quote()` +escapes backslashes BEFORE quotes, since the other order escapes the +backslashes it just added; it truncates before escaping, so a cut cannot land +mid-escape. And `extend()` parenthesises BOTH sides, because the query bar can +hold a hand-written disjunction and `a or b AND c` binds as `a or (b AND c)`, +which widens a search the user asked to narrow, reporting nothing. This is the +same trap the `post-new` hook handles when it scopes a rule with `tag:new`. + **A writer that does not validate what its reader requires loses data silently.** `TagRules::save()` wrote any id and `load()` required `^[a-z0-9][a-z0-9-]*$`, so a rule named `justeat orders` in a field labelled @@ -341,6 +363,22 @@ one route out of three. Assert every route. Underneath sits a second trap: `done()`, so a test for the closed path has to `show()` the dialog first or it asserts nothing at all. +**A modal dialog must close BEFORE the action it asked for runs, not after.** +A signal from a dialog to its parent is a DIRECT connection, so the emit runs +the handler synchronously while `exec()` is still on the stack: the details +dialog's search ran the query, cleared the model and blanked the message pane +while the dialog was still up, holding the `m_items` it was built from. Call +`accept()` first, then emit. The mutation check for this HANGS rather than +failing, since without the `accept()` nothing ever leaves `exec()`, and a hung +test binary is item 84's second trap waiting to mislead the next run. + +**`Qt::RFC2822Date` validates the weekday against the date.** `Thu, 14 Aug +2026` parses as INVALID because that day is a Friday, and an invalid parse here +is indistinguishable from the trailing-comment trap `MimeParser::parseDate` +exists to handle. Two fixtures carried a wrong weekday, one of them +pre-existing and unnoticed until something finally parsed it. Write a date +fixture with `date -d +%A`, never from memory. + **Under a tiling compositor a window's size is not the application's to restore, and the user's desktop is Hyprland.** `saveGeometry` stores `frameGeometry` and `normalGeometry`; `restoreGeometry` restores the NORMAL diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index 8cfa6b0..8ae8dff 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -4741,3 +4741,64 @@ how this was found: the suite ran past its two-minute timeout with no output. The cause is not the account at all, it is `showWarnings()` raising a modal from the MainWindow constructor for any config problem, with nothing offscreen to dismiss it. Filed as item 84. + +## 85. Nothing on screen can be searched for by right-clicking it + +**Observed (2026-08-14).** The user asked, while discussing item 78, to be able +to right-click a sender, a subject, a date, a tag chip or a piece of selected +body text and be offered a search built from it, both replacing the query bar +and narrowing what is already in it. The narrowing case decided the shape: a +query returning a thousand threads is refined by adding a second condition, and +before this that meant retyping a query the user never typed. + +**Cause.** Not a defect, unbuilt. `MessageView` had no context menu on any +surface, and `TagStrip` painted chips with no hit test and no signals. + +**Done 2026-08-14**, designed in +`specs/2026-08-14-search-from-message-design.md` and built over eight tasks. +Five surfaces offer **Search for this** and **Add to search**: the header's +subject and date, its From/To/Cc for a single-message thread, a tag chip, a +body selection, and every header per message in the details dialog. + +**What is worth keeping from it.** + +`SearchTerm` (`src/searchterm.h`) owns the whole query grammar and holds no +widget, so the quoting is tested without a painter or a web engine. That +matters because **a mis-quoted notmuch query is not an error, it matches zero**: +nothing downstream would ever report the feature being broken. `extend()` +parenthesises both sides, since `a or b AND c` binds as `a or (b AND c)` and +silently WIDENS a search the user asked to narrow. + +**`Qt::RFC2822Date` validates the weekday against the date**, so `Thu, 14 Aug +2026` parses as invalid: that day is a Friday. Two test fixtures carried a wrong +weekday, one of them pre-existing, and the failure is indistinguishable from the +trailing-comment trap `MimeParser::parseDate` exists to handle. Check a date +fixture with `date -d +%A` rather than writing one from memory. + +The Date: parse was **extracted** into `MimeParser::parseDate` rather than +rewritten, because the existing copy inside a file-local function already +carried the fix for `Qt::RFC2822Date` rejecting a string with a trailing +timezone comment. A second parser without it would have lost the date on a +large share of real mail, showing up only as a menu entry that never appears. + +**The header lists its fields rather than hit-testing them.** It is one +rich-text `QLabel` of up to four lines, and mapping a point through laid-out +rich text breaks as soon as the label wraps. From/To/Cc are offered exactly when +the header displays them, which is a single-message thread, so the menu can +never name a value the header is withholding. + +**The details dialog stopped being a `QPlainTextEdit`**, which the user had +disliked independently. Its plain-textness was a security property rather than a +style: header values come from strangers and plain text cannot interpret markup. +Every value label therefore states `Qt::PlainText` explicitly, because a +`QLabel` guesses under `Qt::AutoText`, and a test enumerates every label and +asserts the format. + +**A modal dialog must close BEFORE the search it asked for runs.** The +connection is direct, so emitting first runs the query synchronously: the model +clears and the pane blanks while the dialog is still up, holding the `m_items` +it was built from. Caught by a test whose mutation check HUNG rather than +failed, which is what a missing `accept()` does to `exec()`. + +**Item 78 stays open**, carrying the rule shortcut alone. The road from a search +to a rule already exists: save the query, then create a rule from it. 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 f562e97..980aefb 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 @@ -142,14 +142,14 @@ taking that too literally. | 75 | The tagging rules window forgets its size and its column widths | persistence | S | **done** 2026-08-13, shipped in 0.17.0. The window-kind question is left open, see the closed-items file | | 76 | Every field in the rules dialog is free text, so a rule is easy to get wrong | workflow | M | **done** 2026-08-13, shipped in 0.17.0. See `specs/2026-08-13-rule-builder-design.md` | | 77 | No way to see what a rule would collect, in the thread list | workflow | S | **done** 2026-08-13, shipped in 0.17.0 | -| 78 | No way to build a rule from something visible in a message | workflow | M | open, narrowed 2026-08-14; the search half split out as item 85, which is the road a rule is made from. Revisit once 85 has been used | +| 78 | No way to build a rule from something visible in a message | workflow | S | open, narrowed 2026-08-14; the search half shipped as item 85, which is the road a rule is made from. Now a shortcut across that road: the menus and the seeded-dialog path both exist. Use 85 first and see which values are worth promoting | | 80 | A rule with many conditions squeezes the rule list to one visible row | defect | XS | **done** 2026-08-13, shipped in 0.17.0. Follows item 76 | | 79 | Opening the rules dialog and saving destroys the first rule | defect | XS | **fixed on `rule-builder`** 2026-08-13, unreleased. Shipped in 0.16.0; damaged one real rule, repaired by hand | | 81 | No way to turn a saved query into a tagging rule | workflow | S | **done** 2026-08-14, unreleased; see `specs/2026-08-14-query-to-rule-design.md` | | 82 | A saved query cannot be edited, unpinned or deleted from the UI | defect | S | **done** 2026-08-13, shipped in 0.18.0. Right-click offers Edit, Pin/Unpin and Delete | | 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 | open; designed 2026-08-14, see `specs/2026-08-14-search-from-message-design.md`. Split from 78; rebuilds the details dialog as rows | +| 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 | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -483,26 +483,45 @@ query look fast is a worse trade than the wait. **Observed.** The user would like to select an address or another piece of a message in the main window, right-click, and be offered a rule built from it. -**Cause.** Not a defect, unbuilt. The thread list has a context menu (item -24) and the message pane is a `QWebEngineView` whose selection is inside the -render process. - -**Approach.** Start from the thread list's own context menu, where the -sender is already a value the model holds, rather than from a text selection -in the web view. A "Create rule from sender" entry that opens the rules -dialog with the query prefilled covers the case the user described and needs -no new plumbing. - -**Constraints.** JavaScript is disabled in the profile and must stay -disabled, so reading a selection out of the web view means -`QWebEnginePage::selectedText()` and nothing that injects script. Do that -part only if the sender case turns out not to be enough. +**Cause.** Not a defect, unbuilt. + +**Narrowed 2026-08-14, and most of the work is already done.** The search half +shipped as item 85, which is the road a rule is made from: search for a value, +save the query, create a rule from the saved query. What remains here is a +SHORTCUT across that road, and both of its halves now exist. + +- The menus are built and every surface already extracts its value as a + finished query (`SearchOffer`, `src/searchterm.h`). A rule entry is another + action beside the two search ones, not new plumbing. +- The seeded-dialog path exists from item 81: + `MainWindow::showTagRulesDialog(const TagRule &seed)`. A rule from a message + becomes a second caller of it, with a different seed, which is what item 81's + spec anticipated when it made the seed a whole `TagRule` rather than a query + string. + +**Approach.** Decide it after using item 85 for a while. Which values are worth +promoting straight to a rule is a usage question, and the earlier answer to it +was wrong (see below), so it is worth having the evidence first. + +**Constraints.** The original approach here said to start from the thread +list's context menu "where the sender is already a value the model holds". +**That is false and item 85 verified it.** `ThreadSummary::authors` comes from +`notmuch_thread_get_authors` and is a DISPLAY SUMMARY, reading `Alice, Bob` or +`Alice| Bob`, so a `from:` built from it matches nothing. A real address comes +from `MessageNode::from` or `ParsedMessage::from`, neither of which the thread +list carries. Any thread-list entry needs an address resolved from a message +first. + +JavaScript is disabled in the profile and must stay disabled. Item 85 reads a +body selection with `QWebEnginePage::selectedText()`, which injects no script; +reuse that rather than adding anything. The rules file is shared with mailctl, so a rule created here must go through `TagRules` and preserve unknown fields; see "Changing the shared rule format" in CLAUDE.md. -**Size: M.** +**Size: S**, down from M now that item 85 has built the menus and item 81 the +seeded dialog. ## 84. A config problem blocks `test_mainwindow` on a modal nobody can dismiss @@ -558,43 +577,6 @@ here. **Size: S.** The diagnosis is the expensive part and it is already done. -## 85. Nothing on screen can be searched for by right-clicking it - -**Observed (2026-08-14).** The user asked, while discussing item 78, to be able -to right-click a sender, a subject, a date, a tag chip or a piece of selected -body text and be offered a search built from it, both replacing the query bar -and narrowing what is already in it. The narrowing case is the one that decides -the shape: a query returning a thousand threads is refined by adding a second -condition, and today that means retyping a query the user never typed. - -**Cause.** Not a defect, unbuilt. `MessageView` has no context menu on any -surface, and `TagStrip` paints chips with no hit test and no signals. - -**Approach.** Designed 2026-08-14. **Read -`specs/2026-08-14-search-from-message-design.md` rather than planning from this -entry.** - -Three constraints decide whether to open the spec. **This is search only, and -that is the point**: item 78's rule shortcut is deliberately left out, because a -saved query can already be promoted to a rule and searching is the missing step -on that road, as well as the safe one. **The details dialog is rebuilt as rows**, -which the user wanted anyway, and its value labels must stay explicitly -`Qt::PlainText`: the current `QPlainTextEdit` is a deliberate protection against -markup in header values that come from strangers. And **Add to search -parenthesises both sides**, because the bar may hold a hand-written `or` and -`a or b AND c` binds the wrong way, silently widening a search meant to narrow. - -**Constraints.** The thread list is out of scope and the reason is worth -keeping: a row's `authors` comes from `notmuch_thread_get_authors` and is a -display summary reading `Alice, Bob`, so a `from:` built from it matches -nothing. Item 78's own approach line assumed otherwise. - -A mis-quoted query is not an error to notmuch, it matches zero, so the quoting -helper is tested against the constructed string rather than against a provoked -failure. - -**Size: M.** - ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering -- cgit v1.2.3