bdtui 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/app.js +5 -0
- package/src/state.js +48 -40
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - 2026-06-14
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- The **Closed** and **All** tabs now render the epic→children tree like the other tabs; previously they showed a flat, ungrouped list with every bead at the top level. Closed children nest under their epic (parent epics that aren't themselves closed are pulled in as the grouping anchor).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Tree views now build from the `parent` field that `bd` embeds in each `bd list`/`bd ready` row instead of fetching one `bd dep list` per bead. The Closed/All tabs gain the tree at no extra per-bead subprocess cost (lazy "load more" pagination is unchanged), and the Ready/Blocked/In-progress tabs load with fewer `bd` calls. The Blocked tab keeps a per-row `bd dep list` fallback for the derived-blocked rows `bd blocked` returns without a `parent` field.
|
|
16
|
+
|
|
7
17
|
## [0.5.1] - 2026-06-14
|
|
8
18
|
|
|
9
19
|
### Fixed
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -354,6 +354,11 @@ export async function run(cwd) {
|
|
|
354
354
|
const b = state.beadsById.get(id);
|
|
355
355
|
return b?.title?.toLowerCase().includes(q);
|
|
356
356
|
});
|
|
357
|
+
// A title-search result set is no longer a tree, so flatten every surviving
|
|
358
|
+
// row to depth 0 — otherwise a matched child whose parent was filtered out
|
|
359
|
+
// renders as an orphan indented under a branch glyph (mirrors how
|
|
360
|
+
// applyTypeFilter flattens the type-filtered list).
|
|
361
|
+
state.treeMeta = new Map(state.listOrder.map((id) => [id, { depth: 0, isLast: false }]));
|
|
357
362
|
state.visibleCount = isPaged()
|
|
358
363
|
? Math.min(state.pageSize, state.listOrder.length)
|
|
359
364
|
: state.listOrder.length;
|
package/src/state.js
CHANGED
|
@@ -22,8 +22,8 @@ export const state = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
// The "all" and "closed" tabs (no epic filter) can hold huge numbers of beads,
|
|
25
|
-
// so they
|
|
26
|
-
//
|
|
25
|
+
// so they reveal rows lazily in pageSize batches; every other view renders its
|
|
26
|
+
// rows all at once. All tabs build the same epic→children tree (see loadList).
|
|
27
27
|
export function isPaged() {
|
|
28
28
|
return (state.filter === 'all' || state.filter === 'closed') && !state.epicFilter;
|
|
29
29
|
}
|
|
@@ -80,22 +80,33 @@ export function applyTypeFilter() {
|
|
|
80
80
|
|
|
81
81
|
function buildTreeOrder(beads, parentOf) {
|
|
82
82
|
const byId = new Map(beads.map((b) => [b.id, b]));
|
|
83
|
-
|
|
83
|
+
// A bead is a root when it has no parent present in this bead set (epics and
|
|
84
|
+
// standalone beads). The tree is intentionally two levels deep — epics on top,
|
|
85
|
+
// their direct children indented under them.
|
|
86
|
+
const isRoot = (id) => {
|
|
87
|
+
const pid = parentOf.get(id);
|
|
88
|
+
return !pid || !byId.has(pid);
|
|
89
|
+
};
|
|
84
90
|
|
|
91
|
+
const childrenOf = new Map();
|
|
92
|
+
const roots = [];
|
|
85
93
|
for (const b of beads) {
|
|
86
94
|
const pid = parentOf.get(b.id);
|
|
87
|
-
|
|
95
|
+
// Nest a bead only when its parent is itself a root. Anything else — no
|
|
96
|
+
// parent, a parent filtered out of this view, or a parent that is itself
|
|
97
|
+
// nested (deeper than two levels) — renders at the top level so no bead is
|
|
98
|
+
// ever silently dropped from the list.
|
|
99
|
+
if (pid && byId.has(pid) && isRoot(pid)) {
|
|
88
100
|
if (!childrenOf.has(pid)) childrenOf.set(pid, []);
|
|
89
101
|
childrenOf.get(pid).push(b);
|
|
102
|
+
} else {
|
|
103
|
+
roots.push(b);
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
for (const children of childrenOf.values()) {
|
|
94
108
|
children.sort(byUpdatedDesc);
|
|
95
109
|
}
|
|
96
|
-
|
|
97
|
-
const childIds = new Set([...childrenOf.values()].flat().map((b) => b.id));
|
|
98
|
-
const roots = beads.filter((b) => !childIds.has(b.id));
|
|
99
110
|
roots.sort(byUpdatedDesc);
|
|
100
111
|
|
|
101
112
|
const ordered = [];
|
|
@@ -110,50 +121,47 @@ function buildTreeOrder(beads, parentOf) {
|
|
|
110
121
|
return ordered;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
// Flat load for the paged tabs ("all" / "closed"). Skips the per-bead
|
|
114
|
-
// parent-child dep fetch (which spawns one bd subprocess per bead) and the tree
|
|
115
|
-
// build, so even very large repos load in a single bd call; the UI reveals rows
|
|
116
|
-
// in pageSize batches.
|
|
117
|
-
async function loadListFlat() {
|
|
118
|
-
const [beads, blocked] = await Promise.all([
|
|
119
|
-
bdList(state.filter, state.cwd, { unlimited: true }),
|
|
120
|
-
bdList('blocked', state.cwd).catch(() => []),
|
|
121
|
-
]);
|
|
122
|
-
state.blockedIds = new Set((blocked || []).map((b) => b.id));
|
|
123
|
-
for (const b of beads) {
|
|
124
|
-
state.beadsById.set(b.id, { ...state.beadsById.get(b.id), ...b });
|
|
125
|
-
}
|
|
126
|
-
beads.sort(byUpdatedDesc);
|
|
127
|
-
state.fullTreeItems = beads.map((b) => ({ id: b.id, depth: 0, isLast: false }));
|
|
128
|
-
applyTypeFilter();
|
|
129
|
-
return beads;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
124
|
export async function loadList() {
|
|
133
|
-
|
|
125
|
+
const paged = isPaged();
|
|
134
126
|
const [beads, blocked] = await Promise.all([
|
|
135
|
-
|
|
127
|
+
// Paged tabs (all / closed) lift bd's 50-row cap; rows are still revealed in
|
|
128
|
+
// pageSize batches by the renderer.
|
|
129
|
+
bdList(state.filter, state.cwd, { unlimited: paged }),
|
|
136
130
|
bdList('blocked', state.cwd).catch(() => []),
|
|
137
131
|
]);
|
|
138
132
|
state.blockedIds = new Set((blocked || []).map((b) => b.id));
|
|
139
133
|
const byId = new Map(beads.map((b) => [b.id, b]));
|
|
140
134
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
candidates.map((b) => bdDepListDown(b.id, state.cwd))
|
|
146
|
-
);
|
|
147
|
-
|
|
135
|
+
// Parent-child relationships come straight from the `parent` field bd embeds in
|
|
136
|
+
// every list row (the target of the bead's parent-child dependency). This is
|
|
137
|
+
// the same data `bd dep list` returns, so the tree builds from the single
|
|
138
|
+
// `bd list` call above — no per-bead subprocess fan-out, even on huge repos.
|
|
148
139
|
const parentOf = new Map();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
for (const b of beads) {
|
|
141
|
+
if (b.parent && b.parent !== b.id) parentOf.set(b.id, b.parent);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The Blocked tab merges in `bd blocked` (derived-blocked) rows, and that
|
|
145
|
+
// command — unlike `bd list` — omits the `parent` field entirely. Recover the
|
|
146
|
+
// parent for those rows with a dep-list lookup. Scoped to the blocked filter
|
|
147
|
+
// (the only parent-less source) and to non-epic rows without an embedded
|
|
148
|
+
// parent, so it stays a handful of calls on an already-small view.
|
|
149
|
+
if (state.filter === 'blocked') {
|
|
150
|
+
const needsLookup = beads.filter((b) => !('parent' in b) && b.issue_type !== 'epic');
|
|
151
|
+
const depLists = await Promise.all(
|
|
152
|
+
needsLookup.map((b) => bdDepListDown(b.id, state.cwd))
|
|
153
|
+
);
|
|
154
|
+
needsLookup.forEach((b, i) => {
|
|
155
|
+
const pc = depLists[i].find((d) => d.dependency_type === 'parent-child');
|
|
156
|
+
if (pc?.id && pc.id !== b.id) parentOf.set(b.id, pc.id);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
153
159
|
|
|
154
160
|
// Bring in parent epics that aren't already in the filtered list so the tree
|
|
155
161
|
// can root under them. Only add the epic itself, NOT its other children —
|
|
156
|
-
// that's what caused closed beads to leak into ready/open filters.
|
|
162
|
+
// that's what caused closed beads to leak into ready/open filters. Skipped for
|
|
163
|
+
// "all", which already contains every bead. The fetch is bounded by the number
|
|
164
|
+
// of distinct missing parents (≈ epic count), not the bead count.
|
|
157
165
|
if (state.filter !== 'all') {
|
|
158
166
|
const missingParents = [...new Set(parentOf.values())].filter((id) => !byId.has(id));
|
|
159
167
|
const fetched = await Promise.all(
|