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:
532
cmd/bd/graph.go
532
cmd/bd/graph.go
@@ -33,51 +33,44 @@ type GraphLayout struct {
|
|||||||
RootID string
|
RootID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
graphCompact bool
|
||||||
|
graphBox bool
|
||||||
|
graphAll bool
|
||||||
|
)
|
||||||
|
|
||||||
var graphCmd = &cobra.Command{
|
var graphCmd = &cobra.Command{
|
||||||
Use: "graph <issue-id>",
|
Use: "graph [issue-id]",
|
||||||
GroupID: "deps",
|
GroupID: "deps",
|
||||||
Short: "Display issue dependency graph",
|
Short: "Display issue dependency graph",
|
||||||
Long: `Display an ASCII visualization of an issue's dependency graph.
|
Long: `Display a visualization of an issue's dependency graph.
|
||||||
|
|
||||||
For epics, shows all children and their dependencies.
|
For epics, shows all children and their dependencies.
|
||||||
For regular issues, shows the issue and its direct dependencies.
|
For regular issues, shows the issue and its direct dependencies.
|
||||||
|
|
||||||
The graph shows execution order left-to-right:
|
With --all, shows all open issues grouped by connected component.
|
||||||
- Leftmost nodes have no dependencies (can start immediately)
|
|
||||||
- Rightmost nodes depend on everything to their left
|
|
||||||
- Nodes in the same column can run in parallel
|
|
||||||
|
|
||||||
Colors indicate status:
|
Display formats:
|
||||||
- White: open (ready to work)
|
--box (default) ASCII boxes showing layers, more detailed
|
||||||
- Yellow: in progress
|
--compact Tree format, one line per issue, more scannable
|
||||||
- Red: blocked
|
|
||||||
- Green: closed`,
|
The graph shows execution order:
|
||||||
Args: cobra.ExactArgs(1),
|
- Layer 0 / leftmost = no dependencies (can start immediately)
|
||||||
|
- Higher layers depend on lower layers
|
||||||
|
- Nodes in the same layer can run in parallel
|
||||||
|
|
||||||
|
Status icons: ○ open ◐ in_progress ● blocked ✓ closed ❄ deferred`,
|
||||||
|
Args: cobra.RangeArgs(0, 1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
var issueID string
|
|
||||||
|
|
||||||
// Resolve the issue ID
|
// Validate args
|
||||||
if daemonClient != nil {
|
if graphAll && len(args) > 0 {
|
||||||
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
fmt.Fprintf(os.Stderr, "Error: cannot specify issue ID with --all flag\n")
|
||||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: issue '%s' not found\n", args[0])
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(resp.Data, &issueID); err != nil {
|
if !graphAll && len(args) == 0 {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: issue ID required (or use --all for all open issues)\n")
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else if store != nil {
|
|
||||||
var err error
|
|
||||||
issueID, err = utils.ResolvePartialID(ctx, store, args[0])
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: issue '%s' not found\n", args[0])
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +85,66 @@ Colors indicate status:
|
|||||||
defer func() { _ = store.Close() }()
|
defer func() { _ = store.Close() }()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if store == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle --all flag: show graph for all open issues
|
||||||
|
if graphAll {
|
||||||
|
subgraphs, err := loadAllGraphSubgraphs(ctx, store)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error loading all issues: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subgraphs) == 0 {
|
||||||
|
fmt.Println("No open issues found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
outputJSON(subgraphs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render all subgraphs
|
||||||
|
for i, subgraph := range subgraphs {
|
||||||
|
layout := computeLayout(subgraph)
|
||||||
|
if graphCompact {
|
||||||
|
renderGraphCompact(layout, subgraph)
|
||||||
|
} else {
|
||||||
|
renderGraph(layout, subgraph)
|
||||||
|
}
|
||||||
|
if i < len(subgraphs)-1 {
|
||||||
|
fmt.Println(strings.Repeat("─", 60))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single issue mode
|
||||||
|
var issueID string
|
||||||
|
if daemonClient != nil {
|
||||||
|
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
||||||
|
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: issue '%s' not found\n", args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(resp.Data, &issueID); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
issueID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: issue '%s' not found\n", args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load the subgraph
|
// Load the subgraph
|
||||||
subgraph, err := loadGraphSubgraph(ctx, store, issueID)
|
subgraph, err := loadGraphSubgraph(ctx, store, issueID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -111,12 +164,19 @@ Colors indicate status:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render ASCII graph
|
// Render graph - compact tree format or box format (default)
|
||||||
|
if graphCompact {
|
||||||
|
renderGraphCompact(layout, subgraph)
|
||||||
|
} else {
|
||||||
renderGraph(layout, subgraph)
|
renderGraph(layout, subgraph)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
graphCmd.Flags().BoolVar(&graphAll, "all", false, "Show graph for all open issues")
|
||||||
|
graphCmd.Flags().BoolVar(&graphCompact, "compact", false, "Tree format, one line per issue, more scannable")
|
||||||
|
graphCmd.Flags().BoolVar(&graphBox, "box", true, "ASCII boxes showing layers (default)")
|
||||||
graphCmd.ValidArgsFunction = issueIDCompletion
|
graphCmd.ValidArgsFunction = issueIDCompletion
|
||||||
rootCmd.AddCommand(graphCmd)
|
rootCmd.AddCommand(graphCmd)
|
||||||
}
|
}
|
||||||
@@ -191,6 +251,157 @@ func loadGraphSubgraph(ctx context.Context, s storage.Storage, issueID string) (
|
|||||||
return subgraph, nil
|
return subgraph, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadAllGraphSubgraphs loads all open issues and groups them by connected component
|
||||||
|
// Each component is a subgraph of issues that share dependencies
|
||||||
|
func loadAllGraphSubgraphs(ctx context.Context, s storage.Storage) ([]*TemplateSubgraph, error) {
|
||||||
|
if s == nil {
|
||||||
|
return nil, fmt.Errorf("no database connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all open issues (open, in_progress, blocked)
|
||||||
|
// We need to make multiple calls since IssueFilter takes a single status
|
||||||
|
var allIssues []*types.Issue
|
||||||
|
for _, status := range []types.Status{types.StatusOpen, types.StatusInProgress, types.StatusBlocked} {
|
||||||
|
statusCopy := status
|
||||||
|
issues, err := s.SearchIssues(ctx, "", types.IssueFilter{
|
||||||
|
Status: &statusCopy,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to search issues: %w", err)
|
||||||
|
}
|
||||||
|
allIssues = append(allIssues, issues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allIssues) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build issue map
|
||||||
|
issueMap := make(map[string]*types.Issue)
|
||||||
|
for _, issue := range allIssues {
|
||||||
|
issueMap[issue.ID] = issue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all dependencies between these issues
|
||||||
|
allDeps := make([]*types.Dependency, 0)
|
||||||
|
for _, issue := range allIssues {
|
||||||
|
deps, err := s.GetDependencyRecords(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, dep := range deps {
|
||||||
|
// Only include deps where both ends are in our issue set
|
||||||
|
if _, ok := issueMap[dep.DependsOnID]; ok {
|
||||||
|
allDeps = append(allDeps, dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build adjacency list for union-find
|
||||||
|
adj := make(map[string][]string)
|
||||||
|
for _, dep := range allDeps {
|
||||||
|
adj[dep.IssueID] = append(adj[dep.IssueID], dep.DependsOnID)
|
||||||
|
adj[dep.DependsOnID] = append(adj[dep.DependsOnID], dep.IssueID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find connected components using BFS
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
var components [][]string
|
||||||
|
|
||||||
|
for _, issue := range allIssues {
|
||||||
|
if visited[issue.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS to find all connected issues
|
||||||
|
var component []string
|
||||||
|
queue := []string{issue.ID}
|
||||||
|
visited[issue.ID] = true
|
||||||
|
|
||||||
|
for len(queue) > 0 {
|
||||||
|
current := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
component = append(component, current)
|
||||||
|
|
||||||
|
for _, neighbor := range adj[current] {
|
||||||
|
if !visited[neighbor] {
|
||||||
|
visited[neighbor] = true
|
||||||
|
queue = append(queue, neighbor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
components = append(components, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort components by size (largest first) and then by priority of first issue
|
||||||
|
sort.Slice(components, func(i, j int) bool {
|
||||||
|
// First by size (descending)
|
||||||
|
if len(components[i]) != len(components[j]) {
|
||||||
|
return len(components[i]) > len(components[j])
|
||||||
|
}
|
||||||
|
// Then by priority of first issue (ascending = higher priority first)
|
||||||
|
issueI := issueMap[components[i][0]]
|
||||||
|
issueJ := issueMap[components[j][0]]
|
||||||
|
return issueI.Priority < issueJ.Priority
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create subgraph for each component
|
||||||
|
var subgraphs []*TemplateSubgraph
|
||||||
|
for _, component := range components {
|
||||||
|
if len(component) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best "root" for this component
|
||||||
|
// Prefer: epics > highest priority > oldest
|
||||||
|
var root *types.Issue
|
||||||
|
for _, id := range component {
|
||||||
|
issue := issueMap[id]
|
||||||
|
if root == nil {
|
||||||
|
root = issue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Prefer epics
|
||||||
|
if issue.IssueType == types.TypeEpic && root.IssueType != types.TypeEpic {
|
||||||
|
root = issue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if root.IssueType == types.TypeEpic && issue.IssueType != types.TypeEpic {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Prefer higher priority (lower number)
|
||||||
|
if issue.Priority < root.Priority {
|
||||||
|
root = issue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph := &TemplateSubgraph{
|
||||||
|
Root: root,
|
||||||
|
IssueMap: make(map[string]*types.Issue),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range component {
|
||||||
|
issue := issueMap[id]
|
||||||
|
subgraph.Issues = append(subgraph.Issues, issue)
|
||||||
|
subgraph.IssueMap[id] = issue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependencies for this component
|
||||||
|
for _, dep := range allDeps {
|
||||||
|
if _, inComponent := subgraph.IssueMap[dep.IssueID]; inComponent {
|
||||||
|
if _, depInComponent := subgraph.IssueMap[dep.DependsOnID]; depInComponent {
|
||||||
|
subgraph.Dependencies = append(subgraph.Dependencies, dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraphs = append(subgraphs, subgraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subgraphs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// computeLayout assigns layers to nodes using topological sort
|
// computeLayout assigns layers to nodes using topological sort
|
||||||
func computeLayout(subgraph *TemplateSubgraph) *GraphLayout {
|
func computeLayout(subgraph *TemplateSubgraph) *GraphLayout {
|
||||||
layout := &GraphLayout{
|
layout := &GraphLayout{
|
||||||
@@ -379,33 +590,155 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
|
|||||||
fmt.Printf(" Total: %d issues across %d layers\n\n", len(layout.Nodes), len(layout.Layers))
|
fmt.Printf(" Total: %d issues across %d layers\n\n", len(layout.Nodes), len(layout.Layers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderGraphCompact renders the graph in compact tree format
|
||||||
|
// One line per issue, more scannable, uses tree connectors (├──, └──, │)
|
||||||
|
func renderGraphCompact(layout *GraphLayout, subgraph *TemplateSubgraph) {
|
||||||
|
if len(layout.Nodes) == 0 {
|
||||||
|
fmt.Println("Empty graph")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%s Dependency graph for %s (%d issues, %d layers)\n\n",
|
||||||
|
ui.RenderAccent("📊"), layout.RootID, len(layout.Nodes), len(layout.Layers))
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
fmt.Println(" Status: ○ open ◐ in_progress ● blocked ✓ closed ❄ deferred")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Build parent-child map from subgraph dependencies
|
||||||
|
children := make(map[string][]string) // parent -> children
|
||||||
|
childSet := make(map[string]bool) // track which issues are children
|
||||||
|
|
||||||
|
for _, dep := range subgraph.Dependencies {
|
||||||
|
if dep.Type == types.DepParentChild {
|
||||||
|
children[dep.DependsOnID] = append(children[dep.DependsOnID], dep.IssueID)
|
||||||
|
childSet[dep.IssueID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort children by priority then ID for consistent output
|
||||||
|
for parentID := range children {
|
||||||
|
sort.Slice(children[parentID], func(i, j int) bool {
|
||||||
|
nodeI := layout.Nodes[children[parentID][i]]
|
||||||
|
nodeJ := layout.Nodes[children[parentID][j]]
|
||||||
|
if nodeI.Issue.Priority != nodeJ.Issue.Priority {
|
||||||
|
return nodeI.Issue.Priority < nodeJ.Issue.Priority
|
||||||
|
}
|
||||||
|
return nodeI.Issue.ID < nodeJ.Issue.ID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render by layer with tree structure
|
||||||
|
for layerIdx, layer := range layout.Layers {
|
||||||
|
// Layer header
|
||||||
|
layerHeader := fmt.Sprintf("LAYER %d", layerIdx)
|
||||||
|
if layerIdx == 0 {
|
||||||
|
layerHeader += " (ready)"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s\n", ui.RenderAccent(layerHeader))
|
||||||
|
|
||||||
|
for i, id := range layer {
|
||||||
|
node := layout.Nodes[id]
|
||||||
|
isLast := i == len(layer)-1
|
||||||
|
|
||||||
|
// Format node line
|
||||||
|
line := formatCompactNode(node)
|
||||||
|
|
||||||
|
// Tree connector
|
||||||
|
connector := "├── "
|
||||||
|
if isLast {
|
||||||
|
connector = "└── "
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" %s%s\n", connector, line)
|
||||||
|
|
||||||
|
// Render children (if this issue has children in the subgraph)
|
||||||
|
if childIDs, ok := children[id]; ok && len(childIDs) > 0 {
|
||||||
|
childPrefix := "│ "
|
||||||
|
if isLast {
|
||||||
|
childPrefix = " "
|
||||||
|
}
|
||||||
|
renderCompactChildren(layout, childIDs, children, childPrefix, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderCompactChildren recursively renders children in tree format
|
||||||
|
func renderCompactChildren(layout *GraphLayout, childIDs []string, children map[string][]string, prefix string, depth int) {
|
||||||
|
for i, childID := range childIDs {
|
||||||
|
node := layout.Nodes[childID]
|
||||||
|
if node == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isLast := i == len(childIDs)-1
|
||||||
|
connector := "├── "
|
||||||
|
if isLast {
|
||||||
|
connector = "└── "
|
||||||
|
}
|
||||||
|
|
||||||
|
line := formatCompactNode(node)
|
||||||
|
fmt.Printf(" %s%s%s\n", prefix, connector, line)
|
||||||
|
|
||||||
|
// Recurse for nested children
|
||||||
|
if grandchildren, ok := children[childID]; ok && len(grandchildren) > 0 {
|
||||||
|
childPrefix := prefix
|
||||||
|
if isLast {
|
||||||
|
childPrefix += " "
|
||||||
|
} else {
|
||||||
|
childPrefix += "│ "
|
||||||
|
}
|
||||||
|
renderCompactChildren(layout, grandchildren, children, childPrefix, depth+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatCompactNode formats a single node for compact output
|
||||||
|
// Format: STATUS_ICON ID PRIORITY Title
|
||||||
|
func formatCompactNode(node *GraphNode) string {
|
||||||
|
status := string(node.Issue.Status)
|
||||||
|
|
||||||
|
// Use shared status icon with semantic color
|
||||||
|
statusIcon := ui.RenderStatusIcon(status)
|
||||||
|
|
||||||
|
// Priority with icon
|
||||||
|
priorityTag := ui.RenderPriority(node.Issue.Priority)
|
||||||
|
|
||||||
|
// Title - truncate if too long
|
||||||
|
title := truncateTitle(node.Issue.Title, 50)
|
||||||
|
|
||||||
|
// Build line - apply status style to entire line for closed issues
|
||||||
|
style := ui.GetStatusStyle(status)
|
||||||
|
if node.Issue.Status == types.StatusClosed {
|
||||||
|
return fmt.Sprintf("%s %s %s %s",
|
||||||
|
statusIcon,
|
||||||
|
style.Render(node.Issue.ID),
|
||||||
|
style.Render(fmt.Sprintf("● P%d", node.Issue.Priority)),
|
||||||
|
style.Render(title))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s %s %s", statusIcon, node.Issue.ID, priorityTag, title)
|
||||||
|
}
|
||||||
|
|
||||||
// renderNodeBox renders a single node as an ASCII box
|
// renderNodeBox renders a single node as an ASCII box
|
||||||
|
// Uses semantic status styles from ui package for consistency
|
||||||
func renderNodeBox(node *GraphNode, width int) string {
|
func renderNodeBox(node *GraphNode, width int) string {
|
||||||
// Status indicator
|
|
||||||
var statusIcon string
|
|
||||||
var titleStr string
|
|
||||||
|
|
||||||
title := truncateTitle(node.Issue.Title, width-4)
|
title := truncateTitle(node.Issue.Title, width-4)
|
||||||
|
paddedTitle := padRight(title, width-4)
|
||||||
|
status := string(node.Issue.Status)
|
||||||
|
|
||||||
switch node.Issue.Status {
|
// Use shared status icon and style
|
||||||
case types.StatusOpen:
|
statusIcon := ui.RenderStatusIcon(status)
|
||||||
statusIcon = "○"
|
style := ui.GetStatusStyle(status)
|
||||||
titleStr = padRight(title, width-4)
|
|
||||||
case types.StatusInProgress:
|
// Apply style to title for actionable statuses
|
||||||
statusIcon = "◐"
|
var titleStr string
|
||||||
titleStr = ui.RenderWarn(padRight(title, width-4))
|
if node.Issue.Status == types.StatusOpen {
|
||||||
case types.StatusBlocked:
|
titleStr = paddedTitle // no color for open - available but not urgent
|
||||||
statusIcon = "●"
|
} else {
|
||||||
titleStr = ui.RenderFail(padRight(title, width-4))
|
titleStr = style.Render(paddedTitle)
|
||||||
case types.StatusDeferred:
|
|
||||||
statusIcon = "❄"
|
|
||||||
titleStr = ui.RenderAccent(padRight(title, width-4))
|
|
||||||
case types.StatusClosed:
|
|
||||||
statusIcon = "✓"
|
|
||||||
titleStr = ui.RenderPass(padRight(title, width-4))
|
|
||||||
default:
|
|
||||||
statusIcon = "?"
|
|
||||||
titleStr = padRight(title, width-4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id := node.Issue.ID
|
id := node.Issue.ID
|
||||||
@@ -438,6 +771,7 @@ func padRight(s string, width int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// computeDependencyCounts calculates how many issues each issue blocks and is blocked by
|
// computeDependencyCounts calculates how many issues each issue blocks and is blocked by
|
||||||
|
// Excludes parent-child relationships and the root issue from counts to reduce cognitive noise
|
||||||
func computeDependencyCounts(subgraph *TemplateSubgraph) (blocks map[string]int, blockedBy map[string]int) {
|
func computeDependencyCounts(subgraph *TemplateSubgraph) (blocks map[string]int, blockedBy map[string]int) {
|
||||||
blocks = make(map[string]int)
|
blocks = make(map[string]int)
|
||||||
blockedBy = make(map[string]int)
|
blockedBy = make(map[string]int)
|
||||||
@@ -446,61 +780,76 @@ func computeDependencyCounts(subgraph *TemplateSubgraph) (blocks map[string]int,
|
|||||||
return blocks, blockedBy
|
return blocks, blockedBy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootID := ""
|
||||||
|
if subgraph.Root != nil {
|
||||||
|
rootID = subgraph.Root.ID
|
||||||
|
}
|
||||||
|
|
||||||
for _, dep := range subgraph.Dependencies {
|
for _, dep := range subgraph.Dependencies {
|
||||||
if dep.Type == types.DepBlocks {
|
// Only count "blocks" dependencies (not parent-child, related, etc.)
|
||||||
|
if dep.Type != types.DepBlocks {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the blocker is the root issue - this is obvious from graph structure
|
||||||
|
// and showing "needs:1" when it's just the parent epic is cognitive noise
|
||||||
|
if dep.DependsOnID == rootID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// dep.DependsOnID blocks dep.IssueID
|
// dep.DependsOnID blocks dep.IssueID
|
||||||
// So dep.DependsOnID "blocks" count increases
|
// So dep.DependsOnID "blocks" count increases
|
||||||
blocks[dep.DependsOnID]++
|
blocks[dep.DependsOnID]++
|
||||||
// And dep.IssueID "blocked by" count increases
|
// And dep.IssueID "blocked by" count increases
|
||||||
blockedBy[dep.IssueID]++
|
blockedBy[dep.IssueID]++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return blocks, blockedBy
|
return blocks, blockedBy
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderNodeBoxWithDeps renders a node box with dependency information
|
// renderNodeBoxWithDeps renders a node box with dependency information
|
||||||
|
// Uses semantic status styles from ui package for consistency across commands
|
||||||
|
// Design principle: only actionable states get color, closed items fade
|
||||||
func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedByCount int) string {
|
func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedByCount int) string {
|
||||||
// Status indicator
|
|
||||||
var statusIcon string
|
|
||||||
var titleStr string
|
|
||||||
|
|
||||||
title := truncateTitle(node.Issue.Title, width-4)
|
title := truncateTitle(node.Issue.Title, width-4)
|
||||||
|
paddedTitle := padRight(title, width-4)
|
||||||
|
status := string(node.Issue.Status)
|
||||||
|
|
||||||
switch node.Issue.Status {
|
// Use shared status icon and style from ui package
|
||||||
case types.StatusOpen:
|
statusIcon := ui.RenderStatusIcon(status)
|
||||||
statusIcon = "○"
|
style := ui.GetStatusStyle(status)
|
||||||
titleStr = padRight(title, width-4)
|
|
||||||
case types.StatusInProgress:
|
// Apply style to title for actionable statuses
|
||||||
statusIcon = "◐"
|
var titleStr string
|
||||||
titleStr = ui.RenderWarn(padRight(title, width-4))
|
if node.Issue.Status == types.StatusOpen {
|
||||||
case types.StatusBlocked:
|
titleStr = paddedTitle // no color for open - available but not urgent
|
||||||
statusIcon = "●"
|
} else {
|
||||||
titleStr = ui.RenderFail(padRight(title, width-4))
|
titleStr = style.Render(paddedTitle)
|
||||||
case types.StatusDeferred:
|
|
||||||
statusIcon = "❄"
|
|
||||||
titleStr = ui.RenderAccent(padRight(title, width-4))
|
|
||||||
case types.StatusClosed:
|
|
||||||
statusIcon = "✓"
|
|
||||||
titleStr = ui.RenderPass(padRight(title, width-4))
|
|
||||||
default:
|
|
||||||
statusIcon = "?"
|
|
||||||
titleStr = padRight(title, width-4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id := node.Issue.ID
|
id := node.Issue.ID
|
||||||
|
|
||||||
// Build dependency info string
|
// Build dependency info string - only show if meaningful counts exist
|
||||||
var depInfo string
|
// Note: we build the plain text version first for padding, then apply colors
|
||||||
|
var depInfoPlain string
|
||||||
|
var depInfoStyled string
|
||||||
if blocksCount > 0 || blockedByCount > 0 {
|
if blocksCount > 0 || blockedByCount > 0 {
|
||||||
parts := []string{}
|
plainParts := []string{}
|
||||||
|
styledParts := []string{}
|
||||||
if blocksCount > 0 {
|
if blocksCount > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("blocks:%d", blocksCount))
|
plainText := fmt.Sprintf("blocks:%d", blocksCount)
|
||||||
|
plainParts = append(plainParts, plainText)
|
||||||
|
// Use semantic color for blocks indicator - attention-grabbing
|
||||||
|
styledParts = append(styledParts, ui.StatusBlockedStyle.Render(plainText))
|
||||||
}
|
}
|
||||||
if blockedByCount > 0 {
|
if blockedByCount > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("needs:%d", blockedByCount))
|
plainText := fmt.Sprintf("needs:%d", blockedByCount)
|
||||||
|
plainParts = append(plainParts, plainText)
|
||||||
|
// Use muted color for needs indicator - informational
|
||||||
|
styledParts = append(styledParts, ui.MutedStyle.Render(plainText))
|
||||||
}
|
}
|
||||||
depInfo = strings.Join(parts, " ")
|
depInfoPlain = strings.Join(plainParts, " ")
|
||||||
|
depInfoStyled = strings.Join(styledParts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the box
|
// Build the box
|
||||||
@@ -509,8 +858,13 @@ func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedB
|
|||||||
idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
|
idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
|
||||||
|
|
||||||
var result string
|
var result string
|
||||||
if depInfo != "" {
|
if depInfoPlain != "" {
|
||||||
depLine := fmt.Sprintf(" │ %s │", ui.RenderAccent(padRight(depInfo, width-2)))
|
// Pad based on plain text length, then render with styled version
|
||||||
|
padding := width - 2 - len([]rune(depInfoPlain))
|
||||||
|
if padding < 0 {
|
||||||
|
padding = 0
|
||||||
|
}
|
||||||
|
depLine := fmt.Sprintf(" │ %s%s │", depInfoStyled, strings.Repeat(" ", padding))
|
||||||
bottom := " └" + strings.Repeat("─", width) + "┘"
|
bottom := " └" + strings.Repeat("─", width) + "┘"
|
||||||
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom
|
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
192
cmd/bd/list.go
192
cmd/bd/list.go
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/config"
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/rpc"
|
"github.com/steveyegge/beads/internal/rpc"
|
||||||
"github.com/steveyegge/beads/internal/storage"
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/timeparsing"
|
"github.com/steveyegge/beads/internal/timeparsing"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
"github.com/steveyegge/beads/internal/ui"
|
"github.com/steveyegge/beads/internal/ui"
|
||||||
@@ -42,76 +43,131 @@ func pinIndicator(issue *types.Issue) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority symbols for pretty output (GH#654)
|
// Priority tags for pretty output - simple text, semantic colors applied via ui package
|
||||||
var prioritySymbols = map[int]string{
|
// Design principle: only P0/P1 get color for attention, P2-P4 are neutral
|
||||||
0: "🔴", // P0 - Critical
|
func renderPriorityTag(priority int) string {
|
||||||
1: "🟠", // P1 - High
|
return ui.RenderPriority(priority)
|
||||||
2: "🟡", // P2 - Medium (default)
|
|
||||||
3: "🔵", // P3 - Low
|
|
||||||
4: "⚪", // P4 - Lowest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status symbols for pretty output (GH#654)
|
// renderStatusIcon returns the status icon with semantic coloring applied
|
||||||
var statusSymbols = map[types.Status]string{
|
// Delegates to the shared ui.RenderStatusIcon for consistency across commands
|
||||||
"open": "○",
|
func renderStatusIcon(status types.Status) string {
|
||||||
"in_progress": "◐",
|
return ui.RenderStatusIcon(string(status))
|
||||||
"blocked": "⊗",
|
|
||||||
"deferred": "◇",
|
|
||||||
"closed": "●",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatPrettyIssue formats a single issue for pretty output
|
// formatPrettyIssue formats a single issue for pretty output
|
||||||
|
// Uses semantic colors: status icon colored, priority P0/P1 colored, rest neutral
|
||||||
func formatPrettyIssue(issue *types.Issue) string {
|
func formatPrettyIssue(issue *types.Issue) string {
|
||||||
prioritySym := prioritySymbols[issue.Priority]
|
// Use shared helpers from ui package
|
||||||
if prioritySym == "" {
|
statusIcon := ui.RenderStatusIcon(string(issue.Status))
|
||||||
prioritySym = "⚪"
|
priorityTag := renderPriorityTag(issue.Priority)
|
||||||
}
|
|
||||||
statusSym := statusSymbols[issue.Status]
|
|
||||||
if statusSym == "" {
|
|
||||||
statusSym = "○"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Type badge - only show for notable types
|
||||||
typeBadge := ""
|
typeBadge := ""
|
||||||
switch issue.IssueType {
|
switch issue.IssueType {
|
||||||
case "epic":
|
case "epic":
|
||||||
typeBadge = "[EPIC] "
|
typeBadge = ui.TypeEpicStyle.Render("[epic]") + " "
|
||||||
case "feature":
|
|
||||||
typeBadge = "[FEAT] "
|
|
||||||
case "bug":
|
case "bug":
|
||||||
typeBadge = "[BUG] "
|
typeBadge = ui.TypeBugStyle.Render("[bug]") + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %s %s - %s%s", statusSym, prioritySym, issue.ID, typeBadge, issue.Title)
|
// Format: STATUS_ICON ID PRIORITY [Type] Title
|
||||||
|
// Priority uses ● icon with color, no brackets needed
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildIssueTree builds parent-child tree structure from issues
|
// buildIssueTree builds parent-child tree structure from issues
|
||||||
|
// Uses actual parent-child dependencies from the database when store is provided
|
||||||
func buildIssueTree(issues []*types.Issue) (roots []*types.Issue, childrenMap map[string][]*types.Issue) {
|
func buildIssueTree(issues []*types.Issue) (roots []*types.Issue, childrenMap map[string][]*types.Issue) {
|
||||||
issueMap := make(map[string]*types.Issue)
|
return buildIssueTreeWithDeps(issues, nil)
|
||||||
childrenMap = make(map[string][]*types.Issue)
|
|
||||||
|
|
||||||
for _, issue := range issues {
|
|
||||||
issueMap[issue.ID] = issue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildIssueTreeWithDeps builds parent-child tree using dependency records
|
||||||
|
// If allDeps is nil, falls back to dotted ID hierarchy (e.g., "parent.1")
|
||||||
|
// Treats any dependency on an epic as a parent-child relationship
|
||||||
|
func buildIssueTreeWithDeps(issues []*types.Issue, allDeps map[string][]*types.Dependency) (roots []*types.Issue, childrenMap map[string][]*types.Issue) {
|
||||||
|
issueMap := make(map[string]*types.Issue)
|
||||||
|
childrenMap = make(map[string][]*types.Issue)
|
||||||
|
isChild := make(map[string]bool)
|
||||||
|
|
||||||
|
// Build issue map and identify epics
|
||||||
|
epicIDs := make(map[string]bool)
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
// Check if this is a hierarchical subtask (e.g., "parent.1")
|
issueMap[issue.ID] = issue
|
||||||
|
if issue.IssueType == "epic" {
|
||||||
|
epicIDs[issue.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have dependency records, use them to find parent-child relationships
|
||||||
|
if allDeps != nil {
|
||||||
|
for issueID, deps := range allDeps {
|
||||||
|
for _, dep := range deps {
|
||||||
|
parentID := dep.DependsOnID
|
||||||
|
// Only include if both parent and child are in the issue set
|
||||||
|
child, childOk := issueMap[issueID]
|
||||||
|
_, parentOk := issueMap[parentID]
|
||||||
|
if !childOk || !parentOk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat as parent-child if:
|
||||||
|
// 1. Explicit parent-child dependency type, OR
|
||||||
|
// 2. Any dependency where the target is an epic
|
||||||
|
if dep.Type == types.DepParentChild || epicIDs[parentID] {
|
||||||
|
childrenMap[parentID] = append(childrenMap[parentID], child)
|
||||||
|
isChild[issueID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check for hierarchical subtask IDs (e.g., "parent.1")
|
||||||
|
for _, issue := range issues {
|
||||||
|
if isChild[issue.ID] {
|
||||||
|
continue // Already a child via dependency
|
||||||
|
}
|
||||||
if strings.Contains(issue.ID, ".") {
|
if strings.Contains(issue.ID, ".") {
|
||||||
parts := strings.Split(issue.ID, ".")
|
parts := strings.Split(issue.ID, ".")
|
||||||
parentID := strings.Join(parts[:len(parts)-1], ".")
|
parentID := strings.Join(parts[:len(parts)-1], ".")
|
||||||
if _, exists := issueMap[parentID]; exists {
|
if _, exists := issueMap[parentID]; exists {
|
||||||
childrenMap[parentID] = append(childrenMap[parentID], issue)
|
childrenMap[parentID] = append(childrenMap[parentID], issue)
|
||||||
|
isChild[issue.ID] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roots are issues that aren't children of any other issue
|
||||||
|
for _, issue := range issues {
|
||||||
|
if !isChild[issue.ID] {
|
||||||
roots = append(roots, issue)
|
roots = append(roots, issue)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return roots, childrenMap
|
return roots, childrenMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// printPrettyTree recursively prints the issue tree
|
// printPrettyTree recursively prints the issue tree
|
||||||
|
// Children are sorted by priority (P0 first) for intuitive reading
|
||||||
func printPrettyTree(childrenMap map[string][]*types.Issue, parentID string, prefix string) {
|
func printPrettyTree(childrenMap map[string][]*types.Issue, parentID string, prefix string) {
|
||||||
children := childrenMap[parentID]
|
children := childrenMap[parentID]
|
||||||
|
|
||||||
|
// Sort children by priority (ascending: P0 before P1 before P2...)
|
||||||
|
slices.SortFunc(children, func(a, b *types.Issue) int {
|
||||||
|
return cmp.Compare(a.Priority, b.Priority)
|
||||||
|
})
|
||||||
|
|
||||||
for i, child := range children {
|
for i, child := range children {
|
||||||
isLast := i == len(children)-1
|
isLast := i == len(children)-1
|
||||||
connector := "├── "
|
connector := "├── "
|
||||||
@@ -129,7 +185,13 @@ func printPrettyTree(childrenMap map[string][]*types.Issue, parentID string, pre
|
|||||||
}
|
}
|
||||||
|
|
||||||
// displayPrettyList displays issues in pretty tree format (GH#654)
|
// displayPrettyList displays issues in pretty tree format (GH#654)
|
||||||
|
// Uses buildIssueTree which only supports dotted ID hierarchy
|
||||||
func displayPrettyList(issues []*types.Issue, showHeader bool) {
|
func displayPrettyList(issues []*types.Issue, showHeader bool) {
|
||||||
|
displayPrettyListWithDeps(issues, showHeader, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayPrettyListWithDeps displays issues in tree format using dependency data
|
||||||
|
func displayPrettyListWithDeps(issues []*types.Issue, showHeader bool, allDeps map[string][]*types.Dependency) {
|
||||||
if showHeader {
|
if showHeader {
|
||||||
// Clear screen and show header
|
// Clear screen and show header
|
||||||
fmt.Print("\033[2J\033[H")
|
fmt.Print("\033[2J\033[H")
|
||||||
@@ -144,14 +206,11 @@ func displayPrettyList(issues []*types.Issue, showHeader bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
roots, childrenMap := buildIssueTree(issues)
|
roots, childrenMap := buildIssueTreeWithDeps(issues, allDeps)
|
||||||
|
|
||||||
for i, issue := range roots {
|
for _, issue := range roots {
|
||||||
fmt.Println(formatPrettyIssue(issue))
|
fmt.Println(formatPrettyIssue(issue))
|
||||||
printPrettyTree(childrenMap, issue.ID, "")
|
printPrettyTree(childrenMap, issue.ID, "")
|
||||||
if i < len(roots)-1 {
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
@@ -169,7 +228,7 @@ func displayPrettyList(issues []*types.Issue, showHeader bool) {
|
|||||||
}
|
}
|
||||||
fmt.Printf("Total: %d issues (%d open, %d in progress)\n", len(issues), openCount, inProgressCount)
|
fmt.Printf("Total: %d issues (%d open, %d in progress)\n", len(issues), openCount, inProgressCount)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Legend: ○ open | ◐ in progress | ⊗ blocked | 🔴 P0 | 🟠 P1 | 🟡 P2 | 🔵 P3 | ⚪ P4")
|
fmt.Println("Status: ○ open ◐ in_progress ● blocked ✓ closed ❄ deferred")
|
||||||
}
|
}
|
||||||
|
|
||||||
// watchIssues starts watching for changes and re-displays (GH#654)
|
// watchIssues starts watching for changes and re-displays (GH#654)
|
||||||
@@ -330,6 +389,8 @@ func formatAgentIssue(buf *strings.Builder, issue *types.Issue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// formatIssueCompact formats a single issue in compact format to a buffer
|
// formatIssueCompact formats a single issue in compact format to a buffer
|
||||||
|
// Uses status icons for better scanability - consistent with bd graph
|
||||||
|
// Format: [icon] [pin] ID [Priority] [Type] @assignee [labels] - Title
|
||||||
func formatIssueCompact(buf *strings.Builder, issue *types.Issue, labels []string) {
|
func formatIssueCompact(buf *strings.Builder, issue *types.Issue, labels []string) {
|
||||||
labelsStr := ""
|
labelsStr := ""
|
||||||
if len(labels) > 0 {
|
if len(labels) > 0 {
|
||||||
@@ -339,20 +400,25 @@ func formatIssueCompact(buf *strings.Builder, issue *types.Issue, labels []strin
|
|||||||
if issue.Assignee != "" {
|
if issue.Assignee != "" {
|
||||||
assigneeStr = fmt.Sprintf(" @%s", issue.Assignee)
|
assigneeStr = fmt.Sprintf(" @%s", issue.Assignee)
|
||||||
}
|
}
|
||||||
status := string(issue.Status)
|
|
||||||
if status == "closed" {
|
// Get styled status icon
|
||||||
line := fmt.Sprintf("%s%s [P%d] [%s] %s%s%s - %s",
|
statusIcon := renderStatusIcon(issue.Status)
|
||||||
pinIndicator(issue), issue.ID, issue.Priority,
|
|
||||||
issue.IssueType, status, assigneeStr, labelsStr, issue.Title)
|
if issue.Status == types.StatusClosed {
|
||||||
|
// Closed issues: entire line muted (fades visually)
|
||||||
|
line := fmt.Sprintf("%s %s%s [P%d] [%s]%s%s - %s",
|
||||||
|
statusIcon, pinIndicator(issue), issue.ID, issue.Priority,
|
||||||
|
issue.IssueType, assigneeStr, labelsStr, issue.Title)
|
||||||
buf.WriteString(ui.RenderClosedLine(line))
|
buf.WriteString(ui.RenderClosedLine(line))
|
||||||
buf.WriteString("\n")
|
buf.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
buf.WriteString(fmt.Sprintf("%s%s [%s] [%s] %s%s%s - %s\n",
|
// Active issues: status icon + semantic colors for priority/type
|
||||||
|
buf.WriteString(fmt.Sprintf("%s %s%s [%s] [%s]%s%s - %s\n",
|
||||||
|
statusIcon,
|
||||||
pinIndicator(issue),
|
pinIndicator(issue),
|
||||||
ui.RenderID(issue.ID),
|
ui.RenderID(issue.ID),
|
||||||
ui.RenderPriority(issue.Priority),
|
ui.RenderPriority(issue.Priority),
|
||||||
ui.RenderType(string(issue.IssueType)),
|
ui.RenderType(string(issue.IssueType)),
|
||||||
ui.RenderStatus(status),
|
|
||||||
assigneeStr, labelsStr, issue.Title))
|
assigneeStr, labelsStr, issue.Title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,6 +503,8 @@ var listCmd = &cobra.Command{
|
|||||||
|
|
||||||
// Pretty and watch flags (GH#654)
|
// Pretty and watch flags (GH#654)
|
||||||
prettyFormat, _ := cmd.Flags().GetBool("pretty")
|
prettyFormat, _ := cmd.Flags().GetBool("pretty")
|
||||||
|
treeFormat, _ := cmd.Flags().GetBool("tree")
|
||||||
|
prettyFormat = prettyFormat || treeFormat // --tree is alias for --pretty
|
||||||
watchMode, _ := cmd.Flags().GetBool("watch")
|
watchMode, _ := cmd.Flags().GetBool("watch")
|
||||||
|
|
||||||
// Pager control (bd-jdz3)
|
// Pager control (bd-jdz3)
|
||||||
@@ -824,6 +892,33 @@ var listCmd = &cobra.Command{
|
|||||||
// Apply sorting
|
// Apply sorting
|
||||||
sortIssues(issues, sortBy, reverse)
|
sortIssues(issues, sortBy, reverse)
|
||||||
|
|
||||||
|
// Handle watch mode (GH#654)
|
||||||
|
if watchMode {
|
||||||
|
watchIssues(ctx, store, filter, sortBy, reverse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pretty/tree format (GH#654)
|
||||||
|
if prettyFormat {
|
||||||
|
// Load dependencies for tree structure
|
||||||
|
// In daemon mode, open a read-only store to get dependencies
|
||||||
|
var allDeps map[string][]*types.Dependency
|
||||||
|
if store != nil {
|
||||||
|
allDeps, _ = store.GetAllDependencyRecords(ctx)
|
||||||
|
} else if dbPath != "" {
|
||||||
|
// Daemon mode: open read-only connection for tree deps
|
||||||
|
if roStore, err := sqlite.NewReadOnlyWithTimeout(ctx, dbPath, lockTimeout); err == nil {
|
||||||
|
allDeps, _ = roStore.GetAllDependencyRecords(ctx)
|
||||||
|
_ = roStore.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayPrettyListWithDeps(issues, false, allDeps)
|
||||||
|
if effectiveLimit > 0 && len(issues) == effectiveLimit {
|
||||||
|
fmt.Fprintf(os.Stderr, "\nShowing %d issues (use --limit 0 for all)\n", effectiveLimit)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Build output in buffer for pager support (bd-jdz3)
|
// Build output in buffer for pager support (bd-jdz3)
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
if ui.IsAgentMode() {
|
if ui.IsAgentMode() {
|
||||||
@@ -891,7 +986,9 @@ var listCmd = &cobra.Command{
|
|||||||
|
|
||||||
// Handle pretty format (GH#654)
|
// Handle pretty format (GH#654)
|
||||||
if prettyFormat {
|
if prettyFormat {
|
||||||
displayPrettyList(issues, false)
|
// Load dependencies for tree structure
|
||||||
|
allDeps, _ := store.GetAllDependencyRecords(ctx)
|
||||||
|
displayPrettyListWithDeps(issues, false, allDeps)
|
||||||
// Show truncation hint if we hit the limit (GH#788)
|
// Show truncation hint if we hit the limit (GH#788)
|
||||||
if effectiveLimit > 0 && len(issues) == effectiveLimit {
|
if effectiveLimit > 0 && len(issues) == effectiveLimit {
|
||||||
fmt.Fprintf(os.Stderr, "\nShowing %d issues (use --limit 0 for all)\n", effectiveLimit)
|
fmt.Fprintf(os.Stderr, "\nShowing %d issues (use --limit 0 for all)\n", effectiveLimit)
|
||||||
@@ -1055,6 +1152,7 @@ func init() {
|
|||||||
|
|
||||||
// Pretty and watch flags (GH#654)
|
// Pretty and watch flags (GH#654)
|
||||||
listCmd.Flags().Bool("pretty", false, "Display issues in a tree format with status/priority symbols")
|
listCmd.Flags().Bool("pretty", false, "Display issues in a tree format with status/priority symbols")
|
||||||
|
listCmd.Flags().Bool("tree", false, "Alias for --pretty: hierarchical tree format")
|
||||||
listCmd.Flags().BoolP("watch", "w", false, "Watch for changes and auto-update display (implies --pretty)")
|
listCmd.Flags().BoolP("watch", "w", false, "Watch for changes and auto-update display (implies --pretty)")
|
||||||
|
|
||||||
// Pager control (bd-jdz3)
|
// Pager control (bd-jdz3)
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ func TestListFormatPrettyIssue_BadgesAndDefaults(t *testing.T) {
|
|||||||
if !strings.Contains(out, "bd-1") || !strings.Contains(out, "Hello") {
|
if !strings.Contains(out, "bd-1") || !strings.Contains(out, "Hello") {
|
||||||
t.Fatalf("unexpected output: %q", out)
|
t.Fatalf("unexpected output: %q", out)
|
||||||
}
|
}
|
||||||
if !strings.Contains(out, "[BUG]") {
|
if !strings.Contains(out, "[bug]") {
|
||||||
t.Fatalf("expected BUG badge: %q", out)
|
t.Fatalf("expected bug badge: %q", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
405
cmd/bd/show.go
405
cmd/bd/show.go
@@ -131,14 +131,14 @@ var showCmd = &cobra.Command{
|
|||||||
allDetails = append(allDetails, details)
|
allDetails = append(allDetails, details)
|
||||||
} else {
|
} else {
|
||||||
if displayIdx > 0 {
|
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)
|
// Tufte-aligned header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||||
fmt.Printf("Status: %s\n", issue.Status)
|
fmt.Printf("\n%s\n", formatIssueHeader(issue))
|
||||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
// Metadata: Owner · Type | Created · Updated
|
||||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
fmt.Println(formatIssueMetadata(issue))
|
||||||
if issue.Description != "" {
|
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()
|
fmt.Println()
|
||||||
displayIdx++
|
displayIdx++
|
||||||
@@ -188,54 +188,17 @@ var showCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if displayIdx > 0 {
|
if displayIdx > 0 {
|
||||||
fmt.Println("\n" + strings.Repeat("─", 60))
|
fmt.Println("\n" + ui.RenderMuted(strings.Repeat("─", 60)))
|
||||||
}
|
}
|
||||||
displayIdx++
|
displayIdx++
|
||||||
|
|
||||||
// Format output (same as direct mode below)
|
// Tufte-aligned header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||||
tierEmoji := ""
|
fmt.Printf("\n%s\n", formatIssueHeader(issue))
|
||||||
statusSuffix := ""
|
|
||||||
switch issue.CompactionLevel {
|
|
||||||
case 1:
|
|
||||||
tierEmoji = " 🗜️"
|
|
||||||
statusSuffix = " (compacted L1)"
|
|
||||||
case 2:
|
|
||||||
tierEmoji = " 📦"
|
|
||||||
statusSuffix = " (compacted L2)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
|
// Metadata: Owner · Type | Created · Updated
|
||||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
fmt.Println(formatIssueMetadata(issue))
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show compaction status
|
// Compaction info (if applicable)
|
||||||
if issue.CompactionLevel > 0 {
|
if issue.CompactionLevel > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if issue.OriginalSize > 0 {
|
if issue.OriginalSize > 0 {
|
||||||
@@ -243,47 +206,40 @@ var showCmd = &cobra.Command{
|
|||||||
saved := issue.OriginalSize - currentSize
|
saved := issue.OriginalSize - currentSize
|
||||||
if saved > 0 {
|
if saved > 0 {
|
||||||
reduction := float64(saved) / float64(issue.OriginalSize) * 100
|
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)
|
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 != "" {
|
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 != "" {
|
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 != "" {
|
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 != "" {
|
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 {
|
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 {
|
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 {
|
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 {
|
if len(details.Dependents) > 0 {
|
||||||
// Group by dependency type for clarity
|
|
||||||
var blocks, children, related, discovered []*types.IssueWithDependencyMetadata
|
var blocks, children, related, discovered []*types.IssueWithDependencyMetadata
|
||||||
for _, dep := range details.Dependents {
|
for _, dep := range details.Dependents {
|
||||||
switch dep.DependencyType {
|
switch dep.DependencyType {
|
||||||
@@ -301,35 +257,35 @@ var showCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(children) > 0 {
|
if len(children) > 0 {
|
||||||
fmt.Printf("\nChildren (%d):\n", len(children))
|
fmt.Printf("\n%s\n", ui.RenderBold("CHILDREN"))
|
||||||
for _, dep := range 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 {
|
if len(blocks) > 0 {
|
||||||
fmt.Printf("\nBlocks (%d):\n", len(blocks))
|
fmt.Printf("\n%s\n", ui.RenderBold("BLOCKS"))
|
||||||
for _, dep := range 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 {
|
if len(related) > 0 {
|
||||||
fmt.Printf("\nRelated (%d):\n", len(related))
|
fmt.Printf("\n%s\n", ui.RenderBold("RELATED"))
|
||||||
for _, dep := range 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 {
|
if len(discovered) > 0 {
|
||||||
fmt.Printf("\nDiscovered (%d):\n", len(discovered))
|
fmt.Printf("\n%s\n", ui.RenderBold("DISCOVERED"))
|
||||||
for _, dep := range 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 {
|
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 {
|
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")
|
commentLines := strings.Split(comment.Text, "\n")
|
||||||
for _, line := range commentLines {
|
for _, line := range commentLines {
|
||||||
fmt.Printf(" %s\n", line)
|
fmt.Printf(" %s\n", line)
|
||||||
@@ -418,102 +374,55 @@ var showCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
fmt.Println("\n" + strings.Repeat("─", 60))
|
fmt.Println("\n" + ui.RenderMuted(strings.Repeat("─", 60)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add compaction emoji to title line
|
// Tufte-aligned header: STATUS_ICON ID · Title [Priority · STATUS]
|
||||||
tierEmoji := ""
|
fmt.Printf("\n%s\n", formatIssueHeader(issue))
|
||||||
statusSuffix := ""
|
|
||||||
switch issue.CompactionLevel {
|
|
||||||
case 1:
|
|
||||||
tierEmoji = " 🗜️"
|
|
||||||
statusSuffix = " (compacted L1)"
|
|
||||||
case 2:
|
|
||||||
tierEmoji = " 📦"
|
|
||||||
statusSuffix = " (compacted L2)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
|
// Metadata: Owner · Type | Created · Updated
|
||||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
fmt.Println(formatIssueMetadata(issue))
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show compaction status footer
|
// Compaction info (if applicable)
|
||||||
if issue.CompactionLevel > 0 {
|
if issue.CompactionLevel > 0 {
|
||||||
tierEmoji := "🗜️"
|
|
||||||
if issue.CompactionLevel == 2 {
|
|
||||||
tierEmoji = "📦"
|
|
||||||
}
|
|
||||||
tierName := fmt.Sprintf("Tier %d", issue.CompactionLevel)
|
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if issue.OriginalSize > 0 {
|
if issue.OriginalSize > 0 {
|
||||||
currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
|
currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
|
||||||
saved := issue.OriginalSize - currentSize
|
saved := issue.OriginalSize - currentSize
|
||||||
if saved > 0 {
|
if saved > 0 {
|
||||||
reduction := float64(saved) / float64(issue.OriginalSize) * 100
|
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)
|
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 != "" {
|
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 != "" {
|
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 != "" {
|
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 != "" {
|
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
|
// Show labels
|
||||||
labels, _ := issueStore.GetLabels(ctx, issue.ID)
|
labels, _ := issueStore.GetLabels(ctx, issue.ID)
|
||||||
if len(labels) > 0 {
|
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)
|
deps, _ := issueStore.GetDependencies(ctx, issue.ID)
|
||||||
if len(deps) > 0 {
|
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 {
|
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 {
|
if len(children) > 0 {
|
||||||
fmt.Printf("\nChildren (%d):\n", len(children))
|
fmt.Printf("\n%s\n", ui.RenderBold("CHILDREN"))
|
||||||
for _, dep := range 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 {
|
if len(blocks) > 0 {
|
||||||
fmt.Printf("\nBlocks (%d):\n", len(blocks))
|
fmt.Printf("\n%s\n", ui.RenderBold("BLOCKS"))
|
||||||
for _, dep := range 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 {
|
if len(related) > 0 {
|
||||||
fmt.Printf("\nRelated (%d):\n", len(related))
|
fmt.Printf("\n%s\n", ui.RenderBold("RELATED"))
|
||||||
for _, dep := range 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 {
|
if len(discovered) > 0 {
|
||||||
fmt.Printf("\nDiscovered (%d):\n", len(discovered))
|
fmt.Printf("\n%s\n", ui.RenderBold("DISCOVERED"))
|
||||||
for _, dep := range 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
|
// Fallback for non-SQLite storage
|
||||||
dependents, _ := issueStore.GetDependents(ctx, issue.ID)
|
dependents, _ := issueStore.GetDependents(ctx, issue.ID)
|
||||||
if len(dependents) > 0 {
|
if len(dependents) > 0 {
|
||||||
fmt.Printf("\nBlocks (%d):\n", len(dependents))
|
fmt.Printf("\n%s\n", ui.RenderBold("BLOCKS"))
|
||||||
for _, dep := range dependents {
|
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
|
// Show comments
|
||||||
comments, _ := issueStore.GetIssueComments(ctx, issue.ID)
|
comments, _ := issueStore.GetIssueComments(ctx, issue.ID)
|
||||||
if len(comments) > 0 {
|
if len(comments) > 0 {
|
||||||
fmt.Printf("\nComments (%d):\n", len(comments))
|
fmt.Printf("\n%s\n", ui.RenderBold("COMMENTS"))
|
||||||
for _, comment := range 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
|
// 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 {
|
func formatShortIssue(issue *types.Issue) string {
|
||||||
return fmt.Sprintf("%s [%s] P%d %s: %s",
|
statusIcon := ui.RenderStatusIcon(string(issue.Status))
|
||||||
issue.ID, issue.Status, issue.Priority, issue.IssueType, issue.Title)
|
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
|
// 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
|
// 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) {
|
func displayRefGroup(depType types.DependencyType, refs []*types.IssueWithDependencyMetadata) {
|
||||||
// Get emoji for type
|
// Get emoji for type
|
||||||
emoji := getRefTypeEmoji(depType)
|
emoji := getRefTypeEmoji(depType)
|
||||||
fmt.Printf("\n %s %s (%d):\n", emoji, depType, len(refs))
|
fmt.Printf("\n %s %s (%d):\n", emoji, depType, len(refs))
|
||||||
|
|
||||||
for _, ref := range 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
|
var idStr string
|
||||||
switch ref.Status {
|
switch ref.Status {
|
||||||
case types.StatusOpen:
|
case types.StatusOpen:
|
||||||
@@ -762,8 +853,6 @@ func displayRefGroup(depType types.DependencyType, refs []*types.IssueWithDepend
|
|||||||
idStr = ui.StatusInProgressStyle.Render(ref.ID)
|
idStr = ui.StatusInProgressStyle.Render(ref.ID)
|
||||||
case types.StatusBlocked:
|
case types.StatusBlocked:
|
||||||
idStr = ui.StatusBlockedStyle.Render(ref.ID)
|
idStr = ui.StatusBlockedStyle.Render(ref.ID)
|
||||||
case types.StatusClosed:
|
|
||||||
idStr = ui.StatusClosedStyle.Render(ref.ID)
|
|
||||||
default:
|
default:
|
||||||
idStr = ref.ID
|
idStr = ref.ID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ func TestShow_ExternalRef(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := string(showOut)
|
out := string(showOut)
|
||||||
if !strings.Contains(out, "External Ref:") {
|
if !strings.Contains(out, "External:") {
|
||||||
t.Errorf("expected 'External Ref:' in output, got: %s", out)
|
t.Errorf("expected 'External:' in output, got: %s", out)
|
||||||
}
|
}
|
||||||
if !strings.Contains(out, "https://example.com/spec.md") {
|
if !strings.Contains(out, "https://example.com/spec.md") {
|
||||||
t.Errorf("expected external ref URL in output, got: %s", out)
|
t.Errorf("expected external ref URL in output, got: %s", out)
|
||||||
@@ -108,7 +108,7 @@ func TestShow_NoExternalRef(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := string(showOut)
|
out := string(showOut)
|
||||||
if strings.Contains(out, "External Ref:") {
|
if strings.Contains(out, "External:") {
|
||||||
t.Errorf("expected no 'External Ref:' line for issue without external ref, got: %s", out)
|
t.Errorf("expected no 'External:' line for issue without external ref, got: %s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pkgs.buildGoModule {
|
|||||||
subPackages = [ "cmd/bd" ];
|
subPackages = [ "cmd/bd" ];
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
# Go module dependencies hash - if build fails with hash mismatch, update with the "got:" value
|
# Go module dependencies hash - if build fails with hash mismatch, update with the "got:" value
|
||||||
vendorHash = "sha256-l3ctY2hGXskM8U1wLupyvFDWfJu8nCX5tWAH1Macavw=";
|
vendorHash = "sha256-pY5m5ODRgqghyELRwwxOr+xlW41gtJWLXaW53GlLaFw=";
|
||||||
|
|
||||||
# Git is required for tests
|
# Git is required for tests
|
||||||
nativeBuildInputs = [ pkgs.git ];
|
nativeBuildInputs = [ pkgs.git ];
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -8,7 +8,7 @@ require (
|
|||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/anthropics/anthropic-sdk-go v1.19.0
|
github.com/anthropics/anthropic-sdk-go v1.19.0
|
||||||
github.com/charmbracelet/huh v0.8.0
|
github.com/charmbracelet/huh v0.8.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/ncruces/go-sqlite3 v0.30.4
|
github.com/ncruces/go-sqlite3 v0.30.4
|
||||||
@@ -26,29 +26,37 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlekSi/pointer v1.0.0 // indirect
|
github.com/AlekSi/pointer v1.0.0 // indirect
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/catppuccin/go v0.3.0 // indirect
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/glamour v0.10.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gofrs/flock v0.13.0 // indirect
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/ncruces/julianday v1.0.0 // indirect
|
github.com/ncruces/julianday v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
@@ -64,7 +72,10 @@ require (
|
|||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
|
|||||||
27
go.sum
27
go.sum
@@ -4,6 +4,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk
|
|||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE=
|
github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
@@ -12,6 +14,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||||
@@ -20,10 +24,14 @@ github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGs
|
|||||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||||
|
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||||
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
||||||
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
@@ -34,6 +42,8 @@ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9
|
|||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
@@ -47,6 +57,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
@@ -61,6 +73,8 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
|||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -73,14 +87,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA=
|
github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA=
|
||||||
@@ -95,6 +114,7 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -134,12 +154,19 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
|||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
54
internal/ui/markdown.go
Normal file
54
internal/ui/markdown.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Package ui provides terminal styling for beads CLI output.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/glamour"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderMarkdown renders markdown text using glamour with beads theme colors.
|
||||||
|
// Returns the rendered markdown or the original text if rendering fails.
|
||||||
|
// Word wraps at terminal width (or 80 columns if width can't be detected).
|
||||||
|
func RenderMarkdown(markdown string) string {
|
||||||
|
// Skip glamour in agent mode to keep output clean for parsing
|
||||||
|
if IsAgentMode() {
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip glamour if colors are disabled
|
||||||
|
if !ShouldUseColor() {
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect terminal width for word wrap
|
||||||
|
// Cap at 100 chars for readability - wider lines cause eye-tracking fatigue
|
||||||
|
// Typography research suggests 50-75 chars optimal, 80-100 comfortable max
|
||||||
|
const maxReadableWidth = 100
|
||||||
|
wrapWidth := 80 // default if terminal size unavailable
|
||||||
|
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
|
||||||
|
wrapWidth = w
|
||||||
|
}
|
||||||
|
if wrapWidth > maxReadableWidth {
|
||||||
|
wrapWidth = maxReadableWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create renderer with auto-detected style (respects terminal light/dark mode)
|
||||||
|
renderer, err := glamour.NewTermRenderer(
|
||||||
|
glamour.WithAutoStyle(),
|
||||||
|
glamour.WithWordWrap(wrapWidth),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// fallback to raw markdown on error
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered, err := renderer.Render(markdown)
|
||||||
|
if err != nil {
|
||||||
|
// fallback to raw markdown on error
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
return rendered
|
||||||
|
}
|
||||||
@@ -12,9 +12,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Disable colors when not appropriate (non-TTY, NO_COLOR, etc.)
|
|
||||||
if !ShouldUseColor() {
|
if !ShouldUseColor() {
|
||||||
|
// Disable colors when not appropriate (non-TTY, NO_COLOR, etc.)
|
||||||
lipgloss.SetColorProfile(termenv.Ascii)
|
lipgloss.SetColorProfile(termenv.Ascii)
|
||||||
|
} else {
|
||||||
|
// Use TrueColor for distinct priority/status colors in modern terminals
|
||||||
|
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,25 +93,26 @@ var (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Priority Colors ===
|
// === Priority Colors ===
|
||||||
// Only P0/P1 get color - P2/P3/P4 match standard text
|
// Only P0/P1 get color - they need attention
|
||||||
|
// P2/P3/P4 are neutral (medium/low/backlog don't need visual urgency)
|
||||||
ColorPriorityP0 = lipgloss.AdaptiveColor{
|
ColorPriorityP0 = lipgloss.AdaptiveColor{
|
||||||
Light: "#f07171", // bright red - critical
|
Light: "#f07171", // bright red - critical, demands attention
|
||||||
Dark: "#f07178",
|
Dark: "#f07178",
|
||||||
}
|
}
|
||||||
ColorPriorityP1 = lipgloss.AdaptiveColor{
|
ColorPriorityP1 = lipgloss.AdaptiveColor{
|
||||||
Light: "#ff8f40", // orange - high urgency
|
Light: "#ff8f40", // orange - high priority, needs attention soon
|
||||||
Dark: "#ff8f40",
|
Dark: "#ff8f40",
|
||||||
}
|
}
|
||||||
ColorPriorityP2 = lipgloss.AdaptiveColor{
|
ColorPriorityP2 = lipgloss.AdaptiveColor{
|
||||||
Light: "", // standard text color
|
Light: "#e6b450", // muted gold - medium priority, visible but calm
|
||||||
Dark: "",
|
Dark: "#e6b450",
|
||||||
}
|
}
|
||||||
ColorPriorityP3 = lipgloss.AdaptiveColor{
|
ColorPriorityP3 = lipgloss.AdaptiveColor{
|
||||||
Light: "", // standard text color
|
Light: "", // neutral - low priority
|
||||||
Dark: "",
|
Dark: "",
|
||||||
}
|
}
|
||||||
ColorPriorityP4 = lipgloss.AdaptiveColor{
|
ColorPriorityP4 = lipgloss.AdaptiveColor{
|
||||||
Light: "", // standard text color
|
Light: "", // neutral - backlog
|
||||||
Dark: "",
|
Dark: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +203,87 @@ const (
|
|||||||
IconInfo = "ℹ"
|
IconInfo = "ℹ"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Issue status icons - used consistently across all commands
|
||||||
|
// Design principle: icons > text labels for scannability
|
||||||
|
// IMPORTANT: Use small Unicode symbols, NOT emoji-style icons (🔴🟠 etc.)
|
||||||
|
// Emoji blobs cause cognitive overload and break visual consistency
|
||||||
|
const (
|
||||||
|
StatusIconOpen = "○" // available to work (hollow circle)
|
||||||
|
StatusIconInProgress = "◐" // active work (half-filled)
|
||||||
|
StatusIconBlocked = "●" // needs attention (filled circle)
|
||||||
|
StatusIconClosed = "✓" // completed (checkmark)
|
||||||
|
StatusIconDeferred = "❄" // scheduled for later (snowflake)
|
||||||
|
StatusIconPinned = "📌" // elevated priority
|
||||||
|
)
|
||||||
|
|
||||||
|
// Priority icon - small filled circle, colored by priority level
|
||||||
|
// IMPORTANT: Use this small circle, NOT emoji blobs (🔴🟠🟡🔵⚪)
|
||||||
|
const PriorityIcon = "●"
|
||||||
|
|
||||||
|
// RenderStatusIcon returns the appropriate icon for a status with semantic coloring
|
||||||
|
// This is the canonical source for status icon rendering - use this everywhere
|
||||||
|
func RenderStatusIcon(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "open":
|
||||||
|
return StatusIconOpen // no color - available but not urgent
|
||||||
|
case "in_progress":
|
||||||
|
return StatusInProgressStyle.Render(StatusIconInProgress)
|
||||||
|
case "blocked":
|
||||||
|
return StatusBlockedStyle.Render(StatusIconBlocked)
|
||||||
|
case "closed":
|
||||||
|
return StatusClosedStyle.Render(StatusIconClosed)
|
||||||
|
case "deferred":
|
||||||
|
return MutedStyle.Render(StatusIconDeferred)
|
||||||
|
case "pinned":
|
||||||
|
return StatusPinnedStyle.Render(StatusIconPinned)
|
||||||
|
default:
|
||||||
|
return "?" // unknown status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusIcon returns just the icon character without styling
|
||||||
|
// Useful when you need to apply custom styling or for non-TTY output
|
||||||
|
func GetStatusIcon(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "open":
|
||||||
|
return StatusIconOpen
|
||||||
|
case "in_progress":
|
||||||
|
return StatusIconInProgress
|
||||||
|
case "blocked":
|
||||||
|
return StatusIconBlocked
|
||||||
|
case "closed":
|
||||||
|
return StatusIconClosed
|
||||||
|
case "deferred":
|
||||||
|
return StatusIconDeferred
|
||||||
|
case "pinned":
|
||||||
|
return StatusIconPinned
|
||||||
|
default:
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusStyle returns the lipgloss style for a given status
|
||||||
|
// Use this when you need to apply the semantic color to custom text
|
||||||
|
// Example: ui.GetStatusStyle("in_progress").Render(myCustomText)
|
||||||
|
func GetStatusStyle(status string) lipgloss.Style {
|
||||||
|
switch status {
|
||||||
|
case "in_progress":
|
||||||
|
return StatusInProgressStyle
|
||||||
|
case "blocked":
|
||||||
|
return StatusBlockedStyle
|
||||||
|
case "closed":
|
||||||
|
return StatusClosedStyle
|
||||||
|
case "deferred":
|
||||||
|
return MutedStyle
|
||||||
|
case "pinned":
|
||||||
|
return StatusPinnedStyle
|
||||||
|
case "hooked":
|
||||||
|
return StatusHookedStyle
|
||||||
|
default: // open and others - no special styling
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tree characters for hierarchical display
|
// Tree characters for hierarchical display
|
||||||
const (
|
const (
|
||||||
TreeChild = "⎿ " // child indicator
|
TreeChild = "⎿ " // child indicator
|
||||||
@@ -299,8 +384,29 @@ func RenderStatus(status string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderPriority renders a priority level with semantic styling
|
// RenderPriority renders a priority level with semantic styling
|
||||||
|
// Format: ● P0 (icon + label)
|
||||||
// P0/P1 get color; P2/P3/P4 use standard text
|
// P0/P1 get color; P2/P3/P4 use standard text
|
||||||
func RenderPriority(priority int) string {
|
func RenderPriority(priority int) string {
|
||||||
|
label := fmt.Sprintf("%s P%d", PriorityIcon, priority)
|
||||||
|
switch priority {
|
||||||
|
case 0:
|
||||||
|
return PriorityP0Style.Render(label)
|
||||||
|
case 1:
|
||||||
|
return PriorityP1Style.Render(label)
|
||||||
|
case 2:
|
||||||
|
return PriorityP2Style.Render(label)
|
||||||
|
case 3:
|
||||||
|
return PriorityP3Style.Render(label)
|
||||||
|
case 4:
|
||||||
|
return PriorityP4Style.Render(label)
|
||||||
|
default:
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderPriorityCompact renders just the priority label without icon
|
||||||
|
// Use when space is constrained or icon would be redundant
|
||||||
|
func RenderPriorityCompact(priority int) string {
|
||||||
label := fmt.Sprintf("P%d", priority)
|
label := fmt.Sprintf("P%d", priority)
|
||||||
switch priority {
|
switch priority {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
@@ -57,16 +57,17 @@ func TestRenderStatusAndPriority(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderPriority now includes the priority icon (●)
|
||||||
priorityCases := []struct {
|
priorityCases := []struct {
|
||||||
priority int
|
priority int
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{0, PriorityP0Style.Render("P0")},
|
{0, PriorityP0Style.Render(PriorityIcon + " P0")},
|
||||||
{1, PriorityP1Style.Render("P1")},
|
{1, PriorityP1Style.Render(PriorityIcon + " P1")},
|
||||||
{2, PriorityP2Style.Render("P2")},
|
{2, PriorityP2Style.Render(PriorityIcon + " P2")},
|
||||||
{3, PriorityP3Style.Render("P3")},
|
{3, PriorityP3Style.Render(PriorityIcon + " P3")},
|
||||||
{4, PriorityP4Style.Render("P4")},
|
{4, PriorityP4Style.Render(PriorityIcon + " P4")},
|
||||||
{5, "P5"},
|
{5, PriorityIcon + " P5"},
|
||||||
}
|
}
|
||||||
for _, tc := range priorityCases {
|
for _, tc := range priorityCases {
|
||||||
if got := RenderPriority(tc.priority); got != tc.want {
|
if got := RenderPriority(tc.priority); got != tc.want {
|
||||||
@@ -74,6 +75,11 @@ func TestRenderStatusAndPriority(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderPriorityCompact returns just "P0" without icon
|
||||||
|
if got := RenderPriorityCompact(0); !strings.Contains(got, "P0") {
|
||||||
|
t.Fatalf("compact priority should contain P0, got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
if got := RenderPriorityForStatus(0, "closed"); got != "P0" {
|
if got := RenderPriorityForStatus(0, "closed"); got != "P0" {
|
||||||
t.Fatalf("closed priority should be plain text, got %q", got)
|
t.Fatalf("closed priority should be plain text, got %q", got)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user