@brainfile/cli 0.9.2 → 0.10.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +31 -4
  3. package/dist/commands/tui.d.ts +1 -1
  4. package/dist/commands/tui.d.ts.map +1 -1
  5. package/dist/commands/tui.js +8 -2
  6. package/dist/commands/tui.js.map +1 -1
  7. package/dist/tui/BrainfileTUI.d.ts.map +1 -1
  8. package/dist/tui/BrainfileTUI.js +47 -14
  9. package/dist/tui/BrainfileTUI.js.map +1 -1
  10. package/dist/tui/actions.d.ts +32 -2
  11. package/dist/tui/actions.d.ts.map +1 -1
  12. package/dist/tui/actions.js +274 -2
  13. package/dist/tui/actions.js.map +1 -1
  14. package/dist/tui/components/ArchivePanel.d.ts +15 -0
  15. package/dist/tui/components/ArchivePanel.d.ts.map +1 -0
  16. package/dist/tui/components/ArchivePanel.js +114 -0
  17. package/dist/tui/components/ArchivePanel.js.map +1 -0
  18. package/dist/tui/components/HelpOverlay.d.ts.map +1 -1
  19. package/dist/tui/components/HelpOverlay.js +43 -27
  20. package/dist/tui/components/HelpOverlay.js.map +1 -1
  21. package/dist/tui/components/MainPanelTabs.d.ts +9 -0
  22. package/dist/tui/components/MainPanelTabs.d.ts.map +1 -0
  23. package/dist/tui/components/MainPanelTabs.js +37 -0
  24. package/dist/tui/components/MainPanelTabs.js.map +1 -0
  25. package/dist/tui/components/RulesPanel.d.ts +24 -0
  26. package/dist/tui/components/RulesPanel.d.ts.map +1 -0
  27. package/dist/tui/components/RulesPanel.js +125 -0
  28. package/dist/tui/components/RulesPanel.js.map +1 -0
  29. package/dist/tui/components/StatusBar.d.ts +3 -2
  30. package/dist/tui/components/StatusBar.d.ts.map +1 -1
  31. package/dist/tui/components/StatusBar.js +6 -3
  32. package/dist/tui/components/StatusBar.js.map +1 -1
  33. package/dist/tui/components/index.d.ts +6 -0
  34. package/dist/tui/components/index.d.ts.map +1 -1
  35. package/dist/tui/components/index.js +7 -1
  36. package/dist/tui/components/index.js.map +1 -1
  37. package/dist/tui/hooks/useKeyboardNavigation.d.ts.map +1 -1
  38. package/dist/tui/hooks/useKeyboardNavigation.js +508 -190
  39. package/dist/tui/hooks/useKeyboardNavigation.js.map +1 -1
  40. package/dist/tui/index.d.ts +1 -1
  41. package/dist/tui/index.d.ts.map +1 -1
  42. package/dist/tui/theme.d.ts +6 -0
  43. package/dist/tui/theme.d.ts.map +1 -1
  44. package/dist/tui/theme.js +6 -0
  45. package/dist/tui/theme.js.map +1 -1
  46. package/dist/tui/types.d.ts +16 -2
  47. package/dist/tui/types.d.ts.map +1 -1
  48. package/dist/tui/types.js.map +1 -1
  49. package/package.json +1 -1
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useKeyboardNavigation = useKeyboardNavigation;
4
4
  const ink_1 = require("ink");
5
5
  const actions_js_1 = require("../actions.js");
6
+ const RULE_TYPES = ['always', 'never', 'prefer', 'context'];
6
7
  // Helper to show status message
