Merge upstream/main into subtle-ux-improvements

Resolves conflicts and converts new defer/undefer commands from
fatih/color to the lipgloss semantic color system.

Key changes:
- Added StatusDeferred case in graph.go with ui.RenderAccent
- Converted status.go to use ui package for colorized output
- Converted defer.go/undefer.go to use ui package
- Merged GroupID and Aliases for status command
- Updated pre-commit hook version to 0.31.0
- Ran go mod tidy to remove fatih/color dependency
This commit is contained in:
Ryan Snodgrass
2025-12-20 17:22:43 -08:00
45 changed files with 850 additions and 316 deletions

View File

@@ -11,24 +11,15 @@ import (
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
)
// StatusOutput represents the complete status output
type StatusOutput struct {
Summary *StatusSummary `json:"summary"`
Summary *types.Statistics `json:"summary"`
RecentActivity *RecentActivitySummary `json:"recent_activity,omitempty"`
}
// StatusSummary represents counts by state
type StatusSummary struct {
TotalIssues int `json:"total_issues"`
OpenIssues int `json:"open_issues"`
InProgressIssues int `json:"in_progress_issues"`
BlockedIssues int `json:"blocked_issues"`
ClosedIssues int `json:"closed_issues"`
ReadyIssues int `json:"ready_issues"`
}
// RecentActivitySummary represents activity from git history
type RecentActivitySummary struct {
HoursTracked int `json:"hours_tracked"`
@@ -43,11 +34,13 @@ type RecentActivitySummary struct {
var statusCmd = &cobra.Command{
Use: "status",
GroupID: "views",
Short: "Show issue database overview",
Long: `Show a quick snapshot of the issue database state.
Aliases: []string{"stats"},
Short: "Show issue database overview and statistics",
Long: `Show a quick snapshot of the issue database state and statistics.
This command provides a summary of issue counts by state (open, in_progress,
blocked, closed), ready work, and recent activity over the last 24 hours from git history.
blocked, closed), ready work, extended statistics (tombstones, pinned issues,
average lead time), and recent activity over the last 24 hours from git history.
Similar to how 'git status' shows working tree state, 'bd status' gives you
a quick overview of your issue database without needing multiple queries.
@@ -59,13 +52,15 @@ Use cases:
- Daily standup reference
Examples:
bd status # Show summary
bd status # Show summary with activity
bd status --no-activity # Skip git activity (faster)
bd status --json # JSON format output
bd status --assigned # Show issues assigned to current user
bd status --all # Show all issues (same as default)`,
bd stats # Alias for bd status`,
Run: func(cmd *cobra.Command, args []string) {
showAll, _ := cmd.Flags().GetBool("all")
showAssigned, _ := cmd.Flags().GetBool("assigned")
noActivity, _ := cmd.Flags().GetBool("no-activity")
jsonFormat, _ := cmd.Flags().GetBool("json")
// Override global jsonOutput if --json flag is set
@@ -109,28 +104,23 @@ Examples:
}
}
// Build summary
summary := &StatusSummary{
TotalIssues: stats.TotalIssues,
OpenIssues: stats.OpenIssues,
InProgressIssues: stats.InProgressIssues,
BlockedIssues: stats.BlockedIssues,
ClosedIssues: stats.ClosedIssues,
ReadyIssues: stats.ReadyIssues,
// Filter by assignee if requested (overrides stats with filtered counts)
if showAssigned {
stats = getAssignedStatistics(actor)
if stats == nil {
fmt.Fprintf(os.Stderr, "Error: failed to get assigned statistics\n")
os.Exit(1)
}
}
// Get recent activity from git history (last 24 hours)
// Get recent activity from git history (last 24 hours) unless --no-activity
var recentActivity *RecentActivitySummary
recentActivity = getGitActivity(24)
// Filter by assignee if requested
if showAssigned {
// Get filtered statistics for assigned issues
summary = getAssignedStatus(actor)
if !noActivity {
recentActivity = getGitActivity(24)
}
output := &StatusOutput{
Summary: summary,
Summary: stats,
RecentActivity: recentActivity,
}
@@ -140,25 +130,43 @@ Examples:
return
}
// Human-readable output
fmt.Println("\nIssue Database Status")
fmt.Println("=====================")
fmt.Printf("\nSummary:\n")
fmt.Printf(" Total Issues: %d\n", summary.TotalIssues)
fmt.Printf(" Open: %d\n", summary.OpenIssues)
fmt.Printf(" In Progress: %d\n", summary.InProgressIssues)
fmt.Printf(" Blocked: %d\n", summary.BlockedIssues)
fmt.Printf(" Closed: %d\n", summary.ClosedIssues)
fmt.Printf(" Ready to Work: %d\n", summary.ReadyIssues)
// Human-readable colorized output using semantic ui package
fmt.Printf("\n%s Issue Database Status\n\n", ui.RenderAccent("📊"))
fmt.Printf("Summary:\n")
fmt.Printf(" Total Issues: %d\n", stats.TotalIssues)
fmt.Printf(" Open: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.OpenIssues)))
fmt.Printf(" In Progress: %s\n", ui.RenderWarn(fmt.Sprintf("%d", stats.InProgressIssues)))
fmt.Printf(" Blocked: %s\n", ui.RenderFail(fmt.Sprintf("%d", stats.BlockedIssues)))
fmt.Printf(" Closed: %d\n", stats.ClosedIssues)
fmt.Printf(" Ready to Work: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.ReadyIssues)))
// Extended statistics (only show if non-zero)
hasExtended := stats.TombstoneIssues > 0 || stats.PinnedIssues > 0 ||
stats.EpicsEligibleForClosure > 0 || stats.AverageLeadTime > 0
if hasExtended {
fmt.Printf("\nExtended:\n")
if stats.TombstoneIssues > 0 {
fmt.Printf(" Deleted: %d (tombstones)\n", stats.TombstoneIssues)
}
if stats.PinnedIssues > 0 {
fmt.Printf(" Pinned: %d\n", stats.PinnedIssues)
}
if stats.EpicsEligibleForClosure > 0 {
fmt.Printf(" Epics Ready to Close: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.EpicsEligibleForClosure)))
}
if stats.AverageLeadTime > 0 {
fmt.Printf(" Avg Lead Time: %.1f hours\n", stats.AverageLeadTime)
}
}
if recentActivity != nil {
fmt.Printf("\nRecent Activity (last %d hours, from git history):\n", recentActivity.HoursTracked)
fmt.Printf(" Commits: %d\n", recentActivity.CommitCount)
fmt.Printf(" Total Changes: %d\n", recentActivity.TotalChanges)
fmt.Printf(" Issues Created: %d\n", recentActivity.IssuesCreated)
fmt.Printf(" Issues Closed: %d\n", recentActivity.IssuesClosed)
fmt.Printf(" Issues Reopened: %d\n", recentActivity.IssuesReopened)
fmt.Printf(" Issues Updated: %d\n", recentActivity.IssuesUpdated)
fmt.Printf("\nRecent Activity (last %d hours):\n", recentActivity.HoursTracked)
fmt.Printf(" Commits: %d\n", recentActivity.CommitCount)
fmt.Printf(" Total Changes: %d\n", recentActivity.TotalChanges)
fmt.Printf(" Issues Created: %d\n", recentActivity.IssuesCreated)
fmt.Printf(" Issues Closed: %d\n", recentActivity.IssuesClosed)
fmt.Printf(" Issues Reopened: %d\n", recentActivity.IssuesReopened)
fmt.Printf(" Issues Updated: %d\n", recentActivity.IssuesUpdated)
}
// Show hint for more details
@@ -268,8 +276,8 @@ func getGitActivity(hours int) *RecentActivitySummary {
return activity
}
// getAssignedStatus returns status summary for issues assigned to a specific user
func getAssignedStatus(assignee string) *StatusSummary {
// getAssignedStatistics returns statistics for issues assigned to a specific user
func getAssignedStatistics(assignee string) *types.Statistics {
if store == nil {
return nil
}
@@ -287,7 +295,7 @@ func getAssignedStatus(assignee string) *StatusSummary {
return nil
}
summary := &StatusSummary{
stats := &types.Statistics{
TotalIssues: len(issues),
}
@@ -295,13 +303,15 @@ func getAssignedStatus(assignee string) *StatusSummary {
for _, issue := range issues {
switch issue.Status {
case types.StatusOpen:
summary.OpenIssues++
stats.OpenIssues++
case types.StatusInProgress:
summary.InProgressIssues++
stats.InProgressIssues++
case types.StatusBlocked:
summary.BlockedIssues++
stats.BlockedIssues++
case types.StatusDeferred:
stats.DeferredIssues++
case types.StatusClosed:
summary.ClosedIssues++
stats.ClosedIssues++
}
}
@@ -311,15 +321,16 @@ func getAssignedStatus(assignee string) *StatusSummary {
}
readyIssues, err := store.GetReadyWork(ctx, readyFilter)
if err == nil {
summary.ReadyIssues = len(readyIssues)
stats.ReadyIssues = len(readyIssues)
}
return summary
return stats
}
func init() {
statusCmd.Flags().Bool("all", false, "Show all issues (default behavior)")
statusCmd.Flags().Bool("assigned", false, "Show issues assigned to current user")
statusCmd.Flags().Bool("no-activity", false, "Skip git activity tracking (faster)")
// Note: --json flag is defined as a persistent flag in main.go, not here
rootCmd.AddCommand(statusCmd)
}