Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
49f1aa79c0 feat: show gmail thread message counts (#99) (thanks @jeanregisser)
Some checks failed
ci / worker (push) Has been cancelled
ci / darwin-cgo-build (push) Has been cancelled
ci / test (push) Has been cancelled
2026-01-21 19:09:51 +00:00
Jean Regisser
7fc1c962b7 feat(gmail): Add thread message count to search results
Show message count in search output to help agents and users understand
when an email is part of a multi-message conversation.

Before:
  ID  DATE  FROM  SUBJECT  LABELS
  xxx Jan 20  John  Hello    INBOX

After:
  ID  DATE  FROM  SUBJECT  LABELS  THREAD
  xxx Jan 20  John  Hello    INBOX   [3 msgs]

Problem:
When searching Gmail, the results show thread IDs but not the number of
messages in each thread. This makes it hard for AI agents to determine
if a message is a standalone email or part of a conversation. Agents
often miss full thread context when they only fetch a single message.

Solution:
- Add MessageCount field to threadItem struct
- Populate message count from thread.Messages
- Add new THREAD column: [X msgs] for threads, - for single messages
- Keep LABELS column for backward compatibility
- Include messageCount in JSON output for programmatic access
- Update tests to verify MessageCount in JSON output

Prompt context that inspired this change:
> I'm using gogcli from an AI agent and I was looking at recent emails.
> I searched for emails from today and found an email from Claire but I
> couldn't see that there were replies in the thread. The search results
> only showed the original email, not the replies.

This change makes it immediately visible when search results contain
threads with multiple messages, allowing agents to fetch the full thread
when needed.
2026-01-21 19:09:14 +00:00
3 changed files with 24 additions and 14 deletions

View File

@ -7,6 +7,7 @@
- Chat: spaces, messages, threads, and DM commands (Workspace only). (#84) — thanks @salmonumbrella.
- People: profile lookup, directory search, and relations commands. (#84) — thanks @salmonumbrella.
- Calendar: show event timezone and local times; add --weekday output. (#92) — thanks @salmonumbrella.
- Gmail: show thread message count in search output. (#99) — thanks @jeanregisser.
### Fixed

View File

@ -83,11 +83,12 @@ func TestExecute_GmailSearch_JSON(t *testing.T) {
var parsed struct {
Threads []struct {
ID string `json:"id"`
Date string `json:"date"`
From string `json:"from"`
Subject string `json:"subject"`
Labels []string `json:"labels"`
ID string `json:"id"`
Date string `json:"date"`
From string `json:"from"`
Subject string `json:"subject"`
Labels []string `json:"labels"`
MessageCount int `json:"messageCount"`
} `json:"threads"`
NextPageToken string `json:"nextPageToken"`
}
@ -100,6 +101,9 @@ func TestExecute_GmailSearch_JSON(t *testing.T) {
if parsed.Threads[0].ID != "t1" || parsed.Threads[0].Subject != "Hello" {
t.Fatalf("unexpected thread: %#v", parsed.Threads[0])
}
if parsed.Threads[0].MessageCount != 1 {
t.Fatalf("unexpected messageCount: %d", parsed.Threads[0].MessageCount)
}
if parsed.Threads[0].Date != "2006-01-02 22:04" {
t.Fatalf("unexpected date: %q", parsed.Threads[0].Date)
}

View File

@ -121,9 +121,13 @@ func (c *GmailSearchCmd) Run(ctx context.Context, flags *RootFlags) error {
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "ID\tDATE\tFROM\tSUBJECT\tLABELS")
fmt.Fprintln(w, "ID\tDATE\tFROM\tSUBJECT\tLABELS\tTHREAD")
for _, it := range items {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", it.ID, it.Date, it.From, it.Subject, strings.Join(it.Labels, ","))
threadInfo := "-"
if it.MessageCount > 1 {
threadInfo = fmt.Sprintf("[%d msgs]", it.MessageCount)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", it.ID, it.Date, it.From, it.Subject, strings.Join(it.Labels, ","), threadInfo)
}
printNextPageHint(u, resp.NextPageToken)
return nil
@ -316,11 +320,12 @@ func isUnsubscribeLink(raw string) bool {
// threadItem holds parsed thread metadata for display/JSON output
type threadItem struct {
ID string `json:"id"`
Date string `json:"date,omitempty"`
From string `json:"from,omitempty"`
Subject string `json:"subject,omitempty"`
Labels []string `json:"labels,omitempty"`
ID string `json:"id"`
Date string `json:"date,omitempty"`
From string `json:"from,omitempty"`
Subject string `json:"subject,omitempty"`
Labels []string `json:"labels,omitempty"`
MessageCount int `json:"messageCount,omitempty"` // Number of messages in the thread
}
// fetchThreadDetails fetches thread metadata concurrently with bounded parallelism.
@ -372,7 +377,7 @@ func fetchThreadDetails(ctx context.Context, svc *gmail.Service, threads []*gmai
return
}
item := threadItem{ID: threadID}
item := threadItem{ID: threadID, MessageCount: len(thread.Messages)}
if first := firstMessage(thread); first != nil {
item.From = sanitizeTab(headerValue(first.Payload, "From"))
item.Subject = sanitizeTab(headerValue(first.Payload, "Subject"))
@ -413,7 +418,7 @@ func fetchThreadDetails(ctx context.Context, svc *gmail.Service, threads []*gmai
for r := range results {
if r.err != nil {
hasErr = true
ordered[r.index] = threadItem{ID: "", Date: "", From: "", Subject: "", Labels: nil}
ordered[r.index] = threadItem{ID: "", Date: "", From: "", Subject: "", Labels: nil, MessageCount: 0}
continue
}
ordered[r.index] = r.item