bdtui 0.7.2 → 0.8.1

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.
@@ -1,255 +1,269 @@
1
- import blessed from 'blessed';
2
-
3
- function listPicker(screen, label, items, borderColor = 'cyan', width = 36, left = 'center') {
4
- return new Promise((resolve, reject) => {
5
- const height = Math.min(items.length + 4, 20);
6
- const picker = blessed.list({
7
- parent: screen,
8
- label: ` ${label} `,
9
- border: { type: 'line' },
10
- top: 'center',
11
- left,
12
- width,
13
- height,
14
- items,
15
- keys: true,
16
- vi: true,
17
- tags: true,
18
- style: {
19
- selected: { bg: 'blue', fg: 'white', bold: true },
20
- border: { fg: borderColor },
21
- label: { fg: borderColor, bold: true },
22
- },
23
- });
24
-
25
- picker.focus();
26
- screen.render();
27
-
28
- picker.once('select', (item) => {
29
- picker.destroy();
30
- screen.render();
31
- resolve(item.content.trim());
32
- });
33
-
34
- picker.key(['escape', 'q', 'h'], () => {
35
- picker.destroy();
36
- screen.render();
37
- reject(new Error('cancelled'));
38
- });
39
- });
40
- }
41
-
42
- export function statusPicker(screen) {
43
- return listPicker(
44
- screen,
45
- 'Change Status',
46
- ['open', 'in_progress', 'blocked', 'deferred', 'closed'],
47
- 'cyan',
48
- );
49
- }
50
-
51
- export function priorityPicker(screen) {
52
- return listPicker(
53
- screen,
54
- 'Change Priority',
55
- ['0 — Critical', '1 — High', '2 — Normal', '3 — Low', '4 — Trivial'],
56
- 'yellow',
57
- ).then((val) => parseInt(val[0], 10));
58
- }
59
-
60
- export function textPrompt(screen, label, defaultVal = '') {
61
- return new Promise((resolve, reject) => {
62
- const prompt = blessed.prompt({
63
- parent: screen,
64
- border: 'line',
65
- height: 'shrink',
66
- width: '60%',
67
- top: 'center',
68
- left: 'center',
69
- label: ` ${label} `,
70
- tags: true,
71
- keys: true,
72
- vi: true,
73
- style: {
74
- border: { fg: 'green' },
75
- label: { fg: 'green', bold: true },
76
- },
77
- });
78
-
79
- prompt.input(`${label}:`, defaultVal, (err, value) => {
80
- prompt.destroy();
81
- screen.render();
82
- if (err || value == null) return reject(new Error('cancelled'));
83
- resolve(value.trim());
84
- });
85
-
86
- screen.render();
87
- });
88
- }
89
-
90
- const CLEAR_FILTER_TAG = '__clear__';
91
-
92
- export function epicPicker(screen, epics, currentEpicId = null) {
93
- return new Promise((resolve, reject) => {
94
- // Newest epics on top: sort by created_at descending (ISO-8601 UTC strings
95
- // sort lexicographically in chronological order). Fall back to id for ties
96
- // or missing timestamps.
97
- const sorted = [...epics].sort((a, b) => {
98
- const ta = a.created_at || '';
99
- const tb = b.created_at || '';
100
- if (ta !== tb) return tb.localeCompare(ta);
101
- return b.id.localeCompare(a.id);
102
- });
103
- const rows = [];
104
- const ids = [];
105
-
106
- if (currentEpicId) {
107
- rows.push('{yellow-fg}✗ Clear epic filter{/}');
108
- ids.push(CLEAR_FILTER_TAG);
109
- }
110
-
111
- for (const e of sorted) {
112
- const marker = e.id === currentEpicId ? '{cyan-fg}●{/} ' : ' ';
113
- const id = e.id.padEnd(12);
114
- const title = (e.title || '').slice(0, 60);
115
- rows.push(`${marker}{gray-fg}${id}{/} ${title}`);
116
- ids.push(e.id);
117
- }
118
-
119
- if (rows.length === 0) {
120
- rows.push('{gray-fg}(no epics found){/}');
121
- ids.push(null);
122
- }
123
-
124
- const width = 72;
125
- const height = Math.min(rows.length + 4, Math.max(8, screen.height - 4));
126
-
127
- const picker = blessed.list({
128
- parent: screen,
129
- label: ' Filter by Epic ',
130
- border: { type: 'line' },
131
- top: 'center',
132
- left: 'center',
133
- width,
134
- height,
135
- items: rows,
136
- keys: true,
137
- vi: true,
138
- tags: true,
139
- scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'magenta' } },
140
- style: {
141
- selected: { bg: 'blue', fg: 'white', bold: true },
142
- border: { fg: 'magenta' },
143
- label: { fg: 'magenta', bold: true },
144
- },
145
- });
146
-
147
- // Select current epic by default if set
148
- if (currentEpicId) {
149
- const idx = ids.indexOf(currentEpicId);
150
- if (idx >= 0) picker.select(idx);
151
- }
152
-
153
- picker.focus();
154
- screen.render();
155
-
156
- picker.once('select', (_item, index) => {
157
- const picked = ids[index];
158
- picker.destroy();
159
- screen.render();
160
- if (picked === null) return reject(new Error('cancelled'));
161
- resolve(picked === CLEAR_FILTER_TAG ? null : picked);
162
- });
163
-
164
- picker.key(['escape', 'q'], () => {
165
- picker.destroy();
166
- screen.render();
167
- reject(new Error('cancelled'));
168
- });
169
-
170
- picker.key(['x'], () => {
171
- picker.destroy();
172
- screen.render();
173
- resolve(null);
174
- });
175
- });
176
- }
177
-
178
- export function skillPicker(screen, beadId, issueType) {
179
- // Epics get the epic-level executor (runs every ready child sequentially on a
180
- // single branch); tasks/chores/bugs get the per-bead executors.
181
- const items = issueType === 'epic'
182
- ? [
183
- `/executor-epic-sequential ${beadId}`,
184
- `/executor-epic-sequential-worktree ${beadId}`,
185
- ]
186
- : [
187
- `/executor-task ${beadId}`,
188
- `/executor-task-worktree ${beadId}`,
189
- `/executor-epic-task ${beadId}`,
190
- `/executor-epic-task-worktree ${beadId}`,
191
- `/executor-rework-in-place ${beadId}`,
192
- ];
193
- return listPicker(screen, 'Copy workflow command', items, 'magenta', 56, 1);
194
- }
195
-
196
- export function parentPicker(screen, epics, { selfId, currentParentIds = [] } = {}) {
197
- return new Promise((resolve, reject) => {
198
- const exclude = new Set([selfId, ...currentParentIds]);
199
- const sorted = [...epics]
200
- .filter((e) => !exclude.has(e.id))
201
- .sort((a, b) => {
202
- const ta = a.updated_at || '', tb = b.updated_at || '';
203
- if (ta !== tb) return tb.localeCompare(ta);
204
- return b.id.localeCompare(a.id);
205
- });
206
-
207
- const rows = ['{yellow-fg}Standalone bead{/}'];
208
- const ids = [null];
209
-
210
- for (const e of sorted) {
211
- const id = e.id.padEnd(12);
212
- const title = (e.title || '').slice(0, 60);
213
- rows.push(`{gray-fg}${id}{/} ${title}`);
214
- ids.push(e.id);
215
- }
216
-
217
- const width = 72;
218
- const height = Math.min(rows.length + 4, Math.max(8, screen.height - 4));
219
-
220
- const picker = blessed.list({
221
- parent: screen,
222
- label: ' Change Parent ',
223
- border: { type: 'line' },
224
- top: 'center',
225
- left: 'center',
226
- width,
227
- height,
228
- items: rows,
229
- keys: true,
230
- vi: true,
231
- tags: true,
232
- scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'magenta' } },
233
- style: {
234
- selected: { bg: 'blue', fg: 'white', bold: true },
235
- border: { fg: 'magenta' },
236
- label: { fg: 'magenta', bold: true },
237
- },
238
- });
239
-
240
- picker.focus();
241
- screen.render();
242
-
243
- picker.once('select', (_item, index) => {
244
- picker.destroy();
245
- screen.render();
246
- resolve(ids[index]);
247
- });
248
-
249
- picker.key(['escape', 'q'], () => {
250
- picker.destroy();
251
- screen.render();
252
- reject(new Error('cancelled'));
253
- });
254
- });
255
- }
1
+ import blessed from 'blessed';
2
+
3
+ // `headers` holds indexes of non-selectable section-title rows: Enter on one is
4
+ // ignored, and the cursor starts on the first real item.
5
+ function listPicker(screen, label, items, borderColor = 'cyan', width = 36, left = 'center', headers = new Set()) {
6
+ return new Promise((resolve, reject) => {
7
+ const height = Math.min(items.length + 4, 20);
8
+ const picker = blessed.list({
9
+ parent: screen,
10
+ label: ` ${label} `,
11
+ border: { type: 'line' },
12
+ top: 'center',
13
+ left,
14
+ width,
15
+ height,
16
+ items,
17
+ keys: true,
18
+ vi: true,
19
+ tags: true,
20
+ style: {
21
+ selected: { bg: 'blue', fg: 'white', bold: true },
22
+ border: { fg: borderColor },
23
+ label: { fg: borderColor, bold: true },
24
+ },
25
+ });
26
+
27
+ const first = items.findIndex((_, i) => !headers.has(i));
28
+ if (first > 0) picker.select(first);
29
+ picker.focus();
30
+ screen.render();
31
+
32
+ picker.on('select', (item, index) => {
33
+ if (headers.has(index)) return;
34
+ picker.destroy();
35
+ screen.render();
36
+ // Strip color tags so callers get the plain text (e.g. the command to copy).
37
+ resolve(blessed.helpers.cleanTags(item.content));
38
+ });
39
+
40
+ picker.key(['escape', 'q', 'h'], () => {
41
+ picker.destroy();
42
+ screen.render();
43
+ reject(new Error('cancelled'));
44
+ });
45
+ });
46
+ }
47
+
48
+ export function statusPicker(screen) {
49
+ return listPicker(
50
+ screen,
51
+ 'Change Status',
52
+ ['open', 'in_progress', 'blocked', 'deferred', 'closed'],
53
+ 'cyan',
54
+ );
55
+ }
56
+
57
+ export function priorityPicker(screen) {
58
+ return listPicker(
59
+ screen,
60
+ 'Change Priority',
61
+ ['0 — Critical', '1 — High', '2 — Normal', '3 — Low', '4 — Trivial'],
62
+ 'yellow',
63
+ ).then((val) => parseInt(val[0], 10));
64
+ }
65
+
66
+ export function textPrompt(screen, label, defaultVal = '') {
67
+ return new Promise((resolve, reject) => {
68
+ const prompt = blessed.prompt({
69
+ parent: screen,
70
+ border: 'line',
71
+ height: 'shrink',
72
+ width: '60%',
73
+ top: 'center',
74
+ left: 'center',
75
+ label: ` ${label} `,
76
+ tags: true,
77
+ keys: true,
78
+ vi: true,
79
+ style: {
80
+ border: { fg: 'green' },
81
+ label: { fg: 'green', bold: true },
82
+ },
83
+ });
84
+
85
+ prompt.input(`${label}:`, defaultVal, (err, value) => {
86
+ prompt.destroy();
87
+ screen.render();
88
+ if (err || value == null) return reject(new Error('cancelled'));
89
+ resolve(value.trim());
90
+ });
91
+
92
+ screen.render();
93
+ });
94
+ }
95
+
96
+ const CLEAR_FILTER_TAG = '__clear__';
97
+
98
+ export function epicPicker(screen, epics, currentEpicId = null) {
99
+ return new Promise((resolve, reject) => {
100
+ // Newest epics on top: sort by created_at descending (ISO-8601 UTC strings
101
+ // sort lexicographically in chronological order). Fall back to id for ties
102
+ // or missing timestamps.
103
+ const sorted = [...epics].sort((a, b) => {
104
+ const ta = a.created_at || '';
105
+ const tb = b.created_at || '';
106
+ if (ta !== tb) return tb.localeCompare(ta);
107
+ return b.id.localeCompare(a.id);
108
+ });
109
+ const rows = [];
110
+ const ids = [];
111
+
112
+ if (currentEpicId) {
113
+ rows.push('{yellow-fg}✗ Clear epic filter{/}');
114
+ ids.push(CLEAR_FILTER_TAG);
115
+ }
116
+
117
+ for (const e of sorted) {
118
+ const marker = e.id === currentEpicId ? '{cyan-fg}●{/} ' : ' ';
119
+ const id = e.id.padEnd(12);
120
+ const title = (e.title || '').slice(0, 60);
121
+ rows.push(`${marker}{gray-fg}${id}{/} ${title}`);
122
+ ids.push(e.id);
123
+ }
124
+
125
+ if (rows.length === 0) {
126
+ rows.push('{gray-fg}(no epics found){/}');
127
+ ids.push(null);
128
+ }
129
+
130
+ const width = 72;
131
+ const height = Math.min(rows.length + 4, Math.max(8, screen.height - 4));
132
+
133
+ const picker = blessed.list({
134
+ parent: screen,
135
+ label: ' Filter by Epic ',
136
+ border: { type: 'line' },
137
+ top: 'center',
138
+ left: 'center',
139
+ width,
140
+ height,
141
+ items: rows,
142
+ keys: true,
143
+ vi: true,
144
+ tags: true,
145
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'magenta' } },
146
+ style: {
147
+ selected: { bg: 'blue', fg: 'white', bold: true },
148
+ border: { fg: 'magenta' },
149
+ label: { fg: 'magenta', bold: true },
150
+ },
151
+ });
152
+
153
+ // Select current epic by default if set
154
+ if (currentEpicId) {
155
+ const idx = ids.indexOf(currentEpicId);
156
+ if (idx >= 0) picker.select(idx);
157
+ }
158
+
159
+ picker.focus();
160
+ screen.render();
161
+
162
+ picker.once('select', (_item, index) => {
163
+ const picked = ids[index];
164
+ picker.destroy();
165
+ screen.render();
166
+ if (picked === null) return reject(new Error('cancelled'));
167
+ resolve(picked === CLEAR_FILTER_TAG ? null : picked);
168
+ });
169
+
170
+ picker.key(['escape', 'q'], () => {
171
+ picker.destroy();
172
+ screen.render();
173
+ reject(new Error('cancelled'));
174
+ });
175
+
176
+ picker.key(['x'], () => {
177
+ picker.destroy();
178
+ screen.render();
179
+ resolve(null);
180
+ });
181
+ });
182
+ }
183
+
184
+ export function skillPicker(screen, beadId, issueType) {
185
+ // Epics get the epic-level executor (runs every ready child sequentially on a
186
+ // single branch); tasks/chores/bugs get the per-bead executors. The OpenCode
187
+ // section repeats them with the `opencode` argument, which delegates only the
188
+ // implementation step to a headless OpenCode implementor
189
+ // (agent-workflow-beads --with-opencode).
190
+ const skills = issueType === 'epic'
191
+ ? ['executor-epic-sequential', 'executor-epic-sequential-worktree']
192
+ : [
193
+ 'executor-task',
194
+ 'executor-task-worktree',
195
+ 'executor-epic-task',
196
+ 'executor-epic-task-worktree',
197
+ 'executor-rework-in-place',
198
+ ];
199
+ const cmd = (skill) => `{green-fg}/${skill}{/green-fg} {gray-fg}${beadId}{/gray-fg}`;
200
+ const items = [
201
+ '{bold}── Claude skills ──{/bold}',
202
+ ...skills.map(cmd),
203
+ '{bold}── OpenCode skills ──{/bold}',
204
+ ...skills.map((skill) => `${cmd(skill)} {red-fg}opencode{/red-fg}`),
205
+ ];
206
+ const headers = new Set([0, skills.length + 1]);
207
+ return listPicker(screen, 'Copy workflow command', items, 'magenta', 66, 1, headers);
208
+ }
209
+
210
+ export function parentPicker(screen, epics, { selfId, currentParentIds = [] } = {}) {
211
+ return new Promise((resolve, reject) => {
212
+ const exclude = new Set([selfId, ...currentParentIds]);
213
+ const sorted = [...epics]
214
+ .filter((e) => !exclude.has(e.id))
215
+ .sort((a, b) => {
216
+ const ta = a.updated_at || '', tb = b.updated_at || '';
217
+ if (ta !== tb) return tb.localeCompare(ta);
218
+ return b.id.localeCompare(a.id);
219
+ });
220
+
221
+ const rows = ['{yellow-fg}Standalone bead{/}'];
222
+ const ids = [null];
223
+
224
+ for (const e of sorted) {
225
+ const id = e.id.padEnd(12);
226
+ const title = (e.title || '').slice(0, 60);
227
+ rows.push(`{gray-fg}${id}{/} ${title}`);
228
+ ids.push(e.id);
229
+ }
230
+
231
+ const width = 72;
232
+ const height = Math.min(rows.length + 4, Math.max(8, screen.height - 4));
233
+
234
+ const picker = blessed.list({
235
+ parent: screen,
236
+ label: ' Change Parent ',
237
+ border: { type: 'line' },
238
+ top: 'center',
239
+ left: 'center',
240
+ width,
241
+ height,
242
+ items: rows,
243
+ keys: true,
244
+ vi: true,
245
+ tags: true,
246
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'magenta' } },
247
+ style: {
248
+ selected: { bg: 'blue', fg: 'white', bold: true },
249
+ border: { fg: 'magenta' },
250
+ label: { fg: 'magenta', bold: true },
251
+ },
252
+ });
253
+
254
+ picker.focus();
255
+ screen.render();
256
+
257
+ picker.once('select', (_item, index) => {
258
+ picker.destroy();
259
+ screen.render();
260
+ resolve(ids[index]);
261
+ });
262
+
263
+ picker.key(['escape', 'q'], () => {
264
+ picker.destroy();
265
+ screen.render();
266
+ reject(new Error('cancelled'));
267
+ });
268
+ });
269
+ }