···11# Changelog
2233-## 2026-04-01
33+# 2026-04-02
44+- **Auto From on reply** — replying auto-selects the From address that matches the email's To/CC field (e.g. email sent to `simon@domain.com` replies from `simon@domain.com`); `r` now works from inbox list view; `# [neomd: from: ...]` shown in editor; `x` in pre-send discards the email
55+66+# 2026-04-01
47- **Threaded inbox** — related emails are automatically grouped in the inbox list with a Twitter-style vertical connector line (`│`/`╰`); threads detected via `In-Reply-To`/`Message-ID` IMAP envelope headers with a reply-prefix subject fallback (only emails with `Re:`, `AW:`, `Fwd:` etc. are grouped by subject — recurring notifications/invoices stay separate); newest reply on top, root at bottom; threads sorted by most recent email so active conversations float to the top
58- **Clickable tabs** — folder tabs in the top bar are clickable with the mouse; click any tab to switch folders
69- **Spell check in pre-send (`s`)** — opens nvim with spell checking enabled (`en_us` + `de`), cursor jumps to the first misspelled word; use `]s`/`[s` to navigate errors, `z=` for suggestions, `zg` to add to dictionary; corrected body flows back to pre-send
+9-3
internal/editor/editor.go
···9999 return s
100100}
101101102102-// ReplyPrelude builds a quote block for replies. cc may be empty.
103103-func ReplyPrelude(to, cc, subject, originalFrom, originalBody string) string {
102102+// ReplyPrelude builds a quote block for replies. cc and from may be empty.
103103+func ReplyPrelude(to, cc, subject, from, originalFrom, originalBody string) string {
104104 s := fmt.Sprintf("# [neomd: to: %s]\n", to)
105105 if cc != "" {
106106 s += fmt.Sprintf("# [neomd: cc: %s]\n", cc)
107107 }
108108+ if from != "" {
109109+ s += fmt.Sprintf("# [neomd: from: %s]\n", from)
110110+ }
108111 s += fmt.Sprintf("# [neomd: subject: Re: %s]\n\n---\n\n> **%s** wrote:\n>\n%s\n\n---\n\n",
109112 subject, originalFrom, quoteLines(originalBody))
110113 return s
···112115113116// ForwardPrelude builds a quoted forward block. The To field is left empty for
114117// the user to fill in.
115115-func ForwardPrelude(subject, originalFrom, originalDate, originalTo, originalBody string) string {
118118+func ForwardPrelude(subject, from, originalFrom, originalDate, originalTo, originalBody string) string {
116119 s := "# [neomd: to: ]\n"
120120+ if from != "" {
121121+ s += fmt.Sprintf("# [neomd: from: %s]\n", from)
122122+ }
117123 if !strings.HasPrefix(strings.ToLower(subject), "fwd:") {
118124 subject = "Fwd: " + subject
119125 }
+36-5
internal/ui/model.go
···328328 // Screener operations (I/O/F/P/$) are not undoable — they also modify .txt files.
329329 undoStack [][]undoMove
330330331331- // Forward: when true, bodyLoadedMsg launches forward editor instead of reader
332332- pendingForward bool
331331+ // Forward/Reply: when true, bodyLoadedMsg launches the action instead of reader
332332+ pendingForward bool
333333+ pendingReply bool
334334+ pendingReplyAll bool
333335334336 // Chord prefix: "g" or "M" while waiting for second key
335337 pendingKey string
···11101112 m.pendingForward = false
11111113 return m.launchForwardCmd()
11121114 }
11151115+ if m.pendingReply {
11161116+ m.pendingReply = false
11171117+ return m.launchReplyCmd()
11181118+ }
11191119+ if m.pendingReplyAll {
11201120+ m.pendingReplyAll = false
11211121+ return m.launchReplyAllCmd()
11221122+ }
11131123 m.openLinks = extractLinks(msg.body)
11141124 _ = loadEmailIntoReader(&m.reader, msg.email, msg.body, msg.attachments, m.openLinks, m.cfg.UI.Theme, m.width)
11151125 m.state = stateReading
···17661776 m.loading = true
17671777 return m, tea.Batch(m.spinner.Tick, m.fetchBodyCmd(e))
1768177817791779+ case "r":
17801780+ e := selectedEmail(m.inbox)
17811781+ if e == nil {
17821782+ return m, nil
17831783+ }
17841784+ // Pre-select the correct From address before fetching body.
17851785+ if idx := m.matchFromIndex(e.To, e.CC); idx >= 0 {
17861786+ m.presendFromI = idx
17871787+ }
17881788+ m.pendingReply = true
17891789+ m.loading = true
17901790+ return m, tea.Batch(m.spinner.Tick, m.fetchBodyCmd(e))
17911791+17691792 case "f":
17701793 e := selectedEmail(m.inbox)
17711794 if e == nil {
···24762499 m.state = stateCompose
24772500 }
24782501 return m, nil
25022502+ case "x":
25032503+ // Discard the email entirely — clear everything and go back to inbox.
25042504+ m.attachments = nil
25052505+ m.pendingSend = nil
25062506+ m.state = stateInbox
25072507+ m.status = "Discarded."
25082508+ m.isError = false
25092509+ return m, nil
24792510 case "p":
24802511 return m.previewInBrowser()
24812512 case "esc":
···27062737 return m, nil
27072738 }
27082739 subject := e.Subject
27092709- prelude := editor.ForwardPrelude(subject, e.From, e.Date.Format("Mon, 02 Jan 2006 15:04:05 -0700"), e.To, m.openBody)
27402740+ prelude := editor.ForwardPrelude(subject, m.presendFrom(), e.From, e.Date.Format("Mon, 02 Jan 2006 15:04:05 -0700"), e.To, m.openBody)
2710274127112742 f, err := os.CreateTemp(neomdTempDir(), "neomd-*.md")
27122743 if err != nil {
···27892820 }
27902821 }
2791282227922792- prelude := editor.ReplyPrelude(to, cc, subject, e.From, m.openBody)
28232823+ prelude := editor.ReplyPrelude(to, cc, subject, m.presendFrom(), e.From, m.openBody)
2793282427942825 f, err := os.CreateTemp(neomdTempDir(), "neomd-*.md")
27952826 if err != nil {
···30013032 b.WriteString("\n")
30023033 }
3003303430043004- b.WriteString(styleHelp.Render(" enter send · s spell · p preview · ctrl+f from · ctrl+b cc/bcc · a attach · D remove last · d draft · e edit · esc cancel"))
30353035+ b.WriteString(styleHelp.Render(" enter send · s spell · p preview · a attach · D remove attach · ctrl+f from · ctrl+b cc/bcc · e edit · d draft · esc cancel · x discard"))
30053036 return b.String()
30063037}
30073038