Skip to content

feat(threading): JWZ conversation view#1188

Merged
andrinoff merged 5 commits intofloatpane:masterfrom
mvanhorn:feature/509-threaded-conversation-view
May 7, 2026
Merged

feat(threading): JWZ conversation view#1188
andrinoff merged 5 commits intofloatpane:masterfrom
mvanhorn:feature/509-threaded-conversation-view

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Apr 28, 2026

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

demo

Simulated demo (Remotion) — matcha theme, scripted UI, not a live capture.

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 chore: refactor main.go #845 and feat: offline mode initial commit #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.

@mvanhorn mvanhorn requested a review from a team as a code owner April 28, 2026 16:04
@github-actions github-actions Bot added ci CI / build pipeline enhancement New feature or request labels Apr 28, 2026
@andrinoff
Copy link
Copy Markdown
Member

the PR has conflicts with your previous changes in #1186

@floatpanebot floatpanebot added area/build Build system / Makefile / packaging area/cli CLI flags / commands area/daemon Daemon / RPC area/fetcher IMAP fetch / IDLE / search area/sender SMTP send path area/theme Theming / colors area/tui Terminal UI / view layer labels May 3, 2026
Adds an internal/threading package implementing the Jamie Zawinski
threading algorithm (Message-ID + In-Reply-To + References) with
subject-fallback grouping for orphans. The inbox renders one row per
thread root with a count and last sender; pressing Enter toggles
expand/collapse; the per-folder flat-vs-threaded mode persists via
folder_cache.

The MessageID/InReplyTo/References metadata is now carried through
fetcher and 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. Per the maintainer's spec
in floatpane#509 and floatpane#1130: client-side, provider-agnostic, JWZ rather than
X-GM-THRID, deterministic ordering.

- internal/threading/jwz.go: ThreadNode, Thread, Build()
- internal/threading/subject.go: canonicalSubject()
- internal/threading/jwz_test.go: chains, forks, missing parents,
  subject-fallback grouping, deterministic ordering
- tui/inbox.go: threaded mode rendering + 'T' toggle + expand/collapse
- config/folder_cache.go: persist threaded toggle per folder
- backend/{imap,jmap,pop3}: emit MessageID/InReplyTo/References
- screenshots/cmd/threading_demo: VHS helper

Closes floatpane#509. Addresses floatpane#1130.
@mvanhorn mvanhorn force-pushed the feature/509-threaded-conversation-view branch from 6e4d379 to 2d9311e Compare May 4, 2026 20:48
@floatpanebot floatpanebot added the area/config Configuration / settings label May 4, 2026
@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented May 4, 2026

@andrinoff Rebased onto master — now MERGEABLE.

Most of the eight overlapping files were additive (new fields/cases that #1186 and the threading branch each appended to the same struct or switch). The two non-mechanical spots:

  • tui/inbox.go: feat(search): server-side email search #1186 added displayEmails, accountLabelForEmail, dedup helpers, and an inline item-construction loop. The threading branch refactored that same loop into itemsForEmails/itemForEmail. Kept both sets of helpers and routed itemForEmail through accountLabelForEmail so catch-all handling from feat(search): server-side email search #1186 still applies to threaded items.
  • backend/jmap/jmap.go: kept the expanded Properties list with inReplyTo and references from the self-review fix.

go build ./..., go vet ./..., and make test all pass. Ready for re-review.

@andrinoff andrinoff removed ci CI / build pipeline area/tui Terminal UI / view layer area/fetcher IMAP fetch / IDLE / search area/sender SMTP send path area/theme Theming / colors area/config Configuration / settings area/cli CLI flags / commands area/daemon Daemon / RPC area/build Build system / Makefile / packaging labels May 4, 2026
@floatpanebot floatpanebot added area/tui Terminal UI / view layer area/fetcher IMAP fetch / IDLE / search area/config Configuration / settings area/daemon Daemon / RPC area/i18n Localization / translations labels May 4, 2026
@floatpanebot floatpanebot added the area/docs Docs site / README label May 7, 2026
mvanhorn and others added 2 commits May 7, 2026 13:06
- backend/jmap: Email/get now requests inReplyTo and references
  alongside messageId so JMAP-backed accounts thread by real
  References/In-Reply-To rather than falling through to subject grouping
- internal/threading/subject: add Swedish/Norwegian/Danish (SV),
  Finnish (VS), Spanish (RV), Portuguese (ENC), Dutch (Antw), Polish
  (Odp), and Italian (R/I) reply/forward prefixes
- internal/threading/jwz_test: regression coverage for SV/RV/Antw
  subject-fallback grouping
Signed-off-by: drew <me@andrinoff.com>
@andrinoff andrinoff force-pushed the feature/509-threaded-conversation-view branch from 26b80fb to f99d649 Compare May 7, 2026 09:06
Signed-off-by: drew <me@andrinoff.com>
andrinoff
andrinoff previously approved these changes May 7, 2026
Copy link
Copy Markdown
Member

@andrinoff andrinoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now lgtm

Signed-off-by: drew <me@andrinoff.com>
Copy link
Copy Markdown
Member

@andrinoff andrinoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@andrinoff
Copy link
Copy Markdown
Member

/approve

Copy link
Copy Markdown
Member

@floatpanebot floatpanebot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved on behalf of @andrinoff via /approve command.

@andrinoff andrinoff merged commit 726899a into floatpane:master May 7, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Configuration / settings area/daemon Daemon / RPC area/docs Docs site / README area/fetcher IMAP fetch / IDLE / search area/i18n Localization / translations area/tui Terminal UI / view layer enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: Conversation/thread view

4 participants