Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 88 additions & 11 deletions cmd/tui_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,9 +797,8 @@ func (m *planModel) renderBeadLine(i int, bead beadItem) string {

// Tree indentation with connector lines (styled dim)
var treePrefix string
if bead.treeDepth > 0 {
indent := strings.Repeat(" ", bead.treeDepth-1)
treePrefix = issueTreeStyle.Render(indent + "└─")
if bead.treeDepth > 0 && bead.treePrefixPattern != "" {
treePrefix = issueTreeStyle.Render(bead.treePrefixPattern)
}

// Styled issue ID
Expand Down Expand Up @@ -1872,9 +1871,14 @@ func buildBeadTree(items []beadItem, dir string) []beadItem {
}
}

// Sort roots by priority then ID for consistent ordering
// Sort roots: closed parents first (so their open children appear under them),
// then by priority, then by ID
sort.Slice(roots, func(i, j int) bool {
a, b := itemMap[roots[i]], itemMap[roots[j]]
// Closed parents come first
if a.isClosedParent != b.isClosedParent {
return a.isClosedParent
}
if a.priority != b.priority {
return a.priority < b.priority
}
Expand All @@ -1885,8 +1889,12 @@ func buildBeadTree(items []beadItem, dir string) []beadItem {
var result []beadItem
visited := make(map[string]bool)

var visit func(id string, depth int)
visit = func(id string, depth int) {
// ancestorPattern tracks the prefix pattern for ancestor continuation lines.
// Each character represents one depth level:
// - "│" means the ancestor at that level has more siblings (needs continuation line)
// - " " means the ancestor at that level is the last child (no continuation needed)
var visit func(id string, depth int, ancestorPattern string, isLast bool)
visit = func(id string, depth int, ancestorPattern string, isLast bool) {
if visited[id] {
return
}
Expand All @@ -1898,6 +1906,28 @@ func buildBeadTree(items []beadItem, dir string) []beadItem {
}

item.treeDepth = depth
item.isLastChild = isLast

// Build the tree prefix pattern for this item
if depth > 0 {
// Start with ancestor continuation pattern (each character becomes "│ " or " ")
var prefix string
for _, c := range ancestorPattern {
if c == '│' {
prefix += "│ "
} else {
prefix += " "
}
}
// Add the connector for this item
if isLast {
prefix += "└─"
} else {
prefix += "├─"
}
item.treePrefixPattern = prefix
}

result = append(result, *item)

// Sort children by priority
Expand All @@ -1913,14 +1943,29 @@ func buildBeadTree(items []beadItem, dir string) []beadItem {
return a.id < b.id
})

for _, childID := range childIDs {
visit(childID, depth+1)
// Compute the ancestor pattern for children
// If this item is the last child, its continuation is " " (no vertical line)
// Otherwise, it's "│" (vertical line for siblings below)
var childAncestorPattern string
if depth == 0 {
// Root nodes don't add to ancestor pattern
childAncestorPattern = ancestorPattern
} else if isLast {
childAncestorPattern = ancestorPattern + " "
} else {
childAncestorPattern = ancestorPattern + "│"
}

for idx, childID := range childIDs {
isLastChild := idx == len(childIDs)-1
visit(childID, depth+1, childAncestorPattern, isLastChild)
}
}

// Visit all roots
for _, rootID := range roots {
visit(rootID, 0)
for idx, rootID := range roots {
isLastRoot := idx == len(roots)-1
visit(rootID, 0, "", isLastRoot)
}

// Add any orphaned items (not reachable from roots)
Expand All @@ -1931,7 +1976,39 @@ func buildBeadTree(items []beadItem, dir string) []beadItem {
}
}

return result
// Filter out closed parents that have no visible children directly under them.
// They were only fetched to show tree structure, but if their children
// appear under other parents, these closed parents add no value.
// We check by looking at the next items in the result - if a closed parent
// at depth N has no items at depth N+1 immediately following, it has no visible children.
var filtered []beadItem
for i, item := range result {
// Keep the item if it's not a closed parent
if !item.isClosedParent {
filtered = append(filtered, item)
continue
}
// For closed parents, check if there are children directly following
hasVisibleChild := false
expectedChildDepth := item.treeDepth + 1
for j := i + 1; j < len(result); j++ {
nextItem := result[j]
if nextItem.treeDepth <= item.treeDepth {
// We've moved past this parent's subtree
break
}
if nextItem.treeDepth == expectedChildDepth {
// Found a direct child
hasVisibleChild = true
break
}
}
if hasVisibleChild {
filtered = append(filtered, item)
}
}

return filtered
}

// updateAddChildBead handles input for the add child bead dialog
Expand Down
8 changes: 5 additions & 3 deletions cmd/tui_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,11 @@ type beadItem struct {
dependentCount int // number of issues this blocks
dependencies []string // IDs of issues that block this one
children []string // IDs of issues blocked by this one (computed from tree)
treeDepth int // depth in tree view (0 = root)
assignedWorkID string // work ID if already assigned to a work (empty = not assigned)
isClosedParent bool // true if this is a closed bead included for tree context (has visible children)
treeDepth int // depth in tree view (0 = root)
assignedWorkID string // work ID if already assigned to a work (empty = not assigned)
isClosedParent bool // true if this is a closed bead included for tree context (has visible children)
isLastChild bool // true if this bead is the last child of its parent
treePrefixPattern string // precomputed tree prefix pattern (e.g., "│ └─")
}

// beadFilters holds the current filter state for beads
Expand Down