@brainfile/cli 0.4.2 → 0.5.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.
@@ -0,0 +1,736 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.tuiCommand = tuiCommand;
37
+ const react_1 = __importStar(require("react"));
38
+ const ink_1 = require("ink");
39
+ const core_1 = require("@brainfile/core");
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const chokidar = __importStar(require("chokidar"));
43
+ // Unified palette matching VSCode extension priority colors
44
+ // VSCode uses: critical=#d64933, high=#867530, medium=#37505C, low=#bac1b8
45
+ const PALETTE = {
46
+ bg: '#000000',
47
+ panel: '#1e1e1e',
48
+ text: 'white',
49
+ textMuted: 'gray',
50
+ border: 'gray',
51
+ // Priority colors (terminal equivalents)
52
+ critical: 'red', // #d64933
53
+ giga: 'magenta', // Custom high priority
54
+ high: 'yellow', // #867530
55
+ medium: 'cyan', // #37505C
56
+ low: 'gray', // #bac1b8
57
+ // UI accents
58
+ progress: 'blue',
59
+ success: 'green',
60
+ selected: 'white',
61
+ };
62
+ // Box drawing characters
63
+ const BOX = {
64
+ topLeft: '┌',
65
+ topRight: '┐',
66
+ bottomLeft: '└',
67
+ bottomRight: '┘',
68
+ horizontal: '─',
69
+ vertical: '│',
70
+ teeRight: '├',
71
+ teeLeft: '┤',
72
+ };
73
+ const HEADER_ROWS = 7; // title(1) + progress(3: padTop+content+padBottom) + tabs(2: marginTop+content) + separator(1)
74
+ const FOOTER_ROWS = 3; // separator + status bar (2 rows: content + bottom padding)
75
+ function BrainfileTUI({ filePath }) {
76
+ const { exit } = (0, ink_1.useApp)();
77
+ const { stdout } = (0, ink_1.useStdout)();
78
+ const termWidth = stdout?.columns ?? process.stdout.columns ?? 80;
79
+ const termHeight = stdout?.rows ?? process.stdout.rows ?? 24;
80
+ const [state, setState] = (0, react_1.useState)({
81
+ board: null,
82
+ error: null,
83
+ lastUpdated: new Date(),
84
+ activeColumnIndex: 0,
85
+ selectedTaskIndex: 0,
86
+ mode: 'browse',
87
+ searchQuery: '',
88
+ expandedTaskIds: new Set(),
89
+ reloadFlash: false,
90
+ });
91
+ const viewportHeight = Math.max(termHeight - HEADER_ROWS - FOOTER_ROWS, 5);
92
+ const loadBrainfile = (0, react_1.useCallback)(() => {
93
+ try {
94
+ const content = fs.readFileSync(filePath, 'utf-8');
95
+ const result = core_1.Brainfile.parseWithErrors(content);
96
+ if (result.board) {
97
+ setState(prev => {
98
+ // Preserve selection by task ID if possible
99
+ const prevTaskId = prev.board?.columns[prev.activeColumnIndex]?.tasks[prev.selectedTaskIndex]?.id;
100
+ let newSelectedIndex = 0;
101
+ if (prevTaskId && result.board) {
102
+ const col = result.board.columns[prev.activeColumnIndex];
103
+ if (col) {
104
+ const idx = col.tasks.findIndex(t => t.id === prevTaskId);
105
+ if (idx >= 0)
106
+ newSelectedIndex = idx;
107
+ }
108
+ }
109
+ return {
110
+ ...prev,
111
+ board: result.board,
112
+ error: null,
113
+ lastUpdated: new Date(),
114
+ selectedTaskIndex: newSelectedIndex,
115
+ reloadFlash: true,
116
+ };
117
+ });
118
+ // Clear reload flash after 1 second
119
+ setTimeout(() => {
120
+ setState(prev => ({ ...prev, reloadFlash: false }));
121
+ }, 1000);
122
+ }
123
+ else {
124
+ setState(prev => ({
125
+ ...prev,
126
+ error: result.error || 'Failed to parse brainfile',
127
+ }));
128
+ }
129
+ }
130
+ catch (err) {
131
+ setState(prev => ({
132
+ ...prev,
133
+ error: err instanceof Error ? err.message : String(err),
134
+ }));
135
+ }
136
+ }, [filePath]);
137
+ (0, react_1.useEffect)(() => {
138
+ loadBrainfile();
139
+ }, [loadBrainfile]);
140
+ (0, react_1.useEffect)(() => {
141
+ const watcher = chokidar.watch(filePath, {
142
+ persistent: true,
143
+ ignoreInitial: true,
144
+ usePolling: true,
145
+ interval: 750,
146
+ awaitWriteFinish: {
147
+ stabilityThreshold: 250,
148
+ pollInterval: 100,
149
+ },
150
+ depth: 0,
151
+ });
152
+ watcher.on('change', () => loadBrainfile());
153
+ watcher.on('error', (err) => {
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ setState(prev => ({
156
+ ...prev,
157
+ error: `File watcher error: ${message}`,
158
+ }));
159
+ });
160
+ return () => {
161
+ watcher.close();
162
+ };
163
+ }, [filePath, loadBrainfile]);
164
+ // Sort columns by order property (like VSCode extension)
165
+ const orderedColumns = (0, react_1.useMemo)(() => {
166
+ if (!state.board)
167
+ return [];
168
+ return [...state.board.columns].sort((a, b) => {
169
+ const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
170
+ const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
171
+ return orderA - orderB;
172
+ });
173
+ }, [state.board]);
174
+ // Filtered columns based on search
175
+ const filteredColumns = (0, react_1.useMemo)(() => {
176
+ if (!orderedColumns.length)
177
+ return [];
178
+ if (!state.searchQuery.trim())
179
+ return orderedColumns;
180
+ const query = state.searchQuery.toLowerCase();
181
+ return orderedColumns.map(col => ({
182
+ ...col,
183
+ tasks: col.tasks.filter(task => task.title.toLowerCase().includes(query) ||
184
+ task.id.toLowerCase().includes(query) ||
185
+ task.tags?.some(t => t.toLowerCase().includes(query)) ||
186
+ task.priority?.toLowerCase().includes(query)),
187
+ })).filter(col => col.tasks.length > 0);
188
+ }, [orderedColumns, state.searchQuery]);
189
+ // Current column and its tasks
190
+ const currentColumn = filteredColumns[state.activeColumnIndex];
191
+ const currentTasks = currentColumn?.tasks || [];
192
+ const maxTaskIndex = Math.max(0, currentTasks.length - 1);
193
+ // Keep selection in bounds
194
+ (0, react_1.useEffect)(() => {
195
+ setState(prev => ({
196
+ ...prev,
197
+ activeColumnIndex: Math.min(prev.activeColumnIndex, Math.max(0, filteredColumns.length - 1)),
198
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex, maxTaskIndex),
199
+ }));
200
+ }, [filteredColumns.length, maxTaskIndex]);
201
+ // Calculate stats
202
+ const stats = (0, react_1.useMemo)(() => {
203
+ if (!state.board)
204
+ return { total: 0, done: 0, percentage: 0 };
205
+ const total = state.board.columns.reduce((sum, col) => sum + col.tasks.length, 0);
206
+ const doneCol = state.board.columns.find(col => col.id === 'done' || col.title.toLowerCase() === 'done');
207
+ const done = doneCol?.tasks.length || 0;
208
+ const percentage = total > 0 ? Math.round((done / total) * 100) : 0;
209
+ return { total, done, percentage };
210
+ }, [state.board]);
211
+ (0, ink_1.useInput)((input, key) => {
212
+ // Help mode - any key closes
213
+ if (state.mode === 'help') {
214
+ setState(prev => ({ ...prev, mode: 'browse' }));
215
+ return;
216
+ }
217
+ // Search mode handling
218
+ if (state.mode === 'search') {
219
+ if (key.escape) {
220
+ setState(prev => ({ ...prev, mode: 'browse', searchQuery: '' }));
221
+ return;
222
+ }
223
+ if (key.return) {
224
+ setState(prev => ({ ...prev, mode: 'browse' }));
225
+ return;
226
+ }
227
+ if (key.backspace || key.delete) {
228
+ setState(prev => ({ ...prev, searchQuery: prev.searchQuery.slice(0, -1) }));
229
+ return;
230
+ }
231
+ if (input && !key.ctrl && !key.meta) {
232
+ setState(prev => ({ ...prev, searchQuery: prev.searchQuery + input }));
233
+ return;
234
+ }
235
+ return;
236
+ }
237
+ // Browse mode
238
+ if (input === 'q' || (key.ctrl && input === 'c')) {
239
+ exit();
240
+ return;
241
+ }
242
+ if (input === '?') {
243
+ setState(prev => ({ ...prev, mode: 'help' }));
244
+ return;
245
+ }
246
+ if (input === '/') {
247
+ setState(prev => ({ ...prev, mode: 'search', searchQuery: '' }));
248
+ return;
249
+ }
250
+ // Refresh
251
+ if (input === 'r') {
252
+ loadBrainfile();
253
+ return;
254
+ }
255
+ // Navigation: up/down through tasks
256
+ if (key.downArrow || input === 'j') {
257
+ setState(prev => ({
258
+ ...prev,
259
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxTaskIndex),
260
+ }));
261
+ return;
262
+ }
263
+ if (key.upArrow || input === 'k') {
264
+ setState(prev => ({
265
+ ...prev,
266
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0),
267
+ }));
268
+ return;
269
+ }
270
+ // Page scrolling
271
+ if (key.ctrl && input === 'd') {
272
+ setState(prev => ({
273
+ ...prev,
274
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + Math.floor(viewportHeight / 2), maxTaskIndex),
275
+ }));
276
+ return;
277
+ }
278
+ if (key.ctrl && input === 'u') {
279
+ setState(prev => ({
280
+ ...prev,
281
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0),
282
+ }));
283
+ return;
284
+ }
285
+ // Column switching: TAB / left/right
286
+ if (key.tab || key.rightArrow || input === 'l') {
287
+ setState(prev => ({
288
+ ...prev,
289
+ activeColumnIndex: (prev.activeColumnIndex + 1) % filteredColumns.length,
290
+ selectedTaskIndex: 0,
291
+ }));
292
+ return;
293
+ }
294
+ if ((key.shift && key.tab) || key.leftArrow || input === 'h') {
295
+ setState(prev => ({
296
+ ...prev,
297
+ activeColumnIndex: prev.activeColumnIndex === 0
298
+ ? filteredColumns.length - 1
299
+ : prev.activeColumnIndex - 1,
300
+ selectedTaskIndex: 0,
301
+ }));
302
+ return;
303
+ }
304
+ // Jump to column headers with { and }
305
+ if (input === '{') {
306
+ setState(prev => ({
307
+ ...prev,
308
+ activeColumnIndex: Math.max(0, prev.activeColumnIndex - 1),
309
+ selectedTaskIndex: 0,
310
+ }));
311
+ return;
312
+ }
313
+ if (input === '}') {
314
+ setState(prev => ({
315
+ ...prev,
316
+ activeColumnIndex: Math.min(filteredColumns.length - 1, prev.activeColumnIndex + 1),
317
+ selectedTaskIndex: 0,
318
+ }));
319
+ return;
320
+ }
321
+ // Home/End
322
+ if (input === 'g') {
323
+ setState(prev => ({ ...prev, selectedTaskIndex: 0 }));
324
+ return;
325
+ }
326
+ if (input === 'G') {
327
+ setState(prev => ({ ...prev, selectedTaskIndex: maxTaskIndex }));
328
+ return;
329
+ }
330
+ // Expand/collapse task
331
+ if (key.return) {
332
+ const task = currentTasks[state.selectedTaskIndex];
333
+ if (task) {
334
+ setState(prev => {
335
+ const newExpanded = new Set(prev.expandedTaskIds);
336
+ if (newExpanded.has(task.id)) {
337
+ newExpanded.delete(task.id);
338
+ }
339
+ else {
340
+ newExpanded.add(task.id);
341
+ }
342
+ return { ...prev, expandedTaskIds: newExpanded };
343
+ });
344
+ }
345
+ return;
346
+ }
347
+ // Escape: collapse all or clear search
348
+ if (key.escape) {
349
+ setState(prev => ({
350
+ ...prev,
351
+ expandedTaskIds: new Set(),
352
+ searchQuery: '',
353
+ }));
354
+ return;
355
+ }
356
+ });
357
+ // Error state
358
+ if (state.error) {
359
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
360
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.high, bold: true }, "Error"),
361
+ react_1.default.createElement(ink_1.Box, { marginTop: 1 },
362
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted }, state.error)),
363
+ react_1.default.createElement(ink_1.Box, { marginTop: 1 },
364
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted, dimColor: true }, "Press q to quit, r to retry"))));
365
+ }
366
+ // Loading state
367
+ if (!state.board) {
368
+ return (react_1.default.createElement(ink_1.Box, { padding: 1 },
369
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted }, "Loading...")));
370
+ }
371
+ // Help overlay
372
+ if (state.mode === 'help') {
373
+ return react_1.default.createElement(HelpOverlay, { termWidth: termWidth });
374
+ }
375
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: termWidth, height: termHeight },
376
+ react_1.default.createElement(Header, { title: state.board.title || 'Brainfile', stats: stats, reloadFlash: state.reloadFlash }),
377
+ react_1.default.createElement(ProgressBar, { done: stats.done, total: stats.total, width: termWidth - 4 }),
378
+ state.mode === 'search' && (react_1.default.createElement(SearchBar, { query: state.searchQuery, width: termWidth - 2 })),
379
+ react_1.default.createElement(ColumnTabs, { columns: filteredColumns, activeIndex: state.activeColumnIndex, termWidth: termWidth }),
380
+ react_1.default.createElement(ink_1.Box, { paddingLeft: 1 },
381
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border }, BOX.horizontal.repeat(termWidth - 2))),
382
+ react_1.default.createElement(ink_1.Box, { flexGrow: 1, flexDirection: "column" },
383
+ react_1.default.createElement(TaskList, { tasks: currentTasks, selectedIndex: state.selectedTaskIndex, expandedIds: state.expandedTaskIds, viewportHeight: viewportHeight - (state.mode === 'search' ? 1 : 0), termWidth: termWidth })),
384
+ react_1.default.createElement(ink_1.Box, { paddingLeft: 1 },
385
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border }, BOX.horizontal.repeat(termWidth - 2))),
386
+ react_1.default.createElement(StatusBar, { mode: state.mode, columnName: currentColumn?.title || '', taskIndex: state.selectedTaskIndex + 1, taskCount: currentTasks.length, termWidth: termWidth })));
387
+ }
388
+ function Header({ title, reloadFlash }) {
389
+ return (react_1.default.createElement(ink_1.Box, { paddingLeft: 1, paddingRight: 1 },
390
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
391
+ BOX.topLeft,
392
+ BOX.horizontal,
393
+ " ",
394
+ title,
395
+ " "),
396
+ reloadFlash ? (react_1.default.createElement(ink_1.Text, { color: PALETTE.success },
397
+ " ",
398
+ String.fromCharCode(0x21BB),
399
+ " reloaded")) : null));
400
+ }
401
+ function ProgressBar({ done, total, width }) {
402
+ const percentage = total > 0 ? Math.round((done / total) * 100) : 0;
403
+ // Reserve space for: padding + "XX% " + " X of Y complete"
404
+ const textWidth = 20 + String(done).length + String(total).length;
405
+ const barWidth = Math.max(width - textWidth, 20);
406
+ const filled = Math.round((percentage / 100) * barWidth);
407
+ const empty = barWidth - filled;
408
+ return (react_1.default.createElement(ink_1.Box, { paddingLeft: 2, paddingTop: 1, paddingBottom: 1 },
409
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted },
410
+ percentage,
411
+ "% "),
412
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.progress }, '█'.repeat(filled)),
413
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border }, '░'.repeat(empty)),
414
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted },
415
+ " ",
416
+ done,
417
+ " of ",
418
+ total,
419
+ " complete")));
420
+ }
421
+ function SearchBar({ query, width }) {
422
+ const inputWidth = Math.min(width - 6, 60);
423
+ const displayQuery = query.padEnd(inputWidth, ' ').slice(0, inputWidth);
424
+ return (react_1.default.createElement(ink_1.Box, { paddingLeft: 1, marginTop: 0 },
425
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted },
426
+ String.fromCharCode(0x1F50D),
427
+ " "),
428
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, inverse: true },
429
+ "[",
430
+ displayQuery,
431
+ "]")));
432
+ }
433
+ function ColumnTabs({ columns, activeIndex, termWidth }) {
434
+ const maxTabWidth = Math.floor((termWidth - 4) / Math.max(columns.length, 1)) - 2;
435
+ return (react_1.default.createElement(ink_1.Box, { paddingLeft: 1, paddingRight: 1, marginTop: 1 }, columns.map((col, idx) => {
436
+ const isActive = idx === activeIndex;
437
+ const label = truncate(col.title.toUpperCase(), maxTabWidth - 4);
438
+ const count = col.tasks.length;
439
+ return (react_1.default.createElement(ink_1.Box, { key: col.id, marginRight: 1 }, isActive ? (react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true, inverse: true },
440
+ " ",
441
+ label,
442
+ " ",
443
+ count,
444
+ " ")) : (react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted },
445
+ " ",
446
+ label,
447
+ " ",
448
+ count,
449
+ " "))));
450
+ })));
451
+ }
452
+ function TaskList({ tasks, selectedIndex, expandedIds, viewportHeight, termWidth }) {
453
+ // Calculate scroll offset to keep selection visible
454
+ const scrollOffset = Math.max(0, Math.min(selectedIndex - Math.floor(viewportHeight / 3), Math.max(0, tasks.length - Math.floor(viewportHeight / 2))));
455
+ const cardWidth = termWidth - 4;
456
+ // Build visible items with expansion
457
+ const visibleItems = [];
458
+ let linesUsed = 0;
459
+ for (let i = scrollOffset; i < tasks.length && linesUsed < viewportHeight; i++) {
460
+ const task = tasks[i];
461
+ const isSelected = i === selectedIndex;
462
+ const isExpanded = expandedIds.has(task.id);
463
+ // Calculate how many lines this task will take (2 lines base + expanded content)
464
+ const subtaskLines = isExpanded ? Math.min((task.subtasks?.length || 0), 4) : 0;
465
+ const expandedLines = isExpanded ? 2 + subtaskLines + (task.relatedFiles?.length ? 1 : 0) : 0;
466
+ const taskLines = 3 + expandedLines; // title + metadata + margin + expanded
467
+ if (linesUsed + taskLines <= viewportHeight) {
468
+ visibleItems.push({ task, isSelected, isExpanded });
469
+ linesUsed += taskLines;
470
+ }
471
+ else if (visibleItems.length === 0) {
472
+ // Always show at least one item
473
+ visibleItems.push({ task, isSelected, isExpanded });
474
+ linesUsed += taskLines;
475
+ break;
476
+ }
477
+ else {
478
+ break;
479
+ }
480
+ }
481
+ // Calculate remaining empty lines to fill viewport
482
+ const emptyLines = Math.max(0, viewportHeight - linesUsed);
483
+ if (tasks.length === 0) {
484
+ return (react_1.default.createElement(ink_1.Box, { flexGrow: 1, flexDirection: "column", paddingLeft: 2 },
485
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted, dimColor: true }, "No tasks in this column"),
486
+ Array.from({ length: viewportHeight - 1 }).map((_, i) => (react_1.default.createElement(ink_1.Box, { key: `empty-${i}` },
487
+ react_1.default.createElement(ink_1.Text, null, ' '))))));
488
+ }
489
+ return (react_1.default.createElement(ink_1.Box, { flexGrow: 1, flexDirection: "column", paddingLeft: 1 },
490
+ visibleItems.map(({ task, isSelected, isExpanded }) => (react_1.default.createElement(TaskCard, { key: task.id, task: task, isSelected: isSelected, isExpanded: isExpanded, width: cardWidth }))),
491
+ Array.from({ length: emptyLines }).map((_, i) => (react_1.default.createElement(ink_1.Box, { key: `empty-${i}` },
492
+ react_1.default.createElement(ink_1.Text, null, ' '))))));
493
+ }
494
+ function TaskCard({ task, isSelected, isExpanded, width }) {
495
+ const priorityColor = getPriorityColor(task.priority);
496
+ const priorityLabel = task.priority?.toUpperCase() || '';
497
+ const tags = task.tags || [];
498
+ const subtasks = task.subtasks || [];
499
+ const completedSubtasks = subtasks.filter(s => s.completed).length;
500
+ // Arrow indicator: colored when selected
501
+ const arrow = isExpanded ? '▼' : '▶';
502
+ const arrowColor = isSelected ? PALETTE.progress : PALETTE.textMuted;
503
+ const titleWidth = width - task.id.length - 10;
504
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", marginBottom: 1 },
505
+ react_1.default.createElement(ink_1.Box, null,
506
+ react_1.default.createElement(ink_1.Text, { color: arrowColor, bold: isSelected }, arrow),
507
+ react_1.default.createElement(ink_1.Text, null, " "),
508
+ react_1.default.createElement(ink_1.Text, { color: isSelected ? PALETTE.text : PALETTE.textMuted, inverse: isSelected, bold: isSelected },
509
+ ' ',
510
+ truncate(task.title, titleWidth),
511
+ ' '),
512
+ react_1.default.createElement(ink_1.Text, null, " "),
513
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted, dimColor: true }, task.id)),
514
+ react_1.default.createElement(ink_1.Box, { paddingLeft: 3 },
515
+ react_1.default.createElement(ink_1.Text, { color: priorityColor }, priorityLabel || ' '),
516
+ tags.length > 0 ? (react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted, dimColor: true },
517
+ ' ',
518
+ String.fromCharCode(0xB7),
519
+ " ",
520
+ tags.slice(0, 4).join(` ${String.fromCharCode(0xB7)} `))) : null),
521
+ isExpanded && (react_1.default.createElement(ExpandedTaskContent, { task: task, priorityColor: priorityColor, completedSubtasks: completedSubtasks, totalSubtasks: subtasks.length, width: width }))));
522
+ }
523
+ function ExpandedTaskContent({ task, priorityColor, completedSubtasks, totalSubtasks, width }) {
524
+ const subtasks = task.subtasks || [];
525
+ const innerWidth = width - 6;
526
+ // Build content lines - all with consistent left border
527
+ const lines = [];
528
+ // Empty line for spacing
529
+ lines.push(react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted }, ' '));
530
+ if (task.description) {
531
+ lines.push(react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted }, truncate(task.description.split('\n')[0], innerWidth)));
532
+ }
533
+ if (totalSubtasks > 0) {
534
+ // Empty line before subtasks if there was a description
535
+ if (task.description) {
536
+ lines.push(react_1.default.createElement(ink_1.Text, null, ' '));
537
+ }
538
+ lines.push(react_1.default.createElement(react_1.default.Fragment, null,
539
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted }, "Subtasks: "),
540
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.success }, '●'.repeat(Math.min(completedSubtasks, 10))),
541
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border }, '○'.repeat(Math.min(totalSubtasks - completedSubtasks, 10))),
542
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted },
543
+ " ",
544
+ completedSubtasks,
545
+ "/",
546
+ totalSubtasks)));
547
+ subtasks.slice(0, 4).forEach((st) => {
548
+ lines.push(react_1.default.createElement(ink_1.Text, { color: st.completed ? PALETTE.success : PALETTE.textMuted },
549
+ st.completed ? '✓' : '○',
550
+ " ",
551
+ truncate(st.title, innerWidth - 2)));
552
+ });
553
+ }
554
+ if (task.relatedFiles && task.relatedFiles.length > 0) {
555
+ // Empty line before files
556
+ lines.push(react_1.default.createElement(ink_1.Text, null, ' '));
557
+ lines.push(react_1.default.createElement(ink_1.Text, { color: PALETTE.medium },
558
+ "Files: ",
559
+ truncate(task.relatedFiles.slice(0, 2).join(', '), innerWidth - 7)));
560
+ }
561
+ if (lines.length === 0)
562
+ return null;
563
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" }, lines.map((content, idx) => (react_1.default.createElement(ink_1.Box, { key: idx, paddingLeft: 3 },
564
+ react_1.default.createElement(ink_1.Text, { color: priorityColor }, BOX.vertical),
565
+ react_1.default.createElement(ink_1.Text, null, " "),
566
+ content)))));
567
+ }
568
+ function StatusBar({ mode, columnName, taskIndex, taskCount, termWidth, isWatching = true }) {
569
+ // Left section: essential commands
570
+ const leftSection = mode === 'search' ? 'ESC:cancel' : '?:help TAB:column q:quit';
571
+ // Middle section: column and position
572
+ const position = taskCount > 0 ? `${taskIndex}/${taskCount}` : '';
573
+ const middleSection = columnName ? `${columnName.toUpperCase()} ${position}` : '';
574
+ // Right section: status
575
+ const rightSection = isWatching ? '● live' : '';
576
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: termWidth },
577
+ react_1.default.createElement(ink_1.Box, { width: termWidth - 2, paddingLeft: 1, paddingRight: 1 },
578
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted, dimColor: true }, leftSection),
579
+ react_1.default.createElement(ink_1.Box, { flexGrow: 1 }),
580
+ middleSection ? (react_1.default.createElement(ink_1.Text, { color: PALETTE.progress }, middleSection)) : null,
581
+ react_1.default.createElement(ink_1.Box, { flexGrow: 1 }),
582
+ rightSection ? (react_1.default.createElement(ink_1.Text, { color: PALETTE.success, dimColor: true }, rightSection)) : null),
583
+ react_1.default.createElement(ink_1.Box, { paddingLeft: 1 },
584
+ react_1.default.createElement(ink_1.Text, null, ' '))));
585
+ }
586
+ function HelpOverlay({ termWidth }) {
587
+ const boxWidth = Math.min(termWidth - 4, 56);
588
+ const hr = BOX.horizontal.repeat(boxWidth - 2);
589
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
590
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
591
+ BOX.topLeft,
592
+ BOX.horizontal,
593
+ " KEYBOARD SHORTCUTS ",
594
+ BOX.horizontal.repeat(boxWidth - 22),
595
+ BOX.topRight),
596
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
597
+ BOX.vertical,
598
+ ' '.repeat(boxWidth - 2),
599
+ BOX.vertical),
600
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
601
+ BOX.vertical,
602
+ " Navigation",
603
+ ' '.repeat(boxWidth - 14),
604
+ BOX.vertical),
605
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
606
+ BOX.vertical,
607
+ " ",
608
+ BOX.horizontal.repeat(boxWidth - 6),
609
+ " ",
610
+ BOX.vertical),
611
+ react_1.default.createElement(HelpRow, { label: "j/k or \u2193/\u2191", desc: "Move up/down", width: boxWidth }),
612
+ react_1.default.createElement(HelpRow, { label: "h/l or \u2190/\u2192", desc: "Switch columns", width: boxWidth }),
613
+ react_1.default.createElement(HelpRow, { label: "TAB", desc: "Next column", width: boxWidth }),
614
+ react_1.default.createElement(HelpRow, { label: "g / G", desc: "Top / Bottom", width: boxWidth }),
615
+ react_1.default.createElement(HelpRow, { label: "Ctrl+d/u", desc: "Page down/up", width: boxWidth }),
616
+ react_1.default.createElement(HelpRow, { label: "{ / }", desc: "Prev/next column", width: boxWidth }),
617
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
618
+ BOX.vertical,
619
+ ' '.repeat(boxWidth - 2),
620
+ BOX.vertical),
621
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
622
+ BOX.vertical,
623
+ " Actions",
624
+ ' '.repeat(boxWidth - 11),
625
+ BOX.vertical),
626
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
627
+ BOX.vertical,
628
+ " ",
629
+ BOX.horizontal.repeat(boxWidth - 6),
630
+ " ",
631
+ BOX.vertical),
632
+ react_1.default.createElement(HelpRow, { label: "ENTER", desc: "Expand/collapse task", width: boxWidth }),
633
+ react_1.default.createElement(HelpRow, { label: "/", desc: "Search/filter", width: boxWidth }),
634
+ react_1.default.createElement(HelpRow, { label: "r", desc: "Refresh from file", width: boxWidth }),
635
+ react_1.default.createElement(HelpRow, { label: "ESC", desc: "Clear/collapse", width: boxWidth }),
636
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
637
+ BOX.vertical,
638
+ ' '.repeat(boxWidth - 2),
639
+ BOX.vertical),
640
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
641
+ BOX.vertical,
642
+ " Indicators",
643
+ ' '.repeat(boxWidth - 14),
644
+ BOX.vertical),
645
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
646
+ BOX.vertical,
647
+ " ",
648
+ BOX.horizontal.repeat(boxWidth - 6),
649
+ " ",
650
+ BOX.vertical),
651
+ react_1.default.createElement(HelpRow, { label: "\u25B6", desc: "Collapsed task", width: boxWidth }),
652
+ react_1.default.createElement(HelpRow, { label: "\u25BC", desc: "Expanded task", width: boxWidth }),
653
+ react_1.default.createElement(HelpRow, { label: "[inverse]", desc: "Selected task", width: boxWidth }),
654
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
655
+ BOX.vertical,
656
+ ' '.repeat(boxWidth - 2),
657
+ BOX.vertical),
658
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
659
+ BOX.vertical,
660
+ " Quit",
661
+ ' '.repeat(boxWidth - 7),
662
+ BOX.vertical),
663
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
664
+ BOX.vertical,
665
+ " ",
666
+ BOX.horizontal.repeat(boxWidth - 6),
667
+ " ",
668
+ BOX.vertical),
669
+ react_1.default.createElement(HelpRow, { label: "q / Ctrl+C", desc: "Exit", width: boxWidth }),
670
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
671
+ BOX.vertical,
672
+ ' '.repeat(boxWidth - 2),
673
+ BOX.vertical),
674
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text, bold: true },
675
+ BOX.bottomLeft,
676
+ hr,
677
+ BOX.bottomRight),
678
+ react_1.default.createElement(ink_1.Box, { marginTop: 1 },
679
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted, dimColor: true }, "Press any key to close"))));
680
+ }
681
+ function HelpRow({ label, desc, width }) {
682
+ const padding = width - 6 - label.length - desc.length;
683
+ return (react_1.default.createElement(ink_1.Text, { color: PALETTE.border },
684
+ BOX.vertical,
685
+ " ",
686
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.text }, label),
687
+ ' '.repeat(Math.max(padding, 2)),
688
+ react_1.default.createElement(ink_1.Text, { color: PALETTE.textMuted }, desc),
689
+ " ",
690
+ BOX.vertical));
691
+ }
692
+ // ---------------------------------------------------------------------------
693
+ // Helper functions
694
+ // ---------------------------------------------------------------------------
695
+ function truncate(value, maxLength) {
696
+ if (!value)
697
+ return '';
698
+ if (value.length <= maxLength)
699
+ return value;
700
+ return `${value.slice(0, maxLength - 1)}…`;
701
+ }
702
+ function getPriorityColor(priority) {
703
+ switch (priority?.toLowerCase()) {
704
+ case 'critical':
705
+ return PALETTE.critical;
706
+ case 'giga':
707
+ return PALETTE.giga;
708
+ case 'high':
709
+ return PALETTE.high;
710
+ case 'medium':
711
+ return PALETTE.medium;
712
+ case 'low':
713
+ return PALETTE.low;
714
+ default:
715
+ return PALETTE.border;
716
+ }
717
+ }
718
+ function tuiCommand(options) {
719
+ const filePath = path.resolve(options.file);
720
+ if (!fs.existsSync(filePath)) {
721
+ console.error(`Error: File not found: ${filePath}`);
722
+ console.log('');
723
+ console.log('To create a new brainfile, run:');
724
+ console.log(' brainfile init');
725
+ process.exit(1);
726
+ }
727
+ if (!process.stdin.isTTY || !process.stdin.setRawMode) {
728
+ console.error('Error: Terminal UI requires an interactive terminal');
729
+ console.log('');
730
+ console.log('The TUI cannot run in non-interactive environments.');
731
+ console.log('Please run this command in a standard terminal (not piped or in a non-TTY context).');
732
+ process.exit(1);
733
+ }
734
+ (0, ink_1.render)(react_1.default.createElement(BrainfileTUI, { filePath: filePath }));
735
+ }
736
+ //# sourceMappingURL=tui.js.map