feat(tui): show chat thread state column

This commit is contained in:
Vincent Koc 2026-05-04 00:56:02 -07:00
parent eb3235c351
commit 1ccc241eb9
No known key found for this signature in database
2 changed files with 64 additions and 6 deletions

View File

@ -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"))

View File

@ -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,