fix(tui): toggle age sort direction
This commit is contained in:
parent
ce8e8ed436
commit
d722b934e6
@ -976,7 +976,7 @@ func (a *App) runClusterList(ctx context.Context, command string, args []string,
|
||||
fs.SetOutput(io.Discard)
|
||||
minSizeRaw := fs.String("min-size", "", "minimum active member count")
|
||||
limitRaw := fs.String("limit", "", "maximum cluster rows")
|
||||
sortMode := fs.String("sort", "size", "sort mode: recent|size")
|
||||
sortMode := fs.String("sort", "size", "sort mode: recent|oldest|size")
|
||||
includeClosed := fs.Bool("include-closed", false, "deprecated; clusters include closed rows by default")
|
||||
hideClosed := fs.Bool("hide-closed", false, "hide locally closed clusters")
|
||||
jsonOut := fs.Bool("json", false, "write JSON output")
|
||||
@ -1000,7 +1000,7 @@ func (a *App) runClusterList(ctx context.Context, command string, args []string,
|
||||
return usageErr(err)
|
||||
}
|
||||
sort := strings.TrimSpace(*sortMode)
|
||||
if sort != "recent" && sort != "size" {
|
||||
if sort != "recent" && sort != "oldest" && sort != "size" {
|
||||
return usageErr(fmt.Errorf("unsupported sort %q", sort))
|
||||
}
|
||||
|
||||
@ -1040,7 +1040,7 @@ func (a *App) runTUI(ctx context.Context, args []string) error {
|
||||
fs.SetOutput(io.Discard)
|
||||
minSizeRaw := fs.String("min-size", "", "minimum active member count")
|
||||
limitRaw := fs.String("limit", "", "maximum cluster rows")
|
||||
sortMode := fs.String("sort", "", "sort mode: recent|size")
|
||||
sortMode := fs.String("sort", "", "sort mode: recent|oldest|size")
|
||||
includeClosed := fs.Bool("include-closed", false, "deprecated; closed clusters are shown by default")
|
||||
hideClosed := fs.Bool("hide-closed", false, "hide locally closed clusters")
|
||||
jsonOut := fs.Bool("json", false, "write JSON output")
|
||||
@ -1090,7 +1090,7 @@ func (a *App) runTUI(ctx context.Context, args []string) error {
|
||||
if sort == "" {
|
||||
sort = "size"
|
||||
}
|
||||
if sort != "recent" && sort != "size" {
|
||||
if sort != "recent" && sort != "oldest" && sort != "size" {
|
||||
return usageErr(fmt.Errorf("unsupported sort %q", sort))
|
||||
}
|
||||
showClosed := !*hideClosed || *includeClosed
|
||||
@ -2707,7 +2707,7 @@ No API server is provided. There is intentionally no serve command.
|
||||
const tuiUsageText = `gitcrawl tui opens the local terminal cluster browser.
|
||||
|
||||
Usage:
|
||||
gitcrawl tui [owner/repo] [--limit N] [--min-size N] [--sort recent|size] [--hide-closed]
|
||||
gitcrawl tui [owner/repo] [--limit N] [--min-size N] [--sort recent|oldest|size] [--hide-closed]
|
||||
|
||||
If owner/repo is omitted, gitcrawl uses the most recently updated repository in the local database.
|
||||
The TUI starts with ghcrawl-style cluster display defaults: --min-size 5, --sort size, and closed historical clusters visible. Pass --min-size 1 for singleton clusters or --hide-closed to focus open-only.
|
||||
|
||||
@ -77,6 +77,7 @@ type tuiMemberSort string
|
||||
const (
|
||||
memberSortKind tuiMemberSort = "kind"
|
||||
memberSortRecent tuiMemberSort = "recent"
|
||||
memberSortOldest tuiMemberSort = "oldest"
|
||||
memberSortNumber tuiMemberSort = "number"
|
||||
memberSortState tuiMemberSort = "state"
|
||||
memberSortTitle tuiMemberSort = "title"
|
||||
@ -1538,8 +1539,10 @@ func (m clusterBrowserModel) appendViewMenuItems(items *[]tuiMenuItem) {
|
||||
tuiMenuSection("View"),
|
||||
tuiMenuItem{label: "Sort clusters by size", action: "sort-size"},
|
||||
tuiMenuItem{label: "Sort clusters by recent", action: "sort-recent"},
|
||||
tuiMenuItem{label: "Sort clusters by oldest", action: "sort-oldest"},
|
||||
tuiMenuItem{label: "Member sort grouped", action: "member-sort-kind"},
|
||||
tuiMenuItem{label: "Member sort recent", action: "member-sort-recent"},
|
||||
tuiMenuItem{label: "Member sort oldest", action: "member-sort-oldest"},
|
||||
tuiMenuItem{label: "Filter clusters...", action: "filter"},
|
||||
}
|
||||
if strings.TrimSpace(m.search) != "" {
|
||||
@ -1678,6 +1681,12 @@ func (m *clusterBrowserModel) runMenuItem(item tuiMenuItem) bool {
|
||||
m.loadSelectedCluster()
|
||||
m.status = "Sort: recent"
|
||||
return true
|
||||
case "sort-oldest":
|
||||
m.payload.Sort = "oldest"
|
||||
m.sortClusters()
|
||||
m.loadSelectedCluster()
|
||||
m.status = "Sort: oldest"
|
||||
return true
|
||||
case "member-sort-kind":
|
||||
m.memberSort = memberSortKind
|
||||
m.sortMembers()
|
||||
@ -1688,6 +1697,11 @@ func (m *clusterBrowserModel) runMenuItem(item tuiMenuItem) bool {
|
||||
m.sortMembers()
|
||||
m.status = "Member sort: recent"
|
||||
return true
|
||||
case "member-sort-oldest":
|
||||
m.memberSort = memberSortOldest
|
||||
m.sortMembers()
|
||||
m.status = "Member sort: oldest"
|
||||
return true
|
||||
case "refresh":
|
||||
m.refreshFromStore()
|
||||
return true
|
||||
@ -2684,7 +2698,10 @@ func clusterColumns(width int, sortMode string) []table.Column {
|
||||
cntTitle = "cnt*"
|
||||
}
|
||||
if sortMode == "recent" {
|
||||
ageTitle = "age*"
|
||||
ageTitle = "age-"
|
||||
}
|
||||
if sortMode == "oldest" {
|
||||
ageTitle = "age+"
|
||||
}
|
||||
return []table.Column{
|
||||
{Title: "id", Width: idW},
|
||||
@ -2715,7 +2732,10 @@ func memberColumns(width int, sortMode tuiMemberSort) []table.Column {
|
||||
stateTitle = "st*"
|
||||
}
|
||||
if sortMode == memberSortRecent {
|
||||
ageTitle = "age*"
|
||||
ageTitle = "age-"
|
||||
}
|
||||
if sortMode == memberSortOldest {
|
||||
ageTitle = "age+"
|
||||
}
|
||||
if sortMode == memberSortTitle {
|
||||
titleTitle = "title*"
|
||||
@ -2788,6 +2808,14 @@ func (m *clusterBrowserModel) sortClusters() {
|
||||
return left.MemberCount > right.MemberCount
|
||||
}
|
||||
}
|
||||
if m.payload.Sort == "oldest" {
|
||||
leftUpdated := parseTime(left.UpdatedAt)
|
||||
rightUpdated := parseTime(right.UpdatedAt)
|
||||
if !leftUpdated.Equal(rightUpdated) {
|
||||
return leftUpdated.Before(rightUpdated)
|
||||
}
|
||||
return left.ID < right.ID
|
||||
}
|
||||
return parseTime(left.UpdatedAt).After(parseTime(right.UpdatedAt))
|
||||
})
|
||||
m.selected = clampInt(m.selected, 0, maxInt(0, len(m.payload.Clusters)-1))
|
||||
@ -2798,7 +2826,11 @@ func (m *clusterBrowserModel) sortClustersFromHeader(relativeX int) {
|
||||
if relativeX < columnRightEdge(columns, 1) {
|
||||
m.payload.Sort = "size"
|
||||
} else if relativeX >= columnLeftEdge(columns, len(columns)-1) {
|
||||
m.payload.Sort = "recent"
|
||||
if m.payload.Sort == "recent" {
|
||||
m.payload.Sort = "oldest"
|
||||
} else {
|
||||
m.payload.Sort = "recent"
|
||||
}
|
||||
} else if m.payload.Sort == "recent" {
|
||||
m.payload.Sort = "size"
|
||||
} else {
|
||||
@ -3184,7 +3216,11 @@ func (m *clusterBrowserModel) sortMembersFromHeader(relativeX int) {
|
||||
case relativeX < columnRightEdge(columns, 1):
|
||||
m.memberSort = memberSortState
|
||||
case relativeX < columnRightEdge(columns, 2):
|
||||
m.memberSort = memberSortRecent
|
||||
if m.memberSort == memberSortRecent {
|
||||
m.memberSort = memberSortOldest
|
||||
} else {
|
||||
m.memberSort = memberSortRecent
|
||||
}
|
||||
default:
|
||||
if m.memberSort == memberSortTitle {
|
||||
m.memberSort = memberSortKind
|
||||
@ -3252,6 +3288,8 @@ func (m *clusterBrowserModel) sortMembers() {
|
||||
switch m.memberSort {
|
||||
case memberSortRecent:
|
||||
return parseTime(left.UpdatedAtGitHub).After(parseTime(right.UpdatedAtGitHub))
|
||||
case memberSortOldest:
|
||||
return parseTime(left.UpdatedAtGitHub).Before(parseTime(right.UpdatedAtGitHub))
|
||||
case memberSortNumber:
|
||||
return left.Number < right.Number
|
||||
case memberSortState:
|
||||
@ -3724,7 +3762,7 @@ func nextFocus(current tuiFocus, delta int) tuiFocus {
|
||||
}
|
||||
|
||||
func nextMemberSort(current tuiMemberSort) tuiMemberSort {
|
||||
order := []tuiMemberSort{memberSortKind, memberSortRecent, memberSortNumber, memberSortState, memberSortTitle}
|
||||
order := []tuiMemberSort{memberSortKind, memberSortRecent, memberSortOldest, memberSortNumber, memberSortState, memberSortTitle}
|
||||
for index, item := range order {
|
||||
if item == current {
|
||||
return order[(index+1)%len(order)]
|
||||
|
||||
@ -526,6 +526,70 @@ func TestTUIMouseHeaderSortsClusterRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIClusterAgeHeaderTogglesDirection(t *testing.T) {
|
||||
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
|
||||
Repository: "openclaw/openclaw",
|
||||
Sort: "recent",
|
||||
Clusters: sampleTUIClusters(),
|
||||
})
|
||||
model.width = 140
|
||||
model.height = 32
|
||||
columns := clusterColumns(maxInt(24, model.layout().clusters.w-4), model.payload.Sort)
|
||||
ageX := columnLeftEdge(columns, len(columns)-1)
|
||||
|
||||
model.sortClustersFromHeader(ageX)
|
||||
if model.payload.Sort != "oldest" {
|
||||
t.Fatalf("age header sort = %q, want oldest", model.payload.Sort)
|
||||
}
|
||||
if model.payload.Clusters[0].ID != 1 {
|
||||
t.Fatalf("oldest sort first cluster id = %d, want 1", model.payload.Clusters[0].ID)
|
||||
}
|
||||
|
||||
columns = clusterColumns(maxInt(24, model.layout().clusters.w-4), model.payload.Sort)
|
||||
model.sortClustersFromHeader(columnLeftEdge(columns, len(columns)-1))
|
||||
if model.payload.Sort != "recent" {
|
||||
t.Fatalf("age header second sort = %q, want recent", model.payload.Sort)
|
||||
}
|
||||
if model.payload.Clusters[0].ID != 2 {
|
||||
t.Fatalf("recent sort first cluster id = %d, want 2", model.payload.Clusters[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIMemberAgeHeaderTogglesDirection(t *testing.T) {
|
||||
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
|
||||
Repository: "openclaw/openclaw",
|
||||
Sort: "recent",
|
||||
Clusters: sampleTUIClusters(),
|
||||
})
|
||||
model.width = 140
|
||||
model.height = 32
|
||||
model.detail = store.ClusterDetail{Cluster: sampleTUIClusters()[0], Members: []store.ClusterMemberDetail{
|
||||
{Thread: store.Thread{ID: 1, Number: 10, Kind: "issue", State: "open", Title: "Older", UpdatedAtGitHub: "2026-04-27T10:00:00Z"}},
|
||||
{Thread: store.Thread{ID: 2, Number: 11, Kind: "issue", State: "open", Title: "Newer", UpdatedAtGitHub: "2026-04-27T11:00:00Z"}},
|
||||
}}
|
||||
model.hasDetail = true
|
||||
model.sortMembers()
|
||||
columns := memberColumns(maxInt(24, model.layout().members.w-4), model.memberSort)
|
||||
ageX := columnLeftEdge(columns, 2)
|
||||
|
||||
model.sortMembersFromHeader(ageX)
|
||||
if model.memberSort != memberSortRecent {
|
||||
t.Fatalf("member age header sort = %q, want recent", model.memberSort)
|
||||
}
|
||||
if model.memberRows[0].member.Thread.ID != 2 {
|
||||
t.Fatalf("recent member first id = %d, want 2", model.memberRows[0].member.Thread.ID)
|
||||
}
|
||||
|
||||
columns = memberColumns(maxInt(24, model.layout().members.w-4), model.memberSort)
|
||||
model.sortMembersFromHeader(columnLeftEdge(columns, 2))
|
||||
if model.memberSort != memberSortOldest {
|
||||
t.Fatalf("member age header second sort = %q, want oldest", model.memberSort)
|
||||
}
|
||||
if model.memberRows[0].member.Thread.ID != 1 {
|
||||
t.Fatalf("oldest member first id = %d, want 1", model.memberRows[0].member.Thread.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIClusterRowsShowClusterIDs(t *testing.T) {
|
||||
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
|
||||
Repository: "openclaw/openclaw",
|
||||
|
||||
@ -133,6 +133,8 @@ func (s *Store) ListRunClusterSummaries(ctx context.Context, options ClusterSumm
|
||||
orderBy := `latest_updated_at desc, c.id desc`
|
||||
if options.Sort == "size" {
|
||||
orderBy = `c.member_count desc, c.id asc`
|
||||
} else if options.Sort == "oldest" {
|
||||
orderBy = `latest_updated_at asc, c.id asc`
|
||||
}
|
||||
limit := options.Limit
|
||||
if limit <= 0 {
|
||||
@ -212,6 +214,8 @@ func (s *Store) listDurableClusterSummaries(ctx context.Context, options Cluster
|
||||
orderBy := `coalesce(cg.updated_at, '') desc, cg.id desc`
|
||||
if options.Sort == "size" {
|
||||
orderBy = `member_count desc, cg.id asc`
|
||||
} else if options.Sort == "oldest" {
|
||||
orderBy = `coalesce(cg.updated_at, '') asc, cg.id asc`
|
||||
}
|
||||
limit := options.Limit
|
||||
if limit <= 0 {
|
||||
@ -377,6 +381,15 @@ func sortClusterSummaries(clusters []ClusterSummary, sortMode string) {
|
||||
}
|
||||
return left.ID < right.ID
|
||||
}
|
||||
if sortMode == "oldest" {
|
||||
if left.UpdatedAt != right.UpdatedAt {
|
||||
return left.UpdatedAt < right.UpdatedAt
|
||||
}
|
||||
if left.MemberCount != right.MemberCount {
|
||||
return left.MemberCount > right.MemberCount
|
||||
}
|
||||
return left.ID < right.ID
|
||||
}
|
||||
if left.UpdatedAt != right.UpdatedAt {
|
||||
return left.UpdatedAt > right.UpdatedAt
|
||||
}
|
||||
|
||||
@ -63,6 +63,19 @@ func TestListClusterSummaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortClusterSummariesOldest(t *testing.T) {
|
||||
clusters := []ClusterSummary{
|
||||
{ID: 2, MemberCount: 1, UpdatedAt: "2026-04-27T11:00:00Z"},
|
||||
{ID: 1, MemberCount: 5, UpdatedAt: "2026-04-27T10:00:00Z"},
|
||||
}
|
||||
|
||||
sortClusterSummaries(clusters, "oldest")
|
||||
|
||||
if clusters[0].ID != 1 || clusters[1].ID != 2 {
|
||||
t.Fatalf("oldest sort order = %d,%d; want 1,2", clusters[0].ID, clusters[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurableClusterSummariesUsePrimaryOpenMembers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st, err := Open(ctx, filepath.Join(t.TempDir(), "gitcrawl.db"))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user