summaryrefslogtreecommitdiffstats
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.md144
1 files changed, 144 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 26464ba..1216542 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
@@ -131,6 +131,10 @@ taking that too literally.
| 72 | No khard/khal integration | workflow | ? | open, unspecified; the user places it after send, so v2 at the earliest |
| 73 | This backlog is past four thousand lines | maintenance | S | open |
| 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | open; cause measured 2026-08-11, the delay itself is the cold page cache and is not fixable here |
+| 75 | The tagging rules window forgets its size and its column widths | persistence | S | open; follows item 44 |
+| 76 | Every field in the rules dialog is free text, so a rule is easy to get wrong | workflow | M | open; design approved 2026-08-13, 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 | open; the dialog counts matches, it cannot show them |
+| 78 | No way to build a rule from something visible in a message | workflow | M | open; wants 76 first, so the created rule lands in a form that can hold it |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -4722,6 +4726,146 @@ inaccurate, not because anything is expected to happen.
here and should not be attempted: prefaulting 1.1 GB at startup to make one
query look fast is a worse trade than the wait.
+## 75. The tagging rules window forgets its size and its column widths
+
+**Observed.** The rules window opens at the same size every time, whatever
+size it was left at, and the rule table's columns reset to their computed
+widths on every open. The user also asks whether it should present as a
+primary window rather than a popup.
+
+**Cause.** `TagRulesDialog::TagRulesDialog` calls `resize(760, 520)`
+unconditionally (`src/tagrulesdialog.cpp:59`) and never reads or writes a
+saved geometry; there is no `saveGeometry`/`restoreGeometry` pair anywhere in
+the file, and `MainWindow` is the only class that touches `uiStatePath()`
+(`src/mainwindow.cpp:141,183`). The columns reset because `reloadList()`
+calls `resizeColumnToContents` for the enabled and stage columns on every
+repopulate (`src/tagrulesdialog.cpp:213-214`), and `onCountsReady` does the
+same for the count column (`:326`); a width the user dragged is discarded by
+the next reload, not only by a close.
+
+**Approach.** Save `saveGeometry()` and `m_list->header()->saveState()` into
+the machine-written UI state file under keys of their own, and restore both
+in the constructor, keeping the current `resize` as the fallback for a first
+run. Drop the unconditional `resizeColumnToContents` calls once a saved
+header state exists, or the restore is undone on the first reload.
+
+The "popup or primary window" question is a separate decision and not a
+defect: the class is a `QDialog` (`src/tagrulesdialog.h:41`), which is what
+makes it modal to the main window and what puts it above it. Changing it to a
+top-level window means it can be left open beside the main window and can go
+behind it, and the rule edits would then need to survive that. Ask before
+changing it.
+
+**Constraints.** UI state goes to `~/.local/state/qtmaildir/uistate.conf` via
+`MainWindow::uiStatePath()`, never into the hand-edited config. A geometry
+restore under the offscreen platform is what item 46 already tripped over, so
+the test asserts on the saved value rather than on the resulting frame.
+
+**Size: S.**
+
+## 76. Every field in the rules dialog is free text, so a rule is easy to get wrong
+
+**Observed.** A rule is written by typing into four line edits, and the user
+would rather choose from buttons, radios and completion, with typing reduced
+to the parts that genuinely have to be typed.
+
+**Cause.** Not a defect, this is what shipped. The form is four bare
+`QLineEdit`s for id, add, remove and query plus one `QCheckBox`
+(`src/tagrulesdialog.cpp:88-96`), and none of them is attached to a
+completer. The tag completion machinery already exists as `QueryCompleter`
+(`src/querycompleter.h:90`) and is not used here.
+
+**Approach.** Designed 2026-08-13. **Read
+`specs/2026-08-13-rule-builder-design.md` instead of planning from this
+entry.** A `RuleQuery` value type parses and compiles the query string, and a
+row builder in the dialog edits it, in the shape the user asked for after
+showing Thunderbird's filter window: field and operator dropdowns, `+`/`-`
+buttons, an all/any radio, and a separate "but not" block.
+
+Three constraints decide whether to open the spec at all. **The stored format
+does not change**, so this is a single-repo change and mailctl needs no edit.
+**The query string stays authoritative**, so a rule the builder cannot
+represent still opens, saves and runs, in a text mode that every rule
+carries. And **the string is rewritten only when the rows actually changed**,
+compared against the parsed value rather than tracked with a dirty flag,
+which Qt would set during programmatic population.
+
+Measured against the seventeen real rules: sixteen are flat, one nests an
+`or` group inside an `and` chain, which is what the exclusion block exists
+for.
+
+**Completion is the other half of this item** and is independent of the
+builder; it can land before or after. It reuses `QueryCompleter`
+(`src/querycompleter.h:90`) for tag names on the add and remove fields.
+
+**Constraints.** A multi-value field must not use `QLineEdit::setCompleter`.
+CLAUDE.md records this trap twice over: the line edit overwrites the
+completer's prefix with the widget's whole text, so the first tag completes
+and nothing after it does. Attach with `QCompleter::setWidget` and drive the
+prefix by hand. And a test that uses `setText()` passes against the bug,
+because `setText` never drives a completer at all, so the keys have to be
+typed.
+
+The hook refuses to remove `unread` or `inbox`, so a builder that offers
+those as removable tags produces a rule that silently does nothing. Say so in
+the UI rather than letting the hook decline it invisibly.
+
+**Size: M.**
+
+## 77. No way to see what a rule would collect, in the thread list
+
+**Observed.** The user wants a button that runs the rule's query in the main
+window, to look at what it would collect rather than at how many.
+
+**Cause.** Not a defect. The dialog already counts matches, over
+`requestMessageCounts` and `messageCountsReady`
+(`src/notmuchworker.h:172`, `src/mainwindow.cpp:1322-1335`), which answers
+"how many" and cannot answer "which". Nothing carries a query from the dialog
+back to the query bar.
+
+**Approach.** One signal from the dialog carrying the rule's query string,
+and a slot on `MainWindow` that puts it in the query bar and runs it. The
+dialog stays open, since the point is to compare the two.
+
+**Constraints.** The stored query carries no scope on purpose: the hook
+supplies `tag:new` and wraps the rule's query in parentheses. A preview must
+therefore run the query WITHOUT `tag:new`, or it shows nothing at all outside
+a sync window, and it must not add the parentheses silently either, since
+what the user is checking is the query as stored.
+
+A count request must never bump `m_generation`; item 44 already had to add
+`m_ruleCountGeneration` for exactly this reason (`src/mainwindow.h:657-664`).
+A preview is a real query and does bump it, which is correct, but it also
+means the preview discards whatever thread load was in flight.
+
+**Size: S.**
+
+## 78. No way to build a rule from something visible in a message
+
+**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.
+
+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.**
+
## Deferred, unsized, or split out
Items noted while triaging but not part of the original list. Same numbering