diff --git a/CHANGELOG.md b/CHANGELOG.md index e246b16..d08a21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ All notable changes to `discrawl` will be documented in this file. - Label direct-message TUI panes as direct messages instead of raw `@me` guild rows, keeping DM channel/person context readable. - Inherit shared crawlkit TUI improvements for newest-first startup, count-header sorting, selected-message-first chat detail panes, and gitcrawl-style metadata labels. +- Surface Discord attachment filenames and extracted text in TUI detail panes instead of only showing `attachments=true`. ## 0.6.3 - 2026-05-01 diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 770c65d..e9876b1 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -229,28 +229,33 @@ func TestWiretapImportsDesktopDirectMessages(t *testing.T) { func TestDiscordTUIRowsIncludePaneMetadata(t *testing.T) { rows := discordTUIRows([]store.MessageRow{{ - MessageID: "m1", - GuildID: "@me", - GuildName: "Discord Direct Messages", - ChannelID: "c1", - ChannelName: "Vincent K", - AuthorID: "u1", - AuthorName: "Peter", - Content: "hello from desktop", - DisplayContent: "hello from Vincent", - CreatedAt: time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC), - ReplyToMessage: "m0", - HasAttachments: true, - Pinned: true, + MessageID: "m1", + GuildID: "@me", + GuildName: "Discord Direct Messages", + ChannelID: "c1", + ChannelName: "Vincent K", + AuthorID: "u1", + AuthorName: "Peter", + Content: "hello from desktop", + DisplayContent: "hello from Vincent", + CreatedAt: time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC), + ReplyToMessage: "m0", + HasAttachments: true, + AttachmentNames: "trace.txt", + AttachmentText: "stack trace line one", + Pinned: true, }}) require.Len(t, rows, 1) require.Equal(t, "hello from Vincent", rows[0].Title) - require.Equal(t, "hello from Vincent", rows[0].Detail) + require.Contains(t, rows[0].Detail, "hello from Vincent") + require.Contains(t, rows[0].Detail, "Attachments") + require.Contains(t, rows[0].Detail, "stack trace line one") require.Equal(t, "hello from Vincent", rows[0].Text) require.Equal(t, "Direct messages", rows[0].Scope) require.Equal(t, "Vincent K", rows[0].Container) require.Contains(t, rows[0].Tags, "dm") require.Equal(t, "true", rows[0].Fields["attachments"]) + require.Equal(t, "trace.txt", rows[0].Fields["attachment_names"]) require.Equal(t, "true", rows[0].Fields["pinned"]) require.Equal(t, "m0", rows[0].Fields["reply_to"]) require.Equal(t, "@me", rows[0].Fields["guild_id"]) diff --git a/internal/cli/tui_commands.go b/internal/cli/tui_commands.go index 9cd9359..eff2ead 100644 --- a/internal/cli/tui_commands.go +++ b/internal/cli/tui_commands.go @@ -128,8 +128,9 @@ func discordTUIRows(rows []store.MessageRow) []tui.Row { for _, row := range rows { content := discordDisplayContent(row) title := strings.TrimSpace(content) + detail := discordDetailContent(row, content) if title == "" { - title = row.MessageID + title = firstNonEmpty(strings.TrimSpace(row.AttachmentText), row.MessageID) } tags := []string{row.GuildID, row.ChannelID} if row.GuildID == "@me" { @@ -148,24 +149,39 @@ func discordTUIRows(rows []store.MessageRow) []tui.Row { Author: discordAuthorLabel(row), Title: title, Text: content, - Detail: content, + Detail: detail, URL: discordMessageURL(row), CreatedAt: formatTime(row.CreatedAt), Tags: tags, Fields: map[string]string{ - "attachments": boolString(row.HasAttachments), - "author_id": row.AuthorID, - "channel_id": row.ChannelID, - "guild_id": row.GuildID, - "pinned": boolString(row.Pinned), - "reply_to": row.ReplyToMessage, - "source": row.Source, + "attachment_names": row.AttachmentNames, + "attachments": boolString(row.HasAttachments), + "author_id": row.AuthorID, + "channel_id": row.ChannelID, + "guild_id": row.GuildID, + "pinned": boolString(row.Pinned), + "reply_to": row.ReplyToMessage, + "source": row.Source, }, }) } return items } +func discordDetailContent(row store.MessageRow, content string) string { + var parts []string + if strings.TrimSpace(content) != "" { + parts = append(parts, strings.TrimSpace(content)) + } + if strings.TrimSpace(row.AttachmentText) != "" { + parts = append(parts, "Attachments\n"+strings.TrimSpace(row.AttachmentText)) + } + if len(parts) == 0 { + return "" + } + return strings.Join(parts, "\n\n") +} + func discordDisplayContent(row store.MessageRow) string { if content := strings.TrimSpace(row.DisplayContent); content != "" { return content diff --git a/internal/store/mentions_test.go b/internal/store/mentions_test.go index f45a180..2905528 100644 --- a/internal/store/mentions_test.go +++ b/internal/store/mentions_test.go @@ -92,6 +92,8 @@ func TestAttachmentTextAndMentionsAreQueryable(t *testing.T) { require.NoError(t, err) require.Len(t, messages, 1) require.Contains(t, messages[0].Content, "stack trace") + require.Equal(t, "trace.txt", messages[0].AttachmentNames) + require.Contains(t, messages[0].AttachmentText, "stack trace line one") mentions, err := s.ListMentions(ctx, MentionListOptions{Target: "Shadow", Limit: 10}) require.NoError(t, err) diff --git a/internal/store/messages.go b/internal/store/messages.go index 863931f..6210eb6 100644 --- a/internal/store/messages.go +++ b/internal/store/messages.go @@ -35,20 +35,22 @@ type MentionListOptions struct { } type MessageRow struct { - MessageID string `json:"message_id"` - GuildID string `json:"guild_id"` - GuildName string `json:"guild_name,omitempty"` - ChannelID string `json:"channel_id"` - ChannelName string `json:"channel_name"` - AuthorID string `json:"author_id"` - AuthorName string `json:"author_name"` - Content string `json:"content"` - DisplayContent string `json:"display_content,omitempty"` - CreatedAt time.Time `json:"created_at"` - ReplyToMessage string `json:"reply_to_message_id,omitempty"` - Source string `json:"source,omitempty"` - HasAttachments bool `json:"has_attachments"` - Pinned bool `json:"pinned"` + MessageID string `json:"message_id"` + GuildID string `json:"guild_id"` + GuildName string `json:"guild_name,omitempty"` + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name"` + AuthorID string `json:"author_id"` + AuthorName string `json:"author_name"` + Content string `json:"content"` + DisplayContent string `json:"display_content,omitempty"` + CreatedAt time.Time `json:"created_at"` + ReplyToMessage string `json:"reply_to_message_id,omitempty"` + Source string `json:"source,omitempty"` + HasAttachments bool `json:"has_attachments"` + AttachmentNames string `json:"attachment_names,omitempty"` + AttachmentText string `json:"attachment_text,omitempty"` + Pinned bool `json:"pinned"` } func (s *Store) ListMessages(ctx context.Context, opts MessageListOptions) ([]MessageRow, error) { @@ -105,6 +107,8 @@ func (s *Store) ListMessages(ctx context.Context, opts MessageListOptions) ([]Me coalesce(m.reply_to_message_id, ''), coalesce(json_extract(m.raw_json, '$.source'), ''), m.has_attachments, + coalesce((select group_concat(a.filename, ', ') from message_attachments a where a.message_id = m.id), ''), + coalesce((select group_concat(a.text_content, char(10)) from message_attachments a where a.message_id = m.id and trim(a.text_content) <> ''), ''), m.pinned from messages m left join guilds g on g.id = m.guild_id @@ -161,6 +165,8 @@ func (s *Store) ListMessages(ctx context.Context, opts MessageListOptions) ([]Me &row.ReplyToMessage, &row.Source, &hasAttachments, + &row.AttachmentNames, + &row.AttachmentText, &pinned, ); err != nil { return nil, err