aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 16:43:33 +0200
committerDanilo M. <danix@danix.xyz>2026-08-03 16:43:33 +0200
commit7f505624b1f385a89c9ff32e15f4d4e68595e5b8 (patch)
treec06185ba0e51aebd61c2a87b4ed6d19c8330db48
parent60b4346a428550a8a97170c245e8c171606586b1 (diff)
downloadqtmaildir-7f505624b1f385a89c9ff32e15f4d4e68595e5b8.tar.gz
qtmaildir-7f505624b1f385a89c9ff32e15f4d4e68595e5b8.zip
docs: add backlog items 15 to 17 from usage notes
Attachments turned out to be a real gap rather than a discoverability one. MessageView creates the attachment bar and adds it to the pane, but nothing ever populates it: m_attachmentBar appears nowhere else in the codebase, so it has never displayed anything. MimeParser already extracts attachments and Attachment::saveTo() already carries the path-traversal guard, so the backend needs calling, not writing. Item 16 makes delete a toggle, with the open question of what a mixed selection should do. Item 17 needs a new worker call, since there is no way to list tags today. Also records that this document's numbering and the user's own notes have diverged, so a reference to "item 13" stays resolvable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md103
1 files changed, 103 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 02bf7c4..0e0a22f 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
@@ -7,6 +7,12 @@ release is a separate decision.
Source: usage notes taken while running the app, 2026-08-03.
+**Numbers here are this document's own.** The user's own notes were numbered
+independently and the two sequences drifted apart once items were split: what
+those notes called 12 is item 13 here, and item 14 here (the tag column) was
+never in them at all. Items 15 to 17 come from a later pass over the same
+notes. Cite these numbers, not the notes', and do not renumber to reconcile.
+
Numbering is stable. New items append with the next free number and never
renumber, so a note referring to "item 7" keeps meaning the same thing. An item
that is dropped stays in the table marked `dropped` with a one-line reason.
@@ -47,6 +53,9 @@ taking that too literally.
| 11 | Icon, `.desktop` file, SlackBuild | packaging | M | **partly done**: icon and `.desktop` landed, SlackBuild open |
| 13 | No visual feedback that an action stuck | feedback | S | **done** |
| 14 | Tag column unreadable, tags need another home | presentation | M | **done** |
+| 15 | Attachments are parsed but unreachable from the UI | information | M | open |
+| 16 | Delete on an already-deleted thread should undelete | behavior | S | open |
+| 17 | No completion for tags in the query bar | workflow | M | open |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -499,6 +508,100 @@ hierarchical tag, and each fell through to its prefix colour. Fixed by reading
**Deferred:** clicking a chip to search that tag. Display only for now.
+## 15. Attachments are parsed but unreachable from the UI
+
+**Observed, 2026-08-03, with a screenshot.** A thread known to carry
+attachments shows the `attachment` chip and a body that refers to them, but
+there is no way to download or open one. The user also has no way to tell a
+message has an attachment before opening it.
+
+**Cause: the attachment bar is an empty placeholder.**
+`MessageView` creates `m_attachmentBar` and gives it a layout
+(`src/messageview.cpp:130`), adds it to the pane (`:142`), and then **nothing
+ever puts anything in it**. `m_attachmentBar` appears nowhere else in the
+codebase, and neither `render()` nor `showThread()` reads
+`ParsedMessage::attachments`. The bar has never displayed an attachment.
+
+This is a gap in the UI only. The backend is complete and already hardened:
+`MimeParser` fills `attachments`, and `Attachment` has `safeFilename()`,
+`saveTo()` and `isPathInsideDirectory()` with the path-traversal guard
+`CLAUDE.md` describes. None of that work needs redoing; it needs calling.
+
+**Approach, two independent pieces.**
+
+- *Populate the bar.* For each attachment on each rendered message, one button
+ showing the safe filename and the size. Clicking saves, through a
+ `QFileDialog` for the target directory, then `Attachment::saveTo()`. Report
+ the written path in the status bar, since a silent save is the same UX
+ failure as item 13.
+- *A paperclip column in the thread list*, so an attachment is visible before
+ opening. `ThreadSummary` already carries the thread's tags and notmuch
+ applies `attachment`, so the column can be driven from the existing tag data
+ with **no new worker query**. Keep it narrow: an icon column, no text.
+
+**Constraints.**
+
+- **Filenames are untrusted.** Display `safeFilename()`, never the raw
+ `filename`, and never interpolate either into rich text without escaping.
+ The save path must go through `saveTo()`, which is where the boundary check
+ lives.
+- **Do not add "open in default application" in the same change.** That means
+ handing a file from a stranger to `xdg-open`, which is a materially
+ different security decision from writing it to a directory the user picked.
+ If it is wanted, it is its own item with its own reasoning.
+- A thread renders as one document, so the bar must make clear which message
+ an attachment belongs to once a thread has several. Grouping by message, or
+ a per-message row inside the HTML, are both plausible; decide when building.
+
+**Verification:** a message with a known attachment saves a byte-identical
+file. A message with an attachment named `../../etc/passwd` writes inside the
+chosen directory under a sanitised name and nowhere else.
+
+## 16. Delete on an already-deleted thread should undelete
+
+**Observed:** hitting Delete twice on the same message is a natural way to
+express "no, put it back", but the second press does nothing visible because
+adding a tag that is already present is a no-op.
+
+**Approach:** make `delete` a toggle, the way `toggle_unread` already is.
+Deleting a thread that already carries `deleted` removes it instead.
+
+**Constraints.**
+
+- **A multi-row selection must not split.** If some selected threads are
+ deleted and others are not, toggling each independently leaves the selection
+ in two states from one keystroke, which is worse than either outcome. Decide
+ one direction for the whole selection: the natural rule is "if every
+ selected thread is already deleted, undelete them all; otherwise delete them
+ all."
+- Undo already covers the mistake case, so this is a convenience, not a safety
+ fix. It must not grow a confirmation dialog.
+- The same question applies to `spam` and `flag`. Do not change those in this
+ item; note whether the answer generalises once `delete` is built.
+
+## 17. No completion for tags in the query bar
+
+**Observed:** typing a query means remembering the exact tag name, including
+hierarchy (`shopping/amazon`).
+
+**Approach:** a `QCompleter` on the query bar, offering tag names after
+`tag:`.
+
+**Cause of the work being larger than it looks:** there is **no way to list
+tags today**. `NotmuchWorker` has no all-tags call, so this needs a new
+worker request and result signal, following the existing generation-counter
+pattern. `notmuch_database_get_all_tags()` is the underlying call.
+
+**Constraints.**
+
+- The tag list must be fetched on the worker thread like everything else. No
+ `notmuch_*` pointer crosses the thread boundary; the result is a
+ `QStringList`.
+- Refresh after a sync, since a sync can introduce tags. Do not refetch per
+ keystroke.
+- Completion should trigger on the `tag:` prefix specifically rather than on
+ every word, or it will offer tag names where a `from:` value belongs.
+
---
## Deferred, unsized, or split out