From 71b8e0075b3d566b0e9db5add045ff7b98c7b4c0 Mon Sep 17 00:00:00 2001 From: leonid-shutov Date: Tue, 8 Sep 2026 22:11:14 +0200 Subject: [PATCH 1/2] Show a thread's body, not HEY's preview of it `Entry.Summary` is HEY's ~105-character preview of the body, and renderEntries printed it above whatever came next unconditionally. Beside a body that was read, that repeats the message's opening line and then cuts it off with an ellipsis; above an "(body not read: failed)" notice it passes the preview off as the message the reader could not get. The summary now stands in only where there is nothing else to show -- a bodyless entry, which is all HEY serves for one. Each arm of the switch opens with the blank line that used to separate the header from the summary, so the spacing is unchanged. --- internal/tui/mail.go | 12 ++++++---- internal/tui/mail_test.go | 49 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/internal/tui/mail.go b/internal/tui/mail.go index 2a634ade..753a41d7 100644 --- a/internal/tui/mail.go +++ b/internal/tui/mail.go @@ -2981,17 +2981,19 @@ func (v *mailView) renderEntries(entries []mail.Entry) (string, []int) { from = e.AlternativeSenderName } - // A full blank line separates the header from whatever follows it — - // the summary here, or the body's own leading blank when there is none. + // Each arm below opens with a blank line, which is what separates the header + // from whatever follows it. The summary is HEY's ~105-character preview of the + // body, so it stands in only where there is no body to show: printed beside one + // it repeats the message's opening line, and printed for a body that was not + // read it passes a preview off as the message. fmt.Fprintf(&b, "%s %s\n", v.vc.styles.entryFrom.Render(terminal.SanitizeLine(from)), v.vc.styles.entryDate.Render(formatDisplayDateTime(e.CreatedAt))) - if e.Summary != "" { - fmt.Fprintf(&b, "\n%s\n", terminal.SanitizeLine(e.Summary)) - } switch { case !e.Body.IsEmpty(): fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryBody.Render(markdown.Render(e.Body, sepWidth))) case e.BodyState == string(threadload.StateOverLimit), e.BodyState == string(threadload.StateFailed): fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryDate.Render("(body not read: "+e.BodyState+")")) + case e.Summary != "": + fmt.Fprintf(&b, "\n%s\n", terminal.SanitizeLine(e.Summary)) } entryAttachments := attachmentsForMessage(v.attachments, e.ID) if panel := renderAttachmentPanel(entryAttachments, selectedAttachmentForMessage(v.attachments, v.attachmentCursor, e.ID)); panel != "" { diff --git a/internal/tui/mail_test.go b/internal/tui/mail_test.go index 8da197f8..3f69d500 100644 --- a/internal/tui/mail_test.go +++ b/internal/tui/mail_test.go @@ -26,6 +26,7 @@ import ( "github.com/basecamp/hey-cli/internal/apierr" "github.com/basecamp/hey-cli/internal/htmlutil" "github.com/basecamp/hey-cli/internal/mail" + "github.com/basecamp/hey-cli/internal/threadload" ) func testPNG(t *testing.T) []byte { @@ -331,8 +332,8 @@ func TestThreadViewShowsSubjectAndSpacesTheHeader(t *testing.T) { if strings.TrimSpace(lines[header+1]) != "" { t.Errorf("a blank line should follow the header: %q", lines[header+1]) } - if strings.TrimSpace(lines[header+2]) != "Can we meet Thursday?" { - t.Errorf("the summary should follow the blank line: %q", lines[header+2]) + if strings.TrimSpace(lines[header+2]) != "Can we meet Thursday to walk through the numbers?" { + t.Errorf("the body should follow the blank line: %q", lines[header+2]) } } @@ -3710,6 +3711,50 @@ func TestMailViewReadsEveryPageOfAThreadAndMarksUnreadBodies(t *testing.T) { } } +// HEY's summary is its ~105-character preview of the body, so it belongs on screen only +// where the body itself cannot be. Beside a body it repeats the message's opening line; +// for a body that was not read it would pass a preview off as the message. +func TestRenderEntriesShowsTheBodyRatherThanHEYsPreviewOfIt(t *testing.T) { + v := newMailView(testVC()) + v.vc.width = 80 + entries := []mail.Entry{ + { + ID: 1, + Creator: mail.Contact{Name: "Maria Gonzalez"}, + Summary: "Moving the review to Friday so ...", + Body: htmlutil.ToMarkdown("

Moving the review to Friday so Sam can join.

