feat(ux): visual improvements for list tree, graph, and show commands
bd list --tree: - Use actual parent-child dependencies instead of dotted ID hierarchy - Treat epic dependencies as parent-child relationships - Sort children by priority (P0 first) - Fix tree display in daemon mode with read-only store access bd graph: - Add --all flag to show dependency graph of all open issues - Add --compact flag for tree-style rendering (reduces 44+ lines to 13) - Fix "needs:N" cognitive noise by using semantic colors - Add blocks:N indicator with semantic red coloring bd show: - Tufte-aligned header with status icon, priority, and type badges - Add glamour markdown rendering with auto light/dark mode detection - Cap markdown line width at 100 chars for readability - Mute entire row for closed dependencies (work done, no attention needed) Design system: - Add shared status icons (○ ◐ ● ✓ ❄) with semantic colors - Implement priority colors: P0 red, P1 orange, P2 muted gold, P3-P4 neutral - Add TrueColor profile for distinct hex color rendering - Type badges for epic (purple) and bug (red) Design principles: - Semantic colors only for actionable items - Closed items fade (muted gray) - Icons > text labels for better scanability Co-Authored-By: SageOx <ox@sageox.ai>
This commit is contained in:
405
cmd/bd/show.go
405
cmd/bd/show.go
@@ -131,14 +131,14 @@ var showCmd = &cobra.Command{
|
||||
allDetails = append(allDetails, details)
|
||||
} else {
|
||||
if displayIdx > 0 {
|
||||
fmt.Println("\n" + strings.Repeat("─", 60))
|
||||
fmt.Println("\n" + ui.RenderMuted(strings.Repeat("─", 60)))
|
||||
}
|
||||
fmt.Printf("\n%s: %s\n", ui.RenderAccent(issue.ID), issue.Title)
|
||||
fmt.Printf("Status: %s\n", issue.Status)
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
// Tufte-aligned header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||
fmt.Printf("\n%s\n", formatIssueHeader(issue))
|
||||
// Metadata: Owner · Type | Created · Updated
|
||||
fmt.Println(formatIssueMetadata(issue))
|
||||
if issue.Description != "" {
|
||||
fmt.Printf("\nDescription:\n%s\n", issue.Description)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("DESCRIPTION"), ui.RenderMarkdown(issue.Description))
|
||||
}
|
||||
fmt.Println()
|
||||
displayIdx++
|
||||
@@ -188,54 +188,17 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if displayIdx > 0 {
|
||||
fmt.Println("\n" + strings.Repeat("─", 60))
|
||||
fmt.Println("\n" + ui.RenderMuted(strings.Repeat("─", 60)))
|
||||
}
|
||||
displayIdx++
|
||||
|
||||
// Format output (same as direct mode below)
|
||||
tierEmoji := ""
|
||||
statusSuffix := ""
|
||||
switch issue.CompactionLevel {
|
||||
case 1:
|
||||
tierEmoji = " 🗜️"
|
||||
statusSuffix = " (compacted L1)"
|
||||
case 2:
|
||||
tierEmoji = " 📦"
|
||||
statusSuffix = " (compacted L2)"
|
||||
}
|
||||
// Tufte-aligned header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||
fmt.Printf("\n%s\n", formatIssueHeader(issue))
|
||||
|
||||
fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
|
||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
}
|
||||
if issue.ClosedBySession != "" {
|
||||
fmt.Printf("Closed by session: %s\n", issue.ClosedBySession)
|
||||
}
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
fmt.Printf("Assignee: %s\n", issue.Assignee)
|
||||
}
|
||||
if issue.EstimatedMinutes != nil {
|
||||
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
|
||||
}
|
||||
fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
|
||||
if issue.CreatedBy != "" {
|
||||
fmt.Printf("Created by: %s\n", issue.CreatedBy)
|
||||
}
|
||||
fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))
|
||||
if issue.DueAt != nil {
|
||||
fmt.Printf("Due: %s\n", issue.DueAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
if issue.DeferUntil != nil {
|
||||
fmt.Printf("Deferred until: %s\n", issue.DeferUntil.Format("2006-01-02 15:04"))
|
||||
}
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
fmt.Printf("External Ref: %s\n", *issue.ExternalRef)
|
||||
}
|
||||
// Metadata: Owner · Type | Created · Updated
|
||||
fmt.Println(formatIssueMetadata(issue))
|
||||
|
||||
// Show compaction status
|
||||
// Compaction info (if applicable)
|
||||
if issue.CompactionLevel > 0 {
|
||||
fmt.Println()
|
||||
if issue.OriginalSize > 0 {
|
||||
@@ -243,47 +206,40 @@ var showCmd = &cobra.Command{
|
||||
saved := issue.OriginalSize - currentSize
|
||||
if saved > 0 {
|
||||
reduction := float64(saved) / float64(issue.OriginalSize) * 100
|
||||
fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
|
||||
fmt.Printf("📊 %d → %d bytes (%.0f%% reduction)\n",
|
||||
issue.OriginalSize, currentSize, reduction)
|
||||
}
|
||||
}
|
||||
tierEmoji2 := "🗜️"
|
||||
if issue.CompactionLevel == 2 {
|
||||
tierEmoji2 = "📦"
|
||||
}
|
||||
compactedDate := ""
|
||||
if issue.CompactedAt != nil {
|
||||
compactedDate = issue.CompactedAt.Format("2006-01-02")
|
||||
}
|
||||
fmt.Printf("%s Compacted: %s (Tier %d)\n", tierEmoji2, compactedDate, issue.CompactionLevel)
|
||||
}
|
||||
|
||||
// Content sections
|
||||
if issue.Description != "" {
|
||||
fmt.Printf("\nDescription:\n%s\n", issue.Description)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("DESCRIPTION"), ui.RenderMarkdown(issue.Description))
|
||||
}
|
||||
if issue.Design != "" {
|
||||
fmt.Printf("\nDesign:\n%s\n", issue.Design)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("DESIGN"), ui.RenderMarkdown(issue.Design))
|
||||
}
|
||||
if issue.Notes != "" {
|
||||
fmt.Printf("\nNotes:\n%s\n", issue.Notes)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("NOTES"), ui.RenderMarkdown(issue.Notes))
|
||||
}
|
||||
if issue.AcceptanceCriteria != "" {
|
||||
fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("ACCEPTANCE CRITERIA"), ui.RenderMarkdown(issue.AcceptanceCriteria))
|
||||
}
|
||||
|
||||
if len(details.Labels) > 0 {
|
||||
fmt.Printf("\nLabels: %v\n", details.Labels)
|
||||
fmt.Printf("\n%s %s\n", ui.RenderBold("LABELS:"), strings.Join(details.Labels, ", "))
|
||||
}
|
||||
|
||||
// Dependencies with semantic colors
|
||||
if len(details.Dependencies) > 0 {
|
||||
fmt.Printf("\nDepends on (%d):\n", len(details.Dependencies))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("DEPENDS ON"))
|
||||
for _, dep := range details.Dependencies {
|
||||
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
||||
fmt.Println(formatDependencyLine("→", dep))
|
||||
}
|
||||
}
|
||||
|
||||
// Dependents grouped by type with semantic colors
|
||||
if len(details.Dependents) > 0 {
|
||||
// Group by dependency type for clarity
|
||||
var blocks, children, related, discovered []*types.IssueWithDependencyMetadata
|
||||
for _, dep := range details.Dependents {
|
||||
switch dep.DependencyType {
|
||||
@@ -301,35 +257,35 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if len(children) > 0 {
|
||||
fmt.Printf("\nChildren (%d):\n", len(children))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("CHILDREN"))
|
||||
for _, dep := range children {
|
||||
fmt.Printf(" ↳ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("↳", dep))
|
||||
}
|
||||
}
|
||||
if len(blocks) > 0 {
|
||||
fmt.Printf("\nBlocks (%d):\n", len(blocks))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("BLOCKS"))
|
||||
for _, dep := range blocks {
|
||||
fmt.Printf(" ← %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("←", dep))
|
||||
}
|
||||
}
|
||||
if len(related) > 0 {
|
||||
fmt.Printf("\nRelated (%d):\n", len(related))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("RELATED"))
|
||||
for _, dep := range related {
|
||||
fmt.Printf(" ↔ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("↔", dep))
|
||||
}
|
||||
}
|
||||
if len(discovered) > 0 {
|
||||
fmt.Printf("\nDiscovered (%d):\n", len(discovered))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("DISCOVERED"))
|
||||
for _, dep := range discovered {
|
||||
fmt.Printf(" ◊ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("◊", dep))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(details.Comments) > 0 {
|
||||
fmt.Printf("\nComments (%d):\n", len(details.Comments))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("COMMENTS"))
|
||||
for _, comment := range details.Comments {
|
||||
fmt.Printf(" [%s] %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"))
|
||||
fmt.Printf(" %s %s\n", ui.RenderMuted(comment.CreatedAt.Format("2006-01-02")), comment.Author)
|
||||
commentLines := strings.Split(comment.Text, "\n")
|
||||
for _, line := range commentLines {
|
||||
fmt.Printf(" %s\n", line)
|
||||
@@ -418,102 +374,55 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if idx > 0 {
|
||||
fmt.Println("\n" + strings.Repeat("─", 60))
|
||||
fmt.Println("\n" + ui.RenderMuted(strings.Repeat("─", 60)))
|
||||
}
|
||||
|
||||
// Add compaction emoji to title line
|
||||
tierEmoji := ""
|
||||
statusSuffix := ""
|
||||
switch issue.CompactionLevel {
|
||||
case 1:
|
||||
tierEmoji = " 🗜️"
|
||||
statusSuffix = " (compacted L1)"
|
||||
case 2:
|
||||
tierEmoji = " 📦"
|
||||
statusSuffix = " (compacted L2)"
|
||||
}
|
||||
// Tufte-aligned header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||
fmt.Printf("\n%s\n", formatIssueHeader(issue))
|
||||
|
||||
fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
|
||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
}
|
||||
if issue.ClosedBySession != "" {
|
||||
fmt.Printf("Closed by session: %s\n", issue.ClosedBySession)
|
||||
}
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
fmt.Printf("Assignee: %s\n", issue.Assignee)
|
||||
}
|
||||
if issue.EstimatedMinutes != nil {
|
||||
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
|
||||
}
|
||||
fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
|
||||
if issue.CreatedBy != "" {
|
||||
fmt.Printf("Created by: %s\n", issue.CreatedBy)
|
||||
}
|
||||
fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))
|
||||
if issue.DueAt != nil {
|
||||
fmt.Printf("Due: %s\n", issue.DueAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
if issue.DeferUntil != nil {
|
||||
fmt.Printf("Deferred until: %s\n", issue.DeferUntil.Format("2006-01-02 15:04"))
|
||||
}
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
fmt.Printf("External Ref: %s\n", *issue.ExternalRef)
|
||||
}
|
||||
// Metadata: Owner · Type | Created · Updated
|
||||
fmt.Println(formatIssueMetadata(issue))
|
||||
|
||||
// Show compaction status footer
|
||||
// Compaction info (if applicable)
|
||||
if issue.CompactionLevel > 0 {
|
||||
tierEmoji := "🗜️"
|
||||
if issue.CompactionLevel == 2 {
|
||||
tierEmoji = "📦"
|
||||
}
|
||||
tierName := fmt.Sprintf("Tier %d", issue.CompactionLevel)
|
||||
|
||||
fmt.Println()
|
||||
if issue.OriginalSize > 0 {
|
||||
currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
|
||||
saved := issue.OriginalSize - currentSize
|
||||
if saved > 0 {
|
||||
reduction := float64(saved) / float64(issue.OriginalSize) * 100
|
||||
fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
|
||||
fmt.Printf("📊 %d → %d bytes (%.0f%% reduction)\n",
|
||||
issue.OriginalSize, currentSize, reduction)
|
||||
}
|
||||
}
|
||||
compactedDate := ""
|
||||
if issue.CompactedAt != nil {
|
||||
compactedDate = issue.CompactedAt.Format("2006-01-02")
|
||||
}
|
||||
fmt.Printf("%s Compacted: %s (%s)\n", tierEmoji, compactedDate, tierName)
|
||||
}
|
||||
|
||||
// Content sections
|
||||
if issue.Description != "" {
|
||||
fmt.Printf("\nDescription:\n%s\n", issue.Description)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("DESCRIPTION"), ui.RenderMarkdown(issue.Description))
|
||||
}
|
||||
if issue.Design != "" {
|
||||
fmt.Printf("\nDesign:\n%s\n", issue.Design)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("DESIGN"), ui.RenderMarkdown(issue.Design))
|
||||
}
|
||||
if issue.Notes != "" {
|
||||
fmt.Printf("\nNotes:\n%s\n", issue.Notes)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("NOTES"), ui.RenderMarkdown(issue.Notes))
|
||||
}
|
||||
if issue.AcceptanceCriteria != "" {
|
||||
fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
|
||||
fmt.Printf("\n%s\n%s\n", ui.RenderBold("ACCEPTANCE CRITERIA"), ui.RenderMarkdown(issue.AcceptanceCriteria))
|
||||
}
|
||||
|
||||
// Show labels
|
||||
labels, _ := issueStore.GetLabels(ctx, issue.ID)
|
||||
if len(labels) > 0 {
|
||||
fmt.Printf("\nLabels: %v\n", labels)
|
||||
fmt.Printf("\n%s %s\n", ui.RenderBold("LABELS:"), strings.Join(labels, ", "))
|
||||
}
|
||||
|
||||
// Show dependencies
|
||||
// Show dependencies with semantic colors
|
||||
deps, _ := issueStore.GetDependencies(ctx, issue.ID)
|
||||
if len(deps) > 0 {
|
||||
fmt.Printf("\nDepends on (%d):\n", len(deps))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("DEPENDS ON"))
|
||||
for _, dep := range deps {
|
||||
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
||||
fmt.Println(formatSimpleDependencyLine("→", dep))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,27 +450,27 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if len(children) > 0 {
|
||||
fmt.Printf("\nChildren (%d):\n", len(children))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("CHILDREN"))
|
||||
for _, dep := range children {
|
||||
fmt.Printf(" ↳ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("↳", dep))
|
||||
}
|
||||
}
|
||||
if len(blocks) > 0 {
|
||||
fmt.Printf("\nBlocks (%d):\n", len(blocks))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("BLOCKS"))
|
||||
for _, dep := range blocks {
|
||||
fmt.Printf(" ← %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("←", dep))
|
||||
}
|
||||
}
|
||||
if len(related) > 0 {
|
||||
fmt.Printf("\nRelated (%d):\n", len(related))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("RELATED"))
|
||||
for _, dep := range related {
|
||||
fmt.Printf(" ↔ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("↔", dep))
|
||||
}
|
||||
}
|
||||
if len(discovered) > 0 {
|
||||
fmt.Printf("\nDiscovered (%d):\n", len(discovered))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("DISCOVERED"))
|
||||
for _, dep := range discovered {
|
||||
fmt.Printf(" ◊ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatDependencyLine("◊", dep))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,9 +478,9 @@ var showCmd = &cobra.Command{
|
||||
// Fallback for non-SQLite storage
|
||||
dependents, _ := issueStore.GetDependents(ctx, issue.ID)
|
||||
if len(dependents) > 0 {
|
||||
fmt.Printf("\nBlocks (%d):\n", len(dependents))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("BLOCKS"))
|
||||
for _, dep := range dependents {
|
||||
fmt.Printf(" ← %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
|
||||
fmt.Println(formatSimpleDependencyLine("←", dep))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,9 +488,13 @@ var showCmd = &cobra.Command{
|
||||
// Show comments
|
||||
comments, _ := issueStore.GetIssueComments(ctx, issue.ID)
|
||||
if len(comments) > 0 {
|
||||
fmt.Printf("\nComments (%d):\n", len(comments))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("COMMENTS"))
|
||||
for _, comment := range comments {
|
||||
fmt.Printf(" [%s at %s]\n %s\n\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"), comment.Text)
|
||||
fmt.Printf(" %s %s\n", ui.RenderMuted(comment.CreatedAt.Format("2006-01-02")), comment.Author)
|
||||
commentLines := strings.Split(comment.Text, "\n")
|
||||
for _, line := range commentLines {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,10 +518,178 @@ var showCmd = &cobra.Command{
|
||||
|
||||
|
||||
// formatShortIssue returns a compact one-line representation of an issue
|
||||
// Format: <id> [<status>] P<priority> <type>: <title>
|
||||
// Format: STATUS_ICON ID PRIORITY [Type] Title
|
||||
func formatShortIssue(issue *types.Issue) string {
|
||||
return fmt.Sprintf("%s [%s] P%d %s: %s",
|
||||
issue.ID, issue.Status, issue.Priority, issue.IssueType, issue.Title)
|
||||
statusIcon := ui.RenderStatusIcon(string(issue.Status))
|
||||
priorityTag := ui.RenderPriority(issue.Priority)
|
||||
|
||||
// Type badge only for notable types
|
||||
typeBadge := ""
|
||||
switch issue.IssueType {
|
||||
case "epic":
|
||||
typeBadge = ui.TypeEpicStyle.Render("[epic]") + " "
|
||||
case "bug":
|
||||
typeBadge = ui.TypeBugStyle.Render("[bug]") + " "
|
||||
}
|
||||
|
||||
// Closed issues: entire line is muted
|
||||
if issue.Status == types.StatusClosed {
|
||||
return fmt.Sprintf("%s %s %s %s%s",
|
||||
statusIcon,
|
||||
ui.RenderMuted(issue.ID),
|
||||
ui.RenderMuted(fmt.Sprintf("● P%d", issue.Priority)),
|
||||
ui.RenderMuted(string(issue.IssueType)),
|
||||
ui.RenderMuted(" "+issue.Title))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s %s %s%s", statusIcon, issue.ID, priorityTag, typeBadge, issue.Title)
|
||||
}
|
||||
|
||||
// formatIssueHeader returns the Tufte-aligned header line
|
||||
// Format: ID · Title [Priority · STATUS]
|
||||
// All elements in bd show get semantic colors since focus is on one issue
|
||||
func formatIssueHeader(issue *types.Issue) string {
|
||||
// Get status icon and style
|
||||
statusIcon := ui.RenderStatusIcon(string(issue.Status))
|
||||
statusStyle := ui.GetStatusStyle(string(issue.Status))
|
||||
statusStr := statusStyle.Render(strings.ToUpper(string(issue.Status)))
|
||||
|
||||
// Priority with semantic color (includes ● icon)
|
||||
priorityTag := ui.RenderPriority(issue.Priority)
|
||||
|
||||
// Type badge for notable types
|
||||
typeBadge := ""
|
||||
switch issue.IssueType {
|
||||
case "epic":
|
||||
typeBadge = " " + ui.TypeEpicStyle.Render("[EPIC]")
|
||||
case "bug":
|
||||
typeBadge = " " + ui.TypeBugStyle.Render("[BUG]")
|
||||
}
|
||||
|
||||
// Compaction indicator
|
||||
tierEmoji := ""
|
||||
switch issue.CompactionLevel {
|
||||
case 1:
|
||||
tierEmoji = " 🗜️"
|
||||
case 2:
|
||||
tierEmoji = " 📦"
|
||||
}
|
||||
|
||||
// Build header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||
idStyled := ui.RenderAccent(issue.ID)
|
||||
return fmt.Sprintf("%s %s%s · %s%s [%s · %s]",
|
||||
statusIcon, idStyled, typeBadge, issue.Title, tierEmoji, priorityTag, statusStr)
|
||||
}
|
||||
|
||||
// formatIssueMetadata returns the metadata line(s) with grouped info
|
||||
// Format: Owner: user · Type: task
|
||||
//
|
||||
// Created: 2026-01-06 · Updated: 2026-01-08
|
||||
func formatIssueMetadata(issue *types.Issue) string {
|
||||
var lines []string
|
||||
|
||||
// Line 1: Owner/Assignee · Type
|
||||
metaParts := []string{}
|
||||
if issue.CreatedBy != "" {
|
||||
metaParts = append(metaParts, fmt.Sprintf("Owner: %s", issue.CreatedBy))
|
||||
}
|
||||
if issue.Assignee != "" {
|
||||
metaParts = append(metaParts, fmt.Sprintf("Assignee: %s", issue.Assignee))
|
||||
}
|
||||
|
||||
// Type with semantic color
|
||||
typeStr := string(issue.IssueType)
|
||||
switch issue.IssueType {
|
||||
case "epic":
|
||||
typeStr = ui.TypeEpicStyle.Render("epic")
|
||||
case "bug":
|
||||
typeStr = ui.TypeBugStyle.Render("bug")
|
||||
}
|
||||
metaParts = append(metaParts, fmt.Sprintf("Type: %s", typeStr))
|
||||
|
||||
if len(metaParts) > 0 {
|
||||
lines = append(lines, strings.Join(metaParts, " · "))
|
||||
}
|
||||
|
||||
// Line 2: Created · Updated · Due/Defer
|
||||
timeParts := []string{}
|
||||
timeParts = append(timeParts, fmt.Sprintf("Created: %s", issue.CreatedAt.Format("2006-01-02")))
|
||||
timeParts = append(timeParts, fmt.Sprintf("Updated: %s", issue.UpdatedAt.Format("2006-01-02")))
|
||||
|
||||
if issue.DueAt != nil {
|
||||
timeParts = append(timeParts, fmt.Sprintf("Due: %s", issue.DueAt.Format("2006-01-02")))
|
||||
}
|
||||
if issue.DeferUntil != nil {
|
||||
timeParts = append(timeParts, fmt.Sprintf("Deferred: %s", issue.DeferUntil.Format("2006-01-02")))
|
||||
}
|
||||
if len(timeParts) > 0 {
|
||||
lines = append(lines, strings.Join(timeParts, " · "))
|
||||
}
|
||||
|
||||
// Line 3: Close reason (if closed)
|
||||
if issue.Status == types.StatusClosed && issue.CloseReason != "" {
|
||||
lines = append(lines, ui.RenderMuted(fmt.Sprintf("Close reason: %s", issue.CloseReason)))
|
||||
}
|
||||
|
||||
// Line 4: External ref (if exists)
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
lines = append(lines, fmt.Sprintf("External: %s", *issue.ExternalRef))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// formatDependencyLine formats a single dependency with semantic colors
|
||||
// Closed items get entire row muted - the work is done, no need for attention
|
||||
func formatDependencyLine(prefix string, dep *types.IssueWithDependencyMetadata) string {
|
||||
// Status icon (always rendered with semantic color)
|
||||
statusIcon := ui.GetStatusIcon(string(dep.Status))
|
||||
|
||||
// Closed items: mute entire row since the work is complete
|
||||
if dep.Status == types.StatusClosed {
|
||||
return fmt.Sprintf(" %s %s %s: %s %s",
|
||||
prefix, statusIcon,
|
||||
ui.RenderMuted(dep.ID),
|
||||
ui.RenderMuted(dep.Title),
|
||||
ui.RenderMuted(fmt.Sprintf("● P%d", dep.Priority)))
|
||||
}
|
||||
|
||||
// Active items: ID with status color, priority with semantic color
|
||||
style := ui.GetStatusStyle(string(dep.Status))
|
||||
idStr := style.Render(dep.ID)
|
||||
priorityTag := ui.RenderPriority(dep.Priority)
|
||||
|
||||
// Type indicator for epics/bugs
|
||||
typeStr := ""
|
||||
if dep.IssueType == "epic" {
|
||||
typeStr = ui.TypeEpicStyle.Render("(EPIC)") + " "
|
||||
} else if dep.IssueType == "bug" {
|
||||
typeStr = ui.TypeBugStyle.Render("(BUG)") + " "
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" %s %s %s: %s%s %s", prefix, statusIcon, idStr, typeStr, dep.Title, priorityTag)
|
||||
}
|
||||
|
||||
// formatSimpleDependencyLine formats a dependency without metadata (fallback)
|
||||
// Closed items get entire row muted - the work is done, no need for attention
|
||||
func formatSimpleDependencyLine(prefix string, dep *types.Issue) string {
|
||||
statusIcon := ui.GetStatusIcon(string(dep.Status))
|
||||
|
||||
// Closed items: mute entire row since the work is complete
|
||||
if dep.Status == types.StatusClosed {
|
||||
return fmt.Sprintf(" %s %s %s: %s %s",
|
||||
prefix, statusIcon,
|
||||
ui.RenderMuted(dep.ID),
|
||||
ui.RenderMuted(dep.Title),
|
||||
ui.RenderMuted(fmt.Sprintf("● P%d", dep.Priority)))
|
||||
}
|
||||
|
||||
// Active items: use semantic colors
|
||||
style := ui.GetStatusStyle(string(dep.Status))
|
||||
idStr := style.Render(dep.ID)
|
||||
priorityTag := ui.RenderPriority(dep.Priority)
|
||||
|
||||
return fmt.Sprintf(" %s %s %s: %s %s", prefix, statusIcon, idStr, dep.Title, priorityTag)
|
||||
}
|
||||
|
||||
// showIssueRefs displays issues that reference the given issue(s), grouped by relationship type
|
||||
@@ -747,13 +828,23 @@ func showIssueRefs(ctx context.Context, args []string, resolvedIDs []string, rou
|
||||
}
|
||||
|
||||
// displayRefGroup displays a group of references with a given type
|
||||
// Closed items get entire row muted - the work is done, no need for attention
|
||||
func displayRefGroup(depType types.DependencyType, refs []*types.IssueWithDependencyMetadata) {
|
||||
// Get emoji for type
|
||||
emoji := getRefTypeEmoji(depType)
|
||||
fmt.Printf("\n %s %s (%d):\n", emoji, depType, len(refs))
|
||||
|
||||
for _, ref := range refs {
|
||||
// Color ID based on status
|
||||
// Closed items: mute entire row since the work is complete
|
||||
if ref.Status == types.StatusClosed {
|
||||
fmt.Printf(" %s: %s %s\n",
|
||||
ui.RenderMuted(ref.ID),
|
||||
ui.RenderMuted(ref.Title),
|
||||
ui.RenderMuted(fmt.Sprintf("[P%d - %s]", ref.Priority, ref.Status)))
|
||||
continue
|
||||
}
|
||||
|
||||
// Active items: color ID based on status
|
||||
var idStr string
|
||||
switch ref.Status {
|
||||
case types.StatusOpen:
|
||||
@@ -762,8 +853,6 @@ func displayRefGroup(depType types.DependencyType, refs []*types.IssueWithDepend
|
||||
idStr = ui.StatusInProgressStyle.Render(ref.ID)
|
||||
case types.StatusBlocked:
|
||||
idStr = ui.StatusBlockedStyle.Render(ref.ID)
|
||||
case types.StatusClosed:
|
||||
idStr = ui.StatusClosedStyle.Render(ref.ID)
|
||||
default:
|
||||
idStr = ref.ID
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user