diff --git a/tui/tui.go b/tui/tui.go index 14dd692..76262aa 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -3374,6 +3374,8 @@ func (m model) memberTableRows(columns []tableColumn, members []int) []tableRow row = append(row, rowWhere(item)) case "author": row = append(row, itemAuthor(item)) + case "relation": + row = append(row, chatRelationForColumn(item, column.Width)) default: row = append(row, title) } @@ -3516,11 +3518,13 @@ func (m model) memberColumns(width int) []tableColumn { {Key: "title", Title: activeLabel("title", active == sortTitle), Width: titleW}, } } - authorW := minInt(maxInt(5, width/6), 9) - titleW := maxInt(1, width-whenW-ageW-authorW-3) + relationW := 3 + authorW := minInt(maxInt(5, width/7), 9) + titleW := maxInt(1, width-whenW-ageW-relationW-authorW-4) return []tableColumn{ {Key: "time", Title: activeTimeLabel(m.memberTimeLabel(), active), Width: whenW}, {Key: "age", Title: activeTimeLabel("age", active), Width: ageW}, + {Key: "relation", Title: "rel", Width: relationW}, {Key: "author", Title: activeLabel("who", active == sortAuthor), Width: authorW}, {Key: "title", Title: activeLabel("title", active == sortTitle), Width: titleW}, } @@ -3540,10 +3544,12 @@ func (m model) memberColumns(width int) []tableColumn { } } authorW := minInt(maxInt(8, width/7), 18) - titleW := maxInt(1, width-whenW-ageW-authorW-3) + relationW := 5 + titleW := maxInt(1, width-whenW-ageW-relationW-authorW-4) return []tableColumn{ {Key: "time", Title: activeTimeLabel("time", active), Width: whenW}, {Key: "age", Title: activeTimeLabel("age", active), Width: ageW}, + {Key: "relation", Title: "type", Width: relationW}, {Key: "author", Title: activeLabel("who", active == sortAuthor), Width: authorW}, {Key: "title", Title: activeLabel("title", active == sortTitle), Width: titleW}, } @@ -3831,6 +3837,28 @@ func chatReplyCountLabel(item Item) string { return value + " replies" } +func chatRelationForColumn(item Item, width int) string { + label := "msg" + if strings.TrimSpace(item.ParentID) != "" { + label = "reply" + } else if chatReplyCountLabel(item) != "" { + label = "thread" + } else if thread := strings.TrimSpace(fieldValue(item, "thread", "reply_to")); thread != "" && thread != strings.TrimSpace(item.ID) && thread != strings.TrimSpace(fieldValue(item, "ts")) { + label = "thread" + } + if width <= 3 { + switch label { + case "reply": + return "rep" + case "thread": + return "thr" + default: + return "msg" + } + } + return label +} + func chatThreadLabel(item Item) string { parent := strings.TrimSpace(item.ParentID) thread := strings.TrimSpace(fieldValue(item, "thread", "reply_to")) diff --git a/tui/tui_test.go b/tui/tui_test.go index cd4eec7..ff95572 100644 --- a/tui/tui_test.go +++ b/tui/tui_test.go @@ -10,6 +10,7 @@ import ( "testing" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) func stripANSI(value string) string { @@ -766,6 +767,30 @@ func TestChatMembersDefaultToNewestFirstLikeGitcrawl(t *testing.T) { } } +func TestChatMemberColumnsExposeThreadState(t *testing.T) { + m := newModel(Options{ + Title: "discrawl archive", + Layout: LayoutChat, + Items: []Item{ + Row{Kind: "message", ID: "root", Container: "general", Author: "alice", Title: "root", CreatedAt: "2026-05-01T10:00:00Z", Fields: map[string]string{"reply_count": "2"}}.ItemForLayout(LayoutChat), + Row{Kind: "message", ID: "reply", ParentID: "root", Container: "general", Author: "alice", Title: "reply", CreatedAt: "2026-05-01T10:01:00Z"}.ItemForLayout(LayoutChat), + }, + }) + columns := m.memberColumns(52) + header := stripANSI(renderTableHeader(columns, 52, "#9bc53d")) + if !strings.Contains(header, "rel") { + t.Fatalf("chat member header should expose relation column:\n%s", header) + } + rows := m.memberTableRows(columns, m.currentGroupMembers()) + rendered := stripANSI(strings.Join([]string{ + renderTableRow(columns, rows[0], 52, lipgloss.NewStyle()), + renderTableRow(columns, rows[1], 52, lipgloss.NewStyle()), + }, "\n")) + if !strings.Contains(rendered, "thr") || !strings.Contains(rendered, "rep") { + t.Fatalf("chat member rows should show thread/reply state:\n%s", rendered) + } +} + func TestChatGroupScopeSortUsesScopeNotContainer(t *testing.T) { m := newModel(Options{ Title: "slacrawl archive", @@ -1928,9 +1953,14 @@ func TestClickingContextHeaderUsesContextPaneColumns(t *testing.T) { m.height = 24 layout := m.layout() contextWidth := paneContentWidth(layout.context.w) - whenW := minInt(maxInt(10, contextWidth/6), 16) - ageW := minInt(maxInt(4, contextWidth/16), 7) - authorX := whenW + 1 + ageW + 1 + columns := m.memberColumns(contextWidth) + authorX := 0 + for index, column := range columns { + if column.Key == "author" { + authorX = columnLeftEdge(columns, index) + break + } + } updated, _ := m.Update(tea.MouseMsg{ X: layout.context.x + 2 + authorX, Y: layout.context.y + 2,