"), + BodyState: string(threadload.StateHydrated), + }, + { + ID: 2, + Creator: mail.Contact{Name: "Sam Rivera"}, + Summary: "The quarterly figures are attached.", + BodyState: string(threadload.StateBodyless), + }, + { + ID: 3, + Creator: mail.Contact{Name: "Ana Ortiz"}, + Summary: "Never shown, the body went unread ...", + BodyState: string(threadload.StateFailed), + }, + } + + rendered, _ := v.renderEntries(entries) + + if strings.Contains(rendered, "Friday so ...") { + t.Errorf("a hydrated entry repeats HEY's preview of its own body:\n%s", rendered) + } + if !strings.Contains(rendered, "Sam can join.") { + t.Errorf("a hydrated entry lost its body:\n%s", rendered) + } + if !strings.Contains(rendered, "The quarterly figures are attached.") { + t.Errorf("a bodyless entry lost its summary, which is all HEY serves for it:\n%s", rendered) + } + if strings.Contains(rendered, "Never shown") || !strings.Contains(rendered, "(body not read: failed)") { + t.Errorf("an unread body should say so rather than show a preview:\n%s", rendered) + } +} + func TestRenderEntriesReportsEveryMessageHeaderOffset(t *testing.T) { v := newMailView(testVC()) v.vc.width = 60 From e803b8047123f1cce393813eaeaf328f9be52bff Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 9 Sep 2026 15:42:54 -0700 Subject: [PATCH 2/2] Say (empty body) for a hydrated body that rendered to nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary stands in only where HEY served no body — the same ladder printThreadStyled uses for the CLI. --- internal/tui/mail.go | 17 +++++++++++------ internal/tui/mail_test.go | 10 ++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/internal/tui/mail.go b/internal/tui/mail.go index 753a41d7..aa3cfd8c 100644 --- a/internal/tui/mail.go +++ b/internal/tui/mail.go @@ -2983,17 +2983,22 @@ func (v *mailView) renderEntries(entries []mail.Entry) (string, []int) { // Each arm below opens with a blank line, which is what separates the header // from whatever follows it. The summary is HEY's ~105-character preview of the - // body, so it stands in only where there is no body to show: printed beside one - // it repeats the message's opening line, and printed for a body that was not - // read it passes a preview off as the message. + // body, so it stands in only where HEY served no body at all — the same ladder + // as printThreadStyled in internal/cmd/topic.go. Printed beside a body it repeats + // the message's opening line; printed for a body that was read and rendered to + // nothing, or for one that was not read, it passes a preview off as the message. fmt.Fprintf(&b, "%s %s\n", v.vc.styles.entryFrom.Render(terminal.SanitizeLine(from)), v.vc.styles.entryDate.Render(formatDisplayDateTime(e.CreatedAt))) switch { case !e.Body.IsEmpty(): fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryBody.Render(markdown.Render(e.Body, sepWidth))) - case e.BodyState == string(threadload.StateOverLimit), e.BodyState == string(threadload.StateFailed): - fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryDate.Render("(body not read: "+e.BodyState+")")) - case e.Summary != "": + case e.BodyState == string(threadload.StateHydrated): + fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryDate.Render("(empty body)")) + case e.BodyState == string(threadload.StateBodyless) && e.Summary != "": fmt.Fprintf(&b, "\n%s\n", terminal.SanitizeLine(e.Summary)) + case e.BodyState == string(threadload.StateBodyless): + fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryDate.Render("(no body)")) + default: + fmt.Fprintf(&b, "\n%s\n", v.vc.styles.entryDate.Render("(body not read: "+e.BodyState+")")) } entryAttachments := attachmentsForMessage(v.attachments, e.ID) if panel := renderAttachmentPanel(entryAttachments, selectedAttachmentForMessage(v.attachments, v.attachmentCursor, e.ID)); panel != "" { diff --git a/internal/tui/mail_test.go b/internal/tui/mail_test.go index 3f69d500..a1bfe59e 100644 --- a/internal/tui/mail_test.go +++ b/internal/tui/mail_test.go @@ -3737,6 +3737,13 @@ func TestRenderEntriesShowsTheBodyRatherThanHEYsPreviewOfIt(t *testing.T) { Summary: "Never shown, the body went unread ...", BodyState: string(threadload.StateFailed), }, + { + ID: 4, + Creator: mail.Contact{Name: "Lee Park"}, + Summary: "Markup only, so HEY still has a preview ...", + Body: htmlutil.ToMarkdown("

"), + BodyState: string(threadload.StateHydrated), + }, } rendered, _ := v.renderEntries(entries) @@ -3753,6 +3760,9 @@ func TestRenderEntriesShowsTheBodyRatherThanHEYsPreviewOfIt(t *testing.T) { if strings.Contains(rendered, "Never shown") || !strings.Contains(rendered, "(body not read: failed)") { t.Errorf("an unread body should say so rather than show a preview:\n%s", rendered) } + if strings.Contains(rendered, "Markup only") || !strings.Contains(rendered, "(empty body)") { + t.Errorf("a body that was read and rendered to nothing should say so rather than show a preview:\n%s", rendered) + } } func TestRenderEntriesReportsEveryMessageHeaderOffset(t *testing.T) {