7
8
  function showStatus(setState, text, type) {
8
9
  const timestamp = Date.now();
@@ -173,7 +174,7 @@ function useKeyboardNavigation({ state, setState, currentTasks, maxTaskIndex, fi
173
174
  }
174
175
  return;
175
176
  }
176
- // Search mode handling
177
+ // Search mode handling (Tasks panel)
177
178
  if (state.mode === 'search') {
178
179
  if (key.escape) {
179
180
  setState(prev => ({ ...prev, mode: 'browse', searchQuery: '' }));
@@ -193,7 +194,162 @@ function useKeyboardNavigation({ state, setState, currentTasks, maxTaskIndex, fi
193
194
  }
194
195
  return;
195
196
  }
196
- // Browse mode
197
+ // Rule add/edit mode
198
+ if (state.mode === 'rule-add' || state.mode === 'rule-edit') {
199
+ if (key.escape) {
200
+ setState(prev => ({ ...prev, mode: 'browse', ruleEditText: '', ruleEditId: null }));
201
+ return;
202
+ }
203
+ if (key.return) {
204
+ const text = state.ruleEditText.trim();
205
+ if (!text) {
206
+ showStatus(setState, 'Rule text required', 'error');
207
+ return;
208
+ }
209
+ if (state.mode === 'rule-add') {
210
+ const result = (0, actions_js_1.addRuleAction)(filePath, state.activeRuleType, text);
211
+ if (result.success) {
212
+ showStatus(setState, result.message || 'Rule added', 'success');
213
+ loadBrainfile(true);
214
+ }
215
+ else {
216
+ showStatus(setState, result.error || 'Failed to add rule', 'error');
217
+ }
218
+ }
219
+ else if (state.ruleEditId !== null) {
220
+ const result = (0, actions_js_1.updateRuleAction)(filePath, state.activeRuleType, state.ruleEditId, text);
221
+ if (result.success) {
222
+ showStatus(setState, result.message || 'Rule updated', 'success');
223
+ loadBrainfile(true);
224
+ }
225
+ else {
226
+ showStatus(setState, result.error || 'Failed to update rule', 'error');
227
+ }
228
+ }
229
+ setState(prev => ({ ...prev, mode: 'browse', ruleEditText: '', ruleEditId: null }));
230
+ return;
231
+ }
232
+ if (key.backspace || key.delete) {
233
+ setState(prev => ({ ...prev, ruleEditText: prev.ruleEditText.slice(0, -1) }));
234
+ return;
235
+ }
236
+ if (input && !key.ctrl && !key.meta) {
237
+ setState(prev => ({ ...prev, ruleEditText: prev.ruleEditText + input }));
238
+ return;
239
+ }
240
+ return;
241
+ }
242
+ // Rule delete confirmation
243
+ if (state.mode === 'rule-delete-confirm') {
244
+ const rules = state.board?.rules?.[state.activeRuleType] || [];
245
+ const rule = rules[state.selectedRuleIndex];
246
+ if (input === 'y' || input === 'Y') {
247
+ if (rule) {
248
+ const result = (0, actions_js_1.deleteRuleAction)(filePath, state.activeRuleType, rule.id);
249
+ if (result.success) {
250
+ showStatus(setState, result.message || 'Rule deleted', 'success');
251
+ loadBrainfile(true);
252
+ // Adjust selection if needed
253
+ setState(prev => ({
254
+ ...prev,
255
+ mode: 'browse',
256
+ selectedRuleIndex: Math.max(0, prev.selectedRuleIndex - 1),
257
+ }));
258
+ }
259
+ else {
260
+ showStatus(setState, result.error || 'Failed to delete rule', 'error');
261
+ setState(prev => ({ ...prev, mode: 'browse' }));
262
+ }
263
+ }
264
+ return;
265
+ }
266
+ if (input === 'n' || input === 'N' || key.escape) {
267
+ setState(prev => ({ ...prev, mode: 'browse' }));
268
+ showStatus(setState, 'Delete cancelled', 'info');
269
+ return;
270
+ }
271
+ return;
272
+ }
273
+ // Archive restore mode - column picker
274
+ if (state.mode === 'archive-restore') {
275
+ if (key.escape) {
276
+ setState(prev => ({ ...prev, mode: 'browse' }));
277
+ return;
278
+ }
279
+ if (key.downArrow || input === 'j') {
280
+ setState(prev => ({
281
+ ...prev,
282
+ archiveRestoreColumnIndex: Math.min(prev.archiveRestoreColumnIndex + 1, allColumns.length - 1),
283
+ }));
284
+ return;
285
+ }
286
+ if (key.upArrow || input === 'k') {
287
+ setState(prev => ({
288
+ ...prev,
289
+ archiveRestoreColumnIndex: Math.max(prev.archiveRestoreColumnIndex - 1, 0),
290
+ }));
291
+ return;
292
+ }
293
+ if (key.return) {
294
+ const task = state.archive[state.selectedArchiveIndex];
295
+ const column = allColumns[state.archiveRestoreColumnIndex];
296
+ if (task && column) {
297
+ const result = (0, actions_js_1.restoreTaskAction)(filePath, task.id, column.id);
298
+ if (result.success) {
299
+ showStatus(setState, result.message || 'Task restored', 'success');
300
+ loadBrainfile(true);
301
+ // Reload archive
302
+ const archiveResult = (0, actions_js_1.loadArchive)(filePath);
303
+ setState(prev => ({
304
+ ...prev,
305
+ mode: 'browse',
306
+ archive: archiveResult.archive,
307
+ selectedArchiveIndex: Math.max(0, prev.selectedArchiveIndex - 1),
308
+ }));
309
+ }
310
+ else {
311
+ showStatus(setState, result.error || 'Failed to restore task', 'error');
312
+ setState(prev => ({ ...prev, mode: 'browse' }));
313
+ }
314
+ }
315
+ return;
316
+ }
317
+ return;
318
+ }
319
+ // Archive delete confirmation
320
+ if (state.mode === 'archive-delete-confirm') {
321
+ const task = state.archive[state.selectedArchiveIndex];
322
+ if (input === 'y' || input === 'Y') {
323
+ if (task) {
324
+ const result = (0, actions_js_1.deleteArchivedTaskAction)(filePath, task.id);
325
+ if (result.success) {
326
+ showStatus(setState, result.message || 'Task permanently deleted', 'success');
327
+ // Reload archive
328
+ const archiveResult = (0, actions_js_1.loadArchive)(filePath);
329
+ setState(prev => ({
330
+ ...prev,
331
+ mode: 'browse',
332
+ archive: archiveResult.archive,
333
+ selectedArchiveIndex: Math.max(0, prev.selectedArchiveIndex - 1),
334
+ }));
335
+ }
336
+ else {
337
+ showStatus(setState, result.error || 'Failed to delete task', 'error');
338
+ setState(prev => ({ ...prev, mode: 'browse' }));
339
+ }
340
+ }
341
+ return;
342
+ }
343
+ if (input === 'n' || input === 'N' || key.escape) {
344
+ setState(prev => ({ ...prev, mode: 'browse' }));
345
+ showStatus(setState, 'Delete cancelled', 'info');
346
+ return;
347
+ }
348
+ return;
349
+ }
350
+ // ============================================================
351
+ // GLOBAL KEYS (work in browse mode across all panels)
352
+ // ============================================================
197
353
  if (input === 'q' || (key.ctrl && input === 'c')) {
198
354
  exit();
199
355
  return;
@@ -202,232 +358,394 @@ function useKeyboardNavigation({ state, setState, currentTasks, maxTaskIndex, fi
202
358
  setState(prev => ({ ...prev, mode: 'help' }));
203
359
  return;
204
360
  }
205
- if (input === '/') {
206
- setState(prev => ({ ...prev, mode: 'search', searchQuery: '' }));
361
+ // Panel switching: 1/2/3
362
+ if (input === '1') {
363
+ setState(prev => ({ ...prev, activePanel: 'tasks', mode: 'browse' }));
364
+ return;
365
+ }
366
+ if (input === '2') {
367
+ setState(prev => ({ ...prev, activePanel: 'rules', mode: 'browse' }));
368
+ return;
369
+ }
370
+ if (input === '3') {
371
+ setState(prev => ({ ...prev, activePanel: 'archive', mode: 'browse' }));
372
+ // Archive is loaded via useEffect in BrainfileTUI
207
373
  return;
208
374
  }
209
375
  // Refresh (force refresh bypasses hash check)
210
- if (input === 'r') {
376
+ if (input === 'r' && state.activePanel !== 'archive') {
211
377
  loadBrainfile(true);
212
378
  showStatus(setState, 'Refreshed', 'info');
213
379
  return;
214
380
  }
215
- // === Task Management Keys ===
216
- // 'e' - Edit task in $EDITOR (blocks until editor closes)
217
- if (input === 'e') {
218
- if (!currentTask) {
219
- showStatus(setState, 'No task selected', 'error');
381
+ // ============================================================
382
+ // TASKS PANEL - Browse mode
383
+ // ============================================================
384
+ if (state.activePanel === 'tasks') {
385
+ if (input === '/') {
386
+ setState(prev => ({ ...prev, mode: 'search', searchQuery: '' }));
220
387
  return;
221
388
  }
222
- // spawnSync blocks the entire process, giving terminal to editor
223
- const result = (0, actions_js_1.editTaskInEditor)(filePath, currentTask.id);
224
- if (result.success) {
225
- showStatus(setState, result.message || 'Task updated', 'success');
226
- loadBrainfile(true);
389
+ // 'e' - Edit task in $EDITOR
390
+ if (input === 'e') {
391
+ if (!currentTask) {
392
+ showStatus(setState, 'No task selected', 'error');
393
+ return;
394
+ }
395
+ const result = (0, actions_js_1.editTaskInEditor)(filePath, currentTask.id);
396
+ if (result.success) {
397
+ showStatus(setState, result.message || 'Task updated', 'success');
398
+ loadBrainfile(true);
399
+ }
400
+ else {
401
+ showStatus(setState, result.error || 'Edit failed', 'error');
402
+ }
403
+ return;
227
404
  }
228
- else {
229
- showStatus(setState, result.error || 'Edit failed', 'error');
405
+ // 'm' - Move task
406
+ if (input === 'm') {
407
+ if (!currentTask) {
408
+ showStatus(setState, 'No task selected', 'error');
409
+ return;
410
+ }
411
+ setState(prev => ({ ...prev, mode: 'move', moveTargetIndex: prev.activeColumnIndex }));
412
+ return;
230
413
  }
231
- return;
232
- }
233
- // 'm' - Move task (open column picker)
234
- if (input === 'm') {
235
- if (!currentTask) {
236
- showStatus(setState, 'No task selected', 'error');
414
+ // 'd' - Delete task
415
+ if (input === 'd') {
416
+ if (!currentTask) {
417
+ showStatus(setState, 'No task selected', 'error');
418
+ return;
419
+ }
420
+ setState(prev => ({ ...prev, mode: 'delete-confirm' }));
237
421
  return;
238
422
  }
239
- setState(prev => ({ ...prev, mode: 'move', moveTargetIndex: prev.activeColumnIndex }));
240
- return;
241
- }
242
- // 'd' - Delete task (with confirmation)
243
- if (input === 'd') {
244
- if (!currentTask) {
245
- showStatus(setState, 'No task selected', 'error');
423
+ // 'A' - Archive task
424
+ if (input === 'A') {
425
+ if (!currentTask) {
426
+ showStatus(setState, 'No task selected', 'error');
427
+ return;
428
+ }
429
+ const result = (0, actions_js_1.archiveTaskAction)(filePath, currentTask.id);
430
+ if (result.success) {
431
+ showStatus(setState, result.message || 'Task archived', 'success');
432
+ loadBrainfile(true);
433
+ }
434
+ else {
435
+ showStatus(setState, result.error || 'Archive failed', 'error');
436
+ }
246
437
  return;
247
438
  }
248
- setState(prev => ({ ...prev, mode: 'delete-confirm' }));
249
- return;
250
- }
251
- // 'a' - Archive task
252
- if (input === 'a') {
253
- if (!currentTask) {
254
- showStatus(setState, 'No task selected', 'error');
439
+ // 'p' - Cycle priority
440
+ if (input === 'p') {
441
+ if (!currentTask) {
442
+ showStatus(setState, 'No task selected', 'error');
443
+ return;
444
+ }
445
+ const result = (0, actions_js_1.cyclePriorityAction)(filePath, currentTask.id);
446
+ if (result.success) {
447
+ showStatus(setState, result.message || 'Priority updated', 'success');
448
+ loadBrainfile(true);
449
+ }
450
+ else {
451
+ showStatus(setState, result.error || 'Failed to update priority', 'error');
452
+ }
453
+ return;
454
+ }
455
+ // 't' - Toggle subtask
456
+ if (input === 't') {
457
+ if (!currentTask) {
458
+ showStatus(setState, 'No task selected', 'error');
459
+ return;
460
+ }
461
+ if (!currentTask.subtasks || currentTask.subtasks.length === 0) {
462
+ showStatus(setState, 'No subtasks', 'info');
463
+ return;
464
+ }
465
+ setState(prev => ({ ...prev, mode: 'subtask', selectedSubtaskIndex: 0 }));
466
+ return;
467
+ }
468
+ // 'y' - Copy task ID
469
+ if (input === 'y') {
470
+ if (!currentTask) {
471
+ showStatus(setState, 'No task selected', 'error');
472
+ return;
473
+ }
474
+ const result = (0, actions_js_1.copyToClipboard)(currentTask.id);
475
+ if (result.success) {
476
+ showStatus(setState, `Copied ${currentTask.id}`, 'success');
477
+ }
478
+ else {
479
+ showStatus(setState, result.error || 'Copy failed', 'error');
480
+ }
255
481
  return;
256
482
  }
257
- const result = (0, actions_js_1.archiveTaskAction)(filePath, currentTask.id);
258
- if (result.success) {
259
- showStatus(setState, result.message || 'Task archived', 'success');
260
- loadBrainfile(true);
483
+ // 'n' - New task (quick)
484
+ if (input === 'n') {
485
+ setState(prev => ({ ...prev, mode: 'new-task', newTaskTitle: '' }));
486
+ return;
261
487
  }
262
- else {
263
- showStatus(setState, result.error || 'Archive failed', 'error');
488
+ // 'N' - New task (editor)
489
+ if (input === 'N') {
490
+ const currentCol = allColumns[state.activeColumnIndex];
491
+ if (currentCol) {
492
+ const result = (0, actions_js_1.newTaskInEditor)(filePath, currentCol.id);
493
+ if (result.success) {
494
+ showStatus(setState, result.message || 'Task created', 'success');
495
+ loadBrainfile(true);
496
+ }
497
+ else {
498
+ showStatus(setState, result.error || 'Failed to create task', 'error');
499
+ }
500
+ }
501
+ return;
264
502
  }
265
- return;
266
- }
267
- // 'p' - Cycle priority
268
- if (input === 'p') {
269
- if (!currentTask) {
270
- showStatus(setState, 'No task selected', 'error');
503
+ // Navigation: up/down
504
+ if (key.downArrow || input === 'j') {
505
+ setState(prev => ({
506
+ ...prev,
507
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxTaskIndex),
508
+ }));
271
509
  return;
272
510
  }
273
- const result = (0, actions_js_1.cyclePriorityAction)(filePath, currentTask.id);
274
- if (result.success) {
275
- showStatus(setState, result.message || 'Priority updated', 'success');
276
- loadBrainfile(true);
511
+ if (key.upArrow || input === 'k') {
512
+ setState(prev => ({
513
+ ...prev,
514
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0),
515
+ }));
516
+ return;
277
517
  }
278
- else {
279
- showStatus(setState, result.error || 'Failed to update priority', 'error');
518
+ // Page scrolling
519
+ if (key.ctrl && input === 'd') {
520
+ setState(prev => ({
521
+ ...prev,
522
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + Math.floor(viewportHeight / 2), maxTaskIndex),
523
+ }));
524
+ return;
280
525
  }
281
- return;
282
- }
283
- // 't' - Toggle subtask (open subtask picker if has subtasks)
284
- if (input === 't') {
285
- if (!currentTask) {
286
- showStatus(setState, 'No task selected', 'error');
526
+ if (key.ctrl && input === 'u') {
527
+ setState(prev => ({
528
+ ...prev,
529
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0),
530
+ }));
287
531
  return;
288
532
  }
289
- if (!currentTask.subtasks || currentTask.subtasks.length === 0) {
290
- showStatus(setState, 'No subtasks', 'info');
533
+ // Column switching
534
+ if (key.tab || key.rightArrow || input === 'l') {
535
+ setState(prev => ({
536
+ ...prev,
537
+ activeColumnIndex: (prev.activeColumnIndex + 1) % filteredColumnsLength,
538
+ selectedTaskIndex: 0,
539
+ }));
291
540
  return;
292
541
  }
293
- setState(prev => ({ ...prev, mode: 'subtask', selectedSubtaskIndex: 0 }));
294
- return;
295
- }
296
- // 'y' - Yank/copy task ID to clipboard
297
- if (input === 'y') {
298
- if (!currentTask) {
299
- showStatus(setState, 'No task selected', 'error');
542
+ if ((key.shift && key.tab) || key.leftArrow || input === 'h') {
543
+ setState(prev => ({
544
+ ...prev,
545
+ activeColumnIndex: prev.activeColumnIndex === 0
546
+ ? filteredColumnsLength - 1
547
+ : prev.activeColumnIndex - 1,
548
+ selectedTaskIndex: 0,
549
+ }));
300
550
  return;
301
551
  }
302
- const result = (0, actions_js_1.copyToClipboard)(currentTask.id);
303
- if (result.success) {
304
- showStatus(setState, `Copied ${currentTask.id}`, 'success');
552
+ // Home/End
553
+ if (input === 'g') {
554
+ setState(prev => ({ ...prev, selectedTaskIndex: 0 }));
555
+ return;
305
556
  }
306
- else {
307
- showStatus(setState, result.error || 'Copy failed', 'error');
557
+ if (input === 'G') {
558
+ setState(prev => ({ ...prev, selectedTaskIndex: maxTaskIndex }));
559
+ return;
560
+ }
561
+ // Expand/collapse task
562
+ if (key.return) {
563
+ const task = currentTasks[state.selectedTaskIndex];
564
+ if (task) {
565
+ setState(prev => {
566
+ const newExpanded = new Set(prev.expandedTaskIds);
567
+ if (newExpanded.has(task.id)) {
568
+ newExpanded.delete(task.id);
569
+ }
570
+ else {
571
+ newExpanded.add(task.id);
572
+ }
573
+ return { ...prev, expandedTaskIds: newExpanded };
574
+ });
575
+ }
576
+ return;
577
+ }
578
+ // Escape: collapse all or clear search
579
+ if (key.escape) {
580
+ setState(prev => ({
581
+ ...prev,
582
+ expandedTaskIds: new Set(),
583
+ searchQuery: '',
584
+ }));
585
+ return;
308
586
  }
309
- return;
310
- }
311
- // 'n' - New task (quick add - title only)
312
- if (input === 'n') {
313
- setState(prev => ({ ...prev, mode: 'new-task', newTaskTitle: '' }));
314
- return;
315
587
  }
316
- // 'N' - New task (full editor)
317
- if (input === 'N') {
318
- const currentCol = allColumns[state.activeColumnIndex];
319
- if (currentCol) {
320
- const result = (0, actions_js_1.newTaskInEditor)(filePath, currentCol.id);
321
- if (result.success) {
322
- showStatus(setState, result.message || 'Task created', 'success');
323
- loadBrainfile(true);
588
+ // ============================================================
589
+ // RULES PANEL - Browse mode
590
+ // ============================================================
591
+ if (state.activePanel === 'rules') {
592
+ const rules = state.board?.rules?.[state.activeRuleType] || [];
593
+ const maxRuleIndex = Math.max(0, rules.length - 1);
594
+ // h/l or left/right or Shift+TAB to switch rule type left
595
+ if (key.leftArrow || input === 'h' || (key.shift && key.tab)) {
596
+ const currentIdx = RULE_TYPES.indexOf(state.activeRuleType);
597
+ const newIdx = currentIdx === 0 ? RULE_TYPES.length - 1 : currentIdx - 1;
598
+ setState(prev => ({ ...prev, activeRuleType: RULE_TYPES[newIdx], selectedRuleIndex: 0 }));
599
+ return;
600
+ }
601
+ // TAB or l or right to switch rule type right
602
+ if (key.tab || key.rightArrow || input === 'l') {
603
+ const currentIdx = RULE_TYPES.indexOf(state.activeRuleType);
604
+ const newIdx = (currentIdx + 1) % RULE_TYPES.length;
605
+ setState(prev => ({ ...prev, activeRuleType: RULE_TYPES[newIdx], selectedRuleIndex: 0 }));
606
+ return;
607
+ }
608
+ // j/k to navigate rules
609
+ if (key.downArrow || input === 'j') {
610
+ setState(prev => ({
611
+ ...prev,
612
+ selectedRuleIndex: Math.min(prev.selectedRuleIndex + 1, maxRuleIndex),
613
+ }));
614
+ return;
615
+ }
616
+ if (key.upArrow || input === 'k') {
617
+ setState(prev => ({
618
+ ...prev,
619
+ selectedRuleIndex: Math.max(prev.selectedRuleIndex - 1, 0),
620
+ }));
621
+ return;
622
+ }
623
+ // n - New rule
624
+ if (input === 'n') {
625
+ setState(prev => ({ ...prev, mode: 'rule-add', ruleEditText: '', ruleEditId: null }));
626
+ return;
627
+ }
628
+ // e - Edit rule
629
+ if (input === 'e') {
630
+ const rule = rules[state.selectedRuleIndex];
631
+ if (rule) {
632
+ setState(prev => ({
633
+ ...prev,
634
+ mode: 'rule-edit',
635
+ ruleEditText: rule.rule,
636
+ ruleEditId: rule.id,
637
+ }));
324
638
  }
325
639
  else {
326
- showStatus(setState, result.error || 'Failed to create task', 'error');
640
+ showStatus(setState, 'No rule selected', 'error');
327
641
  }
642
+ return;
328
643
  }
329
- return;
330
- }
331
- // Navigation: up/down through tasks
332
- if (key.downArrow || input === 'j') {
333
- setState(prev => ({
334
- ...prev,
335
- selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxTaskIndex),
336
- }));
337
- return;
338
- }
339
- if (key.upArrow || input === 'k') {
340
- setState(prev => ({
341
- ...prev,
342
- selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0),
343
- }));
344
- return;
345
- }
346
- // Page scrolling
347
- if (key.ctrl && input === 'd') {
348
- setState(prev => ({
349
- ...prev,
350
- selectedTaskIndex: Math.min(prev.selectedTaskIndex + Math.floor(viewportHeight / 2), maxTaskIndex),
351
- }));
352
- return;
353
- }
354
- if (key.ctrl && input === 'u') {
355
- setState(prev => ({
356
- ...prev,
357
- selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0),
358
- }));
359
- return;
360
- }
361
- // Column switching: TAB / left/right
362
- if (key.tab || key.rightArrow || input === 'l') {
363
- setState(prev => ({
364
- ...prev,
365
- activeColumnIndex: (prev.activeColumnIndex + 1) % filteredColumnsLength,
366
- selectedTaskIndex: 0,
367
- }));
368
- return;
369
- }
370
- if ((key.shift && key.tab) || key.leftArrow || input === 'h') {
371
- setState(prev => ({
372
- ...prev,
373
- activeColumnIndex: prev.activeColumnIndex === 0
374
- ? filteredColumnsLength - 1
375
- : prev.activeColumnIndex - 1,
376
- selectedTaskIndex: 0,
377
- }));
378
- return;
379
- }
380
- // Jump to column headers with { and }
381
- if (input === '{') {
382
- setState(prev => ({
383
- ...prev,
384
- activeColumnIndex: Math.max(0, prev.activeColumnIndex - 1),
385
- selectedTaskIndex: 0,
386
- }));
387
- return;
388
- }
389
- if (input === '}') {
390
- setState(prev => ({
391
- ...prev,
392
- activeColumnIndex: Math.min(filteredColumnsLength - 1, prev.activeColumnIndex + 1),
393
- selectedTaskIndex: 0,
394
- }));
395
- return;
396
- }
397
- // Home/End
398
- if (input === 'g') {
399
- setState(prev => ({ ...prev, selectedTaskIndex: 0 }));
400
- return;
401
- }
402
- if (input === 'G') {
403
- setState(prev => ({ ...prev, selectedTaskIndex: maxTaskIndex }));
404
- return;
405
- }
406
- // Expand/collapse task
407
- if (key.return) {
408
- const task = currentTasks[state.selectedTaskIndex];
409
- if (task) {
410
- setState(prev => {
411
- const newExpanded = new Set(prev.expandedTaskIds);
412
- if (newExpanded.has(task.id)) {
413
- newExpanded.delete(task.id);
414
- }
415
- else {
416
- newExpanded.add(task.id);
417
- }
418
- return { ...prev, expandedTaskIds: newExpanded };
419
- });
644
+ // d - Delete rule
645
+ if (input === 'd') {
646
+ const rule = rules[state.selectedRuleIndex];
647
+ if (rule) {
648
+ setState(prev => ({ ...prev, mode: 'rule-delete-confirm' }));
649
+ }
650
+ else {
651
+ showStatus(setState, 'No rule selected', 'error');
652
+ }
653
+ return;
654
+ }
655
+ // Home/End
656
+ if (input === 'g') {
657
+ setState(prev => ({ ...prev, selectedRuleIndex: 0 }));
658
+ return;
659
+ }
660
+ if (input === 'G') {
661
+ setState(prev => ({ ...prev, selectedRuleIndex: maxRuleIndex }));
662
+ return;
663
+ }
664
+ // Escape
665
+ if (key.escape) {
666
+ setState(prev => ({ ...prev, selectedRuleIndex: 0 }));
667
+ return;
420
668
  }
421
- return;
422
669
  }
423
- // Escape: collapse all or clear search
424
- if (key.escape) {
425
- setState(prev => ({
426
- ...prev,
427
- expandedTaskIds: new Set(),
428
- searchQuery: '',
429
- }));
430
- return;
670
+ // ============================================================
671
+ // ARCHIVE PANEL - Browse mode
672
+ // ============================================================
673
+ if (state.activePanel === 'archive') {
674
+ const maxArchiveIndex = Math.max(0, state.archive.length - 1);
675
+ // r - Refresh archive
676
+ if (input === 'r') {
677
+ const result = (0, actions_js_1.loadArchive)(filePath);
678
+ setState(prev => ({ ...prev, archive: result.archive }));
679
+ showStatus(setState, 'Archive refreshed', 'info');
680
+ return;
681
+ }
682
+ // j/k navigation
683
+ if (key.downArrow || input === 'j') {
684
+ setState(prev => ({
685
+ ...prev,
686
+ selectedArchiveIndex: Math.min(prev.selectedArchiveIndex + 1, maxArchiveIndex),
687
+ }));
688
+ return;
689
+ }
690
+ if (key.upArrow || input === 'k') {
691
+ setState(prev => ({
692
+ ...prev,
693
+ selectedArchiveIndex: Math.max(prev.selectedArchiveIndex - 1, 0),
694
+ }));
695
+ return;
696
+ }
697
+ // Enter - Expand/collapse
698
+ if (key.return) {
699
+ const task = state.archive[state.selectedArchiveIndex];
700
+ if (task) {
701
+ setState(prev => {
702
+ const newExpanded = new Set(prev.expandedArchiveIds);
703
+ if (newExpanded.has(task.id)) {
704
+ newExpanded.delete(task.id);
705
+ }
706
+ else {
707
+ newExpanded.add(task.id);
708
+ }
709
+ return { ...prev, expandedArchiveIds: newExpanded };
710
+ });
711
+ }
712
+ return;
713
+ }
714
+ // r key for restore (use 'R' to avoid conflict with refresh)
715
+ if (input === 'R') {
716
+ if (state.archive.length === 0) {
717
+ showStatus(setState, 'No archived tasks', 'error');
718
+ return;
719
+ }
720
+ setState(prev => ({ ...prev, mode: 'archive-restore', archiveRestoreColumnIndex: 0 }));
721
+ return;
722
+ }
723
+ // d - Delete permanently
724
+ if (input === 'd') {
725
+ if (state.archive.length === 0) {
726
+ showStatus(setState, 'No archived tasks', 'error');
727
+ return;
728
+ }
729
+ setState(prev => ({ ...prev, mode: 'archive-delete-confirm' }));
730
+ return;
731
+ }
732
+ // Home/End
733
+ if (input === 'g') {
734
+ setState(prev => ({ ...prev, selectedArchiveIndex: 0 }));
735
+ return;
736
+ }
737
+ if (input === 'G') {
738
+ setState(prev => ({ ...prev, selectedArchiveIndex: maxArchiveIndex }));
739
+ return;
740
+ }
741
+ // Escape - collapse all expanded
742
+ if (key.escape) {
743
+ setState(prev => ({
744
+ ...prev,
745
+ expandedArchiveIds: new Set(),
746
+ }));
747
+ return;
748
+ }
431
749
  }
432
750
  });
433
751
  }