Commit 726899a
feat(threading): JWZ conversation view (#1188)
## What?
- Add `internal/threading` package implementing the Jamie Zawinski
threading algorithm against `Message-ID` / `In-Reply-To` / `References`
headers, with subject-fallback grouping for orphans
- Carry `MessageID`, `InReplyTo`, and `References` through fetcher, the
IMAP/JMAP/POP3 backends, the on-disk email cache, the daemon RPC types,
and the inbox model so threading works against cached headers without
server round-trips
- Inbox renders threaded mode with one row per thread root, showing the
count and last-sender; `Enter` toggles expand/collapse; expanded
children render indented with `↪` markers
- `T` keybind toggles flat vs threaded for the current folder; the
per-folder mode persists via `folder_cache.go`
- Subject canonicalization handles `Re:`, `Fwd:`, `Fw:`, `AW:`, `WG:`,
`Tr:` (lowercased, stripped repeatedly so `Re: Re: Foo` -> `foo`)
- Tests cover: 3-message chains, forks, missing-parent placeholders,
subject-fallback grouping, empty References, deterministic ordering
across repeated `Build()` calls
- VHS demo (`screenshots/cmd/threading_demo` +
`screenshots/threading_demo.tape`): flat (5 emails) → threaded (3 rows
with `(3)` count on the root) → expanded (5 rows with `↪` on children) →
collapsed → flat
## Why?
This is the maintainer's spec from issue #509 and the more detailed
#1130:
> "Group emails into conversation threads using `In-Reply-To` and
`References` headers (RFC 5322). Display threads as collapsible groups
in the inbox, showing the latest message and a count of messages in the
thread."
> "Build threads with the Jamie Zawinski algorithm (the one Thunderbird
uses) so we don't have to rely on `X-GM-THRID`. Threading should be done
client-side from the cached header set so it works across providers."
The framing in #1130 is the user-visible argument: "Showing each reply
as a separate inbox row is how Mutt looked in 1999. Modern terminal
clients (aerc, himalaya) all thread."
The launch threads on r/coolgithubprojects + r/CLI + r/selfhosted
(cumulative 161 upvotes, 32 comments) consistently flagged conversation
grouping as the gap users notice first when comparing matcha to
gmail/superhuman/aerc.
## Notes
- Touches `main.go` (alongside in-flight #845 and #686). Conflicts
should be mechanical - the threading wiring in `main.go` is small
(cache-conversion paths to carry References/InReplyTo). Happy to rebase
or stack PRs.
- Ordering ties in JWZ are broken on `EmailID` so `Build()` is
deterministic across runs.
- The implementation deliberately avoids `X-GM-THRID` and IMAP THREAD
(RFC 5256) per the spec - threading is purely client-side over cached
envelope data.
- Out of scope: per-thread mark-as-read propagation rules (kept current
behavior); thread-aware archive/delete (uses single-message semantics
for now).
Closes #509. Addresses #1130.
This contribution was developed with AI assistance.
---------
Signed-off-by: drew <me@andrinoff.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Andriy Chernov <andriy@floatpane.com>
Co-authored-by: drew <me@andrinoff.com>1 parent c94e714 commit 726899a
33 files changed
Lines changed: 1310 additions & 119 deletions
File tree
- backend
- imap
- jmap
- pop3
- config
- daemon
- docs/docs/Features
- fetcher
- i18n/locales
- internal/threading
- screenshots
- cmd/threading_demo
- tui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| 85 | + | |
85 | 86 | | |
86 | 87 | | |
87 | 88 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
| 147 | + | |
147 | 148 | | |
148 | 149 | | |
149 | 150 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
168 | | - | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
169 | 173 | | |
170 | 174 | | |
171 | 175 | | |
| |||
697 | 701 | | |
698 | 702 | | |
699 | 703 | | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
700 | 708 | | |
701 | 709 | | |
702 | 710 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| 31 | + | |
| 32 | + | |
30 | 33 | | |
31 | 34 | | |
32 | 35 | | |
| |||
298 | 301 | | |
299 | 302 | | |
300 | 303 | | |
| 304 | + | |
| 305 | + | |
301 | 306 | | |
302 | 307 | | |
303 | 308 | | |
| |||
339 | 344 | | |
340 | 345 | | |
341 | 346 | | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
347 | | - | |
348 | | - | |
349 | | - | |
350 | | - | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
351 | 373 | | |
| 374 | + | |
352 | 375 | | |
353 | 376 | | |
354 | 377 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
94 | 95 | | |
95 | 96 | | |
96 | 97 | | |
| |||
398 | 399 | | |
399 | 400 | | |
400 | 401 | | |
| 402 | + | |
401 | 403 | | |
402 | 404 | | |
403 | 405 | | |
| 406 | + | |
404 | 407 | | |
405 | 408 | | |
406 | 409 | | |
| |||
543 | 546 | | |
544 | 547 | | |
545 | 548 | | |
| 549 | + | |
546 | 550 | | |
547 | 551 | | |
548 | 552 | | |
| |||
579 | 583 | | |
580 | 584 | | |
581 | 585 | | |
| 586 | + | |
582 | 587 | | |
583 | 588 | | |
584 | 589 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
| 10 | + | |
| 11 | + | |
9 | 12 | | |
10 | 13 | | |
11 | 14 | | |
| |||
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
20 | | - | |
21 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
22 | 26 | | |
23 | 27 | | |
24 | 28 | | |
| |||
179 | 183 | | |
180 | 184 | | |
181 | 185 | | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
| |||
140 | 141 | | |
141 | 142 | | |
142 | 143 | | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
152 | 154 | | |
153 | 155 | | |
154 | 156 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
360 | 360 | | |
361 | 361 | | |
362 | 362 | | |
363 | | - | |
364 | | - | |
365 | | - | |
366 | | - | |
367 | | - | |
368 | | - | |
369 | | - | |
370 | | - | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
371 | 373 | | |
372 | 374 | | |
373 | 375 | | |
| |||
474 | 476 | | |
475 | 477 | | |
476 | 478 | | |
477 | | - | |
478 | | - | |
479 | | - | |
480 | | - | |
481 | | - | |
482 | | - | |
483 | | - | |
484 | | - | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
485 | 489 | | |
486 | 490 | | |
487 | 491 | | |
| |||
0 commit comments